Merge pull request #2958 from fonttools/varc

VarComposites
This commit is contained in:
Behdad Esfahbod 2023-02-07 10:28:00 -07:00 committed by GitHub
commit 58bc16e58f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3511 additions and 50 deletions

View File

@ -209,6 +209,7 @@ ttLib.getTableClass("glyf").mergeMap = {
"tableTag": equal,
"glyphs": sumDicts,
"glyphOrder": sumLists,
"axisTags": equal,
}
@ -222,7 +223,7 @@ def merge(self, m, tables):
g.removeHinting()
# Expand composite glyphs to load their
# composite glyph names.
if g.isComposite():
if g.isComposite() or g.isVarComposite():
g.expand(table)
return DefaultTable.merge(self, m, tables)

View File

@ -19,6 +19,9 @@ Offset
Scale
Convenience function that returns a scaling transformation
The DecomposedTransform class implements a transformation with separate
translate, rotation, scale, skew, and transformation-center components.
:Example:
>>> t = Transform(2, 0, 0, 3, 0, 0)
@ -49,10 +52,12 @@ Scale
>>>
"""
import math
from typing import NamedTuple
from dataclasses import dataclass
__all__ = ["Transform", "Identity", "Offset", "Scale"]
__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
_EPSILON = 1e-15
@ -344,6 +349,10 @@ class Transform(NamedTuple):
"""
return "[%s %s %s %s %s %s]" % self
def toDecomposed(self) -> "DecomposedTransform":
"""Decompose into a DecomposedTransform."""
return DecomposedTransform.fromTransform(self)
def __bool__(self):
"""Returns True if transform is not identity, False otherwise.
@ -398,6 +407,87 @@ def Scale(x, y=None):
return Transform(x, 0, 0, y, 0, 0)
@dataclass
class DecomposedTransform:
"""The DecomposedTransform class implements a transformation with separate
translate, rotation, scale, skew, and transformation-center components.
"""
translateX: float = 0
translateY: float = 0
rotation: float = 0 # in degrees, counter-clockwise
scaleX: float = 1
scaleY: float = 1
skewX: float = 0 # in degrees, clockwise
skewY: float = 0 # in degrees, counter-clockwise
tCenterX: float = 0
tCenterY: float = 0
@classmethod
def fromTransform(self, transform):
# Adapted from an answer on
# https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
a, b, c, d, x, y = transform
sx = math.copysign(1, a)
if sx < 0:
a *= sx
b *= sx
delta = a * d - b * c
rotation = 0
scaleX = scaleY = 0
skewX = skewY = 0
# Apply the QR-like decomposition.
if a != 0 or b != 0:
r = math.sqrt(a * a + b * b)
rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
scaleX, scaleY = (r, delta / r)
skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0)
elif c != 0 or d != 0:
s = math.sqrt(c * c + d * d)
rotation = math.pi / 2 - (
math.acos(-c / s) if d >= 0 else -math.acos(c / s)
)
scaleX, scaleY = (delta / s, s)
skewX, skewY = (0, math.atan((a * c + b * d) / (s * s)))
else:
# a = b = c = d = 0
pass
return DecomposedTransform(
x,
y,
math.degrees(rotation),
scaleX * sx,
scaleY,
math.degrees(skewX) * sx,
math.degrees(skewY),
0,
0,
)
def toTransform(self):
"""Return the Transform() equivalent of this transformation.
:Example:
>>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
<Transform [2 0 0 2 0 0]>
>>>
"""
t = Transform()
t = t.translate(
self.translateX + self.tCenterX, self.translateY + self.tCenterY
)
t = t.rotate(math.radians(self.rotation))
t = t.scale(self.scaleX, self.scaleY)
t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
t = t.translate(-self.tCenterX, -self.tCenterY)
return t
if __name__ == "__main__":
import sys
import doctest

View File

@ -36,9 +36,10 @@ Coordinates are usually expressed as (x, y) tuples, but generally any
sequence of length 2 will do.
"""
from typing import Tuple
from typing import Tuple, Dict
from fontTools.misc.loggingTools import LogMixin
from fontTools.misc.transform import DecomposedTransform
__all__ = [
"AbstractPen",
@ -131,6 +132,20 @@ class AbstractPen:
"""
raise NotImplementedError
def addVarComponent(
self,
glyphName: str,
transformation: DecomposedTransform,
location: Dict[str, float],
) -> None:
"""Add a VarComponent sub glyph. The 'transformation' argument
must be a DecomposedTransform from the fontTools.misc.transform module,
and the 'location' argument must be a dictionary mapping axis tags
to their locations.
"""
# GlyphSet decomposes for us
raise AttributeError
class NullPen(AbstractPen):
@ -157,6 +172,9 @@ class NullPen(AbstractPen):
def addComponent(self, glyphName, transformation):
pass
def addVarComponent(self, glyphName, transformation, location):
pass
class LoggingPen(LogMixin, AbstractPen):
"""A pen with a ``log`` property (see fontTools.misc.loggingTools.LogMixin)"""
@ -205,13 +223,17 @@ class DecomposingPen(LoggingPen):
tPen = TransformPen(self, transformation)
glyph.draw(tPen)
def addVarComponent(self, glyphName, transformation, location):
# GlyphSet decomposes for us
raise AttributeError
class BasePen(DecomposingPen):
"""Base class for drawing pens. You must override _moveTo, _lineTo and
_curveToOne. You may additionally override _closePath, _endPath,
addComponent and/or _qCurveToOne. You should not override any other
methods.
addComponent, addVarComponent, and/or _qCurveToOne. You should not
override any other methods.
"""
def __init__(self, glyphSet=None):

View File

@ -13,9 +13,10 @@ For instance, whether or not a point is smooth, and its name.
"""
import math
from typing import Any, Optional, Tuple
from typing import Any, Optional, Tuple, Dict
from fontTools.pens.basePen import AbstractPen, PenError
from fontTools.misc.transform import DecomposedTransform
__all__ = [
"AbstractPointPen",
@ -60,6 +61,22 @@ class AbstractPointPen:
"""Add a sub glyph."""
raise NotImplementedError
def addVarComponent(
self,
glyphName: str,
transformation: DecomposedTransform,
location: Dict[str, float],
identifier: Optional[str] = None,
**kwargs: Any,
) -> None:
"""Add a VarComponent sub glyph. The 'transformation' argument
must be a DecomposedTransform from the fontTools.misc.transform module,
and the 'location' argument must be a dictionary mapping axis tags
to their locations.
"""
# ttGlyphSet decomposes for us
raise AttributeError
class BasePointToSegmentPen(AbstractPointPen):
"""
@ -405,6 +422,15 @@ class GuessSmoothPointPen(AbstractPointPen):
kwargs["identifier"] = identifier
self._outPen.addComponent(glyphName, transformation, **kwargs)
def addVarComponent(
self, glyphName, transformation, location, identifier=None, **kwargs
):
if self._points is not None:
raise PenError("VarComponents must be added before or after contours")
if identifier is not None:
kwargs["identifier"] = identifier
self._outPen.addVarComponent(glyphName, transformation, location, **kwargs)
class ReverseContourPointPen(AbstractPointPen):
"""

View File

@ -70,6 +70,9 @@ class RecordingPen(AbstractPen):
def addComponent(self, glyphName, transformation):
self.value.append(("addComponent", (glyphName, transformation)))
def addVarComponent(self, glyphName, transformation, location):
self.value.append(("addVarComponent", (glyphName, transformation, location)))
def replay(self, pen):
replayRecording(self.value, pen)
@ -151,6 +154,15 @@ class RecordingPointPen(AbstractPointPen):
kwargs["identifier"] = identifier
self.value.append(("addComponent", (baseGlyphName, transformation), kwargs))
def addVarComponent(
self, baseGlyphName, transformation, location, identifier=None, **kwargs
):
if identifier is not None:
kwargs["identifier"] = identifier
self.value.append(
("addVarComponent", (baseGlyphName, transformation, location), kwargs)
)
def replay(self, pointPen):
for operator, args, kwargs in self.value:
getattr(pointPen, operator)(*args, **kwargs)

View File

@ -12,6 +12,7 @@ from fontTools.cffLib import VarStoreData
import fontTools.cffLib.specializer as cffSpecializer
from fontTools.varLib import builder # for VarData.calculateNumShorts
from fontTools.misc.fixedTools import otRound
from fontTools.ttLib.tables._g_l_y_f import VarComponentFlags
__all__ = ["scale_upem", "ScalerVisitor"]
@ -112,30 +113,83 @@ def visit(visitor, obj, attr, VOriginRecords):
@ScalerVisitor.register_attr(ttLib.getTableClass("glyf"), "glyphs")
def visit(visitor, obj, attr, glyphs):
for g in glyphs.values():
for attr in ("xMin", "xMax", "yMin", "yMax"):
v = getattr(g, attr, None)
if v is not None:
setattr(g, attr, visitor.scale(v))
if g.isComposite():
for component in g.components:
component.x = visitor.scale(component.x)
component.y = visitor.scale(component.y)
else:
for attr in ("xMin", "xMax", "yMin", "yMax"):
v = getattr(g, attr, None)
if v is not None:
setattr(g, attr, visitor.scale(v))
continue
glyf = visitor.font["glyf"]
coordinates = g.getCoordinates(glyf)[0]
for i, (x, y) in enumerate(coordinates):
coordinates[i] = visitor.scale(x), visitor.scale(y)
if g.isVarComposite():
for component in g.components:
for attr in ("translateX", "translateY", "tCenterX", "tCenterY"):
v = getattr(component.transform, attr)
setattr(component.transform, attr, visitor.scale(v))
continue
if hasattr(g, "coordinates"):
coordinates = g.coordinates
for i, (x, y) in enumerate(coordinates):
coordinates[i] = visitor.scale(x), visitor.scale(y)
@ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations")
def visit(visitor, obj, attr, variations):
for varlist in variations.values():
# VarComposites are a pain to handle :-(
glyfTable = visitor.font["glyf"]
for glyphName, varlist in variations.items():
glyph = glyfTable[glyphName]
isVarComposite = glyph.isVarComposite()
for var in varlist:
coordinates = var.coordinates
for i, xy in enumerate(coordinates):
if xy is None:
continue
if not isVarComposite:
for i, xy in enumerate(coordinates):
if xy is None:
continue
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
continue
# VarComposite glyph
i = 0
for component in glyph.components:
if component.flags & VarComponentFlags.AXES_HAVE_VARIATION:
i += len(component.location)
if component.flags & (
VarComponentFlags.HAVE_TRANSLATE_X
| VarComponentFlags.HAVE_TRANSLATE_Y
):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
i += 1
if component.flags & VarComponentFlags.HAVE_ROTATION:
i += 1
if component.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
i += 1
if component.flags & (
VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y
):
i += 1
if component.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
i += 1
# Phantom points
assert i + 4 == len(coordinates)
for i in range(i, len(coordinates)):
xy = coordinates[i]
coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])

