commit
58bc16e58f
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
BIN
Tests/ttLib/data/varc-6868.ttf
Normal file
BIN
Tests/ttLib/data/varc-6868.ttf
Normal file
Binary file not shown.
2054
Tests/ttLib/data/varc-ac00-ac01-500upem.ttx
Normal file
2054
Tests/ttLib/data/varc-ac00-ac01-500upem.ttx
Normal file
File diff suppressed because it is too large
Load Diff
BIN
Tests/ttLib/data/varc-ac00-ac01.ttf
Normal file
BIN
Tests/ttLib/data/varc-ac00-ac01.ttf
Normal file
Binary file not shown.
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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())
|
||||
|
Loading…
x
Reference in New Issue
Block a user