xref: /aosp_15_r20/external/fonttools/Lib/fontTools/pens/ttGlyphPen.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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