Fixes https://github.com/googlefonts/fontmake/issues/558 When drawing a composite glyph with a scaled component using the TTGlyphPen, the bounding box coordinates may change depending on whether one computes them *before* compiling or *after* decompiling. Before compiling, component.transform holds double precision floats, but after compiling and decompiling, these are necessarily clamped to F2Dot14 fixed precision. The TTGlyphPen needs to quantize transform floats to F2Dot14 so that the values don't change after compilation. Without this change, it may happen that round-tripping fonts through ttx (which by default recalcBBoxes) will produce different bounding boxes for composite glyphs that have scaled or transformed components.
159 lines
5.8 KiB
Python
159 lines
5.8 KiB
Python
from fontTools.misc.py23 import *
|
|
from array import array
|
|
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat
|
|
from fontTools.pens.basePen import LoggingPen
|
|
from fontTools.pens.transformPen import TransformPen
|
|
from fontTools.ttLib.tables import ttProgram
|
|
from fontTools.ttLib.tables._g_l_y_f import Glyph
|
|
from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
|
|
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
|
|
|
|
|
__all__ = ["TTGlyphPen"]
|
|
|
|
|
|
class TTGlyphPen(LoggingPen):
|
|
"""Pen used for drawing to a TrueType glyph.
|
|
|
|
If `handleOverflowingTransforms` is True, the components' transform values
|
|
are checked that they don't overflow the limits of a F2Dot14 number:
|
|
-2.0 <= v < +2.0. If any transform value exceeds these, the composite
|
|
glyph is decomposed.
|
|
An exception to this rule is done for values that are very close to +2.0
|
|
(both for consistency with the -2.0 case, and for the relative frequency
|
|
these occur in real fonts). When almost +2.0 values occur (and all other
|
|
values are within the range -2.0 <= x <= +2.0), they are clamped to the
|
|
maximum positive value that can still be encoded as an F2Dot14: i.e.
|
|
1.99993896484375.
|
|
If False, no check is done and all components are translated unmodified
|
|
into the glyf table, followed by an inevitable `struct.error` once an
|
|
attempt is made to compile them.
|
|
"""
|
|
|
|
def __init__(self, glyphSet, handleOverflowingTransforms=True):
|
|
self.glyphSet = glyphSet
|
|
self.handleOverflowingTransforms = handleOverflowingTransforms
|
|
self.init()
|
|
|
|
def init(self):
|
|
self.points = []
|
|
self.endPts = []
|
|
self.types = []
|
|
self.components = []
|
|
|
|
def _addPoint(self, pt, onCurve):
|
|
self.points.append(pt)
|
|
self.types.append(onCurve)
|
|
|
|
def _popPoint(self):
|
|
self.points.pop()
|
|
self.types.pop()
|
|
|
|
def _isClosed(self):
|
|
return (
|
|
(not self.points) or
|
|
(self.endPts and self.endPts[-1] == len(self.points) - 1))
|
|
|
|
def lineTo(self, pt):
|
|
self._addPoint(pt, 1)
|
|
|
|
def moveTo(self, pt):
|
|
assert self._isClosed(), '"move"-type point must begin a new contour.'
|
|
self._addPoint(pt, 1)
|
|
|
|
def qCurveTo(self, *points):
|
|
assert len(points) >= 1
|
|
for pt in points[:-1]:
|
|
self._addPoint(pt, 0)
|
|
|
|
# last point is None if there are no on-curve points
|
|
if points[-1] is not None:
|
|
self._addPoint(points[-1], 1)
|
|
|
|
def closePath(self):
|
|
endPt = len(self.points) - 1
|
|
|
|
# ignore anchors (one-point paths)
|
|
if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
|
|
self._popPoint()
|
|
return
|
|
|
|
# if first and last point on this path are the same, remove last
|
|
startPt = 0
|
|
if self.endPts:
|
|
startPt = self.endPts[-1] + 1
|
|
if self.points[startPt] == self.points[endPt]:
|
|
self._popPoint()
|
|
endPt -= 1
|
|
|
|
self.endPts.append(endPt)
|
|
|
|
def endPath(self):
|
|
# TrueType contours are always "closed"
|
|
self.closePath()
|
|
|
|
def addComponent(self, glyphName, transformation):
|
|
self.components.append((glyphName, transformation))
|
|
|
|
def _buildComponents(self, componentFlags):
|
|
if self.handleOverflowingTransforms:
|
|
# we can't encode transform values > 2 or < -2 in F2Dot14,
|
|
# so we must decompose the glyph if any transform exceeds these
|
|
overflowing = any(s > 2 or s < -2
|
|
for (glyphName, transformation) in self.components
|
|
for s in transformation[:4])
|
|
components = []
|
|
for glyphName, transformation in self.components:
|
|
if glyphName not in self.glyphSet:
|
|
self.log.warning(
|
|
"skipped non-existing component '%s'", glyphName
|
|
)
|
|
continue
|
|
if (self.points or
|
|
(self.handleOverflowingTransforms and overflowing)):
|
|
# can't have both coordinates and components, so decompose
|
|
tpen = TransformPen(self, transformation)
|
|
self.glyphSet[glyphName].draw(tpen)
|
|
continue
|
|
|
|
component = GlyphComponent()
|
|
component.glyphName = glyphName
|
|
component.x, component.y = (otRound(v) for v in transformation[4:])
|
|
# quantize floats to F2Dot14 so we get same values as when decompiled
|
|
# from a binary glyf table
|
|
transformation = tuple(
|
|
floatToFixedToFloat(v, 14) for v in transformation[:4]
|
|
)
|
|
if transformation != (1, 0, 0, 1):
|
|
if (self.handleOverflowingTransforms and
|
|
any(MAX_F2DOT14 < s <= 2 for s in transformation)):
|
|
# clamp values ~= +2.0 so we can keep the component
|
|
transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2
|
|
else s for s in transformation)
|
|
component.transform = (transformation[:2], transformation[2:])
|
|
component.flags = componentFlags
|
|
components.append(component)
|
|
return components
|
|
|
|
def glyph(self, componentFlags=0x4):
|
|
assert self._isClosed(), "Didn't close last contour."
|
|
|
|
components = self._buildComponents(componentFlags)
|
|
|
|
glyph = Glyph()
|
|
glyph.coordinates = GlyphCoordinates(self.points)
|
|
glyph.coordinates.toInt()
|
|
glyph.endPtsOfContours = self.endPts
|
|
glyph.flags = array("B", self.types)
|
|
self.init()
|
|
|
|
if components:
|
|
glyph.components = components
|
|
glyph.numberOfContours = -1
|
|
else:
|
|
glyph.numberOfContours = len(glyph.endPtsOfContours)
|
|
glyph.program = ttProgram.Program()
|
|
glyph.program.fromBytecode(b"")
|
|
|
|
return glyph
|