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