diff options
Diffstat (limited to 'Lib/fontTools/pens/ttGlyphPen.py')
-rw-r--r-- | Lib/fontTools/pens/ttGlyphPen.py | 283 |
1 files changed, 197 insertions, 86 deletions
diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py index e7841efc..5087e158 100644 --- a/Lib/fontTools/pens/ttGlyphPen.py +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -1,30 +1,31 @@ from array import array -from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat +from typing import Any, Dict, Optional, Tuple +from fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat +from fontTools.misc.loggingTools import LogMixin +from fontTools.pens.pointPen import AbstractPointPen from fontTools.misc.roundTools import otRound -from fontTools.pens.basePen import LoggingPen -from fontTools.pens.transformPen import TransformPen +from fontTools.pens.basePen import LoggingPen, PenError +from fontTools.pens.transformPen import TransformPen, TransformPointPen 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"] +__all__ = ["TTGlyphPen", "TTGlyphPointPen"] -class TTGlyphPen(LoggingPen): - """Pen used for drawing to a TrueType glyph. - - This pen can be used to construct or modify glyphs in a TrueType format - font. After using the pen to draw, use the ``.glyph()`` method to retrieve - a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. - """ - - def __init__(self, glyphSet, handleOverflowingTransforms=True): - """Construct a new pen. +class _TTGlyphBasePen: + def __init__( + self, + glyphSet: Optional[Dict[str, Any]], + handleOverflowingTransforms: bool = True, + ) -> None: + """ + Construct a new pen. Args: - glyphSet (ttLib._TTGlyphSet): A glyphset object, used to resolve components. + glyphSet (Dict[str, Any]): A glyphset object, used to resolve components. handleOverflowingTransforms (bool): See below. If ``handleOverflowingTransforms`` is True, the components' transform values @@ -42,41 +43,152 @@ class TTGlyphPen(LoggingPen): 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. + + If both contours and components are present in a glyph, the components + are decomposed. """ self.glyphSet = glyphSet self.handleOverflowingTransforms = handleOverflowingTransforms self.init() - def init(self): + def _decompose( + self, + glyphName: str, + transformation: Tuple[float, float, float, float, float, float], + ): + tpen = self.transformPen(self, transformation) + getattr(self.glyphSet[glyphName], self.drawMethod)(tpen) + + def _isClosed(self): + """ + Check if the current path is closed. + """ + raise NotImplementedError + + def init(self) -> None: self.points = [] self.endPts = [] self.types = [] self.components = [] - def _addPoint(self, pt, onCurve): + def addComponent( + self, + baseGlyphName: str, + transformation: Tuple[float, float, float, float, float, float], + identifier: Optional[str] = None, + **kwargs: Any, + ) -> None: + """ + Add a sub glyph. + """ + self.components.append((baseGlyphName, 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(f"skipped non-existing component '{glyphName}'") + continue + if self.points or (self.handleOverflowingTransforms and overflowing): + # can't have both coordinates and components, so decompose + self._decompose(glyphName, transformation) + 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: int = 0x4) -> Glyph: + """ + Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. + """ + if not self._isClosed(): + raise PenError("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: + # If both components and contours were present, they have by now + # been decomposed by _buildComponents. + glyph.components = components + glyph.numberOfContours = -1 + else: + glyph.numberOfContours = len(glyph.endPtsOfContours) + glyph.program = ttProgram.Program() + glyph.program.fromBytecode(b"") + + return glyph + + +class TTGlyphPen(_TTGlyphBasePen, LoggingPen): + """ + Pen used for drawing to a TrueType glyph. + + This pen can be used to construct or modify glyphs in a TrueType format + font. After using the pen to draw, use the ``.glyph()`` method to retrieve + a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. + """ + + drawMethod = "draw" + transformPen = TransformPen + + def _addPoint(self, pt: Tuple[float, float], onCurve: int) -> None: self.points.append(pt) self.types.append(onCurve) - def _popPoint(self): + def _popPoint(self) -> None: 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 _isClosed(self) -> bool: + return (not self.points) or ( + self.endPts and self.endPts[-1] == len(self.points) - 1 + ) - def lineTo(self, pt): + def lineTo(self, pt: Tuple[float, float]) -> None: self._addPoint(pt, 1) - def moveTo(self, pt): - assert self._isClosed(), '"move"-type point must begin a new contour.' + def moveTo(self, pt: Tuple[float, float]) -> None: + if not self._isClosed(): + raise PenError('"move"-type point must begin a new contour.') self._addPoint(pt, 1) - def curveTo(self, *points): + def curveTo(self, *points) -> None: raise NotImplementedError - def qCurveTo(self, *points): + def qCurveTo(self, *points) -> None: assert len(points) >= 1 for pt in points[:-1]: self._addPoint(pt, 0) @@ -85,7 +197,7 @@ class TTGlyphPen(LoggingPen): if points[-1] is not None: self._addPoint(points[-1], 1) - def closePath(self): + def closePath(self) -> None: endPt = len(self.points) - 1 # ignore anchors (one-point paths) @@ -103,72 +215,71 @@ class TTGlyphPen(LoggingPen): self.endPts.append(endPt) - def endPath(self): + def endPath(self) -> None: # 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 +class TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen): + """ + Point pen used for drawing to a TrueType glyph. - 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 + This pen can be used to construct or modify glyphs in a TrueType format + font. After using the pen to draw, use the ``.glyph()`` method to retrieve + a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. + """ - def glyph(self, componentFlags=0x4): - """Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.""" - assert self._isClosed(), "Didn't close last contour." + drawMethod = "drawPoints" + transformPen = TransformPointPen - components = self._buildComponents(componentFlags) + def init(self) -> None: + super().init() + self._currentContourStartIndex = None - glyph = Glyph() - glyph.coordinates = GlyphCoordinates(self.points) - glyph.coordinates.toInt() - glyph.endPtsOfContours = self.endPts - glyph.flags = array("B", self.types) - self.init() + def _isClosed(self) -> bool: + return self._currentContourStartIndex is None - if components: - glyph.components = components - glyph.numberOfContours = -1 + def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None: + """ + Start a new sub path. + """ + if not self._isClosed(): + raise PenError("Didn't close previous contour.") + self._currentContourStartIndex = len(self.points) + + def endPath(self) -> None: + """ + End the current sub path. + """ + # TrueType contours are always "closed" + if self._isClosed(): + raise PenError("Contour is already closed.") + if self._currentContourStartIndex == len(self.points): + raise PenError("Tried to end an empty contour.") + self.endPts.append(len(self.points) - 1) + self._currentContourStartIndex = None + + def addPoint( + self, + pt: Tuple[float, float], + segmentType: Optional[str] = None, + smooth: bool = False, + name: Optional[str] = None, + identifier: Optional[str] = None, + **kwargs: Any, + ) -> None: + """ + Add a point to the current sub path. + """ + if self._isClosed(): + raise PenError("Can't add a point to a closed contour.") + if segmentType is None: + self.types.append(0) # offcurve + elif segmentType in ("qcurve", "line", "move"): + self.types.append(1) # oncurve + elif segmentType == "curve": + raise NotImplementedError("cubic curves are not supported") else: - glyph.numberOfContours = len(glyph.endPtsOfContours) - glyph.program = ttProgram.Program() - glyph.program.fromBytecode(b"") + raise AssertionError(segmentType) - return glyph + self.points.append(pt) |