1*e1fe3e4aSElliott Hughesfrom array import array 2*e1fe3e4aSElliott Hughesfrom typing import Any, Callable, Dict, Optional, Tuple 3*e1fe3e4aSElliott Hughesfrom fontTools.misc.fixedTools import MAX_F2DOT14, floatToFixedToFloat 4*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import LogMixin 5*e1fe3e4aSElliott Hughesfrom fontTools.pens.pointPen import AbstractPointPen 6*e1fe3e4aSElliott Hughesfrom fontTools.misc.roundTools import otRound 7*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import LoggingPen, PenError 8*e1fe3e4aSElliott Hughesfrom fontTools.pens.transformPen import TransformPen, TransformPointPen 9*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import ttProgram 10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import flagOnCurve, flagCubic 11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import Glyph 12*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import GlyphComponent 13*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates 14*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import dropImpliedOnCurvePoints 15*e1fe3e4aSElliott Hughesimport math 16*e1fe3e4aSElliott Hughes 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughes__all__ = ["TTGlyphPen", "TTGlyphPointPen"] 19*e1fe3e4aSElliott Hughes 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughesclass _TTGlyphBasePen: 22*e1fe3e4aSElliott Hughes def __init__( 23*e1fe3e4aSElliott Hughes self, 24*e1fe3e4aSElliott Hughes glyphSet: Optional[Dict[str, Any]], 25*e1fe3e4aSElliott Hughes handleOverflowingTransforms: bool = True, 26*e1fe3e4aSElliott Hughes ) -> None: 27*e1fe3e4aSElliott Hughes """ 28*e1fe3e4aSElliott Hughes Construct a new pen. 29*e1fe3e4aSElliott Hughes 30*e1fe3e4aSElliott Hughes Args: 31*e1fe3e4aSElliott Hughes glyphSet (Dict[str, Any]): A glyphset object, used to resolve components. 32*e1fe3e4aSElliott Hughes handleOverflowingTransforms (bool): See below. 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott Hughes If ``handleOverflowingTransforms`` is True, the components' transform values 35*e1fe3e4aSElliott Hughes are checked that they don't overflow the limits of a F2Dot14 number: 36*e1fe3e4aSElliott Hughes -2.0 <= v < +2.0. If any transform value exceeds these, the composite 37*e1fe3e4aSElliott Hughes glyph is decomposed. 38*e1fe3e4aSElliott Hughes 39*e1fe3e4aSElliott Hughes An exception to this rule is done for values that are very close to +2.0 40*e1fe3e4aSElliott Hughes (both for consistency with the -2.0 case, and for the relative frequency 41*e1fe3e4aSElliott Hughes these occur in real fonts). When almost +2.0 values occur (and all other 42*e1fe3e4aSElliott Hughes values are within the range -2.0 <= x <= +2.0), they are clamped to the 43*e1fe3e4aSElliott Hughes maximum positive value that can still be encoded as an F2Dot14: i.e. 44*e1fe3e4aSElliott Hughes 1.99993896484375. 45*e1fe3e4aSElliott Hughes 46*e1fe3e4aSElliott Hughes If False, no check is done and all components are translated unmodified 47*e1fe3e4aSElliott Hughes into the glyf table, followed by an inevitable ``struct.error`` once an 48*e1fe3e4aSElliott Hughes attempt is made to compile them. 49*e1fe3e4aSElliott Hughes 50*e1fe3e4aSElliott Hughes If both contours and components are present in a glyph, the components 51*e1fe3e4aSElliott Hughes are decomposed. 52*e1fe3e4aSElliott Hughes """ 53*e1fe3e4aSElliott Hughes self.glyphSet = glyphSet 54*e1fe3e4aSElliott Hughes self.handleOverflowingTransforms = handleOverflowingTransforms 55*e1fe3e4aSElliott Hughes self.init() 56*e1fe3e4aSElliott Hughes 57*e1fe3e4aSElliott Hughes def _decompose( 58*e1fe3e4aSElliott Hughes self, 59*e1fe3e4aSElliott Hughes glyphName: str, 60*e1fe3e4aSElliott Hughes transformation: Tuple[float, float, float, float, float, float], 61*e1fe3e4aSElliott Hughes ): 62*e1fe3e4aSElliott Hughes tpen = self.transformPen(self, transformation) 63*e1fe3e4aSElliott Hughes getattr(self.glyphSet[glyphName], self.drawMethod)(tpen) 64*e1fe3e4aSElliott Hughes 65*e1fe3e4aSElliott Hughes def _isClosed(self): 66*e1fe3e4aSElliott Hughes """ 67*e1fe3e4aSElliott Hughes Check if the current path is closed. 68*e1fe3e4aSElliott Hughes """ 69*e1fe3e4aSElliott Hughes raise NotImplementedError 70*e1fe3e4aSElliott Hughes 71*e1fe3e4aSElliott Hughes def init(self) -> None: 72*e1fe3e4aSElliott Hughes self.points = [] 73*e1fe3e4aSElliott Hughes self.endPts = [] 74*e1fe3e4aSElliott Hughes self.types = [] 75*e1fe3e4aSElliott Hughes self.components = [] 76*e1fe3e4aSElliott Hughes 77*e1fe3e4aSElliott Hughes def addComponent( 78*e1fe3e4aSElliott Hughes self, 79*e1fe3e4aSElliott Hughes baseGlyphName: str, 80*e1fe3e4aSElliott Hughes transformation: Tuple[float, float, float, float, float, float], 81*e1fe3e4aSElliott Hughes identifier: Optional[str] = None, 82*e1fe3e4aSElliott Hughes **kwargs: Any, 83*e1fe3e4aSElliott Hughes ) -> None: 84*e1fe3e4aSElliott Hughes """ 85*e1fe3e4aSElliott Hughes Add a sub glyph. 86*e1fe3e4aSElliott Hughes """ 87*e1fe3e4aSElliott Hughes self.components.append((baseGlyphName, transformation)) 88*e1fe3e4aSElliott Hughes 89*e1fe3e4aSElliott Hughes def _buildComponents(self, componentFlags): 90*e1fe3e4aSElliott Hughes if self.handleOverflowingTransforms: 91*e1fe3e4aSElliott Hughes # we can't encode transform values > 2 or < -2 in F2Dot14, 92*e1fe3e4aSElliott Hughes # so we must decompose the glyph if any transform exceeds these 93*e1fe3e4aSElliott Hughes overflowing = any( 94*e1fe3e4aSElliott Hughes s > 2 or s < -2 95*e1fe3e4aSElliott Hughes for (glyphName, transformation) in self.components 96*e1fe3e4aSElliott Hughes for s in transformation[:4] 97*e1fe3e4aSElliott Hughes ) 98*e1fe3e4aSElliott Hughes components = [] 99*e1fe3e4aSElliott Hughes for glyphName, transformation in self.components: 100*e1fe3e4aSElliott Hughes if glyphName not in self.glyphSet: 101*e1fe3e4aSElliott Hughes self.log.warning(f"skipped non-existing component '{glyphName}'") 102*e1fe3e4aSElliott Hughes continue 103*e1fe3e4aSElliott Hughes if self.points or (self.handleOverflowingTransforms and overflowing): 104*e1fe3e4aSElliott Hughes # can't have both coordinates and components, so decompose 105*e1fe3e4aSElliott Hughes self._decompose(glyphName, transformation) 106*e1fe3e4aSElliott Hughes continue 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughes component = GlyphComponent() 109*e1fe3e4aSElliott Hughes component.glyphName = glyphName 110*e1fe3e4aSElliott Hughes component.x, component.y = (otRound(v) for v in transformation[4:]) 111*e1fe3e4aSElliott Hughes # quantize floats to F2Dot14 so we get same values as when decompiled 112*e1fe3e4aSElliott Hughes # from a binary glyf table 113*e1fe3e4aSElliott Hughes transformation = tuple( 114*e1fe3e4aSElliott Hughes floatToFixedToFloat(v, 14) for v in transformation[:4] 115*e1fe3e4aSElliott Hughes ) 116*e1fe3e4aSElliott Hughes if transformation != (1, 0, 0, 1): 117*e1fe3e4aSElliott Hughes if self.handleOverflowingTransforms and any( 118*e1fe3e4aSElliott Hughes MAX_F2DOT14 < s <= 2 for s in transformation 119*e1fe3e4aSElliott Hughes ): 120*e1fe3e4aSElliott Hughes # clamp values ~= +2.0 so we can keep the component 121*e1fe3e4aSElliott Hughes transformation = tuple( 122*e1fe3e4aSElliott Hughes MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 else s 123*e1fe3e4aSElliott Hughes for s in transformation 124*e1fe3e4aSElliott Hughes ) 125*e1fe3e4aSElliott Hughes component.transform = (transformation[:2], transformation[2:]) 126*e1fe3e4aSElliott Hughes component.flags = componentFlags 127*e1fe3e4aSElliott Hughes components.append(component) 128*e1fe3e4aSElliott Hughes return components 129*e1fe3e4aSElliott Hughes 130*e1fe3e4aSElliott Hughes def glyph( 131*e1fe3e4aSElliott Hughes self, 132*e1fe3e4aSElliott Hughes componentFlags: int = 0x04, 133*e1fe3e4aSElliott Hughes dropImpliedOnCurves: bool = False, 134*e1fe3e4aSElliott Hughes *, 135*e1fe3e4aSElliott Hughes round: Callable[[float], int] = otRound, 136*e1fe3e4aSElliott Hughes ) -> Glyph: 137*e1fe3e4aSElliott Hughes """ 138*e1fe3e4aSElliott Hughes Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. 139*e1fe3e4aSElliott Hughes 140*e1fe3e4aSElliott Hughes Args: 141*e1fe3e4aSElliott Hughes componentFlags: Flags to use for component glyphs. (default: 0x04) 142*e1fe3e4aSElliott Hughes 143*e1fe3e4aSElliott Hughes dropImpliedOnCurves: Whether to remove implied-oncurve points. (default: False) 144*e1fe3e4aSElliott Hughes """ 145*e1fe3e4aSElliott Hughes if not self._isClosed(): 146*e1fe3e4aSElliott Hughes raise PenError("Didn't close last contour.") 147*e1fe3e4aSElliott Hughes components = self._buildComponents(componentFlags) 148*e1fe3e4aSElliott Hughes 149*e1fe3e4aSElliott Hughes glyph = Glyph() 150*e1fe3e4aSElliott Hughes glyph.coordinates = GlyphCoordinates(self.points) 151*e1fe3e4aSElliott Hughes glyph.endPtsOfContours = self.endPts 152*e1fe3e4aSElliott Hughes glyph.flags = array("B", self.types) 153*e1fe3e4aSElliott Hughes self.init() 154*e1fe3e4aSElliott Hughes 155*e1fe3e4aSElliott Hughes if components: 156*e1fe3e4aSElliott Hughes # If both components and contours were present, they have by now 157*e1fe3e4aSElliott Hughes # been decomposed by _buildComponents. 158*e1fe3e4aSElliott Hughes glyph.components = components 159*e1fe3e4aSElliott Hughes glyph.numberOfContours = -1 160*e1fe3e4aSElliott Hughes else: 161*e1fe3e4aSElliott Hughes glyph.numberOfContours = len(glyph.endPtsOfContours) 162*e1fe3e4aSElliott Hughes glyph.program = ttProgram.Program() 163*e1fe3e4aSElliott Hughes glyph.program.fromBytecode(b"") 164*e1fe3e4aSElliott Hughes if dropImpliedOnCurves: 165*e1fe3e4aSElliott Hughes dropImpliedOnCurvePoints(glyph) 166*e1fe3e4aSElliott Hughes glyph.coordinates.toInt(round=round) 167*e1fe3e4aSElliott Hughes 168*e1fe3e4aSElliott Hughes return glyph 169*e1fe3e4aSElliott Hughes 170*e1fe3e4aSElliott Hughes 171*e1fe3e4aSElliott Hughesclass TTGlyphPen(_TTGlyphBasePen, LoggingPen): 172*e1fe3e4aSElliott Hughes """ 173*e1fe3e4aSElliott Hughes Pen used for drawing to a TrueType glyph. 174*e1fe3e4aSElliott Hughes 175*e1fe3e4aSElliott Hughes This pen can be used to construct or modify glyphs in a TrueType format 176*e1fe3e4aSElliott Hughes font. After using the pen to draw, use the ``.glyph()`` method to retrieve 177*e1fe3e4aSElliott Hughes a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. 178*e1fe3e4aSElliott Hughes """ 179*e1fe3e4aSElliott Hughes 180*e1fe3e4aSElliott Hughes drawMethod = "draw" 181*e1fe3e4aSElliott Hughes transformPen = TransformPen 182*e1fe3e4aSElliott Hughes 183*e1fe3e4aSElliott Hughes def __init__( 184*e1fe3e4aSElliott Hughes self, 185*e1fe3e4aSElliott Hughes glyphSet: Optional[Dict[str, Any]] = None, 186*e1fe3e4aSElliott Hughes handleOverflowingTransforms: bool = True, 187*e1fe3e4aSElliott Hughes outputImpliedClosingLine: bool = False, 188*e1fe3e4aSElliott Hughes ) -> None: 189*e1fe3e4aSElliott Hughes super().__init__(glyphSet, handleOverflowingTransforms) 190*e1fe3e4aSElliott Hughes self.outputImpliedClosingLine = outputImpliedClosingLine 191*e1fe3e4aSElliott Hughes 192*e1fe3e4aSElliott Hughes def _addPoint(self, pt: Tuple[float, float], tp: int) -> None: 193*e1fe3e4aSElliott Hughes self.points.append(pt) 194*e1fe3e4aSElliott Hughes self.types.append(tp) 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes def _popPoint(self) -> None: 197*e1fe3e4aSElliott Hughes self.points.pop() 198*e1fe3e4aSElliott Hughes self.types.pop() 199*e1fe3e4aSElliott Hughes 200*e1fe3e4aSElliott Hughes def _isClosed(self) -> bool: 201*e1fe3e4aSElliott Hughes return (not self.points) or ( 202*e1fe3e4aSElliott Hughes self.endPts and self.endPts[-1] == len(self.points) - 1 203*e1fe3e4aSElliott Hughes ) 204*e1fe3e4aSElliott Hughes 205*e1fe3e4aSElliott Hughes def lineTo(self, pt: Tuple[float, float]) -> None: 206*e1fe3e4aSElliott Hughes self._addPoint(pt, flagOnCurve) 207*e1fe3e4aSElliott Hughes 208*e1fe3e4aSElliott Hughes def moveTo(self, pt: Tuple[float, float]) -> None: 209*e1fe3e4aSElliott Hughes if not self._isClosed(): 210*e1fe3e4aSElliott Hughes raise PenError('"move"-type point must begin a new contour.') 211*e1fe3e4aSElliott Hughes self._addPoint(pt, flagOnCurve) 212*e1fe3e4aSElliott Hughes 213*e1fe3e4aSElliott Hughes def curveTo(self, *points) -> None: 214*e1fe3e4aSElliott Hughes assert len(points) % 2 == 1 215*e1fe3e4aSElliott Hughes for pt in points[:-1]: 216*e1fe3e4aSElliott Hughes self._addPoint(pt, flagCubic) 217*e1fe3e4aSElliott Hughes 218*e1fe3e4aSElliott Hughes # last point is None if there are no on-curve points 219*e1fe3e4aSElliott Hughes if points[-1] is not None: 220*e1fe3e4aSElliott Hughes self._addPoint(points[-1], 1) 221*e1fe3e4aSElliott Hughes 222*e1fe3e4aSElliott Hughes def qCurveTo(self, *points) -> None: 223*e1fe3e4aSElliott Hughes assert len(points) >= 1 224*e1fe3e4aSElliott Hughes for pt in points[:-1]: 225*e1fe3e4aSElliott Hughes self._addPoint(pt, 0) 226*e1fe3e4aSElliott Hughes 227*e1fe3e4aSElliott Hughes # last point is None if there are no on-curve points 228*e1fe3e4aSElliott Hughes if points[-1] is not None: 229*e1fe3e4aSElliott Hughes self._addPoint(points[-1], 1) 230*e1fe3e4aSElliott Hughes 231*e1fe3e4aSElliott Hughes def closePath(self) -> None: 232*e1fe3e4aSElliott Hughes endPt = len(self.points) - 1 233*e1fe3e4aSElliott Hughes 234*e1fe3e4aSElliott Hughes # ignore anchors (one-point paths) 235*e1fe3e4aSElliott Hughes if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1): 236*e1fe3e4aSElliott Hughes self._popPoint() 237*e1fe3e4aSElliott Hughes return 238*e1fe3e4aSElliott Hughes 239*e1fe3e4aSElliott Hughes if not self.outputImpliedClosingLine: 240*e1fe3e4aSElliott Hughes # if first and last point on this path are the same, remove last 241*e1fe3e4aSElliott Hughes startPt = 0 242*e1fe3e4aSElliott Hughes if self.endPts: 243*e1fe3e4aSElliott Hughes startPt = self.endPts[-1] + 1 244*e1fe3e4aSElliott Hughes if self.points[startPt] == self.points[endPt]: 245*e1fe3e4aSElliott Hughes self._popPoint() 246*e1fe3e4aSElliott Hughes endPt -= 1 247*e1fe3e4aSElliott Hughes 248*e1fe3e4aSElliott Hughes self.endPts.append(endPt) 249*e1fe3e4aSElliott Hughes 250*e1fe3e4aSElliott Hughes def endPath(self) -> None: 251*e1fe3e4aSElliott Hughes # TrueType contours are always "closed" 252*e1fe3e4aSElliott Hughes self.closePath() 253*e1fe3e4aSElliott Hughes 254*e1fe3e4aSElliott Hughes 255*e1fe3e4aSElliott Hughesclass TTGlyphPointPen(_TTGlyphBasePen, LogMixin, AbstractPointPen): 256*e1fe3e4aSElliott Hughes """ 257*e1fe3e4aSElliott Hughes Point pen used for drawing to a TrueType glyph. 258*e1fe3e4aSElliott Hughes 259*e1fe3e4aSElliott Hughes This pen can be used to construct or modify glyphs in a TrueType format 260*e1fe3e4aSElliott Hughes font. After using the pen to draw, use the ``.glyph()`` method to retrieve 261*e1fe3e4aSElliott Hughes a :py:class:`~._g_l_y_f.Glyph` object representing the glyph. 262*e1fe3e4aSElliott Hughes """ 263*e1fe3e4aSElliott Hughes 264*e1fe3e4aSElliott Hughes drawMethod = "drawPoints" 265*e1fe3e4aSElliott Hughes transformPen = TransformPointPen 266*e1fe3e4aSElliott Hughes 267*e1fe3e4aSElliott Hughes def init(self) -> None: 268*e1fe3e4aSElliott Hughes super().init() 269*e1fe3e4aSElliott Hughes self._currentContourStartIndex = None 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes def _isClosed(self) -> bool: 272*e1fe3e4aSElliott Hughes return self._currentContourStartIndex is None 273*e1fe3e4aSElliott Hughes 274*e1fe3e4aSElliott Hughes def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None: 275*e1fe3e4aSElliott Hughes """ 276*e1fe3e4aSElliott Hughes Start a new sub path. 277*e1fe3e4aSElliott Hughes """ 278*e1fe3e4aSElliott Hughes if not self._isClosed(): 279*e1fe3e4aSElliott Hughes raise PenError("Didn't close previous contour.") 280*e1fe3e4aSElliott Hughes self._currentContourStartIndex = len(self.points) 281*e1fe3e4aSElliott Hughes 282*e1fe3e4aSElliott Hughes def endPath(self) -> None: 283*e1fe3e4aSElliott Hughes """ 284*e1fe3e4aSElliott Hughes End the current sub path. 285*e1fe3e4aSElliott Hughes """ 286*e1fe3e4aSElliott Hughes # TrueType contours are always "closed" 287*e1fe3e4aSElliott Hughes if self._isClosed(): 288*e1fe3e4aSElliott Hughes raise PenError("Contour is already closed.") 289*e1fe3e4aSElliott Hughes if self._currentContourStartIndex == len(self.points): 290*e1fe3e4aSElliott Hughes # ignore empty contours 291*e1fe3e4aSElliott Hughes self._currentContourStartIndex = None 292*e1fe3e4aSElliott Hughes return 293*e1fe3e4aSElliott Hughes 294*e1fe3e4aSElliott Hughes contourStart = self.endPts[-1] + 1 if self.endPts else 0 295*e1fe3e4aSElliott Hughes self.endPts.append(len(self.points) - 1) 296*e1fe3e4aSElliott Hughes self._currentContourStartIndex = None 297*e1fe3e4aSElliott Hughes 298*e1fe3e4aSElliott Hughes # Resolve types for any cubic segments 299*e1fe3e4aSElliott Hughes flags = self.types 300*e1fe3e4aSElliott Hughes for i in range(contourStart, len(flags)): 301*e1fe3e4aSElliott Hughes if flags[i] == "curve": 302*e1fe3e4aSElliott Hughes j = i - 1 303*e1fe3e4aSElliott Hughes if j < contourStart: 304*e1fe3e4aSElliott Hughes j = len(flags) - 1 305*e1fe3e4aSElliott Hughes while flags[j] == 0: 306*e1fe3e4aSElliott Hughes flags[j] = flagCubic 307*e1fe3e4aSElliott Hughes j -= 1 308*e1fe3e4aSElliott Hughes flags[i] = flagOnCurve 309*e1fe3e4aSElliott Hughes 310*e1fe3e4aSElliott Hughes def addPoint( 311*e1fe3e4aSElliott Hughes self, 312*e1fe3e4aSElliott Hughes pt: Tuple[float, float], 313*e1fe3e4aSElliott Hughes segmentType: Optional[str] = None, 314*e1fe3e4aSElliott Hughes smooth: bool = False, 315*e1fe3e4aSElliott Hughes name: Optional[str] = None, 316*e1fe3e4aSElliott Hughes identifier: Optional[str] = None, 317*e1fe3e4aSElliott Hughes **kwargs: Any, 318*e1fe3e4aSElliott Hughes ) -> None: 319*e1fe3e4aSElliott Hughes """ 320*e1fe3e4aSElliott Hughes Add a point to the current sub path. 321*e1fe3e4aSElliott Hughes """ 322*e1fe3e4aSElliott Hughes if self._isClosed(): 323*e1fe3e4aSElliott Hughes raise PenError("Can't add a point to a closed contour.") 324*e1fe3e4aSElliott Hughes if segmentType is None: 325*e1fe3e4aSElliott Hughes self.types.append(0) 326*e1fe3e4aSElliott Hughes elif segmentType in ("line", "move"): 327*e1fe3e4aSElliott Hughes self.types.append(flagOnCurve) 328*e1fe3e4aSElliott Hughes elif segmentType == "qcurve": 329*e1fe3e4aSElliott Hughes self.types.append(flagOnCurve) 330*e1fe3e4aSElliott Hughes elif segmentType == "curve": 331*e1fe3e4aSElliott Hughes self.types.append("curve") 332*e1fe3e4aSElliott Hughes else: 333*e1fe3e4aSElliott Hughes raise AssertionError(segmentType) 334*e1fe3e4aSElliott Hughes 335*e1fe3e4aSElliott Hughes self.points.append(pt) 336