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