xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/_g_l_y_f.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""_g_l_y_f.py -- Converter classes for the 'glyf' table."""
2
3from collections import namedtuple
4from fontTools.misc import sstruct
5from fontTools import ttLib
6from fontTools import version
7from fontTools.misc.transform import DecomposedTransform
8from fontTools.misc.textTools import tostr, safeEval, pad
9from fontTools.misc.arrayTools import updateBounds, pointInRect
10from fontTools.misc.bezierTools import calcQuadraticBounds
11from fontTools.misc.fixedTools import (
12    fixedToFloat as fi2fl,
13    floatToFixed as fl2fi,
14    floatToFixedToStr as fl2str,
15    strToFixedToFloat as str2fl,
16)
17from fontTools.misc.roundTools import noRound, otRound
18from fontTools.misc.vector import Vector
19from numbers import Number
20from . import DefaultTable
21from . import ttProgram
22import sys
23import struct
24import array
25import logging
26import math
27import os
28from fontTools.misc import xmlWriter
29from fontTools.misc.filenames import userNameToFileName
30from fontTools.misc.loggingTools import deprecateFunction
31from enum import IntFlag
32from functools import partial
33from types import SimpleNamespace
34from typing import Set
35
36log = logging.getLogger(__name__)
37
38# We compute the version the same as is computed in ttlib/__init__
39# so that we can write 'ttLibVersion' attribute of the glyf TTX files
40# when glyf is written to separate files.
41version = ".".join(version.split(".")[:2])
42
43#
44# The Apple and MS rasterizers behave differently for
45# scaled composite components: one does scale first and then translate
46# and the other does it vice versa. MS defined some flags to indicate
47# the difference, but it seems nobody actually _sets_ those flags.
48#
49# Funny thing: Apple seems to _only_ do their thing in the
50# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE
51# (eg. Charcoal)...
52#
53SCALE_COMPONENT_OFFSET_DEFAULT = 0  # 0 == MS, 1 == Apple
54
55
56class table__g_l_y_f(DefaultTable.DefaultTable):
57    """Glyph Data Table
58
59    This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_
60    table, which contains outlines for glyphs in TrueType format. In many cases,
61    it is easier to access and manipulate glyph outlines through the ``GlyphSet``
62    object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`::
63
64                    >> from fontTools.pens.boundsPen import BoundsPen
65                    >> glyphset = font.getGlyphSet()
66                    >> bp = BoundsPen(glyphset)
67                    >> glyphset["A"].draw(bp)
68                    >> bp.bounds
69                    (19, 0, 633, 716)
70
71    However, this class can be used for low-level access to the ``glyf`` table data.
72    Objects of this class support dictionary-like access, mapping glyph names to
73    :py:class:`Glyph` objects::
74
75                    >> glyf = font["glyf"]
76                    >> len(glyf["Aacute"].components)
77                    2
78
79    Note that when adding glyphs to the font via low-level access to the ``glyf``
80    table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table::
81
82                    >> font["glyf"]["divisionslash"] = Glyph()
83                    >> font["hmtx"]["divisionslash"] = (640, 0)
84
85    """
86
87    dependencies = ["fvar"]
88
89    # this attribute controls the amount of padding applied to glyph data upon compile.
90    # Glyph lenghts are aligned to multiples of the specified value.
91    # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
92    # no padding, except for when padding would allow to use short loca offsets.
93    padding = 1
94
95    def decompile(self, data, ttFont):
96        self.axisTags = (
97            [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
98        )
99        loca = ttFont["loca"]
100        pos = int(loca[0])
101        nextPos = 0
102        noname = 0
103        self.glyphs = {}
104        self.glyphOrder = glyphOrder = ttFont.getGlyphOrder()
105        self._reverseGlyphOrder = {}
106        for i in range(0, len(loca) - 1):
107            try:
108                glyphName = glyphOrder[i]
109            except IndexError:
110                noname = noname + 1
111                glyphName = "ttxautoglyph%s" % i
112            nextPos = int(loca[i + 1])
113            glyphdata = data[pos:nextPos]
114            if len(glyphdata) != (nextPos - pos):
115                raise ttLib.TTLibError("not enough 'glyf' table data")
116            glyph = Glyph(glyphdata)
117            self.glyphs[glyphName] = glyph
118            pos = nextPos
119        if len(data) - nextPos >= 4:
120            log.warning(
121                "too much 'glyf' table data: expected %d, received %d bytes",
122                nextPos,
123                len(data),
124            )
125        if noname:
126            log.warning("%s glyphs have no name", noname)
127        if ttFont.lazy is False:  # Be lazy for None and True
128            self.ensureDecompiled()
129
130    def ensureDecompiled(self, recurse=False):
131        # The recurse argument is unused, but part of the signature of
132        # ensureDecompiled across the library.
133        for glyph in self.glyphs.values():
134            glyph.expand(self)
135
136    def compile(self, ttFont):
137        self.axisTags = (
138            [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
139        )
140        if not hasattr(self, "glyphOrder"):
141            self.glyphOrder = ttFont.getGlyphOrder()
142        padding = self.padding
143        assert padding in (0, 1, 2, 4)
144        locations = []
145        currentLocation = 0
146        dataList = []
147        recalcBBoxes = ttFont.recalcBBoxes
148        boundsDone = set()
149        for glyphName in self.glyphOrder:
150            glyph = self.glyphs[glyphName]
151            glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone)
152            if padding > 1:
153                glyphData = pad(glyphData, size=padding)
154            locations.append(currentLocation)
155            currentLocation = currentLocation + len(glyphData)
156            dataList.append(glyphData)
157        locations.append(currentLocation)
158
159        if padding == 1 and currentLocation < 0x20000:
160            # See if we can pad any odd-lengthed glyphs to allow loca
161            # table to use the short offsets.
162            indices = [
163                i for i, glyphData in enumerate(dataList) if len(glyphData) % 2 == 1
164            ]
165            if indices and currentLocation + len(indices) < 0x20000:
166                # It fits.  Do it.
167                for i in indices:
168                    dataList[i] += b"\0"
169                currentLocation = 0
170                for i, glyphData in enumerate(dataList):
171                    locations[i] = currentLocation
172                    currentLocation += len(glyphData)
173                locations[len(dataList)] = currentLocation
174
175        data = b"".join(dataList)
176        if "loca" in ttFont:
177            ttFont["loca"].set(locations)
178        if "maxp" in ttFont:
179            ttFont["maxp"].numGlyphs = len(self.glyphs)
180        if not data:
181            # As a special case when all glyph in the font are empty, add a zero byte
182            # to the table, so that OTS doesn’t reject it, and to make the table work
183            # on Windows as well.
184            # See https://github.com/khaledhosny/ots/issues/52
185            data = b"\0"
186        return data
187
188    def toXML(self, writer, ttFont, splitGlyphs=False):
189        notice = (
190            "The xMin, yMin, xMax and yMax values\n"
191            "will be recalculated by the compiler."
192        )
193        glyphNames = ttFont.getGlyphNames()
194        if not splitGlyphs:
195            writer.newline()
196            writer.comment(notice)
197            writer.newline()
198            writer.newline()
199        numGlyphs = len(glyphNames)
200        if splitGlyphs:
201            path, ext = os.path.splitext(writer.file.name)
202            existingGlyphFiles = set()
203        for glyphName in glyphNames:
204            glyph = self.get(glyphName)
205            if glyph is None:
206                log.warning("glyph '%s' does not exist in glyf table", glyphName)
207                continue
208            if glyph.numberOfContours:
209                if splitGlyphs:
210                    glyphPath = userNameToFileName(
211                        tostr(glyphName, "utf-8"),
212                        existingGlyphFiles,
213                        prefix=path + ".",
214                        suffix=ext,
215                    )
216                    existingGlyphFiles.add(glyphPath.lower())
217                    glyphWriter = xmlWriter.XMLWriter(
218                        glyphPath,
219                        idlefunc=writer.idlefunc,
220                        newlinestr=writer.newlinestr,
221                    )
222                    glyphWriter.begintag("ttFont", ttLibVersion=version)
223                    glyphWriter.newline()
224                    glyphWriter.begintag("glyf")
225                    glyphWriter.newline()
226                    glyphWriter.comment(notice)
227                    glyphWriter.newline()
228                    writer.simpletag("TTGlyph", src=os.path.basename(glyphPath))
229                else:
230                    glyphWriter = writer
231                glyphWriter.begintag(
232                    "TTGlyph",
233                    [
234                        ("name", glyphName),
235                        ("xMin", glyph.xMin),
236                        ("yMin", glyph.yMin),
237                        ("xMax", glyph.xMax),
238                        ("yMax", glyph.yMax),
239                    ],
240                )
241                glyphWriter.newline()
242                glyph.toXML(glyphWriter, ttFont)
243                glyphWriter.endtag("TTGlyph")
244                glyphWriter.newline()
245                if splitGlyphs:
246                    glyphWriter.endtag("glyf")
247                    glyphWriter.newline()
248                    glyphWriter.endtag("ttFont")
249                    glyphWriter.newline()
250                    glyphWriter.close()
251            else:
252                writer.simpletag("TTGlyph", name=glyphName)
253                writer.comment("contains no outline data")
254                if not splitGlyphs:
255                    writer.newline()
256            writer.newline()
257
258    def fromXML(self, name, attrs, content, ttFont):
259        if name != "TTGlyph":
260            return
261        if not hasattr(self, "glyphs"):
262            self.glyphs = {}
263        if not hasattr(self, "glyphOrder"):
264            self.glyphOrder = ttFont.getGlyphOrder()
265        glyphName = attrs["name"]
266        log.debug("unpacking glyph '%s'", glyphName)
267        glyph = Glyph()
268        for attr in ["xMin", "yMin", "xMax", "yMax"]:
269            setattr(glyph, attr, safeEval(attrs.get(attr, "0")))
270        self.glyphs[glyphName] = glyph
271        for element in content:
272            if not isinstance(element, tuple):
273                continue
274            name, attrs, content = element
275            glyph.fromXML(name, attrs, content, ttFont)
276        if not ttFont.recalcBBoxes:
277            glyph.compact(self, 0)
278
279    def setGlyphOrder(self, glyphOrder):
280        """Sets the glyph order
281
282        Args:
283                glyphOrder ([str]): List of glyph names in order.
284        """
285        self.glyphOrder = glyphOrder
286        self._reverseGlyphOrder = {}
287
288    def getGlyphName(self, glyphID):
289        """Returns the name for the glyph with the given ID.
290
291        Raises a ``KeyError`` if the glyph name is not found in the font.
292        """
293        return self.glyphOrder[glyphID]
294
295    def _buildReverseGlyphOrderDict(self):
296        self._reverseGlyphOrder = d = {}
297        for glyphID, glyphName in enumerate(self.glyphOrder):
298            d[glyphName] = glyphID
299
300    def getGlyphID(self, glyphName):
301        """Returns the ID of the glyph with the given name.
302
303        Raises a ``ValueError`` if the glyph is not found in the font.
304        """
305        glyphOrder = self.glyphOrder
306        id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName)
307        if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName:
308            self._buildReverseGlyphOrderDict()
309            id = self._reverseGlyphOrder.get(glyphName)
310        if id is None:
311            raise ValueError(glyphName)
312        return id
313
314    def removeHinting(self):
315        """Removes TrueType hints from all glyphs in the glyphset.
316
317        See :py:meth:`Glyph.removeHinting`.
318        """
319        for glyph in self.glyphs.values():
320            glyph.removeHinting()
321
322    def keys(self):
323        return self.glyphs.keys()
324
325    def has_key(self, glyphName):
326        return glyphName in self.glyphs
327
328    __contains__ = has_key
329
330    def get(self, glyphName, default=None):
331        glyph = self.glyphs.get(glyphName, default)
332        if glyph is not None:
333            glyph.expand(self)
334        return glyph
335
336    def __getitem__(self, glyphName):
337        glyph = self.glyphs[glyphName]
338        glyph.expand(self)
339        return glyph
340
341    def __setitem__(self, glyphName, glyph):
342        self.glyphs[glyphName] = glyph
343        if glyphName not in self.glyphOrder:
344            self.glyphOrder.append(glyphName)
345
346    def __delitem__(self, glyphName):
347        del self.glyphs[glyphName]
348        self.glyphOrder.remove(glyphName)
349
350    def __len__(self):
351        assert len(self.glyphOrder) == len(self.glyphs)
352        return len(self.glyphs)
353
354    def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None):
355        """Compute the four "phantom points" for the given glyph from its bounding box
356        and the horizontal and vertical advance widths and sidebearings stored in the
357        ttFont's "hmtx" and "vmtx" tables.
358
359        'hMetrics' should be ttFont['hmtx'].metrics.
360
361        'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise.
362        If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate.
363
364        https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms
365        """
366        glyph = self[glyphName]
367        if not hasattr(glyph, "xMin"):
368            glyph.recalcBounds(self)
369
370        horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName]
371        leftSideX = glyph.xMin - leftSideBearing
372        rightSideX = leftSideX + horizontalAdvanceWidth
373
374        if vMetrics:
375            verticalAdvanceWidth, topSideBearing = vMetrics[glyphName]
376            topSideY = topSideBearing + glyph.yMax
377            bottomSideY = topSideY - verticalAdvanceWidth
378        else:
379            bottomSideY = topSideY = 0
380
381        return [
382            (leftSideX, 0),
383            (rightSideX, 0),
384            (0, topSideY),
385            (0, bottomSideY),
386        ]
387
388    def _getCoordinatesAndControls(
389        self, glyphName, hMetrics, vMetrics=None, *, round=otRound
390    ):
391        """Return glyph coordinates and controls as expected by "gvar" table.
392
393        The coordinates includes four "phantom points" for the glyph metrics,
394        as mandated by the "gvar" spec.
395
396        The glyph controls is a namedtuple with the following attributes:
397                - numberOfContours: -1 for composite glyphs.
398                - endPts: list of indices of end points for each contour in simple
399                glyphs, or component indices in composite glyphs (used for IUP
400                optimization).
401                - flags: array of contour point flags for simple glyphs (None for
402                composite glyphs).
403                - components: list of base glyph names (str) for each component in
404                composite glyphs (None for simple glyphs).
405
406        The "hMetrics" and vMetrics are used to compute the "phantom points" (see
407        the "_getPhantomPoints" method).
408
409        Return None if the requested glyphName is not present.
410        """
411        glyph = self.get(glyphName)
412        if glyph is None:
413            return None
414        if glyph.isComposite():
415            coords = GlyphCoordinates(
416                [(getattr(c, "x", 0), getattr(c, "y", 0)) for c in glyph.components]
417            )
418            controls = _GlyphControls(
419                numberOfContours=glyph.numberOfContours,
420                endPts=list(range(len(glyph.components))),
421                flags=None,
422                components=[
423                    (c.glyphName, getattr(c, "transform", None))
424                    for c in glyph.components
425                ],
426            )
427        elif glyph.isVarComposite():
428            coords = []
429            controls = []
430
431            for component in glyph.components:
432                (
433                    componentCoords,
434                    componentControls,
435                ) = component.getCoordinatesAndControls()
436                coords.extend(componentCoords)
437                controls.extend(componentControls)
438
439            coords = GlyphCoordinates(coords)
440
441            controls = _GlyphControls(
442                numberOfContours=glyph.numberOfContours,
443                endPts=list(range(len(coords))),
444                flags=None,
445                components=[
446                    (c.glyphName, getattr(c, "flags", None)) for c in glyph.components
447                ],
448            )
449
450        else:
451            coords, endPts, flags = glyph.getCoordinates(self)
452            coords = coords.copy()
453            controls = _GlyphControls(
454                numberOfContours=glyph.numberOfContours,
455                endPts=endPts,
456                flags=flags,
457                components=None,
458            )
459        # Add phantom points for (left, right, top, bottom) positions.
460        phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics)
461        coords.extend(phantomPoints)
462        coords.toInt(round=round)
463        return coords, controls
464
465    def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None):
466        """Set coordinates and metrics for the given glyph.
467
468        "coord" is an array of GlyphCoordinates which must include the "phantom
469        points" as the last four coordinates.
470
471        Both the horizontal/vertical advances and left/top sidebearings in "hmtx"
472        and "vmtx" tables (if any) are updated from four phantom points and
473        the glyph's bounding boxes.
474
475        The "hMetrics" and vMetrics are used to propagate "phantom points"
476        into "hmtx" and "vmtx" tables if desired.  (see the "_getPhantomPoints"
477        method).
478        """
479        glyph = self[glyphName]
480
481        # Handle phantom points for (left, right, top, bottom) positions.
482        assert len(coord) >= 4
483        leftSideX = coord[-4][0]
484        rightSideX = coord[-3][0]
485        topSideY = coord[-2][1]
486        bottomSideY = coord[-1][1]
487
488        coord = coord[:-4]
489
490        if glyph.isComposite():
491            assert len(coord) == len(glyph.components)
492            for p, comp in zip(coord, glyph.components):
493                if hasattr(comp, "x"):
494                    comp.x, comp.y = p
495        elif glyph.isVarComposite():
496            for comp in glyph.components:
497                coord = comp.setCoordinates(coord)
498            assert not coord
499        elif glyph.numberOfContours == 0:
500            assert len(coord) == 0
501        else:
502            assert len(coord) == len(glyph.coordinates)
503            glyph.coordinates = GlyphCoordinates(coord)
504
505        glyph.recalcBounds(self, boundsDone=set())
506
507        horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
508        if horizontalAdvanceWidth < 0:
509            # unlikely, but it can happen, see:
510            # https://github.com/fonttools/fonttools/pull/1198
511            horizontalAdvanceWidth = 0
512        leftSideBearing = otRound(glyph.xMin - leftSideX)
513        hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
514
515        if vMetrics is not None:
516            verticalAdvanceWidth = otRound(topSideY - bottomSideY)
517            if verticalAdvanceWidth < 0:  # unlikely but do the same as horizontal
518                verticalAdvanceWidth = 0
519            topSideBearing = otRound(topSideY - glyph.yMax)
520            vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing
521
522    # Deprecated
523
524    def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin):
525        """This method is wrong and deprecated.
526        For rationale see:
527        https://github.com/fonttools/fonttools/pull/2266/files#r613569473
528        """
529        vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
530        if vMetrics is None:
531            verticalAdvanceWidth = ttFont["head"].unitsPerEm
532            topSideY = getattr(ttFont.get("hhea"), "ascent", None)
533            if topSideY is None:
534                if defaultVerticalOrigin is not None:
535                    topSideY = defaultVerticalOrigin
536                else:
537                    topSideY = verticalAdvanceWidth
538            glyph = self[glyphName]
539            glyph.recalcBounds(self)
540            topSideBearing = otRound(topSideY - glyph.yMax)
541            vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)}
542        return vMetrics
543
544    @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning)
545    def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None):
546        """Old public name for self._getPhantomPoints().
547        See: https://github.com/fonttools/fonttools/pull/2266"""
548        hMetrics = ttFont["hmtx"].metrics
549        vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
550        return self._getPhantomPoints(glyphName, hMetrics, vMetrics)
551
552    @deprecateFunction(
553        "use '_getCoordinatesAndControls' instead", category=DeprecationWarning
554    )
555    def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None):
556        """Old public name for self._getCoordinatesAndControls().
557        See: https://github.com/fonttools/fonttools/pull/2266"""
558        hMetrics = ttFont["hmtx"].metrics
559        vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
560        return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics)
561
562    @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning)
563    def setCoordinates(self, glyphName, ttFont):
564        """Old public name for self._setCoordinates().
565        See: https://github.com/fonttools/fonttools/pull/2266"""
566        hMetrics = ttFont["hmtx"].metrics
567        vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
568        self._setCoordinates(glyphName, hMetrics, vMetrics)
569
570
571_GlyphControls = namedtuple(
572    "_GlyphControls", "numberOfContours endPts flags components"
573)
574
575
576glyphHeaderFormat = """
577		>	# big endian
578		numberOfContours:	h
579		xMin:				h
580		yMin:				h
581		xMax:				h
582		yMax:				h
583"""
584
585# flags
586flagOnCurve = 0x01
587flagXShort = 0x02
588flagYShort = 0x04
589flagRepeat = 0x08
590flagXsame = 0x10
591flagYsame = 0x20
592flagOverlapSimple = 0x40
593flagCubic = 0x80
594
595# These flags are kept for XML output after decompiling the coordinates
596keepFlags = flagOnCurve + flagOverlapSimple + flagCubic
597
598_flagSignBytes = {
599    0: 2,
600    flagXsame: 0,
601    flagXShort | flagXsame: +1,
602    flagXShort: -1,
603    flagYsame: 0,
604    flagYShort | flagYsame: +1,
605    flagYShort: -1,
606}
607
608
609def flagBest(x, y, onCurve):
610    """For a given x,y delta pair, returns the flag that packs this pair
611    most efficiently, as well as the number of byte cost of such flag."""
612
613    flag = flagOnCurve if onCurve else 0
614    cost = 0
615    # do x
616    if x == 0:
617        flag = flag | flagXsame
618    elif -255 <= x <= 255:
619        flag = flag | flagXShort
620        if x > 0:
621            flag = flag | flagXsame
622        cost += 1
623    else:
624        cost += 2
625    # do y
626    if y == 0:
627        flag = flag | flagYsame
628    elif -255 <= y <= 255:
629        flag = flag | flagYShort
630        if y > 0:
631            flag = flag | flagYsame
632        cost += 1
633    else:
634        cost += 2
635    return flag, cost
636
637
638def flagFits(newFlag, oldFlag, mask):
639    newBytes = _flagSignBytes[newFlag & mask]
640    oldBytes = _flagSignBytes[oldFlag & mask]
641    return newBytes == oldBytes or abs(newBytes) > abs(oldBytes)
642
643
644def flagSupports(newFlag, oldFlag):
645    return (
646        (oldFlag & flagOnCurve) == (newFlag & flagOnCurve)
647        and flagFits(newFlag, oldFlag, flagXsame | flagXShort)
648        and flagFits(newFlag, oldFlag, flagYsame | flagYShort)
649    )
650
651
652def flagEncodeCoord(flag, mask, coord, coordBytes):
653    byteCount = _flagSignBytes[flag & mask]
654    if byteCount == 1:
655        coordBytes.append(coord)
656    elif byteCount == -1:
657        coordBytes.append(-coord)
658    elif byteCount == 2:
659        coordBytes.extend(struct.pack(">h", coord))
660
661
662def flagEncodeCoords(flag, x, y, xBytes, yBytes):
663    flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes)
664    flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes)
665
666
667ARG_1_AND_2_ARE_WORDS = 0x0001  # if set args are words otherwise they are bytes
668ARGS_ARE_XY_VALUES = 0x0002  # if set args are xy values, otherwise they are points
669ROUND_XY_TO_GRID = 0x0004  # for the xy values if above is true
670WE_HAVE_A_SCALE = 0x0008  # Sx = Sy, otherwise scale == 1.0
671NON_OVERLAPPING = 0x0010  # set to same value for all components (obsolete!)
672MORE_COMPONENTS = 0x0020  # indicates at least one more glyph after this one
673WE_HAVE_AN_X_AND_Y_SCALE = 0x0040  # Sx, Sy
674WE_HAVE_A_TWO_BY_TWO = 0x0080  # t00, t01, t10, t11
675WE_HAVE_INSTRUCTIONS = 0x0100  # instructions follow
676USE_MY_METRICS = 0x0200  # apply these metrics to parent glyph
677OVERLAP_COMPOUND = 0x0400  # used by Apple in GX fonts
678SCALED_COMPONENT_OFFSET = 0x0800  # composite designed to have the component offset scaled (designed for Apple)
679UNSCALED_COMPONENT_OFFSET = 0x1000  # composite designed not to have the component offset scaled (designed for MS)
680
681
682CompositeMaxpValues = namedtuple(
683    "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"]
684)
685
686
687class Glyph(object):
688    """This class represents an individual TrueType glyph.
689
690    TrueType glyph objects come in two flavours: simple and composite. Simple
691    glyph objects contain contours, represented via the ``.coordinates``,
692    ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes;
693    composite glyphs contain components, available through the ``.components``
694    attributes.
695
696    Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned
697    above) is only set on simple glyphs and the ``.components`` attribute is only
698    set on composite glyphs, it is necessary to use the :py:meth:`isComposite`
699    method to test whether a glyph is simple or composite before attempting to
700    access its data.
701
702    For a composite glyph, the components can also be accessed via array-like access::
703
704            >> assert(font["glyf"]["Aacute"].isComposite())
705            >> font["glyf"]["Aacute"][0]
706            <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0>
707
708    """
709
710    def __init__(self, data=b""):
711        if not data:
712            # empty char
713            self.numberOfContours = 0
714            return
715        self.data = data
716
717    def compact(self, glyfTable, recalcBBoxes=True):
718        data = self.compile(glyfTable, recalcBBoxes)
719        self.__dict__.clear()
720        self.data = data
721
722    def expand(self, glyfTable):
723        if not hasattr(self, "data"):
724            # already unpacked
725            return
726        if not self.data:
727            # empty char
728            del self.data
729            self.numberOfContours = 0
730            return
731        dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self)
732        del self.data
733        # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in
734        # some glyphs; decompileCoordinates assumes that there's at least
735        # one, so short-circuit here.
736        if self.numberOfContours == 0:
737            return
738        if self.isComposite():
739            self.decompileComponents(data, glyfTable)
740        elif self.isVarComposite():
741            self.decompileVarComponents(data, glyfTable)
742        else:
743            self.decompileCoordinates(data)
744
745    def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None):
746        if hasattr(self, "data"):
747            if recalcBBoxes:
748                # must unpack glyph in order to recalculate bounding box
749                self.expand(glyfTable)
750            else:
751                return self.data
752        if self.numberOfContours == 0:
753            return b""
754
755        if recalcBBoxes:
756            self.recalcBounds(glyfTable, boundsDone=boundsDone)
757
758        data = sstruct.pack(glyphHeaderFormat, self)
759        if self.isComposite():
760            data = data + self.compileComponents(glyfTable)
761        elif self.isVarComposite():
762            data = data + self.compileVarComponents(glyfTable)
763        else:
764            data = data + self.compileCoordinates()
765        return data
766
767    def toXML(self, writer, ttFont):
768        if self.isComposite():
769            for compo in self.components:
770                compo.toXML(writer, ttFont)
771            haveInstructions = hasattr(self, "program")
772        elif self.isVarComposite():
773            for compo in self.components:
774                compo.toXML(writer, ttFont)
775            haveInstructions = False
776        else:
777            last = 0
778            for i in range(self.numberOfContours):
779                writer.begintag("contour")
780                writer.newline()
781                for j in range(last, self.endPtsOfContours[i] + 1):
782                    attrs = [
783                        ("x", self.coordinates[j][0]),
784                        ("y", self.coordinates[j][1]),
785                        ("on", self.flags[j] & flagOnCurve),
786                    ]
787                    if self.flags[j] & flagOverlapSimple:
788                        # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
789                        attrs.append(("overlap", 1))
790                    if self.flags[j] & flagCubic:
791                        attrs.append(("cubic", 1))
792                    writer.simpletag("pt", attrs)
793                    writer.newline()
794                last = self.endPtsOfContours[i] + 1
795                writer.endtag("contour")
796                writer.newline()
797            haveInstructions = self.numberOfContours > 0
798        if haveInstructions:
799            if self.program:
800                writer.begintag("instructions")
801                writer.newline()
802                self.program.toXML(writer, ttFont)
803                writer.endtag("instructions")
804            else:
805                writer.simpletag("instructions")
806            writer.newline()
807
808    def fromXML(self, name, attrs, content, ttFont):
809        if name == "contour":
810            if self.numberOfContours < 0:
811                raise ttLib.TTLibError("can't mix composites and contours in glyph")
812            self.numberOfContours = self.numberOfContours + 1
813            coordinates = GlyphCoordinates()
814            flags = bytearray()
815            for element in content:
816                if not isinstance(element, tuple):
817                    continue
818                name, attrs, content = element
819                if name != "pt":
820                    continue  # ignore anything but "pt"
821                coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
822                flag = bool(safeEval(attrs["on"]))
823                if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
824                    flag |= flagOverlapSimple
825                if "cubic" in attrs and bool(safeEval(attrs["cubic"])):
826                    flag |= flagCubic
827                flags.append(flag)
828            if not hasattr(self, "coordinates"):
829                self.coordinates = coordinates
830                self.flags = flags
831                self.endPtsOfContours = [len(coordinates) - 1]
832            else:
833                self.coordinates.extend(coordinates)
834                self.flags.extend(flags)
835                self.endPtsOfContours.append(len(self.coordinates) - 1)
836        elif name == "component":
837            if self.numberOfContours > 0:
838                raise ttLib.TTLibError("can't mix composites and contours in glyph")
839            self.numberOfContours = -1
840            if not hasattr(self, "components"):
841                self.components = []
842            component = GlyphComponent()
843            self.components.append(component)
844            component.fromXML(name, attrs, content, ttFont)
845        elif name == "varComponent":
846            if self.numberOfContours > 0:
847                raise ttLib.TTLibError("can't mix composites and contours in glyph")
848            self.numberOfContours = -2
849            if not hasattr(self, "components"):
850                self.components = []
851            component = GlyphVarComponent()
852            self.components.append(component)
853            component.fromXML(name, attrs, content, ttFont)
854        elif name == "instructions":
855            self.program = ttProgram.Program()
856            for element in content:
857                if not isinstance(element, tuple):
858                    continue
859                name, attrs, content = element
860                self.program.fromXML(name, attrs, content, ttFont)
861
862    def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
863        assert self.isComposite() or self.isVarComposite()
864        nContours = 0
865        nPoints = 0
866        initialMaxComponentDepth = maxComponentDepth
867        for compo in self.components:
868            baseGlyph = glyfTable[compo.glyphName]
869            if baseGlyph.numberOfContours == 0:
870                continue
871            elif baseGlyph.numberOfContours > 0:
872                nP, nC = baseGlyph.getMaxpValues()
873            else:
874                nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues(
875                    glyfTable, initialMaxComponentDepth + 1
876                )
877                maxComponentDepth = max(maxComponentDepth, componentDepth)
878            nPoints = nPoints + nP
879            nContours = nContours + nC
880        return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
881
882    def getMaxpValues(self):
883        assert self.numberOfContours > 0
884        return len(self.coordinates), len(self.endPtsOfContours)
885
886    def decompileComponents(self, data, glyfTable):
887        self.components = []
888        more = 1
889        haveInstructions = 0
890        while more:
891            component = GlyphComponent()
892            more, haveInstr, data = component.decompile(data, glyfTable)
893            haveInstructions = haveInstructions | haveInstr
894            self.components.append(component)
895        if haveInstructions:
896            (numInstructions,) = struct.unpack(">h", data[:2])
897            data = data[2:]
898            self.program = ttProgram.Program()
899            self.program.fromBytecode(data[:numInstructions])
900            data = data[numInstructions:]
901            if len(data) >= 4:
902                log.warning(
903                    "too much glyph data at the end of composite glyph: %d excess bytes",
904                    len(data),
905                )
906
907    def decompileVarComponents(self, data, glyfTable):
908        self.components = []
909        while len(data) >= GlyphVarComponent.MIN_SIZE:
910            component = GlyphVarComponent()
911            data = component.decompile(data, glyfTable)
912            self.components.append(component)
913
914    def decompileCoordinates(self, data):
915        endPtsOfContours = array.array("H")
916        endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
917        if sys.byteorder != "big":
918            endPtsOfContours.byteswap()
919        self.endPtsOfContours = endPtsOfContours.tolist()
920
921        pos = 2 * self.numberOfContours
922        (instructionLength,) = struct.unpack(">h", data[pos : pos + 2])
923        self.program = ttProgram.Program()
924        self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength])
925        pos += 2 + instructionLength
926        nCoordinates = self.endPtsOfContours[-1] + 1
927        flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw(
928            nCoordinates, data, pos
929        )
930
931        # fill in repetitions and apply signs
932        self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates)
933        xIndex = 0
934        yIndex = 0
935        for i in range(nCoordinates):
936            flag = flags[i]
937            # x coordinate
938            if flag & flagXShort:
939                if flag & flagXsame:
940                    x = xCoordinates[xIndex]
941                else:
942                    x = -xCoordinates[xIndex]
943                xIndex = xIndex + 1
944            elif flag & flagXsame:
945                x = 0
946            else:
947                x = xCoordinates[xIndex]
948                xIndex = xIndex + 1
949            # y coordinate
950            if flag & flagYShort:
951                if flag & flagYsame:
952                    y = yCoordinates[yIndex]
953                else:
954                    y = -yCoordinates[yIndex]
955                yIndex = yIndex + 1
956            elif flag & flagYsame:
957                y = 0
958            else:
959                y = yCoordinates[yIndex]
960                yIndex = yIndex + 1
961            coordinates[i] = (x, y)
962        assert xIndex == len(xCoordinates)
963        assert yIndex == len(yCoordinates)
964        coordinates.relativeToAbsolute()
965        # discard all flags except "keepFlags"
966        for i in range(len(flags)):
967            flags[i] &= keepFlags
968        self.flags = flags
969
970    def decompileCoordinatesRaw(self, nCoordinates, data, pos=0):
971        # unpack flags and prepare unpacking of coordinates
972        flags = bytearray(nCoordinates)
973        # Warning: deep Python trickery going on. We use the struct module to unpack
974        # the coordinates. We build a format string based on the flags, so we can
975        # unpack the coordinates in one struct.unpack() call.
976        xFormat = ">"  # big endian
977        yFormat = ">"  # big endian
978        j = 0
979        while True:
980            flag = data[pos]
981            pos += 1
982            repeat = 1
983            if flag & flagRepeat:
984                repeat = data[pos] + 1
985                pos += 1
986            for k in range(repeat):
987                if flag & flagXShort:
988                    xFormat = xFormat + "B"
989                elif not (flag & flagXsame):
990                    xFormat = xFormat + "h"
991                if flag & flagYShort:
992                    yFormat = yFormat + "B"
993                elif not (flag & flagYsame):
994                    yFormat = yFormat + "h"
995                flags[j] = flag
996                j = j + 1
997            if j >= nCoordinates:
998                break
999        assert j == nCoordinates, "bad glyph flags"
1000        # unpack raw coordinates, krrrrrr-tching!
1001        xDataLen = struct.calcsize(xFormat)
1002        yDataLen = struct.calcsize(yFormat)
1003        if len(data) - pos - (xDataLen + yDataLen) >= 4:
1004            log.warning(
1005                "too much glyph data: %d excess bytes",
1006                len(data) - pos - (xDataLen + yDataLen),
1007            )
1008        xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen])
1009        yCoordinates = struct.unpack(
1010            yFormat, data[pos + xDataLen : pos + xDataLen + yDataLen]
1011        )
1012        return flags, xCoordinates, yCoordinates
1013
1014    def compileComponents(self, glyfTable):
1015        data = b""
1016        lastcomponent = len(self.components) - 1
1017        more = 1
1018        haveInstructions = 0
1019        for i in range(len(self.components)):
1020            if i == lastcomponent:
1021                haveInstructions = hasattr(self, "program")
1022                more = 0
1023            compo = self.components[i]
1024            data = data + compo.compile(more, haveInstructions, glyfTable)
1025        if haveInstructions:
1026            instructions = self.program.getBytecode()
1027            data = data + struct.pack(">h", len(instructions)) + instructions
1028        return data
1029
1030    def compileVarComponents(self, glyfTable):
1031        return b"".join(c.compile(glyfTable) for c in self.components)
1032
1033    def compileCoordinates(self):
1034        assert len(self.coordinates) == len(self.flags)
1035        data = []
1036        endPtsOfContours = array.array("H", self.endPtsOfContours)
1037        if sys.byteorder != "big":
1038            endPtsOfContours.byteswap()
1039        data.append(endPtsOfContours.tobytes())
1040        instructions = self.program.getBytecode()
1041        data.append(struct.pack(">h", len(instructions)))
1042        data.append(instructions)
1043
1044        deltas = self.coordinates.copy()
1045        deltas.toInt()
1046        deltas.absoluteToRelative()
1047
1048        # TODO(behdad): Add a configuration option for this?
1049        deltas = self.compileDeltasGreedy(self.flags, deltas)
1050        # deltas = self.compileDeltasOptimal(self.flags, deltas)
1051
1052        data.extend(deltas)
1053        return b"".join(data)
1054
1055    def compileDeltasGreedy(self, flags, deltas):
1056        # Implements greedy algorithm for packing coordinate deltas:
1057        # uses shortest representation one coordinate at a time.
1058        compressedFlags = bytearray()
1059        compressedXs = bytearray()
1060        compressedYs = bytearray()
1061        lastflag = None
1062        repeat = 0
1063        for flag, (x, y) in zip(flags, deltas):
1064            # Oh, the horrors of TrueType
1065            # do x
1066            if x == 0:
1067                flag = flag | flagXsame
1068            elif -255 <= x <= 255:
1069                flag = flag | flagXShort
1070                if x > 0:
1071                    flag = flag | flagXsame
1072                else:
1073                    x = -x
1074                compressedXs.append(x)
1075            else:
1076                compressedXs.extend(struct.pack(">h", x))
1077            # do y
1078            if y == 0:
1079                flag = flag | flagYsame
1080            elif -255 <= y <= 255:
1081                flag = flag | flagYShort
1082                if y > 0:
1083                    flag = flag | flagYsame
1084                else:
1085                    y = -y
1086                compressedYs.append(y)
1087            else:
1088                compressedYs.extend(struct.pack(">h", y))
1089            # handle repeating flags
1090            if flag == lastflag and repeat != 255:
1091                repeat = repeat + 1
1092                if repeat == 1:
1093                    compressedFlags.append(flag)
1094                else:
1095                    compressedFlags[-2] = flag | flagRepeat
1096                    compressedFlags[-1] = repeat
1097            else:
1098                repeat = 0
1099                compressedFlags.append(flag)
1100            lastflag = flag
1101        return (compressedFlags, compressedXs, compressedYs)
1102
1103    def compileDeltasOptimal(self, flags, deltas):
1104        # Implements optimal, dynaic-programming, algorithm for packing coordinate
1105        # deltas.  The savings are negligible :(.
1106        candidates = []
1107        bestTuple = None
1108        bestCost = 0
1109        repeat = 0
1110        for flag, (x, y) in zip(flags, deltas):
1111            # Oh, the horrors of TrueType
1112            flag, coordBytes = flagBest(x, y, flag)
1113            bestCost += 1 + coordBytes
1114            newCandidates = [
1115                (bestCost, bestTuple, flag, coordBytes),
1116                (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes),
1117            ]
1118            for lastCost, lastTuple, lastFlag, coordBytes in candidates:
1119                if (
1120                    lastCost + coordBytes <= bestCost + 1
1121                    and (lastFlag & flagRepeat)
1122                    and (lastFlag < 0xFF00)
1123                    and flagSupports(lastFlag, flag)
1124                ):
1125                    if (lastFlag & 0xFF) == (
1126                        flag | flagRepeat
1127                    ) and lastCost == bestCost + 1:
1128                        continue
1129                    newCandidates.append(
1130                        (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes)
1131                    )
1132            candidates = newCandidates
1133            bestTuple = min(candidates, key=lambda t: t[0])
1134            bestCost = bestTuple[0]
1135
1136        flags = []
1137        while bestTuple:
1138            cost, bestTuple, flag, coordBytes = bestTuple
1139            flags.append(flag)
1140        flags.reverse()
1141
1142        compressedFlags = bytearray()
1143        compressedXs = bytearray()
1144        compressedYs = bytearray()
1145        coords = iter(deltas)
1146        ff = []
1147        for flag in flags:
1148            repeatCount, flag = flag >> 8, flag & 0xFF
1149            compressedFlags.append(flag)
1150            if flag & flagRepeat:
1151                assert repeatCount > 0
1152                compressedFlags.append(repeatCount)
1153            else:
1154                assert repeatCount == 0
1155            for i in range(1 + repeatCount):
1156                x, y = next(coords)
1157                flagEncodeCoords(flag, x, y, compressedXs, compressedYs)
1158                ff.append(flag)
1159        try:
1160            next(coords)
1161            raise Exception("internal error")
1162        except StopIteration:
1163            pass
1164
1165        return (compressedFlags, compressedXs, compressedYs)
1166
1167    def recalcBounds(self, glyfTable, *, boundsDone=None):
1168        """Recalculates the bounds of the glyph.
1169
1170        Each glyph object stores its bounding box in the
1171        ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1172        recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1173        must be provided to resolve component bounds.
1174        """
1175        if self.isComposite() and self.tryRecalcBoundsComposite(
1176            glyfTable, boundsDone=boundsDone
1177        ):
1178            return
1179        try:
1180            coords, endPts, flags = self.getCoordinates(glyfTable)
1181            self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
1182        except NotImplementedError:
1183            pass
1184
1185    def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None):
1186        """Try recalculating the bounds of a composite glyph that has
1187        certain constrained properties. Namely, none of the components
1188        have a transform other than an integer translate, and none
1189        uses the anchor points.
1190
1191        Each glyph object stores its bounding box in the
1192        ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1193        recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1194        must be provided to resolve component bounds.
1195
1196        Return True if bounds were calculated, False otherwise.
1197        """
1198        for compo in self.components:
1199            if hasattr(compo, "firstPt") or hasattr(compo, "transform"):
1200                return False
1201            if not float(compo.x).is_integer() or not float(compo.y).is_integer():
1202                return False
1203
1204        # All components are untransformed and have an integer x/y translate
1205        bounds = None
1206        for compo in self.components:
1207            glyphName = compo.glyphName
1208            g = glyfTable[glyphName]
1209
1210            if boundsDone is None or glyphName not in boundsDone:
1211                g.recalcBounds(glyfTable, boundsDone=boundsDone)
1212                if boundsDone is not None:
1213                    boundsDone.add(glyphName)
1214            # empty components shouldn't update the bounds of the parent glyph
1215            if g.numberOfContours == 0:
1216                continue
1217
1218            x, y = compo.x, compo.y
1219            bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y))
1220            bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y))
1221
1222        if bounds is None:
1223            bounds = (0, 0, 0, 0)
1224        self.xMin, self.yMin, self.xMax, self.yMax = bounds
1225        return True
1226
1227    def isComposite(self):
1228        """Test whether a glyph has components"""
1229        if hasattr(self, "data"):
1230            return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False
1231        else:
1232            return self.numberOfContours == -1
1233
1234    def isVarComposite(self):
1235        """Test whether a glyph has variable components"""
1236        if hasattr(self, "data"):
1237            return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False
1238        else:
1239            return self.numberOfContours == -2
1240
1241    def getCoordinates(self, glyfTable):
1242        """Return the coordinates, end points and flags
1243
1244        This method returns three values: A :py:class:`GlyphCoordinates` object,
1245        a list of the indexes of the final points of each contour (allowing you
1246        to split up the coordinates list into contours) and a list of flags.
1247
1248        On simple glyphs, this method returns information from the glyph's own
1249        contours; on composite glyphs, it "flattens" all components recursively
1250        to return a list of coordinates representing all the components involved
1251        in the glyph.
1252
1253        To interpret the flags for each point, see the "Simple Glyph Flags"
1254        section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`.
1255        """
1256
1257        if self.numberOfContours > 0:
1258            return self.coordinates, self.endPtsOfContours, self.flags
1259        elif self.isComposite():
1260            # it's a composite
1261            allCoords = GlyphCoordinates()
1262            allFlags = bytearray()
1263            allEndPts = []
1264            for compo in self.components:
1265                g = glyfTable[compo.glyphName]
1266                try:
1267                    coordinates, endPts, flags = g.getCoordinates(glyfTable)
1268                except RecursionError:
1269                    raise ttLib.TTLibError(
1270                        "glyph '%s' contains a recursive component reference"
1271                        % compo.glyphName
1272                    )
1273                coordinates = GlyphCoordinates(coordinates)
1274                if hasattr(compo, "firstPt"):
1275                    # component uses two reference points: we apply the transform _before_
1276                    # computing the offset between the points
1277                    if hasattr(compo, "transform"):
1278                        coordinates.transform(compo.transform)
1279                    x1, y1 = allCoords[compo.firstPt]
1280                    x2, y2 = coordinates[compo.secondPt]
1281                    move = x1 - x2, y1 - y2
1282                    coordinates.translate(move)
1283                else:
1284                    # component uses XY offsets
1285                    move = compo.x, compo.y
1286                    if not hasattr(compo, "transform"):
1287                        coordinates.translate(move)
1288                    else:
1289                        apple_way = compo.flags & SCALED_COMPONENT_OFFSET
1290                        ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
1291                        assert not (apple_way and ms_way)
1292                        if not (apple_way or ms_way):
1293                            scale_component_offset = (
1294                                SCALE_COMPONENT_OFFSET_DEFAULT  # see top of this file
1295                            )
1296                        else:
1297                            scale_component_offset = apple_way
1298                        if scale_component_offset:
1299                            # the Apple way: first move, then scale (ie. scale the component offset)
1300                            coordinates.translate(move)
1301                            coordinates.transform(compo.transform)
1302                        else:
1303                            # the MS way: first scale, then move
1304                            coordinates.transform(compo.transform)
1305                            coordinates.translate(move)
1306                offset = len(allCoords)
1307                allEndPts.extend(e + offset for e in endPts)
1308                allCoords.extend(coordinates)
1309                allFlags.extend(flags)
1310            return allCoords, allEndPts, allFlags
1311        elif self.isVarComposite():
1312            raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs")
1313        else:
1314            return GlyphCoordinates(), [], bytearray()
1315
1316    def getComponentNames(self, glyfTable):
1317        """Returns a list of names of component glyphs used in this glyph
1318
1319        This method can be used on simple glyphs (in which case it returns an
1320        empty list) or composite glyphs.
1321        """
1322        if hasattr(self, "data") and self.isVarComposite():
1323            # TODO(VarComposite) Add implementation without expanding glyph
1324            self.expand(glyfTable)
1325
1326        if not hasattr(self, "data"):
1327            if self.isComposite() or self.isVarComposite():
1328                return [c.glyphName for c in self.components]
1329            else:
1330                return []
1331
1332        # Extract components without expanding glyph
1333
1334        if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
1335            return []  # Not composite
1336
1337        data = self.data
1338        i = 10
1339        components = []
1340        more = 1
1341        while more:
1342            flags, glyphID = struct.unpack(">HH", data[i : i + 4])
1343            i += 4
1344            flags = int(flags)
1345            components.append(glyfTable.getGlyphName(int(glyphID)))
1346
1347            if flags & ARG_1_AND_2_ARE_WORDS:
1348                i += 4
1349            else:
1350                i += 2
1351            if flags & WE_HAVE_A_SCALE:
1352                i += 2
1353            elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1354                i += 4
1355            elif flags & WE_HAVE_A_TWO_BY_TWO:
1356                i += 8
1357            more = flags & MORE_COMPONENTS
1358
1359        return components
1360
1361    def trim(self, remove_hinting=False):
1362        """Remove padding and, if requested, hinting, from a glyph.
1363        This works on both expanded and compacted glyphs, without
1364        expanding it."""
1365        if not hasattr(self, "data"):
1366            if remove_hinting:
1367                if self.isComposite():
1368                    if hasattr(self, "program"):
1369                        del self.program
1370                elif self.isVarComposite():
1371                    pass  # Doesn't have hinting
1372                else:
1373                    self.program = ttProgram.Program()
1374                    self.program.fromBytecode([])
1375            # No padding to trim.
1376            return
1377        if not self.data:
1378            return
1379        numContours = struct.unpack(">h", self.data[:2])[0]
1380        data = bytearray(self.data)
1381        i = 10
1382        if numContours >= 0:
1383            i += 2 * numContours  # endPtsOfContours
1384            nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1
1385            instructionLen = (data[i] << 8) | data[i + 1]
1386            if remove_hinting:
1387                # Zero instruction length
1388                data[i] = data[i + 1] = 0
1389                i += 2
1390                if instructionLen:
1391                    # Splice it out
1392                    data = data[:i] + data[i + instructionLen :]
1393                instructionLen = 0
1394            else:
1395                i += 2 + instructionLen
1396
1397            coordBytes = 0
1398            j = 0
1399            while True:
1400                flag = data[i]
1401                i = i + 1
1402                repeat = 1
1403                if flag & flagRepeat:
1404                    repeat = data[i] + 1
1405                    i = i + 1
1406                xBytes = yBytes = 0
1407                if flag & flagXShort:
1408                    xBytes = 1
1409                elif not (flag & flagXsame):
1410                    xBytes = 2
1411                if flag & flagYShort:
1412                    yBytes = 1
1413                elif not (flag & flagYsame):
1414                    yBytes = 2
1415                coordBytes += (xBytes + yBytes) * repeat
1416                j += repeat
1417                if j >= nCoordinates:
1418                    break
1419            assert j == nCoordinates, "bad glyph flags"
1420            i += coordBytes
1421            # Remove padding
1422            data = data[:i]
1423        elif self.isComposite():
1424            more = 1
1425            we_have_instructions = False
1426            while more:
1427                flags = (data[i] << 8) | data[i + 1]
1428                if remove_hinting:
1429                    flags &= ~WE_HAVE_INSTRUCTIONS
1430                if flags & WE_HAVE_INSTRUCTIONS:
1431                    we_have_instructions = True
1432                data[i + 0] = flags >> 8
1433                data[i + 1] = flags & 0xFF
1434                i += 4
1435                flags = int(flags)
1436
1437                if flags & ARG_1_AND_2_ARE_WORDS:
1438                    i += 4
1439                else:
1440                    i += 2
1441                if flags & WE_HAVE_A_SCALE:
1442                    i += 2
1443                elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1444                    i += 4
1445                elif flags & WE_HAVE_A_TWO_BY_TWO:
1446                    i += 8
1447                more = flags & MORE_COMPONENTS
1448            if we_have_instructions:
1449                instructionLen = (data[i] << 8) | data[i + 1]
1450                i += 2 + instructionLen
1451            # Remove padding
1452            data = data[:i]
1453        elif self.isVarComposite():
1454            i = 0
1455            MIN_SIZE = GlyphVarComponent.MIN_SIZE
1456            while len(data[i : i + MIN_SIZE]) >= MIN_SIZE:
1457                size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE])
1458                i += size
1459            data = data[:i]
1460
1461        self.data = data
1462
1463    def removeHinting(self):
1464        """Removes TrueType hinting instructions from the glyph."""
1465        self.trim(remove_hinting=True)
1466
1467    def draw(self, pen, glyfTable, offset=0):
1468        """Draws the glyph using the supplied pen object.
1469
1470        Arguments:
1471                pen: An object conforming to the pen protocol.
1472                glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components.
1473                offset (int): A horizontal offset. If provided, all coordinates are
1474                        translated by this offset.
1475        """
1476
1477        if self.isComposite():
1478            for component in self.components:
1479                glyphName, transform = component.getComponentInfo()
1480                pen.addComponent(glyphName, transform)
1481            return
1482
1483        coordinates, endPts, flags = self.getCoordinates(glyfTable)
1484        if offset:
1485            coordinates = coordinates.copy()
1486            coordinates.translate((offset, 0))
1487        start = 0
1488        maybeInt = lambda v: int(v) if v == int(v) else v
1489        for end in endPts:
1490            end = end + 1
1491            contour = coordinates[start:end]
1492            cFlags = [flagOnCurve & f for f in flags[start:end]]
1493            cuFlags = [flagCubic & f for f in flags[start:end]]
1494            start = end
1495            if 1 not in cFlags:
1496                assert all(cuFlags) or not any(cuFlags)
1497                cubic = all(cuFlags)
1498                if cubic:
1499                    count = len(contour)
1500                    assert count % 2 == 0, "Odd number of cubic off-curves undefined"
1501                    l = contour[-1]
1502                    f = contour[0]
1503                    p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5))
1504                    pen.moveTo(p0)
1505                    for i in range(0, count, 2):
1506                        p1 = contour[i]
1507                        p2 = contour[i + 1]
1508                        p4 = contour[i + 2 if i + 2 < count else 0]
1509                        p3 = (
1510                            maybeInt((p2[0] + p4[0]) * 0.5),
1511                            maybeInt((p2[1] + p4[1]) * 0.5),
1512                        )
1513                        pen.curveTo(p1, p2, p3)
1514                else:
1515                    # There is not a single on-curve point on the curve,
1516                    # use pen.qCurveTo's special case by specifying None
1517                    # as the on-curve point.
1518                    contour.append(None)
1519                    pen.qCurveTo(*contour)
1520            else:
1521                # Shuffle the points so that the contour is guaranteed
1522                # to *end* in an on-curve point, which we'll use for
1523                # the moveTo.
1524                firstOnCurve = cFlags.index(1) + 1
1525                contour = contour[firstOnCurve:] + contour[:firstOnCurve]
1526                cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
1527                cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve]
1528                pen.moveTo(contour[-1])
1529                while contour:
1530                    nextOnCurve = cFlags.index(1) + 1
1531                    if nextOnCurve == 1:
1532                        # Skip a final lineTo(), as it is implied by
1533                        # pen.closePath()
1534                        if len(contour) > 1:
1535                            pen.lineTo(contour[0])
1536                    else:
1537                        cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]]
1538                        assert all(cubicFlags) or not any(cubicFlags)
1539                        cubic = any(cubicFlags)
1540                        if cubic:
1541                            assert all(
1542                                cubicFlags
1543                            ), "Mixed cubic and quadratic segment undefined"
1544
1545                            count = nextOnCurve
1546                            assert (
1547                                count >= 3
1548                            ), "At least two cubic off-curve points required"
1549                            assert (
1550                                count - 1
1551                            ) % 2 == 0, "Odd number of cubic off-curves undefined"
1552                            for i in range(0, count - 3, 2):
1553                                p1 = contour[i]
1554                                p2 = contour[i + 1]
1555                                p4 = contour[i + 2]
1556                                p3 = (
1557                                    maybeInt((p2[0] + p4[0]) * 0.5),
1558                                    maybeInt((p2[1] + p4[1]) * 0.5),
1559                                )
1560                                lastOnCurve = p3
1561                                pen.curveTo(p1, p2, p3)
1562                            pen.curveTo(*contour[count - 3 : count])
1563                        else:
1564                            pen.qCurveTo(*contour[:nextOnCurve])
1565                    contour = contour[nextOnCurve:]
1566                    cFlags = cFlags[nextOnCurve:]
1567                    cuFlags = cuFlags[nextOnCurve:]
1568            pen.closePath()
1569
1570    def drawPoints(self, pen, glyfTable, offset=0):
1571        """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(),
1572        this will not change the point indices.
1573        """
1574
1575        if self.isComposite():
1576            for component in self.components:
1577                glyphName, transform = component.getComponentInfo()
1578                pen.addComponent(glyphName, transform)
1579            return
1580
1581        coordinates, endPts, flags = self.getCoordinates(glyfTable)
1582        if offset:
1583            coordinates = coordinates.copy()
1584            coordinates.translate((offset, 0))
1585        start = 0
1586        for end in endPts:
1587            end = end + 1
1588            contour = coordinates[start:end]
1589            cFlags = flags[start:end]
1590            start = end
1591            pen.beginPath()
1592            # Start with the appropriate segment type based on the final segment
1593
1594            if cFlags[-1] & flagOnCurve:
1595                segmentType = "line"
1596            elif cFlags[-1] & flagCubic:
1597                segmentType = "curve"
1598            else:
1599                segmentType = "qcurve"
1600            for i, pt in enumerate(contour):
1601                if cFlags[i] & flagOnCurve:
1602                    pen.addPoint(pt, segmentType=segmentType)
1603                    segmentType = "line"
1604                else:
1605                    pen.addPoint(pt)
1606                    segmentType = "curve" if cFlags[i] & flagCubic else "qcurve"
1607            pen.endPath()
1608
1609    def __eq__(self, other):
1610        if type(self) != type(other):
1611            return NotImplemented
1612        return self.__dict__ == other.__dict__
1613
1614    def __ne__(self, other):
1615        result = self.__eq__(other)
1616        return result if result is NotImplemented else not result
1617
1618
1619# Vector.__round__ uses the built-in (Banker's) `round` but we want
1620# to use otRound below
1621_roundv = partial(Vector.__round__, round=otRound)
1622
1623
1624def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool:
1625    # True if p1 is in the middle of p0 and p2, either before or after rounding
1626    p0 = Vector(p0)
1627    p1 = Vector(p1)
1628    p2 = Vector(p2)
1629    return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2
1630
1631
1632def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
1633    """Drop impliable on-curve points from the (simple) glyph or glyphs.
1634
1635    In TrueType glyf outlines, on-curve points can be implied when they are located at
1636    the midpoint of the line connecting two consecutive off-curve points.
1637
1638    If more than one glyphs are passed, these are assumed to be interpolatable masters
1639    of the same glyph impliable, and thus only the on-curve points that are impliable
1640    for all of them will actually be implied.
1641    Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more
1642    contours are considered.
1643    The input glyph(s) is/are modified in-place.
1644
1645    Args:
1646        interpolatable_glyphs: The glyph or glyphs to modify in-place.
1647
1648    Returns:
1649        The set of point indices that were dropped if any.
1650
1651    Raises:
1652        ValueError if simple glyphs are not in fact interpolatable because they have
1653        different point flags or number of contours.
1654
1655    Reference:
1656    https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
1657    """
1658    staticAttributes = SimpleNamespace(
1659        numberOfContours=None, flags=None, endPtsOfContours=None
1660    )
1661    drop = None
1662    simple_glyphs = []
1663    for i, glyph in enumerate(interpolatable_glyphs):
1664        if glyph.numberOfContours < 1:
1665            # ignore composite or empty glyphs
1666            continue
1667
1668        for attr in staticAttributes.__dict__:
1669            expected = getattr(staticAttributes, attr)
1670            found = getattr(glyph, attr)
1671            if expected is None:
1672                setattr(staticAttributes, attr, found)
1673            elif expected != found:
1674                raise ValueError(
1675                    f"Incompatible {attr} for glyph at master index {i}: "
1676                    f"expected {expected}, found {found}"
1677                )
1678
1679        may_drop = set()
1680        start = 0
1681        coords = glyph.coordinates
1682        flags = staticAttributes.flags
1683        endPtsOfContours = staticAttributes.endPtsOfContours
1684        for last in endPtsOfContours:
1685            for i in range(start, last + 1):
1686                if not (flags[i] & flagOnCurve):
1687                    continue
1688                prv = i - 1 if i > start else last
1689                nxt = i + 1 if i < last else start
1690                if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
1691                    continue
1692                # we may drop the ith on-curve if halfway between previous/next off-curves
1693                if not _is_mid_point(coords[prv], coords[i], coords[nxt]):
1694                    continue
1695
1696                may_drop.add(i)
1697            start = last + 1
1698        # we only want to drop if ALL interpolatable glyphs have the same implied oncurves
1699        if drop is None:
1700            drop = may_drop
1701        else:
1702            drop.intersection_update(may_drop)
1703
1704        simple_glyphs.append(glyph)
1705
1706    if drop:
1707        # Do the actual dropping
1708        flags = staticAttributes.flags
1709        assert flags is not None
1710        newFlags = array.array(
1711            "B", (flags[i] for i in range(len(flags)) if i not in drop)
1712        )
1713
1714        endPts = staticAttributes.endPtsOfContours
1715        assert endPts is not None
1716        newEndPts = []
1717        i = 0
1718        delta = 0
1719        for d in sorted(drop):
1720            while d > endPts[i]:
1721                newEndPts.append(endPts[i] - delta)
1722                i += 1
1723            delta += 1
1724        while i < len(endPts):
1725            newEndPts.append(endPts[i] - delta)
1726            i += 1
1727
1728        for glyph in simple_glyphs:
1729            coords = glyph.coordinates
1730            glyph.coordinates = GlyphCoordinates(
1731                coords[i] for i in range(len(coords)) if i not in drop
1732            )
1733            glyph.flags = newFlags
1734            glyph.endPtsOfContours = newEndPts
1735
1736    return drop if drop is not None else set()
1737
1738
1739class GlyphComponent(object):
1740    """Represents a component within a composite glyph.
1741
1742    The component is represented internally with four attributes: ``glyphName``,
1743    ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e
1744    no scaling, reflection, or rotation; only translation), the ``transform``
1745    attribute is not present.
1746    """
1747
1748    # The above documentation is not *completely* true, but is *true enough* because
1749    # the rare firstPt/lastPt attributes are not totally supported and nobody seems to
1750    # mind - see below.
1751
1752    def __init__(self):
1753        pass
1754
1755    def getComponentInfo(self):
1756        """Return information about the component
1757
1758        This method returns a tuple of two values: the glyph name of the component's
1759        base glyph, and a transformation matrix. As opposed to accessing the attributes
1760        directly, ``getComponentInfo`` always returns a six-element tuple of the
1761        component's transformation matrix, even when the two-by-two ``.transform``
1762        matrix is not present.
1763        """
1764        # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
1765        # something equivalent in fontTools.objects.glyph (I'd rather not
1766        # convert it to an absolute offset, since it is valuable information).
1767        # This method will now raise "AttributeError: x" on glyphs that use
1768        # this TT feature.
1769        if hasattr(self, "transform"):
1770            [[xx, xy], [yx, yy]] = self.transform
1771            trans = (xx, xy, yx, yy, self.x, self.y)
1772        else:
1773            trans = (1, 0, 0, 1, self.x, self.y)
1774        return self.glyphName, trans
1775
1776    def decompile(self, data, glyfTable):
1777        flags, glyphID = struct.unpack(">HH", data[:4])
1778        self.flags = int(flags)
1779        glyphID = int(glyphID)
1780        self.glyphName = glyfTable.getGlyphName(int(glyphID))
1781        data = data[4:]
1782
1783        if self.flags & ARG_1_AND_2_ARE_WORDS:
1784            if self.flags & ARGS_ARE_XY_VALUES:
1785                self.x, self.y = struct.unpack(">hh", data[:4])
1786            else:
1787                x, y = struct.unpack(">HH", data[:4])
1788                self.firstPt, self.secondPt = int(x), int(y)
1789            data = data[4:]
1790        else:
1791            if self.flags & ARGS_ARE_XY_VALUES:
1792                self.x, self.y = struct.unpack(">bb", data[:2])
1793            else:
1794                x, y = struct.unpack(">BB", data[:2])
1795                self.firstPt, self.secondPt = int(x), int(y)
1796            data = data[2:]
1797
1798        if self.flags & WE_HAVE_A_SCALE:
1799            (scale,) = struct.unpack(">h", data[:2])
1800            self.transform = [
1801                [fi2fl(scale, 14), 0],
1802                [0, fi2fl(scale, 14)],
1803            ]  # fixed 2.14
1804            data = data[2:]
1805        elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
1806            xscale, yscale = struct.unpack(">hh", data[:4])
1807            self.transform = [
1808                [fi2fl(xscale, 14), 0],
1809                [0, fi2fl(yscale, 14)],
1810            ]  # fixed 2.14
1811            data = data[4:]
1812        elif self.flags & WE_HAVE_A_TWO_BY_TWO:
1813            (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8])
1814            self.transform = [
1815                [fi2fl(xscale, 14), fi2fl(scale01, 14)],
1816                [fi2fl(scale10, 14), fi2fl(yscale, 14)],
1817            ]  # fixed 2.14
1818            data = data[8:]
1819        more = self.flags & MORE_COMPONENTS
1820        haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
1821        self.flags = self.flags & (
1822            ROUND_XY_TO_GRID
1823            | USE_MY_METRICS
1824            | SCALED_COMPONENT_OFFSET
1825            | UNSCALED_COMPONENT_OFFSET
1826            | NON_OVERLAPPING
1827            | OVERLAP_COMPOUND
1828        )
1829        return more, haveInstructions, data
1830
1831    def compile(self, more, haveInstructions, glyfTable):
1832        data = b""
1833
1834        # reset all flags we will calculate ourselves
1835        flags = self.flags & (
1836            ROUND_XY_TO_GRID
1837            | USE_MY_METRICS
1838            | SCALED_COMPONENT_OFFSET
1839            | UNSCALED_COMPONENT_OFFSET
1840            | NON_OVERLAPPING
1841            | OVERLAP_COMPOUND
1842        )
1843        if more:
1844            flags = flags | MORE_COMPONENTS
1845        if haveInstructions:
1846            flags = flags | WE_HAVE_INSTRUCTIONS
1847
1848        if hasattr(self, "firstPt"):
1849            if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255):
1850                data = data + struct.pack(">BB", self.firstPt, self.secondPt)
1851            else:
1852                data = data + struct.pack(">HH", self.firstPt, self.secondPt)
1853                flags = flags | ARG_1_AND_2_ARE_WORDS
1854        else:
1855            x = otRound(self.x)
1856            y = otRound(self.y)
1857            flags = flags | ARGS_ARE_XY_VALUES
1858            if (-128 <= x <= 127) and (-128 <= y <= 127):
1859                data = data + struct.pack(">bb", x, y)
1860            else:
1861                data = data + struct.pack(">hh", x, y)
1862                flags = flags | ARG_1_AND_2_ARE_WORDS
1863
1864        if hasattr(self, "transform"):
1865            transform = [[fl2fi(x, 14) for x in row] for row in self.transform]
1866            if transform[0][1] or transform[1][0]:
1867                flags = flags | WE_HAVE_A_TWO_BY_TWO
1868                data = data + struct.pack(
1869                    ">hhhh",
1870                    transform[0][0],
1871                    transform[0][1],
1872                    transform[1][0],
1873                    transform[1][1],
1874                )
1875            elif transform[0][0] != transform[1][1]:
1876                flags = flags | WE_HAVE_AN_X_AND_Y_SCALE
1877                data = data + struct.pack(">hh", transform[0][0], transform[1][1])
1878            else:
1879                flags = flags | WE_HAVE_A_SCALE
1880                data = data + struct.pack(">h", transform[0][0])
1881
1882        glyphID = glyfTable.getGlyphID(self.glyphName)
1883        return struct.pack(">HH", flags, glyphID) + data
1884
1885    def toXML(self, writer, ttFont):
1886        attrs = [("glyphName", self.glyphName)]
1887        if not hasattr(self, "firstPt"):
1888            attrs = attrs + [("x", self.x), ("y", self.y)]
1889        else:
1890            attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)]
1891
1892        if hasattr(self, "transform"):
1893            transform = self.transform
1894            if transform[0][1] or transform[1][0]:
1895                attrs = attrs + [
1896                    ("scalex", fl2str(transform[0][0], 14)),
1897                    ("scale01", fl2str(transform[0][1], 14)),
1898                    ("scale10", fl2str(transform[1][0], 14)),
1899                    ("scaley", fl2str(transform[1][1], 14)),
1900                ]
1901            elif transform[0][0] != transform[1][1]:
1902                attrs = attrs + [
1903                    ("scalex", fl2str(transform[0][0], 14)),
1904                    ("scaley", fl2str(transform[1][1], 14)),
1905                ]
1906            else:
1907                attrs = attrs + [("scale", fl2str(transform[0][0], 14))]
1908        attrs = attrs + [("flags", hex(self.flags))]
1909        writer.simpletag("component", attrs)
1910        writer.newline()
1911
1912    def fromXML(self, name, attrs, content, ttFont):
1913        self.glyphName = attrs["glyphName"]
1914        if "firstPt" in attrs:
1915            self.firstPt = safeEval(attrs["firstPt"])
1916            self.secondPt = safeEval(attrs["secondPt"])
1917        else:
1918            self.x = safeEval(attrs["x"])
1919            self.y = safeEval(attrs["y"])
1920        if "scale01" in attrs:
1921            scalex = str2fl(attrs["scalex"], 14)
1922            scale01 = str2fl(attrs["scale01"], 14)
1923            scale10 = str2fl(attrs["scale10"], 14)
1924            scaley = str2fl(attrs["scaley"], 14)
1925            self.transform = [[scalex, scale01], [scale10, scaley]]
1926        elif "scalex" in attrs:
1927            scalex = str2fl(attrs["scalex"], 14)
1928            scaley = str2fl(attrs["scaley"], 14)
1929            self.transform = [[scalex, 0], [0, scaley]]
1930        elif "scale" in attrs:
1931            scale = str2fl(attrs["scale"], 14)
1932            self.transform = [[scale, 0], [0, scale]]
1933        self.flags = safeEval(attrs["flags"])
1934
1935    def __eq__(self, other):
1936        if type(self) != type(other):
1937            return NotImplemented
1938        return self.__dict__ == other.__dict__
1939
1940    def __ne__(self, other):
1941        result = self.__eq__(other)
1942        return result if result is NotImplemented else not result
1943
1944
1945#
1946# Variable Composite glyphs
1947# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
1948#
1949
1950
1951class VarComponentFlags(IntFlag):
1952    USE_MY_METRICS = 0x0001
1953    AXIS_INDICES_ARE_SHORT = 0x0002
1954    UNIFORM_SCALE = 0x0004
1955    HAVE_TRANSLATE_X = 0x0008
1956    HAVE_TRANSLATE_Y = 0x0010
1957    HAVE_ROTATION = 0x0020
1958    HAVE_SCALE_X = 0x0040
1959    HAVE_SCALE_Y = 0x0080
1960    HAVE_SKEW_X = 0x0100
1961    HAVE_SKEW_Y = 0x0200
1962    HAVE_TCENTER_X = 0x0400
1963    HAVE_TCENTER_Y = 0x0800
1964    GID_IS_24BIT = 0x1000
1965    AXES_HAVE_VARIATION = 0x2000
1966    RESET_UNSPECIFIED_AXES = 0x4000
1967
1968
1969VarComponentTransformMappingValues = namedtuple(
1970    "VarComponentTransformMappingValues",
1971    ["flag", "fractionalBits", "scale", "defaultValue"],
1972)
1973
1974VAR_COMPONENT_TRANSFORM_MAPPING = {
1975    "translateX": VarComponentTransformMappingValues(
1976        VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
1977    ),
1978    "translateY": VarComponentTransformMappingValues(
1979        VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
1980    ),
1981    "rotation": VarComponentTransformMappingValues(
1982        VarComponentFlags.HAVE_ROTATION, 12, 180, 0
1983    ),
1984    "scaleX": VarComponentTransformMappingValues(
1985        VarComponentFlags.HAVE_SCALE_X, 10, 1, 1
1986    ),
1987    "scaleY": VarComponentTransformMappingValues(
1988        VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1
1989    ),
1990    "skewX": VarComponentTransformMappingValues(
1991        VarComponentFlags.HAVE_SKEW_X, 12, -180, 0
1992    ),
1993    "skewY": VarComponentTransformMappingValues(
1994        VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0
1995    ),
1996    "tCenterX": VarComponentTransformMappingValues(
1997        VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0
1998    ),
1999    "tCenterY": VarComponentTransformMappingValues(
2000        VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0
2001    ),
2002}
2003
2004
2005class GlyphVarComponent(object):
2006    MIN_SIZE = 5
2007
2008    def __init__(self):
2009        self.location = {}
2010        self.transform = DecomposedTransform()
2011
2012    @staticmethod
2013    def getSize(data):
2014        size = 5
2015        flags = struct.unpack(">H", data[:2])[0]
2016        numAxes = int(data[2])
2017
2018        if flags & VarComponentFlags.GID_IS_24BIT:
2019            size += 1
2020
2021        size += numAxes
2022        if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
2023            size += 2 * numAxes
2024        else:
2025            axisIndices = array.array("B", data[:numAxes])
2026            size += numAxes
2027
2028        for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2029            if flags & mapping_values.flag:
2030                size += 2
2031
2032        return size
2033
2034    def decompile(self, data, glyfTable):
2035        flags = struct.unpack(">H", data[:2])[0]
2036        self.flags = int(flags)
2037        data = data[2:]
2038
2039        numAxes = int(data[0])
2040        data = data[1:]
2041
2042        if flags & VarComponentFlags.GID_IS_24BIT:
2043            glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0])
2044            data = data[3:]
2045            flags ^= VarComponentFlags.GID_IS_24BIT
2046        else:
2047            glyphID = int(struct.unpack(">H", data[:2])[0])
2048            data = data[2:]
2049        self.glyphName = glyfTable.getGlyphName(int(glyphID))
2050
2051        if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
2052            axisIndices = array.array("H", data[: 2 * numAxes])
2053            if sys.byteorder != "big":
2054                axisIndices.byteswap()
2055            data = data[2 * numAxes :]
2056            flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT
2057        else:
2058            axisIndices = array.array("B", data[:numAxes])
2059            data = data[numAxes:]
2060        assert len(axisIndices) == numAxes
2061        axisIndices = list(axisIndices)
2062
2063        axisValues = array.array("h", data[: 2 * numAxes])
2064        if sys.byteorder != "big":
2065            axisValues.byteswap()
2066        data = data[2 * numAxes :]
2067        assert len(axisValues) == numAxes
2068        axisValues = [fi2fl(v, 14) for v in axisValues]
2069
2070        self.location = {
2071            glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues)
2072        }
2073
2074        def read_transform_component(data, values):
2075            if flags & values.flag:
2076                return (
2077                    data[2:],
2078                    fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits)
2079                    * values.scale,
2080                )
2081            else:
2082                return data, values.defaultValue
2083
2084        for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2085            data, value = read_transform_component(data, mapping_values)
2086            setattr(self.transform, attr_name, value)
2087
2088        if flags & VarComponentFlags.UNIFORM_SCALE:
2089            if flags & VarComponentFlags.HAVE_SCALE_X and not (
2090                flags & VarComponentFlags.HAVE_SCALE_Y
2091            ):
2092                self.transform.scaleY = self.transform.scaleX
2093                flags |= VarComponentFlags.HAVE_SCALE_Y
2094            flags ^= VarComponentFlags.UNIFORM_SCALE
2095
2096        return data
2097
2098    def compile(self, glyfTable):
2099        data = b""
2100
2101        if not hasattr(self, "flags"):
2102            flags = 0
2103            # Calculate optimal transform component flags
2104            for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2105                value = getattr(self.transform, attr_name)
2106                if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi(
2107                    mapping.defaultValue / mapping.scale, mapping.fractionalBits
2108                ):
2109                    flags |= mapping.flag
2110        else:
2111            flags = self.flags
2112
2113        if (
2114            flags & VarComponentFlags.HAVE_SCALE_X
2115            and flags & VarComponentFlags.HAVE_SCALE_Y
2116            and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10)
2117        ):
2118            flags |= VarComponentFlags.UNIFORM_SCALE
2119            flags ^= VarComponentFlags.HAVE_SCALE_Y
2120
2121        numAxes = len(self.location)
2122
2123        data = data + struct.pack(">B", numAxes)
2124
2125        glyphID = glyfTable.getGlyphID(self.glyphName)
2126        if glyphID > 65535:
2127            flags |= VarComponentFlags.GID_IS_24BIT
2128            data = data + struct.pack(">L", glyphID)[1:]
2129        else:
2130            data = data + struct.pack(">H", glyphID)
2131
2132        axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()]
2133        if all(a <= 255 for a in axisIndices):
2134            axisIndices = array.array("B", axisIndices)
2135        else:
2136            axisIndices = array.array("H", axisIndices)
2137            if sys.byteorder != "big":
2138                axisIndices.byteswap()
2139            flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT
2140        data = data + bytes(axisIndices)
2141
2142        axisValues = self.location.values()
2143        axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues))
2144        if sys.byteorder != "big":
2145            axisValues.byteswap()
2146        data = data + bytes(axisValues)
2147
2148        def write_transform_component(data, value, values):
2149            if flags & values.flag:
2150                return data + struct.pack(
2151                    ">h", fl2fi(value / values.scale, values.fractionalBits)
2152                )
2153            else:
2154                return data
2155
2156        for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2157            value = getattr(self.transform, attr_name)
2158            data = write_transform_component(data, value, mapping_values)
2159
2160        return struct.pack(">H", flags) + data
2161
2162    def toXML(self, writer, ttFont):
2163        attrs = [("glyphName", self.glyphName)]
2164
2165        if hasattr(self, "flags"):
2166            attrs = attrs + [("flags", hex(self.flags))]
2167
2168        for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2169            v = getattr(self.transform, attr_name)
2170            if v != mapping.defaultValue:
2171                attrs.append((attr_name, fl2str(v, mapping.fractionalBits)))
2172
2173        writer.begintag("varComponent", attrs)
2174        writer.newline()
2175
2176        writer.begintag("location")
2177        writer.newline()
2178        for tag, v in self.location.items():
2179            writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))])
2180            writer.newline()
2181        writer.endtag("location")
2182        writer.newline()
2183
2184        writer.endtag("varComponent")
2185        writer.newline()
2186
2187    def fromXML(self, name, attrs, content, ttFont):
2188        self.glyphName = attrs["glyphName"]
2189
2190        if "flags" in attrs:
2191            self.flags = safeEval(attrs["flags"])
2192
2193        for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2194            if attr_name not in attrs:
2195                continue
2196            v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits)
2197            setattr(self.transform, attr_name, v)
2198
2199        for c in content:
2200            if not isinstance(c, tuple):
2201                continue
2202            name, attrs, content = c
2203            if name != "location":
2204                continue
2205            for c in content:
2206                if not isinstance(c, tuple):
2207                    continue
2208                name, attrs, content = c
2209                assert name == "axis"
2210                assert not content
2211                self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14)
2212
2213    def getPointCount(self):
2214        assert hasattr(self, "flags"), "VarComponent with variations must have flags"
2215
2216        count = 0
2217
2218        if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
2219            count += len(self.location)
2220
2221        if self.flags & (
2222            VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
2223        ):
2224            count += 1
2225        if self.flags & VarComponentFlags.HAVE_ROTATION:
2226            count += 1
2227        if self.flags & (
2228            VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
2229        ):
2230            count += 1
2231        if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
2232            count += 1
2233        if self.flags & (
2234            VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
2235        ):
2236            count += 1
2237
2238        return count
2239
2240    def getCoordinatesAndControls(self):
2241        coords = []
2242        controls = []
2243
2244        if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
2245            for tag, v in self.location.items():
2246                controls.append(tag)
2247                coords.append((fl2fi(v, 14), 0))
2248
2249        if self.flags & (
2250            VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
2251        ):
2252            controls.append("translate")
2253            coords.append((self.transform.translateX, self.transform.translateY))
2254        if self.flags & VarComponentFlags.HAVE_ROTATION:
2255            controls.append("rotation")
2256            coords.append((fl2fi(self.transform.rotation / 180, 12), 0))
2257        if self.flags & (
2258            VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
2259        ):
2260            controls.append("scale")
2261            coords.append(
2262                (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10))
2263            )
2264        if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
2265            controls.append("skew")
2266            coords.append(
2267                (
2268                    fl2fi(self.transform.skewX / -180, 12),
2269                    fl2fi(self.transform.skewY / 180, 12),
2270                )
2271            )
2272        if self.flags & (
2273            VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
2274        ):
2275            controls.append("tCenter")
2276            coords.append((self.transform.tCenterX, self.transform.tCenterY))
2277
2278        return coords, controls
2279
2280    def setCoordinates(self, coords):
2281        i = 0
2282
2283        if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
2284            newLocation = {}
2285            for tag in self.location:
2286                newLocation[tag] = fi2fl(coords[i][0], 14)
2287                i += 1
2288            self.location = newLocation
2289
2290        self.transform = DecomposedTransform()
2291        if self.flags & (
2292            VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
2293        ):
2294            self.transform.translateX, self.transform.translateY = coords[i]
2295            i += 1
2296        if self.flags & VarComponentFlags.HAVE_ROTATION:
2297            self.transform.rotation = fi2fl(coords[i][0], 12) * 180
2298            i += 1
2299        if self.flags & (
2300            VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
2301        ):
2302            self.transform.scaleX, self.transform.scaleY = fi2fl(
2303                coords[i][0], 10
2304            ), fi2fl(coords[i][1], 10)
2305            i += 1
2306        if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
2307            self.transform.skewX, self.transform.skewY = (
2308                fi2fl(coords[i][0], 12) * -180,
2309                fi2fl(coords[i][1], 12) * 180,
2310            )
2311            i += 1
2312        if self.flags & (
2313            VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
2314        ):
2315            self.transform.tCenterX, self.transform.tCenterY = coords[i]
2316            i += 1
2317
2318        return coords[i:]
2319
2320    def __eq__(self, other):
2321        if type(self) != type(other):
2322            return NotImplemented
2323        return self.__dict__ == other.__dict__
2324
2325    def __ne__(self, other):
2326        result = self.__eq__(other)
2327        return result if result is NotImplemented else not result
2328
2329
2330class GlyphCoordinates(object):
2331    """A list of glyph coordinates.
2332
2333    Unlike an ordinary list, this is a numpy-like matrix object which supports
2334    matrix addition, scalar multiplication and other operations described below.
2335    """
2336
2337    def __init__(self, iterable=[]):
2338        self._a = array.array("d")
2339        self.extend(iterable)
2340
2341    @property
2342    def array(self):
2343        """Returns the underlying array of coordinates"""
2344        return self._a
2345
2346    @staticmethod
2347    def zeros(count):
2348        """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)"""
2349        g = GlyphCoordinates()
2350        g._a.frombytes(bytes(count * 2 * g._a.itemsize))
2351        return g
2352
2353    def copy(self):
2354        """Creates a new ``GlyphCoordinates`` object which is a copy of the current one."""
2355        c = GlyphCoordinates()
2356        c._a.extend(self._a)
2357        return c
2358
2359    def __len__(self):
2360        """Returns the number of coordinates in the array."""
2361        return len(self._a) // 2
2362
2363    def __getitem__(self, k):
2364        """Returns a two element tuple (x,y)"""
2365        a = self._a
2366        if isinstance(k, slice):
2367            indices = range(*k.indices(len(self)))
2368            # Instead of calling ourselves recursively, duplicate code; faster
2369            ret = []
2370            for k in indices:
2371                x = a[2 * k]
2372                y = a[2 * k + 1]
2373                ret.append(
2374                    (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2375                )
2376            return ret
2377        x = a[2 * k]
2378        y = a[2 * k + 1]
2379        return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2380
2381    def __setitem__(self, k, v):
2382        """Sets a point's coordinates to a two element tuple (x,y)"""
2383        if isinstance(k, slice):
2384            indices = range(*k.indices(len(self)))
2385            # XXX This only works if len(v) == len(indices)
2386            for j, i in enumerate(indices):
2387                self[i] = v[j]
2388            return
2389        self._a[2 * k], self._a[2 * k + 1] = v
2390
2391    def __delitem__(self, i):
2392        """Removes a point from the list"""
2393        i = (2 * i) % len(self._a)
2394        del self._a[i]
2395        del self._a[i]
2396
2397    def __repr__(self):
2398        return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])"
2399
2400    def append(self, p):
2401        self._a.extend(tuple(p))
2402
2403    def extend(self, iterable):
2404        for p in iterable:
2405            self._a.extend(p)
2406
2407    def toInt(self, *, round=otRound):
2408        if round is noRound:
2409            return
2410        a = self._a
2411        for i in range(len(a)):
2412            a[i] = round(a[i])
2413
2414    def calcBounds(self):
2415        a = self._a
2416        if not a:
2417            return 0, 0, 0, 0
2418        xs = a[0::2]
2419        ys = a[1::2]
2420        return min(xs), min(ys), max(xs), max(ys)
2421
2422    def calcIntBounds(self, round=otRound):
2423        return tuple(round(v) for v in self.calcBounds())
2424
2425    def relativeToAbsolute(self):
2426        a = self._a
2427        x, y = 0, 0
2428        for i in range(0, len(a), 2):
2429            a[i] = x = a[i] + x
2430            a[i + 1] = y = a[i + 1] + y
2431
2432    def absoluteToRelative(self):
2433        a = self._a
2434        x, y = 0, 0
2435        for i in range(0, len(a), 2):
2436            nx = a[i]
2437            ny = a[i + 1]
2438            a[i] = nx - x
2439            a[i + 1] = ny - y
2440            x = nx
2441            y = ny
2442
2443    def translate(self, p):
2444        """
2445        >>> GlyphCoordinates([(1,2)]).translate((.5,0))
2446        """
2447        x, y = p
2448        if x == 0 and y == 0:
2449            return
2450        a = self._a
2451        for i in range(0, len(a), 2):
2452            a[i] += x
2453            a[i + 1] += y
2454
2455    def scale(self, p):
2456        """
2457        >>> GlyphCoordinates([(1,2)]).scale((.5,0))
2458        """
2459        x, y = p
2460        if x == 1 and y == 1:
2461            return
2462        a = self._a
2463        for i in range(0, len(a), 2):
2464            a[i] *= x
2465            a[i + 1] *= y
2466
2467    def transform(self, t):
2468        """
2469        >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
2470        """
2471        a = self._a
2472        for i in range(0, len(a), 2):
2473            x = a[i]
2474            y = a[i + 1]
2475            px = x * t[0][0] + y * t[1][0]
2476            py = x * t[0][1] + y * t[1][1]
2477            a[i] = px
2478            a[i + 1] = py
2479
2480    def __eq__(self, other):
2481        """
2482        >>> g = GlyphCoordinates([(1,2)])
2483        >>> g2 = GlyphCoordinates([(1.0,2)])
2484        >>> g3 = GlyphCoordinates([(1.5,2)])
2485        >>> g == g2
2486        True
2487        >>> g == g3
2488        False
2489        >>> g2 == g3
2490        False
2491        """
2492        if type(self) != type(other):
2493            return NotImplemented
2494        return self._a == other._a
2495
2496    def __ne__(self, other):
2497        """
2498        >>> g = GlyphCoordinates([(1,2)])
2499        >>> g2 = GlyphCoordinates([(1.0,2)])
2500        >>> g3 = GlyphCoordinates([(1.5,2)])
2501        >>> g != g2
2502        False
2503        >>> g != g3
2504        True
2505        >>> g2 != g3
2506        True
2507        """
2508        result = self.__eq__(other)
2509        return result if result is NotImplemented else not result
2510
2511    # Math operations
2512
2513    def __pos__(self):
2514        """
2515        >>> g = GlyphCoordinates([(1,2)])
2516        >>> g
2517        GlyphCoordinates([(1, 2)])
2518        >>> g2 = +g
2519        >>> g2
2520        GlyphCoordinates([(1, 2)])
2521        >>> g2.translate((1,0))
2522        >>> g2
2523        GlyphCoordinates([(2, 2)])
2524        >>> g
2525        GlyphCoordinates([(1, 2)])
2526        """
2527        return self.copy()
2528
2529    def __neg__(self):
2530        """
2531        >>> g = GlyphCoordinates([(1,2)])
2532        >>> g
2533        GlyphCoordinates([(1, 2)])
2534        >>> g2 = -g
2535        >>> g2
2536        GlyphCoordinates([(-1, -2)])
2537        >>> g
2538        GlyphCoordinates([(1, 2)])
2539        """
2540        r = self.copy()
2541        a = r._a
2542        for i in range(len(a)):
2543            a[i] = -a[i]
2544        return r
2545
2546    def __round__(self, *, round=otRound):
2547        r = self.copy()
2548        r.toInt(round=round)
2549        return r
2550
2551    def __add__(self, other):
2552        return self.copy().__iadd__(other)
2553
2554    def __sub__(self, other):
2555        return self.copy().__isub__(other)
2556
2557    def __mul__(self, other):
2558        return self.copy().__imul__(other)
2559
2560    def __truediv__(self, other):
2561        return self.copy().__itruediv__(other)
2562
2563    __radd__ = __add__
2564    __rmul__ = __mul__
2565
2566    def __rsub__(self, other):
2567        return other + (-self)
2568
2569    def __iadd__(self, other):
2570        """
2571        >>> g = GlyphCoordinates([(1,2)])
2572        >>> g += (.5,0)
2573        >>> g
2574        GlyphCoordinates([(1.5, 2)])
2575        >>> g2 = GlyphCoordinates([(3,4)])
2576        >>> g += g2
2577        >>> g
2578        GlyphCoordinates([(4.5, 6)])
2579        """
2580        if isinstance(other, tuple):
2581            assert len(other) == 2
2582            self.translate(other)
2583            return self
2584        if isinstance(other, GlyphCoordinates):
2585            other = other._a
2586            a = self._a
2587            assert len(a) == len(other)
2588            for i in range(len(a)):
2589                a[i] += other[i]
2590            return self
2591        return NotImplemented
2592
2593    def __isub__(self, other):
2594        """
2595        >>> g = GlyphCoordinates([(1,2)])
2596        >>> g -= (.5,0)
2597        >>> g
2598        GlyphCoordinates([(0.5, 2)])
2599        >>> g2 = GlyphCoordinates([(3,4)])
2600        >>> g -= g2
2601        >>> g
2602        GlyphCoordinates([(-2.5, -2)])
2603        """
2604        if isinstance(other, tuple):
2605            assert len(other) == 2
2606            self.translate((-other[0], -other[1]))
2607            return self
2608        if isinstance(other, GlyphCoordinates):
2609            other = other._a
2610            a = self._a
2611            assert len(a) == len(other)
2612            for i in range(len(a)):
2613                a[i] -= other[i]
2614            return self
2615        return NotImplemented
2616
2617    def __imul__(self, other):
2618        """
2619        >>> g = GlyphCoordinates([(1,2)])
2620        >>> g *= (2,.5)
2621        >>> g *= 2
2622        >>> g
2623        GlyphCoordinates([(4, 2)])
2624        >>> g = GlyphCoordinates([(1,2)])
2625        >>> g *= 2
2626        >>> g
2627        GlyphCoordinates([(2, 4)])
2628        """
2629        if isinstance(other, tuple):
2630            assert len(other) == 2
2631            self.scale(other)
2632            return self
2633        if isinstance(other, Number):
2634            if other == 1:
2635                return self
2636            a = self._a
2637            for i in range(len(a)):
2638                a[i] *= other
2639            return self
2640        return NotImplemented
2641
2642    def __itruediv__(self, other):
2643        """
2644        >>> g = GlyphCoordinates([(1,3)])
2645        >>> g /= (.5,1.5)
2646        >>> g /= 2
2647        >>> g
2648        GlyphCoordinates([(1, 1)])
2649        """
2650        if isinstance(other, Number):
2651            other = (other, other)
2652        if isinstance(other, tuple):
2653            if other == (1, 1):
2654                return self
2655            assert len(other) == 2
2656            self.scale((1.0 / other[0], 1.0 / other[1]))
2657            return self
2658        return NotImplemented
2659
2660    def __bool__(self):
2661        """
2662        >>> g = GlyphCoordinates([])
2663        >>> bool(g)
2664        False
2665        >>> g = GlyphCoordinates([(0,0), (0.,0)])
2666        >>> bool(g)
2667        True
2668        >>> g = GlyphCoordinates([(0,0), (1,0)])
2669        >>> bool(g)
2670        True
2671        >>> g = GlyphCoordinates([(0,.5), (0,0)])
2672        >>> bool(g)
2673        True
2674        """
2675        return bool(self._a)
2676
2677    __nonzero__ = __bool__
2678
2679
2680if __name__ == "__main__":
2681    import doctest, sys
2682
2683    sys.exit(doctest.testmod().failed)
2684