Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_g_l_y_f.py: 13%
1639 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""_g_l_y_f.py -- Converter classes for the 'glyf' table."""
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
36log = logging.getLogger(__name__)
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])
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
56class table__g_l_y_f(DefaultTable.DefaultTable):
57 """Glyph Data Table
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`::
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)
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::
75 >> glyf = font["glyf"]
76 >> len(glyf["Aacute"].components)
77 2
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::
82 >> font["glyf"]["divisionslash"] = Glyph()
83 >> font["hmtx"]["divisionslash"] = (640, 0)
85 """
87 dependencies = ["fvar"]
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
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()
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)
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)
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
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
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()
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)
279 def setGlyphOrder(self, glyphOrder):
280 """Sets the glyph order
282 Args:
283 glyphOrder ([str]): List of glyph names in order.
284 """
285 self.glyphOrder = glyphOrder
286 self._reverseGlyphOrder = {}
288 def getGlyphName(self, glyphID):
289 """Returns the name for the glyph with the given ID.
291 Raises a ``KeyError`` if the glyph name is not found in the font.
292 """
293 return self.glyphOrder[glyphID]
295 def _buildReverseGlyphOrderDict(self):
296 self._reverseGlyphOrder = d = {}
297 for glyphID, glyphName in enumerate(self.glyphOrder):
298 d[glyphName] = glyphID
300 def getGlyphID(self, glyphName):
301 """Returns the ID of the glyph with the given name.
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
314 def removeHinting(self):
315 """Removes TrueType hints from all glyphs in the glyphset.
317 See :py:meth:`Glyph.removeHinting`.
318 """
319 for glyph in self.glyphs.values():
320 glyph.removeHinting()
322 def keys(self):
323 return self.glyphs.keys()
325 def has_key(self, glyphName):
326 return glyphName in self.glyphs
328 __contains__ = has_key
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
336 def __getitem__(self, glyphName):
337 glyph = self.glyphs[glyphName]
338 glyph.expand(self)
339 return glyph
341 def __setitem__(self, glyphName, glyph):
342 self.glyphs[glyphName] = glyph
343 if glyphName not in self.glyphOrder:
344 self.glyphOrder.append(glyphName)
346 def __delitem__(self, glyphName):
347 del self.glyphs[glyphName]
348 self.glyphOrder.remove(glyphName)
350 def __len__(self):
351 assert len(self.glyphOrder) == len(self.glyphs)
352 return len(self.glyphs)
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.
359 'hMetrics' should be ttFont['hmtx'].metrics.
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.
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)
370 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName]
371 leftSideX = glyph.xMin - leftSideBearing
372 rightSideX = leftSideX + horizontalAdvanceWidth
374 if vMetrics:
375 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName]
376 topSideY = topSideBearing + glyph.yMax
377 bottomSideY = topSideY - verticalAdvanceWidth
378 else:
379 bottomSideY = topSideY = 0
381 return [
382 (leftSideX, 0),
383 (rightSideX, 0),
384 (0, topSideY),
385 (0, bottomSideY),
386 ]
388 def _getCoordinatesAndControls(
389 self, glyphName, hMetrics, vMetrics=None, *, round=otRound
390 ):
391 """Return glyph coordinates and controls as expected by "gvar" table.
393 The coordinates includes four "phantom points" for the glyph metrics,
394 as mandated by the "gvar" spec.
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).
406 The "hMetrics" and vMetrics are used to compute the "phantom points" (see
407 the "_getPhantomPoints" method).
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 = []
431 for component in glyph.components:
432 (
433 componentCoords,
434 componentControls,
435 ) = component.getCoordinatesAndControls()
436 coords.extend(componentCoords)
437 controls.extend(componentControls)
439 coords = GlyphCoordinates(coords)
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 )
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
465 def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None):
466 """Set coordinates and metrics for the given glyph.
468 "coord" is an array of GlyphCoordinates which must include the "phantom
469 points" as the last four coordinates.
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.
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]
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]
488 coord = coord[:-4]
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)
505 glyph.recalcBounds(self, boundsDone=set())
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
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
522 # Deprecated
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
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)
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)
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)
571_GlyphControls = namedtuple(
572 "_GlyphControls", "numberOfContours endPts flags components"
573)
576glyphHeaderFormat = """
577 > # big endian
578 numberOfContours: h
579 xMin: h
580 yMin: h
581 xMax: h
582 yMax: h
583"""
585# flags
586flagOnCurve = 0x01
587flagXShort = 0x02
588flagYShort = 0x04
589flagRepeat = 0x08
590flagXsame = 0x10
591flagYsame = 0x20
592flagOverlapSimple = 0x40
593flagCubic = 0x80
595# These flags are kept for XML output after decompiling the coordinates
596keepFlags = flagOnCurve + flagOverlapSimple + flagCubic
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}
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."""
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
638def flagFits(newFlag, oldFlag, mask):
639 newBytes = _flagSignBytes[newFlag & mask]
640 oldBytes = _flagSignBytes[oldFlag & mask]
641 return newBytes == oldBytes or abs(newBytes) > abs(oldBytes)
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 )
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))
662def flagEncodeCoords(flag, x, y, xBytes, yBytes):
663 flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes)
664 flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes)
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)
682CompositeMaxpValues = namedtuple(
683 "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"]
684)
687class Glyph(object):
688 """This class represents an individual TrueType glyph.
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.
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.
702 For a composite glyph, the components can also be accessed via array-like access::
704 >> assert(font["glyf"]["Aacute"].isComposite())
705 >> font["glyf"]["Aacute"][0]
706 <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0>
708 """
710 def __init__(self, data=b""):
711 if not data:
712 # empty char
713 self.numberOfContours = 0
714 return
715 self.data = data
717 def compact(self, glyfTable, recalcBBoxes=True):
718 data = self.compile(glyfTable, recalcBBoxes)
719 self.__dict__.clear()
720 self.data = data
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)
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""
755 if recalcBBoxes:
756 self.recalcBounds(glyfTable, boundsDone=boundsDone)
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
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()
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)
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)
882 def getMaxpValues(self):
883 assert self.numberOfContours > 0
884 return len(self.coordinates), len(self.endPtsOfContours)
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 )
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)
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()
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 )
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
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
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
1030 def compileVarComponents(self, glyfTable):
1031 return b"".join(c.compile(glyfTable) for c in self.components)
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)
1044 deltas = self.coordinates.copy()
1045 deltas.toInt()
1046 deltas.absoluteToRelative()
1048 # TODO(behdad): Add a configuration option for this?
1049 deltas = self.compileDeltasGreedy(self.flags, deltas)
1050 # deltas = self.compileDeltasOptimal(self.flags, deltas)
1052 data.extend(deltas)
1053 return b"".join(data)
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)
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]
1136 flags = []
1137 while bestTuple:
1138 cost, bestTuple, flag, coordBytes = bestTuple
1139 flags.append(flag)
1140 flags.reverse()
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
1165 return (compressedFlags, compressedXs, compressedYs)
1167 def recalcBounds(self, glyfTable, *, boundsDone=None):
1168 """Recalculates the bounds of the glyph.
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
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.
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.
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
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]
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
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))
1222 if bounds is None:
1223 bounds = (0, 0, 0, 0)
1224 self.xMin, self.yMin, self.xMax, self.yMax = bounds
1225 return True
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
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
1241 def getCoordinates(self, glyfTable):
1242 """Return the coordinates, end points and flags
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.
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.
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 """
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()
1316 def getComponentNames(self, glyfTable):
1317 """Returns a list of names of component glyphs used in this glyph
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)
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 []
1332 # Extract components without expanding glyph
1334 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
1335 return [] # Not composite
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)))
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
1359 return components
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
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)
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]
1461 self.data = data
1463 def removeHinting(self):
1464 """Removes TrueType hinting instructions from the glyph."""
1465 self.trim(remove_hinting=True)
1467 def draw(self, pen, glyfTable, offset=0):
1468 """Draws the glyph using the supplied pen object.
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 """
1477 if self.isComposite():
1478 for component in self.components:
1479 glyphName, transform = component.getComponentInfo()
1480 pen.addComponent(glyphName, transform)
1481 return
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"
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()
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 """
1575 if self.isComposite():
1576 for component in self.components:
1577 glyphName, transform = component.getComponentInfo()
1578 pen.addComponent(glyphName, transform)
1579 return
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
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()
1609 def __eq__(self, other):
1610 if type(self) != type(other):
1611 return NotImplemented
1612 return self.__dict__ == other.__dict__
1614 def __ne__(self, other):
1615 result = self.__eq__(other)
1616 return result if result is NotImplemented else not result
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)
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
1632def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
1633 """Drop impliable on-curve points from the (simple) glyph or glyphs.
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.
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.
1645 Args:
1646 interpolatable_glyphs: The glyph or glyphs to modify in-place.
1648 Returns:
1649 The set of point indices that were dropped if any.
1651 Raises:
1652 ValueError if simple glyphs are not in fact interpolatable because they have
1653 different point flags or number of contours.
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
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 )
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
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)
1704 simple_glyphs.append(glyph)
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 )
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
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
1736 return drop if drop is not None else set()
1739class GlyphComponent(object):
1740 """Represents a component within a composite glyph.
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 """
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.
1752 def __init__(self):
1753 pass
1755 def getComponentInfo(self):
1756 """Return information about the component
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
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:]
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:]
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
1831 def compile(self, more, haveInstructions, glyfTable):
1832 data = b""
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
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
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])
1882 glyphID = glyfTable.getGlyphID(self.glyphName)
1883 return struct.pack(">HH", flags, glyphID) + data
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)]
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()
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"])
1935 def __eq__(self, other):
1936 if type(self) != type(other):
1937 return NotImplemented
1938 return self.__dict__ == other.__dict__
1940 def __ne__(self, other):
1941 result = self.__eq__(other)
1942 return result if result is NotImplemented else not result
1945#
1946# Variable Composite glyphs
1947# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
1948#
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
1969VarComponentTransformMappingValues = namedtuple(
1970 "VarComponentTransformMappingValues",
1971 ["flag", "fractionalBits", "scale", "defaultValue"],
1972)
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}
2005class GlyphVarComponent(object):
2006 MIN_SIZE = 5
2008 def __init__(self):
2009 self.location = {}
2010 self.transform = DecomposedTransform()
2012 @staticmethod
2013 def getSize(data):
2014 size = 5
2015 flags = struct.unpack(">H", data[:2])[0]
2016 numAxes = int(data[2])
2018 if flags & VarComponentFlags.GID_IS_24BIT:
2019 size += 1
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
2028 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
2029 if flags & mapping_values.flag:
2030 size += 2
2032 return size
2034 def decompile(self, data, glyfTable):
2035 flags = struct.unpack(">H", data[:2])[0]
2036 self.flags = int(flags)
2037 data = data[2:]
2039 numAxes = int(data[0])
2040 data = data[1:]
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))
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)
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]
2070 self.location = {
2071 glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues)
2072 }
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
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)
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
2096 return data
2098 def compile(self, glyfTable):
2099 data = b""
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
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
2121 numAxes = len(self.location)
2123 data = data + struct.pack(">B", numAxes)
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)
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)
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)
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
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)
2160 return struct.pack(">H", flags) + data
2162 def toXML(self, writer, ttFont):
2163 attrs = [("glyphName", self.glyphName)]
2165 if hasattr(self, "flags"):
2166 attrs = attrs + [("flags", hex(self.flags))]
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)))
2173 writer.begintag("varComponent", attrs)
2174 writer.newline()
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()
2184 writer.endtag("varComponent")
2185 writer.newline()
2187 def fromXML(self, name, attrs, content, ttFont):
2188 self.glyphName = attrs["glyphName"]
2190 if "flags" in attrs:
2191 self.flags = safeEval(attrs["flags"])
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)
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)
2213 def getPointCount(self):
2214 assert hasattr(self, "flags"), "VarComponent with variations must have flags"
2216 count = 0
2218 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
2219 count += len(self.location)
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
2238 return count
2240 def getCoordinatesAndControls(self):
2241 coords = []
2242 controls = []
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))
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))
2278 return coords, controls
2280 def setCoordinates(self, coords):
2281 i = 0
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
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
2318 return coords[i:]
2320 def __eq__(self, other):
2321 if type(self) != type(other):
2322 return NotImplemented
2323 return self.__dict__ == other.__dict__
2325 def __ne__(self, other):
2326 result = self.__eq__(other)
2327 return result if result is NotImplemented else not result
2330class GlyphCoordinates(object):
2331 """A list of glyph coordinates.
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 """
2337 def __init__(self, iterable=[]):
2338 self._a = array.array("d")
2339 self.extend(iterable)
2341 @property
2342 def array(self):
2343 """Returns the underlying array of coordinates"""
2344 return self._a
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
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
2359 def __len__(self):
2360 """Returns the number of coordinates in the array."""
2361 return len(self._a) // 2
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)
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
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]
2397 def __repr__(self):
2398 return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])"
2400 def append(self, p):
2401 self._a.extend(tuple(p))
2403 def extend(self, iterable):
2404 for p in iterable:
2405 self._a.extend(p)
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])
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)
2422 def calcIntBounds(self, round=otRound):
2423 return tuple(round(v) for v in self.calcBounds())
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
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
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
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
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
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
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
2511 # Math operations
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()
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
2546 def __round__(self, *, round=otRound):
2547 r = self.copy()
2548 r.toInt(round=round)
2549 return r
2551 def __add__(self, other):
2552 return self.copy().__iadd__(other)
2554 def __sub__(self, other):
2555 return self.copy().__isub__(other)
2557 def __mul__(self, other):
2558 return self.copy().__imul__(other)
2560 def __truediv__(self, other):
2561 return self.copy().__itruediv__(other)
2563 __radd__ = __add__
2564 __rmul__ = __mul__
2566 def __rsub__(self, other):
2567 return other + (-self)
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
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
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
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
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)
2677 __nonzero__ = __bool__
2680if __name__ == "__main__":
2681 import doctest, sys
2683 sys.exit(doctest.testmod().failed)