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