View File

@ -4,6 +4,7 @@ from collections import namedtuple
from fontTools.misc import sstruct
from fontTools import ttLib
from fontTools import version
from fontTools.misc.transform import DecomposedTransform
from fontTools.misc.textTools import tostr, safeEval, pad
from fontTools.misc.arrayTools import calcIntBounds, pointInRect
from fontTools.misc.bezierTools import calcQuadraticBounds
@ -25,6 +26,7 @@ import os
from fontTools.misc import xmlWriter
from fontTools.misc.filenames import userNameToFileName
from fontTools.misc.loggingTools import deprecateFunction
from enum import IntFlag
log = logging.getLogger(__name__)
@ -77,6 +79,8 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
"""
dependencies = ["fvar"]
# this attribute controls the amount of padding applied to glyph data upon compile.
# Glyph lenghts are aligned to multiples of the specified value.
# Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
@ -84,6 +88,9 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
padding = 1
def decompile(self, data, ttFont):
self.axisTags = (
[axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
)
loca = ttFont["loca"]
pos = int(loca[0])
nextPos = 0
@ -121,6 +128,9 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
glyph.expand(self)
def compile(self, ttFont):
self.axisTags = (
[axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
)
if not hasattr(self, "glyphOrder"):
self.glyphOrder = ttFont.getGlyphOrder()
padding = self.padding
@ -393,6 +403,30 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
for c in glyph.components
],
)
elif glyph.isVarComposite():
coords = []
controls = []
for component in glyph.components:
(
componentCoords,
componentControls,
) = component.getCoordinatesAndControls()
coords.extend(componentCoords)
controls.extend(componentControls)
coords = GlyphCoordinates(coords)
controls = _GlyphControls(
numberOfContours=glyph.numberOfContours,
endPts=list(range(len(coords))),
flags=None,
components=[
(c.glyphName, getattr(c, "flags", None)) for c in glyph.components
],
)
else:
coords, endPts, flags = glyph.getCoordinates(self)
coords = coords.copy()
@ -437,6 +471,10 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
for p, comp in zip(coord, glyph.components):
if hasattr(comp, "x"):
comp.x, comp.y = p
elif glyph.isVarComposite():
for comp in glyph.components:
coord = comp.setCoordinates(coord)
assert not coord
elif glyph.numberOfContours == 0:
assert len(coord) == 0
else:
@ -678,6 +716,8 @@ class Glyph(object):
return
if self.isComposite():
self.decompileComponents(data, glyfTable)
elif self.isVarComposite():
self.decompileVarComponents(data, glyfTable)
else:
self.decompileCoordinates(data)
@ -695,6 +735,8 @@ class Glyph(object):
data = sstruct.pack(glyphHeaderFormat, self)
if self.isComposite():
data = data + self.compileComponents(glyfTable)
elif self.isVarComposite():
data = data + self.compileVarComponents(glyfTable)
else:
data = data + self.compileCoordinates()
return data
@ -704,6 +746,10 @@ class Glyph(object):
for compo in self.components:
compo.toXML(writer, ttFont)
haveInstructions = hasattr(self, "program")
if self.isVarComposite():
for compo in self.components:
compo.toXML(writer, ttFont)
haveInstructions = False
else:
last = 0
for i in range(self.numberOfContours):
@ -769,6 +815,15 @@ class Glyph(object):
component = GlyphComponent()
self.components.append(component)
component.fromXML(name, attrs, content, ttFont)
elif name == "varComponent":
if self.numberOfContours > 0:
raise ttLib.TTLibError("can't mix composites and contours in glyph")
self.numberOfContours = -2
if not hasattr(self, "components"):
self.components = []
component = GlyphVarComponent()
self.components.append(component)
component.fromXML(name, attrs, content, ttFont)
elif name == "instructions":
self.program = ttProgram.Program()
for element in content:
@ -778,7 +833,7 @@ class Glyph(object):
self.program.fromXML(name, attrs, content, ttFont)
def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
assert self.isComposite()
assert self.isComposite() or self.isVarComposite()
nContours = 0
nPoints = 0
initialMaxComponentDepth = maxComponentDepth
@ -822,6 +877,13 @@ class Glyph(object):
len(data),
)
def decompileVarComponents(self, data, glyfTable):
self.components = []
while len(data) >= GlyphVarComponent.MIN_SIZE:
component = GlyphVarComponent()
data = component.decompile(data, glyfTable)
self.components.append(component)
def decompileCoordinates(self, data):
endPtsOfContours = array.array("H")
endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
@ -938,6 +1000,9 @@ class Glyph(object):
data = data + struct.pack(">h", len(instructions)) + instructions
return data
def compileVarComponents(self, glyfTable):
return b"".join(c.compile(glyfTable) for c in self.components)
def compileCoordinates(self):
assert len(self.coordinates) == len(self.flags)
data = []
@ -1080,20 +1145,25 @@ class Glyph(object):
recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
must be provided to resolve component bounds.
"""
coords, endPts, flags = self.getCoordinates(glyfTable)
self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
try:
coords, endPts, flags = self.getCoordinates(glyfTable)
self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords)
except NotImplementedError:
pass
def isComposite(self):
"""Test whether a glyph has components"""
if hasattr(self, "data") and self.data:
return struct.unpack(">h", self.data[:2])[0] == -1
if hasattr(self, "data"):
return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False
else:
return self.numberOfContours == -1
def __getitem__(self, componentIndex):
if not self.isComposite():
raise ttLib.TTLibError("can't use glyph as sequence")
return self.components[componentIndex]
def isVarComposite(self):
"""Test whether a glyph has variable components"""
if hasattr(self, "data"):
return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False
else:
return self.numberOfContours == -2
def getCoordinates(self, glyfTable):
"""Return the coordinates, end points and flags
@ -1165,6 +1235,8 @@ class Glyph(object):
allCoords.extend(coordinates)
allFlags.extend(flags)
return allCoords, allEndPts, allFlags
elif self.isVarComposite():
raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs")
else:
return GlyphCoordinates(), [], bytearray()
@ -1174,8 +1246,12 @@ class Glyph(object):
This method can be used on simple glyphs (in which case it returns an
empty list) or composite glyphs.
"""
if hasattr(self, "data") and self.isVarComposite():
# TODO(VarComposite) Add implementation without expanding glyph
self.expand(glyfTable)
if not hasattr(self, "data"):
if self.isComposite():
if self.isComposite() or self.isVarComposite():
return [c.glyphName for c in self.components]
else:
return []
@ -1218,6 +1294,8 @@ class Glyph(object):
if self.isComposite():
if hasattr(self, "program"):
del self.program
elif self.isVarComposite():
pass # Doesn't have hinting
else:
self.program = ttProgram.Program()
self.program.fromBytecode([])
@ -1269,7 +1347,7 @@ class Glyph(object):
i += coordBytes
# Remove padding
data = data[:i]
else:
elif self.isComposite():
more = 1
we_have_instructions = False
while more:
@ -1299,6 +1377,13 @@ class Glyph(object):
i += 2 + instructionLen
# Remove padding
data = data[:i]
elif self.isVarComposite():
i = 0
MIN_SIZE = GlyphVarComponent.MIN_SIZE
while len(data[i : i + MIN_SIZE]) >= MIN_SIZE:
size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE])
i += size
data = data[:i]
self.data = data
@ -1608,6 +1693,393 @@ class GlyphComponent(object):
return result if result is NotImplemented else not result
#
# Variable Composite glyphs
# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
#
class VarComponentFlags(IntFlag):
USE_MY_METRICS = 0x0001
AXIS_INDICES_ARE_SHORT = 0x0002
UNIFORM_SCALE = 0x0004
HAVE_TRANSLATE_X = 0x0008
HAVE_TRANSLATE_Y = 0x0010
HAVE_ROTATION = 0x0020
HAVE_SCALE_X = 0x0040
HAVE_SCALE_Y = 0x0080
HAVE_SKEW_X = 0x0100
HAVE_SKEW_Y = 0x0200
HAVE_TCENTER_X = 0x0400
HAVE_TCENTER_Y = 0x0800
GID_IS_24 = 0x1000
AXES_HAVE_VARIATION = 0x2000
RESET_UNSPECIFIED_AXES = 0x4000
VarComponentTransformMappingValues = namedtuple(
"VarComponentTransformMappingValues",
["flag", "fractionalBits", "scale", "defaultValue"],
)
VAR_COMPONENT_TRANSFORM_MAPPING = {
"translateX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
),
"translateY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
),
"rotation": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_ROTATION, 12, 180, 0
),
"scaleX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SCALE_X, 10, 1, 1
),
"scaleY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1
),
"skewX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SKEW_X, 12, -180, 0
),
"skewY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0
),
"tCenterX": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0
),
"tCenterY": VarComponentTransformMappingValues(
VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0
),
}
class GlyphVarComponent(object):
MIN_SIZE = 5
def __init__(self):
self.location = {}
self.transform = DecomposedTransform()
@staticmethod
def getSize(data):
size = 5
flags = struct.unpack(">H", data[:2])[0]
numAxes = int(data[2])
if flags & VarComponentFlags.GID_IS_24:
size += 1
size += numAxes
if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
size += 2 * numAxes
else:
axisIndices = array.array("B", data[:numAxes])
size += numAxes
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
if flags & mapping_values.flag:
size += 2
return size
def decompile(self, data, glyfTable):
flags = struct.unpack(">H", data[:2])[0]
self.flags = int(flags)
data = data[2:]
numAxes = int(data[0])
data = data[1:]
if flags & VarComponentFlags.GID_IS_24:
glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0])
data = data[3:]
flags ^= VarComponentFlags.GID_IS_24
else:
glyphID = int(struct.unpack(">H", data[:2])[0])
data = data[2:]
self.glyphName = glyfTable.getGlyphName(int(glyphID))
if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
axisIndices = array.array("H", data[: 2 * numAxes])
if sys.byteorder != "big":
axisIndices.byteswap()
data = data[2 * numAxes :]
flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT
else:
axisIndices = array.array("B", data[:numAxes])
data = data[numAxes:]
assert len(axisIndices) == numAxes
axisIndices = list(axisIndices)
axisValues = array.array("h", data[: 2 * numAxes])
if sys.byteorder != "big":
axisValues.byteswap()
data = data[2 * numAxes :]
assert len(axisValues) == numAxes
axisValues = [fi2fl(v, 14) for v in axisValues]
self.location = {
glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues)
}
def read_transform_component(data, values):
if flags & values.flag:
return (
data[2:],
fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits)
* values.scale,
)
else:
return data, values.defaultValue
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
data, value = read_transform_component(data, mapping_values)
setattr(self.transform, attr_name, value)
if flags & VarComponentFlags.UNIFORM_SCALE:
if flags & VarComponentFlags.HAVE_SCALE_X and not (
flags & VarComponentFlags.HAVE_SCALE_Y
):
self.transform.scaleY = self.transform.scaleX
flags |= VarComponentFlags.HAVE_SCALE_Y
flags ^= VarComponentFlags.UNIFORM_SCALE
return data
def compile(self, glyfTable):
data = b""
if not hasattr(self, "flags"):
flags = 0
# Calculate optimal transform component flags
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
value = getattr(self.transform, attr_name)
if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi(
mapping.defaultValue / mapping.scale, mapping.fractionalBits
):
flags |= mapping.flag
else:
flags = self.flags
if (
flags & VarComponentFlags.HAVE_SCALE_X
and flags & VarComponentFlags.HAVE_SCALE_Y
and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10)
):
flags |= VarComponentFlags.UNIFORM_SCALE
flags ^= VarComponentFlags.HAVE_SCALE_Y
numAxes = len(self.location)
data = data + struct.pack(">B", numAxes)
glyphID = glyfTable.getGlyphID(self.glyphName)
if glyphID > 65535:
flags |= VarComponentFlags.GID_IS_24
data = data + struct.pack(">L", glyphID)[1:]
else:
data = data + struct.pack(">H", glyphID)
axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()]
if all(a <= 255 for a in axisIndices):
axisIndices = array.array("B", axisIndices)
else:
axisIndices = array.array("H", axisIndices)
if sys.byteorder != "big":
axisIndices.byteswap()
flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT
data = data + bytes(axisIndices)
axisValues = self.location.values()
axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues))
if sys.byteorder != "big":
axisValues.byteswap()
data = data + bytes(axisValues)
def write_transform_component(data, value, values):
if flags & values.flag:
return data + struct.pack(
">h", fl2fi(value / values.scale, values.fractionalBits)
)
else:
return data
for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
value = getattr(self.transform, attr_name)
data = write_transform_component(data, value, mapping_values)
return struct.pack(">H", flags) + data
def toXML(self, writer, ttFont):
attrs = [("glyphName", self.glyphName)]
if hasattr(self, "flags"):
attrs = attrs + [("flags", hex(self.flags))]
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
v = getattr(self.transform, attr_name)
if v != mapping.defaultValue:
attrs.append((attr_name, fl2str(v, mapping.fractionalBits)))
writer.begintag("varComponent", attrs)
writer.newline()
writer.begintag("location")
writer.newline()
for tag, v in self.location.items():
writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))])
writer.newline()
writer.endtag("location")
writer.newline()
writer.endtag("varComponent")
writer.newline()
def fromXML(self, name, attrs, content, ttFont):
self.glyphName = attrs["glyphName"]
if "flags" in attrs:
self.flags = safeEval(attrs["flags"])
for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
if attr_name not in attrs:
continue
v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits)
setattr(self.transform, attr_name, v)
for c in content:
if not isinstance(c, tuple):
continue
name, attrs, content = c
if name != "location":
continue
for c in content:
if not isinstance(c, tuple):
continue
name, attrs, content = c
assert name == "axis"
assert not content
self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14)
def getPointCount(self):
assert hasattr(self, "flags"), "VarComponent with variations must have flags"
count = 0
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
count += len(self.location)
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
count += 1
if self.flags & VarComponentFlags.HAVE_ROTATION:
count += 1
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
count += 1
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
count += 1
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
count += 1
return count
def getCoordinatesAndControls(self):
coords = []
controls = []
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
for tag, v in self.location.items():
controls.append(tag)
coords.append((fl2fi(v, 14), 0))
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
controls.append("translate")
coords.append((self.transform.translateX, self.transform.translateY))
if self.flags & VarComponentFlags.HAVE_ROTATION:
controls.append("rotation")
coords.append((fl2fi(self.transform.rotation / 180, 12), 0))
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
controls.append("scale")
coords.append(
(fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10))
)
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
controls.append("skew")
coords.append(
(
fl2fi(self.transform.skewX / -180, 12),
fl2fi(self.transform.skewY / 180, 12),
)
)
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
controls.append("tCenter")
coords.append((self.transform.tCenterX, self.transform.tCenterY))
return coords, controls
def setCoordinates(self, coords):
i = 0
if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
newLocation = {}
for tag in self.location:
newLocation[tag] = fi2fl(coords[i][0], 14)
i += 1
self.location = newLocation
self.transform = DecomposedTransform()
if self.flags & (
VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
):
self.transform.translateX, self.transform.translateY = coords[i]
i += 1
if self.flags & VarComponentFlags.HAVE_ROTATION:
self.transform.rotation = fi2fl(coords[i][0], 12) * 180
i += 1
if self.flags & (
VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
):
self.transform.scaleX, self.transform.scaleY = fi2fl(
coords[i][0], 10
), fi2fl(coords[i][1], 10)
i += 1
if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
self.transform.skewX, self.transform.skewY = (
fi2fl(coords[i][0], 12) * -180,
fi2fl(coords[i][1], 12) * 180,
)
i += 1
if self.flags & (
VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
):
self.transform.tCenterX, self.transform.tCenterY = coords[i]
i += 1
return coords[i:]
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
return self.__dict__ == other.__dict__
def __ne__(self, other):
result = self.__eq__(other)
return result if result is NotImplemented else not result
class GlyphCoordinates(object):
"""A list of glyph coordinates.

View File

@ -242,8 +242,14 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
@staticmethod
def getNumPoints_(glyph):
NUM_PHANTOM_POINTS = 4
if glyph.isComposite():
return len(glyph.components) + NUM_PHANTOM_POINTS
elif glyph.isVarComposite():
count = 0
for component in glyph.components:
count += component.getPointCount()
return count + NUM_PHANTOM_POINTS
else:
# Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS

View File

@ -85,7 +85,7 @@ class table__m_a_x_p(DefaultTable.DefaultTable):
nPoints, nContours = g.getMaxpValues()
maxPoints = max(maxPoints, nPoints)
maxContours = max(maxContours, nContours)
else:
elif g.isComposite():
nPoints, nContours, componentDepth = g.getCompositeMaxpValues(
glyfTable
)

View File

@ -2,9 +2,13 @@
from abc import ABC, abstractmethod
from collections.abc import Mapping
from contextlib import contextmanager
from copy import copy
from types import SimpleNamespace
from fontTools.misc.fixedTools import otRound
from fontTools.misc.loggingTools import deprecateFunction
from fontTools.misc.transform import Transform
from fontTools.pens.transformPen import TransformPen, TransformPointPen
class _TTGlyphSet(Mapping):
@ -15,10 +19,21 @@ class _TTGlyphSet(Mapping):
def __init__(self, font, location, glyphsMapping):
self.font = font
self.location = location
self.defaultLocationNormalized = (
{axis.axisTag: 0 for axis in self.font["fvar"].axes}
if "fvar" in self.font
else {}
)
self.location = location if location is not None else {}
self.rawLocation = {} # VarComponent-only location
self.originalLocation = location if location is not None else {}
self.depth = 0
self.locationStack = []
self.rawLocationStack = []
self.glyphsMapping = glyphsMapping
self.hMetrics = font["hmtx"].metrics
self.vMetrics = getattr(font.get("vmtx"), "metrics", None)
self.hvarTable = None
if location:
from fontTools.varLib.varStore import VarStoreInstancer
@ -29,6 +44,34 @@ class _TTGlyphSet(Mapping):
)
# TODO VVAR, VORG
@contextmanager
def pushLocation(self, location, reset: bool):
self.locationStack.append(self.location)
self.rawLocationStack.append(self.rawLocation)
if reset:
self.location = self.originalLocation.copy()
self.rawLocation = self.defaultLocationNormalized.copy()
else:
self.location = self.location.copy()
self.rawLocation = {}
self.location.update(location)
self.rawLocation.update(location)
try:
yield None
finally:
self.location = self.locationStack.pop()
self.rawLocation = self.rawLocationStack.pop()
@contextmanager
def pushDepth(self):
try:
depth = self.depth
self.depth += 1
yield depth
finally:
self.depth -= 1
def __contains__(self, glyphName):
return glyphName in self.glyphsMapping
@ -49,8 +92,7 @@ class _TTGlyphSetGlyf(_TTGlyphSet):
def __init__(self, font, location):
self.glyfTable = font["glyf"]
super().__init__(font, location, self.glyfTable)
if location:
self.gvarTable = font.get("gvar")
self.gvarTable = font.get("gvar")
def __getitem__(self, glyphName):
return _TTGlyphGlyf(self, glyphName)
@ -126,14 +168,59 @@ class _TTGlyphGlyf(_TTGlyph):
how that works.
"""
glyph, offset = self._getGlyphAndOffset()
glyph.draw(pen, self.glyphSet.glyfTable, offset)
with self.glyphSet.pushDepth() as depth:
if depth:
offset = 0 # Offset should only apply at top-level
if glyph.isVarComposite():
self._drawVarComposite(glyph, pen, False)
return
glyph.draw(pen, self.glyphSet.glyfTable, offset)
def drawPoints(self, pen):
"""Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
how that works.
"""
glyph, offset = self._getGlyphAndOffset()
glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
with self.glyphSet.pushDepth() as depth:
if depth:
offset = 0 # Offset should only apply at top-level
if glyph.isVarComposite():
self._drawVarComposite(glyph, pen, True)
return
glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
def _drawVarComposite(self, glyph, pen, isPointPen):
from fontTools.ttLib.tables._g_l_y_f import (
VarComponentFlags,
VAR_COMPONENT_TRANSFORM_MAPPING,
)
for comp in glyph.components:
with self.glyphSet.pushLocation(
comp.location, comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
):
try:
pen.addVarComponent(
comp.glyphName, comp.transform, self.glyphSet.rawLocation
)
except AttributeError:
t = comp.transform.toTransform()
if isPointPen:
tPen = TransformPointPen(pen, t)
self.glyphSet[comp.glyphName].drawPoints(tPen)
else:
tPen = TransformPen(pen, t)
self.glyphSet[comp.glyphName].draw(tPen)
def _getGlyphAndOffset(self):
if self.glyphSet.location and self.glyphSet.gvarTable is not None:
@ -210,6 +297,11 @@ def _setCoordinates(glyph, coord, glyfTable):
for p, comp in zip(coord, glyph.components):
if hasattr(comp, "x"):
comp.x, comp.y = p
elif glyph.isVarComposite():
glyph.components = [copy(comp) for comp in glyph.components] # Shallow copy
for comp in glyph.components:
coord = comp.setCoordinates(coord)
assert not coord
elif glyph.numberOfContours == 0:
assert len(coord) == 0
else:

