fonttools/Lib/fontTools/pens/ttGlyphPen.py
Cosimo Lupo 79449bc2d4
ttGlyphPen: always skip non-existing components
It doesn't make sense to add components that reference non-existing base glyphs
(ie. glyphs not in the input glyphSet, hence missing from the generated glyf table).
Even if we let them in here, it will fail immediately as soon as we attempt to
compile this glyf table containing 'dangling' component references.
Better to warn and skip.
2018-06-11 19:25:15 +01:00

159 lines
5.7 KiB
Python

from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
from array import array
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"]
# the max value that can still fit in an F2Dot14:
# 1.99993896484375
MAX_F2DOT14 = 0x7FFF / (1 << 14)
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 = transformation[4:]
transformation = 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.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