View File

@ -266,7 +266,9 @@ class WOFF2Writer(SFNTWriter):
# See:
# https://github.com/google/woff2/pull/3
# https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html
# TODO(user): remove to match spec once browsers are on newer OTS
#
# 2023: We rely on this in _transformTables where we expect that
# "loca" comes after "glyf" table.
self.tables = OrderedDict(sorted(self.tables.items()))
self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets()
@ -292,8 +294,9 @@ class WOFF2Writer(SFNTWriter):
if self.sfntVersion == "OTTO":
return
for tag in ("maxp", "head", "loca", "glyf"):
self._decompileTable(tag)
for tag in ("maxp", "head", "loca", "glyf", "fvar"):
if tag in self.tables:
self._decompileTable(tag)
self.ttFont["glyf"].padding = padding
for tag in ("glyf", "loca"):
self._compileTable(tag)
@ -355,6 +358,10 @@ class WOFF2Writer(SFNTWriter):
if data is not None:
entry.transformed = True
if data is None:
if tag == "glyf":
# Currently we always sort table tags so
# 'loca' comes after 'glyf'.
transformedTables.discard("loca")
# pass-through the table data without transformation
data = entry.data
entry.transformed = False
@ -845,7 +852,10 @@ class WOFF2GlyfTable(getTableClass("glyf")):
self.overlapSimpleBitmap = array.array("B", [0] * ((self.numGlyphs + 7) >> 3))
for glyphID in range(self.numGlyphs):
self._encodeGlyph(glyphID)
try:
self._encodeGlyph(glyphID)
except NotImplementedError:
return None
hasOverlapSimpleBitmap = any(self.overlapSimpleBitmap)
self.bboxStream = self.bboxBitmap.tobytes() + self.bboxStream
@ -1009,6 +1019,8 @@ class WOFF2GlyfTable(getTableClass("glyf")):
return
elif glyph.isComposite():
self._encodeComponents(glyph)
elif glyph.isVarComposite():
raise NotImplementedError
else:
self._encodeCoordinates(glyph)
self._encodeOverlapSimpleFlag(glyph, glyphID)

View File

@ -492,6 +492,23 @@ def _instantiateGvarGlyph(
if defaultDeltas:
coordinates += _g_l_y_f.GlyphCoordinates(defaultDeltas)
glyph = glyf[glyphname]
if glyph.isVarComposite():
for component in glyph.components:
newLocation = {}
for tag, loc in component.location.items():
if tag not in axisLimits:
newLocation[tag] = loc
continue
if component.flags & _g_l_y_f.VarComponentFlags.AXES_HAVE_VARIATION:
raise NotImplementedError(
"Instancing accross VarComposite axes with variation is not supported."
)
limits = axisLimits[tag]
loc = normalizeValue(loc, limits)
newLocation[tag] = loc
component.location = newLocation
# _setCoordinates also sets the hmtx/vmtx advance widths and sidebearings from
# the four phantom points and glyph bounding boxes.
# We call it unconditionally even if a glyph has no variations or no deltas are
@ -541,7 +558,7 @@ def instantiateGvar(varfont, axisLimits, optimize=True):
glyf.glyphOrder,
key=lambda name: (
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
if glyf[name].isComposite()
if glyf[name].isComposite() or glyf[name].isVarComposite()
else 0,
name,
),

View File

@ -199,7 +199,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
gvar.variations.keys(),
key=lambda name: (
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
if glyf[name].isComposite()
if glyf[name].isComposite() or glyf[name].isVarComposite()
else 0,
name,
),

View File

@ -1,4 +1,10 @@
from fontTools.misc.transform import Transform, Identity, Offset, Scale
from fontTools.misc.transform import (
Transform,
Identity,
Offset,
Scale,
DecomposedTransform,
)
import math
import pytest
@ -108,3 +114,85 @@ class TransformTest(object):
assert Scale(1) == Transform(1, 0, 0, 1, 0, 0)
assert Scale(2) == Transform(2, 0, 0, 2, 0, 0)
assert Scale(1, 2) == Transform(1, 0, 0, 2, 0, 0)
def test_decompose(self):
t = Transform(2, 0, 0, 3, 5, 7)
d = t.toDecomposed()
assert d.scaleX == 2
assert d.scaleY == 3
assert d.translateX == 5
assert d.translateY == 7
def test_decompose(self):
t = Transform(-1, 0, 0, 1, 0, 0)
d = t.toDecomposed()
assert d.scaleX == -1
assert d.scaleY == 1
assert d.rotation == 0
t = Transform(1, 0, 0, -1, 0, 0)
d = t.toDecomposed()
assert d.scaleX == 1
assert d.scaleY == -1
assert d.rotation == 0
class DecomposedTransformTest(object):
def test_identity(self):
t = DecomposedTransform()
assert (
repr(t)
== "DecomposedTransform(translateX=0, translateY=0, rotation=0, scaleX=1, scaleY=1, skewX=0, skewY=0, tCenterX=0, tCenterY=0)"
)
assert t == DecomposedTransform(scaleX=1.0)
def test_scale(self):
t = DecomposedTransform(scaleX=2, scaleY=3)
assert t.scaleX == 2
assert t.scaleY == 3
def test_toTransform(self):
t = DecomposedTransform(scaleX=2, scaleY=3)
assert t.toTransform() == (2, 0, 0, 3, 0, 0)
@pytest.mark.parametrize(
"decomposed",
[
DecomposedTransform(scaleX=1, scaleY=0),
DecomposedTransform(scaleX=0, scaleY=1),
DecomposedTransform(scaleX=1, scaleY=0, rotation=30),
DecomposedTransform(scaleX=0, scaleY=1, rotation=30),
DecomposedTransform(scaleX=1, scaleY=1),
DecomposedTransform(scaleX=-1, scaleY=1),
DecomposedTransform(scaleX=1, scaleY=-1),
DecomposedTransform(scaleX=-1, scaleY=-1),
DecomposedTransform(rotation=90),
DecomposedTransform(rotation=-90),
DecomposedTransform(skewX=45),
DecomposedTransform(skewY=45),
DecomposedTransform(scaleX=-1, skewX=45),
DecomposedTransform(scaleX=-1, skewY=45),
DecomposedTransform(scaleY=-1, skewX=45),
DecomposedTransform(scaleY=-1, skewY=45),
DecomposedTransform(scaleX=-1, skewX=45, rotation=30),
DecomposedTransform(scaleX=-1, skewY=45, rotation=30),
DecomposedTransform(scaleY=-1, skewX=45, rotation=30),
DecomposedTransform(scaleY=-1, skewY=45, rotation=30),
DecomposedTransform(scaleX=-1, skewX=45, rotation=-30),
DecomposedTransform(scaleX=-1, skewY=45, rotation=-30),
DecomposedTransform(scaleY=-1, skewX=45, rotation=-30),
DecomposedTransform(scaleY=-1, skewY=45, rotation=-30),
DecomposedTransform(scaleX=-2, skewX=45, rotation=30),
DecomposedTransform(scaleX=-2, skewY=45, rotation=30),
DecomposedTransform(scaleY=-2, skewX=45, rotation=30),
DecomposedTransform(scaleY=-2, skewY=45, rotation=30),
DecomposedTransform(scaleX=-2, skewX=45, rotation=-30),
DecomposedTransform(scaleX=-2, skewY=45, rotation=-30),
DecomposedTransform(scaleY=-2, skewX=45, rotation=-30),
DecomposedTransform(scaleY=-2, skewY=45, rotation=-30),
],
)
def test_roundtrip(lst, decomposed):
assert decomposed.toTransform().toDecomposed().toTransform() == pytest.approx(
tuple(decomposed.toTransform())
), decomposed

View File

@ -32,9 +32,9 @@ class SubsetTest:
shutil.rmtree(cls.tempdir, ignore_errors=True)
@staticmethod
def getpath(testfile):
def getpath(*testfile):
path, _ = os.path.split(__file__)
return os.path.join(path, "data", testfile)
return os.path.join(path, "data", *testfile)
@classmethod
def temp_path(cls, suffix):
@ -425,6 +425,18 @@ class SubsetTest:
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_sbix.ttx"), ["sbix"])
def test_varComposite(self):
fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf")
origfont = TTFont(fontpath)
assert len(origfont.getGlyphOrder()) == 6
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
assert len(subsetfont.getGlyphOrder()) == 4
subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
assert len(subsetfont.getGlyphOrder()) == 5
def test_timing_publishes_parts(self):
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -65,6 +65,20 @@ class ScaleUpemTest(unittest.TestCase):
expected_ttx_path = self.get_path("I-512upem.ttx")
self.expect_ttx(font, expected_ttx_path, tables)
def test_scale_upem_varComposite(self):
font = TTFont(self.get_path("varc-ac00-ac01.ttf"))
tables = [table_tag for table_tag in font.keys() if table_tag != "head"]
scale_upem(font, 500)
expected_ttx_path = self.get_path("varc-ac00-ac01-500upem.ttx")
self.expect_ttx(font, expected_ttx_path, tables)
# Scale our other varComposite font as well; without checking the expected
font = TTFont(self.get_path("varc-6868.ttf"))
scale_upem(font, 500)
def test_scale_upem_otf(self):
# Just test that it doesn't crash

View File

@ -18,7 +18,7 @@ from fontTools.ttLib.tables._g_l_y_f import (
from fontTools.ttLib.tables import ttProgram
import sys
import array
from io import StringIO
from io import StringIO, BytesIO
import itertools
import pytest
import re
@ -664,6 +664,65 @@ class GlyphComponentTest:
assert (comp.firstPt, comp.secondPt) == (1, 2)
assert not hasattr(comp, "transform")
def test_trim_varComposite_glyph(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
glyf = font["glyf"]
glyf.glyphs["uniAC00"].trim()
glyf.glyphs["uniAC01"].trim()
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
glyf = font["glyf"]
glyf.glyphs["uni6868"].trim()
def test_varComposite_basic(self):
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-ac00-ac01.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
font_path = os.path.join(DATA_DIR, "..", "..", "data", "varc-6868.ttf")
font = TTFont(font_path)
tables = [
table_tag
for table_tag in font.keys()
if table_tag not in {"head", "maxp", "hhea", "name", "fvar"}
]
xml = StringIO()
font.saveXML(xml)
xml1 = StringIO()
font.saveXML(xml1, tables=tables)
xml.seek(0)
font = TTFont()
font.importXML(xml)
ttf = BytesIO()
font.save(ttf)
ttf.seek(0)
font = TTFont(ttf)
xml2 = StringIO()
font.saveXML(xml2, tables=tables)
assert xml1.getvalue() == xml2.getvalue()
if __name__ == "__main__":
import sys

View File

@ -1,6 +1,12 @@
from fontTools.ttLib import TTFont
from fontTools.ttLib import ttGlyphSet
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.recordingPen import (
RecordingPen,
RecordingPointPen,
DecomposingRecordingPen,
)
from fontTools.misc.roundTools import otRound
from fontTools.misc.transform import DecomposedTransform
import os
import pytest
@ -155,5 +161,386 @@ class TTGlyphSetTest(object):
glyph.draw(pen)
actual = pen.value
print(actual)
assert actual == expected, (location, actual, expected)
def test_glyphset_varComposite_components(self):
font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
glyphset = font.getGlyphSet()
pen = RecordingPen()
glyph = glyphset["uniAC00"]
glyph.draw(pen)
actual = pen.value
expected = [
(
"addVarComponent",
(
"glyph00003",
DecomposedTransform(460.0, 676.0, 0, 1, 1, 0, 0, 0, 0),
{
"0000": 0.84661865234375,
"0001": 0.98944091796875,
"0002": 0.47283935546875,
"0003": 0.446533203125,
},
),
),
(
"addVarComponent",
(
"glyph00004",
DecomposedTransform(932.0, 382.0, 0, 1, 1, 0, 0, 0, 0),
{
"0000": 0.93359375,
"0001": 0.916015625,
"0002": 0.523193359375,
"0003": 0.32806396484375,
"0004": 0.85089111328125,
},
),
),
]
assert actual == expected, (actual, expected)
def test_glyphset_varComposite1(self):
font = TTFont(self.getpath("varc-ac00-ac01.ttf"))
glyphset = font.getGlyphSet(location={"wght": 600})
pen = DecomposingRecordingPen(glyphset)
glyph = glyphset["uniAC00"]
glyph.draw(pen)
actual = pen.value
expected = [
("moveTo", ((432, 678),)),
("lineTo", ((432, 620),)),
(
"qCurveTo",
(
(419, 620),
(374, 621),
(324, 619),
(275, 618),
(237, 617),
(228, 616),
),
),
("qCurveTo", ((218, 616), (188, 612), (160, 605), (149, 601))),
("qCurveTo", ((127, 611), (83, 639), (67, 654))),
("qCurveTo", ((64, 657), (63, 662), (64, 666))),
("lineTo", ((72, 678),)),
("qCurveTo", ((93, 674), (144, 672), (164, 672))),
(
"qCurveTo",
(
(173, 672),
(213, 672),
(266, 673),
(323, 674),
(377, 675),
(421, 678),
(432, 678),
),
),
("closePath", ()),
("moveTo", ((525, 619),)),
("lineTo", ((412, 620),)),
("lineTo", ((429, 678),)),
("lineTo", ((466, 697),)),
("qCurveTo", ((470, 698), (482, 698), (486, 697))),
("qCurveTo", ((494, 693), (515, 682), (536, 670), (541, 667))),
("qCurveTo", ((545, 663), (545, 656), (543, 652))),
("lineTo", ((525, 619),)),
("closePath", ()),
("moveTo", ((63, 118),)),
("lineTo", ((47, 135),)),
("qCurveTo", ((42, 141), (48, 146))),
("qCurveTo", ((135, 213), (278, 373), (383, 541), (412, 620))),
("lineTo", ((471, 642),)),
("lineTo", ((525, 619),)),
("qCurveTo", ((496, 529), (365, 342), (183, 179), (75, 121))),
("qCurveTo", ((72, 119), (65, 118), (63, 118))),
("closePath", ()),
("moveTo", ((925, 372),)),
("lineTo", ((739, 368),)),
("lineTo", ((739, 427),)),
("lineTo", ((822, 430),)),
("lineTo", ((854, 451),)),
("qCurveTo", ((878, 453), (930, 449), (944, 445))),
("qCurveTo", ((961, 441), (962, 426))),
("qCurveTo", ((964, 411), (956, 386), (951, 381))),
("qCurveTo", ((947, 376), (931, 372), (925, 372))),
("closePath", ()),
("moveTo", ((729, -113),)),
("lineTo", ((674, -113),)),
("qCurveTo", ((671, -98), (669, -42), (666, 22), (665, 83), (665, 102))),
("lineTo", ((665, 763),)),
("qCurveTo", ((654, 780), (608, 810), (582, 820))),
("lineTo", ((593, 850),)),
("qCurveTo", ((594, 852), (599, 856), (607, 856))),
("qCurveTo", ((628, 855), (684, 846), (736, 834), (752, 827))),
("qCurveTo", ((766, 818), (766, 802))),
("lineTo", ((762, 745),)),
("lineTo", ((762, 134),)),
("qCurveTo", ((762, 107), (757, 43), (749, -25), (737, -87), (729, -113))),
("closePath", ()),
]
actual = [
(op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args))
for op, args in actual
]
assert actual == expected, (actual, expected)
# Test that drawing twice works, we accidentally don't change the component
pen = DecomposingRecordingPen(glyphset)
glyph.draw(pen)
actual = pen.value
actual = [
(op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args))
for op, args in actual
]
assert actual == expected, (actual, expected)
pen = RecordingPointPen()
glyph.drawPoints(pen)
assert pen.value
def test_glyphset_varComposite2(self):
# This test font has axis variations
font = TTFont(self.getpath("varc-6868.ttf"))
glyphset = font.getGlyphSet(location={"wght": 600})
pen = DecomposingRecordingPen(glyphset)
glyph = glyphset["uni6868"]
glyph.draw(pen)
actual = pen.value
expected = [
("moveTo", ((460, 565),)),
(
"qCurveTo",
(
(482, 577),
(526, 603),
(568, 632),
(607, 663),
(644, 698),
(678, 735),
(708, 775),
(721, 796),
),
),
("lineTo", ((632, 835),)),
(
"qCurveTo",
(
(621, 817),
(595, 784),
(566, 753),
(534, 724),
(499, 698),
(462, 675),
(423, 653),
(403, 644),
),
),
("closePath", ()),
("moveTo", ((616, 765),)),
("lineTo", ((590, 682),)),
("lineTo", ((830, 682),)),
("lineTo", ((833, 682),)),
("lineTo", ((828, 693),)),
(
"qCurveTo",
(
(817, 671),
(775, 620),
(709, 571),
(615, 525),
(492, 490),
(413, 480),
),
),
("lineTo", ((454, 386),)),
(
"qCurveTo",
(
(544, 403),
(687, 455),
(798, 519),
(877, 590),
(926, 655),
(937, 684),
),
),
("lineTo", ((937, 765),)),
("closePath", ()),
("moveTo", ((723, 555),)),
(
"qCurveTo",
(
(713, 563),
(693, 579),
(672, 595),
(651, 610),
(629, 625),
(606, 638),
(583, 651),
(572, 657),
),
),
("lineTo", ((514, 590),)),
(
"qCurveTo",
(
(525, 584),
(547, 572),
(568, 559),
(589, 545),
(609, 531),
(629, 516),
(648, 500),
(657, 492),
),
),
("closePath", ()),
("moveTo", ((387, 375),)),
("lineTo", ((387, 830),)),
("lineTo", ((289, 830),)),
("lineTo", ((289, 375),)),
("closePath", ()),
("moveTo", ((96, 383),)),
(
"qCurveTo",
(
(116, 390),
(156, 408),
(194, 427),
(231, 449),
(268, 472),
(302, 497),
(335, 525),
(351, 539),
),
),
("lineTo", ((307, 610),)),
(
"qCurveTo",
(
(291, 597),
(257, 572),
(221, 549),
(185, 528),
(147, 509),
(108, 492),
(69, 476),
(48, 469),
),
),
("closePath", ()),
("moveTo", ((290, 653),)),
(
"qCurveTo",
(
(281, 664),
(261, 687),
(240, 708),
(219, 729),
(196, 749),
(173, 768),
(148, 786),
(136, 794),
),
),
("lineTo", ((69, 727),)),
(
"qCurveTo",
(
(81, 719),
(105, 702),
(129, 684),
(151, 665),
(173, 645),
(193, 625),
(213, 604),
(222, 593),
),
),
("closePath", ()),
("moveTo", ((913, -57),)),
("lineTo", ((953, 30),)),
(
"qCurveTo",
(
(919, 41),
(854, 67),
(790, 98),
(729, 134),
(671, 173),
(616, 217),
(564, 264),
(540, 290),
),
),
("lineTo", ((522, 286),)),
("qCurveTo", ((511, 267), (498, 235), (493, 213), (492, 206))),
("lineTo", ((515, 209),)),
("qCurveTo", ((569, 146), (695, 44), (835, -32), (913, -57))),
("closePath", ()),
("moveTo", ((474, 274),)),
("lineTo", ((452, 284),)),
(
"qCurveTo",
(
(428, 260),
(377, 214),
(323, 172),
(266, 135),
(206, 101),
(144, 71),
(80, 46),
(47, 36),
),
),
("lineTo", ((89, -53),)),
("qCurveTo", ((163, -29), (299, 46), (423, 142), (476, 201))),
("lineTo", ((498, 196),)),
("qCurveTo", ((498, 203), (494, 225), (482, 255), (474, 274))),
("closePath", ()),
("moveTo", ((450, 250),)),
("lineTo", ((550, 250),)),
("lineTo", ((550, 379),)),
("lineTo", ((450, 379),)),
("closePath", ()),
("moveTo", ((68, 215),)),
("lineTo", ((932, 215),)),
("lineTo", ((932, 305),)),
("lineTo", ((68, 305),)),
("closePath", ()),
("moveTo", ((450, -71),)),
("lineTo", ((550, -71),)),
("lineTo", ((550, -71),)),
("lineTo", ((550, 267),)),
("lineTo", ((450, 267),)),
("lineTo", ((450, -71),)),
("closePath", ()),
]
actual = [
(op, tuple((otRound(pt[0]), otRound(pt[1])) for pt in args))
for op, args in actual
]
assert actual == expected, (actual, expected)
pen = RecordingPointPen()
glyph.drawPoints(pen)
assert pen.value

View File

@ -1491,6 +1491,20 @@ class UShort255Test(unittest.TestCase):
)
class VarCompositeTest(unittest.TestCase):
def test_var_composite(self):
input_path = os.path.join(data_dir, "varc-ac00-ac01.ttf")
ttf = ttLib.TTFont(input_path)
ttf.flavor = "woff2"
out = BytesIO()
ttf.save(out)
ttf = ttLib.TTFont(out)
ttf.flavor = None
out = BytesIO()
ttf.save(out)
if __name__ == "__main__":
import sys

View File

@ -1563,6 +1563,26 @@ class InstantiateVariableFontTest(object):
assert _dump_ttx(instance) == expected
def test_varComposite(self):
input_path = os.path.join(
TESTDATA, "..", "..", "..", "ttLib", "data", "varc-ac00-ac01.ttf"
)
varfont = ttLib.TTFont(input_path)
location = {"wght": 600}
instance = instancer.instantiateVariableFont(
varfont,
location,
)
location = {"0000": 0.5}
instance = instancer.instantiateVariableFont(
varfont,
location,
)
def _conditionSetAsDict(conditionSet, axisOrder):
result = {}

View File

@ -35,9 +35,9 @@ class InterpolatableTest(unittest.TestCase):
shutil.rmtree(self.tempdir)
@staticmethod
def get_test_input(test_file_or_folder):
def get_test_input(*test_file_or_folder):
path, _ = os.path.split(__file__)
return os.path.join(path, "data", test_file_or_folder)
return os.path.join(path, "data", *test_file_or_folder)
@staticmethod
def get_file_list(folder, suffix, prefix=""):
@ -93,6 +93,15 @@ class InterpolatableTest(unittest.TestCase):
otf_paths = self.get_file_list(self.tempdir, suffix)
self.assertIsNone(interpolatable_main(otf_paths))
def test_interpolatable_varComposite(self):
input_path = self.get_test_input(
"..", "..", "ttLib", "data", "varc-ac00-ac01.ttf"
)
# This particular test font which was generated by machine-learning
# exhibits an "error" in one of the masters; it's a false-positive.
# Just make sure the code runs.
interpolatable_main((input_path,))
if __name__ == "__main__":
sys.exit(unittest.main())