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, compo in enumerate(self.components):
978 if i == lastcomponent:
979 haveInstructions = hasattr(self, "program")
980 more = 0
981 data = data + compo.compile(more, haveInstructions, glyfTable)
982 if haveInstructions:
983 instructions = self.program.getBytecode()
984 data = data + struct.pack(">h", len(instructions)) + instructions
985 return data
986
987 def compileCoordinates(self, *, optimizeSize=True):
988 assert len(self.coordinates) == len(self.flags)
989 data = []
990 endPtsOfContours = array.array("H", self.endPtsOfContours)
991 if sys.byteorder != "big":
992 endPtsOfContours.byteswap()
993 data.append(endPtsOfContours.tobytes())
994 instructions = self.program.getBytecode()
995 data.append(struct.pack(">h", len(instructions)))
996 data.append(instructions)
997
998 deltas = self.coordinates.copy()
999 deltas.toInt()
1000 deltas.absoluteToRelative()
1001
1002 if optimizeSize:
1003 # TODO(behdad): Add a configuration option for this?
1004 deltas = self.compileDeltasGreedy(self.flags, deltas)
1005 # deltas = self.compileDeltasOptimal(self.flags, deltas)
1006 else:
1007 deltas = self.compileDeltasForSpeed(self.flags, deltas)
1008
1009 data.extend(deltas)
1010 return b"".join(data)
1011
1012 def compileDeltasGreedy(self, flags, deltas):
1013 # Implements greedy algorithm for packing coordinate deltas:
1014 # uses shortest representation one coordinate at a time.
1015 compressedFlags = bytearray()
1016 compressedXs = bytearray()
1017 compressedYs = bytearray()
1018 lastflag = None
1019 repeat = 0
1020 for flag, (x, y) in zip(flags, deltas):
1021 # Oh, the horrors of TrueType
1022 # do x
1023 if x == 0:
1024 flag = flag | flagXsame
1025 elif -255 <= x <= 255:
1026 flag = flag | flagXShort
1027 if x > 0:
1028 flag = flag | flagXsame
1029 else:
1030 x = -x
1031 compressedXs.append(x)
1032 else:
1033 compressedXs.extend(struct.pack(">h", x))
1034 # do y
1035 if y == 0:
1036 flag = flag | flagYsame
1037 elif -255 <= y <= 255:
1038 flag = flag | flagYShort
1039 if y > 0:
1040 flag = flag | flagYsame
1041 else:
1042 y = -y
1043 compressedYs.append(y)
1044 else:
1045 compressedYs.extend(struct.pack(">h", y))
1046 # handle repeating flags
1047 if flag == lastflag and repeat != 255:
1048 repeat = repeat + 1
1049 if repeat == 1:
1050 compressedFlags.append(flag)
1051 else:
1052 compressedFlags[-2] = flag | flagRepeat
1053 compressedFlags[-1] = repeat
1054 else:
1055 repeat = 0
1056 compressedFlags.append(flag)
1057 lastflag = flag
1058 return (compressedFlags, compressedXs, compressedYs)
1059
1060 def compileDeltasOptimal(self, flags, deltas):
1061 # Implements optimal, dynaic-programming, algorithm for packing coordinate
1062 # deltas. The savings are negligible :(.
1063 candidates = []
1064 bestTuple = None
1065 bestCost = 0
1066 repeat = 0
1067 for flag, (x, y) in zip(flags, deltas):
1068 # Oh, the horrors of TrueType
1069 flag, coordBytes = flagBest(x, y, flag)
1070 bestCost += 1 + coordBytes
1071 newCandidates = [
1072 (bestCost, bestTuple, flag, coordBytes),
1073 (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes),
1074 ]
1075 for lastCost, lastTuple, lastFlag, coordBytes in candidates:
1076 if (
1077 lastCost + coordBytes <= bestCost + 1
1078 and (lastFlag & flagRepeat)
1079 and (lastFlag < 0xFF00)
1080 and flagSupports(lastFlag, flag)
1081 ):
1082 if (lastFlag & 0xFF) == (
1083 flag | flagRepeat
1084 ) and lastCost == bestCost + 1:
1085 continue
1086 newCandidates.append(
1087 (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes)
1088 )
1089 candidates = newCandidates
1090 bestTuple = min(candidates, key=lambda t: t[0])
1091 bestCost = bestTuple[0]
1092
1093 flags = []
1094 while bestTuple:
1095 cost, bestTuple, flag, coordBytes = bestTuple
1096 flags.append(flag)
1097 flags.reverse()
1098
1099 compressedFlags = bytearray()
1100 compressedXs = bytearray()
1101 compressedYs = bytearray()
1102 coords = iter(deltas)
1103 ff = []
1104 for flag in flags:
1105 repeatCount, flag = flag >> 8, flag & 0xFF
1106 compressedFlags.append(flag)
1107 if flag & flagRepeat:
1108 assert repeatCount > 0
1109 compressedFlags.append(repeatCount)
1110 else:
1111 assert repeatCount == 0
1112 for i in range(1 + repeatCount):
1113 x, y = next(coords)
1114 flagEncodeCoords(flag, x, y, compressedXs, compressedYs)
1115 ff.append(flag)
1116 try:
1117 next(coords)
1118 raise Exception("internal error")
1119 except StopIteration:
1120 pass
1121
1122 return (compressedFlags, compressedXs, compressedYs)
1123
1124 def compileDeltasForSpeed(self, flags, deltas):
1125 # uses widest representation needed, for all deltas.
1126 compressedFlags = bytearray()
1127 compressedXs = bytearray()
1128 compressedYs = bytearray()
1129
1130 # Compute the necessary width for each axis
1131 xs = [d[0] for d in deltas]
1132 ys = [d[1] for d in deltas]
1133 minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys)
1134 xZero = minX == 0 and maxX == 0
1135 yZero = minY == 0 and maxY == 0
1136 xShort = -255 <= minX <= maxX <= 255
1137 yShort = -255 <= minY <= maxY <= 255
1138
1139 lastflag = None
1140 repeat = 0
1141 for flag, (x, y) in zip(flags, deltas):
1142 # Oh, the horrors of TrueType
1143 # do x
1144 if xZero:
1145 flag = flag | flagXsame
1146 elif xShort:
1147 flag = flag | flagXShort
1148 if x > 0:
1149 flag = flag | flagXsame
1150 else:
1151 x = -x
1152 compressedXs.append(x)
1153 else:
1154 compressedXs.extend(struct.pack(">h", x))
1155 # do y
1156 if yZero:
1157 flag = flag | flagYsame
1158 elif yShort:
1159 flag = flag | flagYShort
1160 if y > 0:
1161 flag = flag | flagYsame
1162 else:
1163 y = -y
1164 compressedYs.append(y)
1165 else:
1166 compressedYs.extend(struct.pack(">h", y))
1167 # handle repeating flags
1168 if flag == lastflag and repeat != 255:
1169 repeat = repeat + 1
1170 if repeat == 1:
1171 compressedFlags.append(flag)
1172 else:
1173 compressedFlags[-2] = flag | flagRepeat
1174 compressedFlags[-1] = repeat
1175 else:
1176 repeat = 0
1177 compressedFlags.append(flag)
1178 lastflag = flag
1179 return (compressedFlags, compressedXs, compressedYs)
1180
1181 def recalcBounds(self, glyfTable, *, boundsDone=None):
1182 """Recalculates the bounds of the glyph.
1183
1184 Each glyph object stores its bounding box in the
1185 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1186 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1187 must be provided to resolve component bounds.
1188 """
1189 if self.isComposite() and self.tryRecalcBoundsComposite(
1190 glyfTable, boundsDone=boundsDone
1191 ):
1192 return
1193 try:
1194 coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound)
1195 self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
1196 except NotImplementedError:
1197 pass
1198
1199 def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None):
1200 """Try recalculating the bounds of a composite glyph that has
1201 certain constrained properties. Namely, none of the components
1202 have a transform other than an integer translate, and none
1203 uses the anchor points.
1204
1205 Each glyph object stores its bounding box in the
1206 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
1207 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
1208 must be provided to resolve component bounds.
1209
1210 Return True if bounds were calculated, False otherwise.
1211 """
1212 for compo in self.components:
1213 if not compo._hasOnlyIntegerTranslate():
1214 return False
1215
1216 # All components are untransformed and have an integer x/y translate
1217 bounds = None
1218 for compo in self.components:
1219 glyphName = compo.glyphName
1220 g = glyfTable[glyphName]
1221
1222 if boundsDone is None or glyphName not in boundsDone:
1223 g.recalcBounds(glyfTable, boundsDone=boundsDone)
1224 if boundsDone is not None:
1225 boundsDone.add(glyphName)
1226 # empty components shouldn't update the bounds of the parent glyph
1227 if g.yMin == g.yMax and g.xMin == g.xMax:
1228 continue
1229
1230 x, y = compo.x, compo.y
1231 bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y))
1232 bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y))
1233
1234 if bounds is None:
1235 bounds = (0, 0, 0, 0)
1236 self.xMin, self.yMin, self.xMax, self.yMax = bounds
1237 return True
1238
1239 def isComposite(self):
1240 """Test whether a glyph has components"""
1241 if hasattr(self, "data"):
1242 return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False
1243 else:
1244 return self.numberOfContours == -1
1245
1246 def getCoordinates(self, glyfTable, *, round=noRound):
1247 """Return the coordinates, end points and flags
1248
1249 This method returns three values: A :py:class:`GlyphCoordinates` object,
1250 a list of the indexes of the final points of each contour (allowing you
1251 to split up the coordinates list into contours) and a list of flags.
1252
1253 On simple glyphs, this method returns information from the glyph's own
1254 contours; on composite glyphs, it "flattens" all components recursively
1255 to return a list of coordinates representing all the components involved
1256 in the glyph.
1257
1258 To interpret the flags for each point, see the "Simple Glyph Flags"
1259 section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`.
1260 """
1261
1262 if self.numberOfContours > 0:
1263 return self.coordinates, self.endPtsOfContours, self.flags
1264 elif self.isComposite():
1265 # it's a composite
1266 allCoords = GlyphCoordinates()
1267 allFlags = bytearray()
1268 allEndPts = []
1269 for compo in self.components:
1270 g = glyfTable[compo.glyphName]
1271 try:
1272 coordinates, endPts, flags = g.getCoordinates(
1273 glyfTable, round=round
1274 )
1275 except RecursionError:
1276 raise ttLib.TTLibError(
1277 "glyph '%s' contains a recursive component reference"
1278 % compo.glyphName
1279 )
1280 coordinates = GlyphCoordinates(coordinates)
1281 # if asked to round e.g. while computing bboxes, it's important we
1282 # do it immediately before a component transform is applied to a
1283 # simple glyph's coordinates in case these might still contain floats;
1284 # however, if the referenced component glyph is another composite, we
1285 # must not round here but only at the end, after all the nested
1286 # transforms have been applied, or else rounding errors will compound.
1287 if round is not noRound and g.numberOfContours > 0:
1288 coordinates.toInt(round=round)
1289 if hasattr(compo, "firstPt"):
1290 # component uses two reference points: we apply the transform _before_
1291 # computing the offset between the points
1292 if hasattr(compo, "transform"):
1293 coordinates.transform(compo.transform)
1294 x1, y1 = allCoords[compo.firstPt]
1295 x2, y2 = coordinates[compo.secondPt]
1296 move = x1 - x2, y1 - y2
1297 coordinates.translate(move)
1298 else:
1299 # component uses XY offsets
1300 move = compo.x, compo.y
1301 if not hasattr(compo, "transform"):
1302 coordinates.translate(move)
1303 else:
1304 apple_way = compo.flags & SCALED_COMPONENT_OFFSET
1305 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
1306 assert not (apple_way and ms_way)
1307 if not (apple_way or ms_way):
1308 scale_component_offset = (
1309 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
1310 )
1311 else:
1312 scale_component_offset = apple_way
1313 if scale_component_offset:
1314 # the Apple way: first move, then scale (ie. scale the component offset)
1315 coordinates.translate(move)
1316 coordinates.transform(compo.transform)
1317 else:
1318 # the MS way: first scale, then move
1319 coordinates.transform(compo.transform)
1320 coordinates.translate(move)
1321 offset = len(allCoords)
1322 allEndPts.extend(e + offset for e in endPts)
1323 allCoords.extend(coordinates)
1324 allFlags.extend(flags)
1325 return allCoords, allEndPts, allFlags
1326 else:
1327 return GlyphCoordinates(), [], bytearray()
1328
1329 def getComponentNames(self, glyfTable):
1330 """Returns a list of names of component glyphs used in this glyph
1331
1332 This method can be used on simple glyphs (in which case it returns an
1333 empty list) or composite glyphs.
1334 """
1335 if not hasattr(self, "data"):
1336 if self.isComposite():
1337 return [c.glyphName for c in self.components]
1338 else:
1339 return []
1340
1341 # Extract components without expanding glyph
1342
1343 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
1344 return [] # Not composite
1345
1346 data = self.data
1347 i = 10
1348 components = []
1349 more = 1
1350 while more:
1351 flags, glyphID = struct.unpack(">HH", data[i : i + 4])
1352 i += 4
1353 flags = int(flags)
1354 components.append(glyfTable.getGlyphName(int(glyphID)))
1355
1356 if flags & ARG_1_AND_2_ARE_WORDS:
1357 i += 4
1358 else:
1359 i += 2
1360 if flags & WE_HAVE_A_SCALE:
1361 i += 2
1362 elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1363 i += 4
1364 elif flags & WE_HAVE_A_TWO_BY_TWO:
1365 i += 8
1366 more = flags & MORE_COMPONENTS
1367
1368 return components
1369
1370 def trim(self, remove_hinting=False):
1371 """Remove padding and, if requested, hinting, from a glyph.
1372 This works on both expanded and compacted glyphs, without
1373 expanding it."""
1374 if not hasattr(self, "data"):
1375 if remove_hinting:
1376 if self.isComposite():
1377 if hasattr(self, "program"):
1378 del self.program
1379 else:
1380 self.program = ttProgram.Program()
1381 self.program.fromBytecode([])
1382 # No padding to trim.
1383 return
1384 if not self.data:
1385 return
1386 numContours = struct.unpack(">h", self.data[:2])[0]
1387 data = bytearray(self.data)
1388 i = 10
1389 if numContours >= 0:
1390 i += 2 * numContours # endPtsOfContours
1391 nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1
1392 instructionLen = (data[i] << 8) | data[i + 1]
1393 if remove_hinting:
1394 # Zero instruction length
1395 data[i] = data[i + 1] = 0
1396 i += 2
1397 if instructionLen:
1398 # Splice it out
1399 data = data[:i] + data[i + instructionLen :]
1400 instructionLen = 0
1401 else:
1402 i += 2 + instructionLen
1403
1404 coordBytes = 0
1405 j = 0
1406 while True:
1407 flag = data[i]
1408 i = i + 1
1409 repeat = 1
1410 if flag & flagRepeat:
1411 repeat = data[i] + 1
1412 i = i + 1
1413 xBytes = yBytes = 0
1414 if flag & flagXShort:
1415 xBytes = 1
1416 elif not (flag & flagXsame):
1417 xBytes = 2
1418 if flag & flagYShort:
1419 yBytes = 1
1420 elif not (flag & flagYsame):
1421 yBytes = 2
1422 coordBytes += (xBytes + yBytes) * repeat
1423 j += repeat
1424 if j >= nCoordinates:
1425 break
1426 assert j == nCoordinates, "bad glyph flags"
1427 i += coordBytes
1428 # Remove padding
1429 data = data[:i]
1430 elif self.isComposite():
1431 more = 1
1432 we_have_instructions = False
1433 while more:
1434 flags = (data[i] << 8) | data[i + 1]
1435 if remove_hinting:
1436 flags &= ~WE_HAVE_INSTRUCTIONS
1437 if flags & WE_HAVE_INSTRUCTIONS:
1438 we_have_instructions = True
1439 data[i + 0] = flags >> 8
1440 data[i + 1] = flags & 0xFF
1441 i += 4
1442 flags = int(flags)
1443
1444 if flags & ARG_1_AND_2_ARE_WORDS:
1445 i += 4
1446 else:
1447 i += 2
1448 if flags & WE_HAVE_A_SCALE:
1449 i += 2
1450 elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
1451 i += 4
1452 elif flags & WE_HAVE_A_TWO_BY_TWO:
1453 i += 8
1454 more = flags & MORE_COMPONENTS
1455 if we_have_instructions:
1456 instructionLen = (data[i] << 8) | data[i + 1]
1457 i += 2 + instructionLen
1458 # Remove padding
1459 data = data[:i]
1460
1461 self.data = data
1462
1463 def removeHinting(self):
1464 """Removes TrueType hinting instructions from the glyph."""
1465 self.trim(remove_hinting=True)
1466
1467 def draw(self, pen, glyfTable, offset=0):
1468 """Draws the glyph using the supplied pen object.
1469
1470 Arguments:
1471 pen: An object conforming to the pen protocol.
1472 glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components.
1473 offset (int): A horizontal offset. If provided, all coordinates are
1474 translated by this offset.
1475 """
1476
1477 if self.isComposite():
1478 for component in self.components:
1479 glyphName, transform = component.getComponentInfo()
1480 pen.addComponent(glyphName, transform)
1481 return
1482
1483 self.expand(glyfTable)
1484 coordinates, endPts, flags = self.getCoordinates(glyfTable)
1485 if offset:
1486 coordinates = coordinates.copy()
1487 coordinates.translate((offset, 0))
1488 start = 0
1489 maybeInt = lambda v: int(v) if v == int(v) else v
1490 for end in endPts:
1491 end = end + 1
1492 contour = coordinates[start:end]
1493 cFlags = [flagOnCurve & f for f in flags[start:end]]
1494 cuFlags = [flagCubic & f for f in flags[start:end]]
1495 start = end
1496 if 1 not in cFlags:
1497 assert all(cuFlags) or not any(cuFlags)
1498 cubic = all(cuFlags)
1499 if cubic:
1500 count = len(contour)
1501 assert count % 2 == 0, "Odd number of cubic off-curves undefined"
1502 l = contour[-1]
1503 f = contour[0]
1504 p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5))
1505 pen.moveTo(p0)
1506 for i in range(0, count, 2):
1507 p1 = contour[i]
1508 p2 = contour[i + 1]
1509 p4 = contour[i + 2 if i + 2 < count else 0]
1510 p3 = (
1511 maybeInt((p2[0] + p4[0]) * 0.5),
1512 maybeInt((p2[1] + p4[1]) * 0.5),
1513 )
1514 pen.curveTo(p1, p2, p3)
1515 else:
1516 # There is not a single on-curve point on the curve,
1517 # use pen.qCurveTo's special case by specifying None
1518 # as the on-curve point.
1519 contour.append(None)
1520 pen.qCurveTo(*contour)
1521 else:
1522 # Shuffle the points so that the contour is guaranteed
1523 # to *end* in an on-curve point, which we'll use for
1524 # the moveTo.
1525 firstOnCurve = cFlags.index(1) + 1
1526 contour = contour[firstOnCurve:] + contour[:firstOnCurve]
1527 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
1528 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve]
1529 pen.moveTo(contour[-1])
1530 while contour:
1531 nextOnCurve = cFlags.index(1) + 1
1532 if nextOnCurve == 1:
1533 # Skip a final lineTo(), as it is implied by
1534 # pen.closePath()
1535 if len(contour) > 1:
1536 pen.lineTo(contour[0])
1537 else:
1538 cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]]
1539 assert all(cubicFlags) or not any(cubicFlags)
1540 cubic = any(cubicFlags)
1541 if cubic:
1542 assert all(
1543 cubicFlags
1544 ), "Mixed cubic and quadratic segment undefined"
1545
1546 count = nextOnCurve
1547 assert (
1548 count >= 3
1549 ), "At least two cubic off-curve points required"
1550 assert (
1551 count - 1
1552 ) % 2 == 0, "Odd number of cubic off-curves undefined"
1553 for i in range(0, count - 3, 2):
1554 p1 = contour[i]
1555 p2 = contour[i + 1]
1556 p4 = contour[i + 2]
1557 p3 = (
1558 maybeInt((p2[0] + p4[0]) * 0.5),
1559 maybeInt((p2[1] + p4[1]) * 0.5),
1560 )
1561 lastOnCurve = p3
1562 pen.curveTo(p1, p2, p3)
1563 pen.curveTo(*contour[count - 3 : count])
1564 else:
1565 pen.qCurveTo(*contour[:nextOnCurve])
1566 contour = contour[nextOnCurve:]
1567 cFlags = cFlags[nextOnCurve:]
1568 cuFlags = cuFlags[nextOnCurve:]
1569 pen.closePath()
1570
1571 def drawPoints(self, pen, glyfTable, offset=0):
1572 """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(),
1573 this will not change the point indices.
1574 """
1575
1576 if self.isComposite():
1577 for component in self.components:
1578 glyphName, transform = component.getComponentInfo()
1579 pen.addComponent(glyphName, transform)
1580 return
1581
1582 coordinates, endPts, flags = self.getCoordinates(glyfTable)
1583 if offset:
1584 coordinates = coordinates.copy()
1585 coordinates.translate((offset, 0))
1586 start = 0
1587 for end in endPts:
1588 end = end + 1
1589 contour = coordinates[start:end]
1590 cFlags = flags[start:end]
1591 start = end
1592 pen.beginPath()
1593 # Start with the appropriate segment type based on the final segment
1594
1595 if cFlags[-1] & flagOnCurve:
1596 segmentType = "line"
1597 elif cFlags[-1] & flagCubic:
1598 segmentType = "curve"
1599 else:
1600 segmentType = "qcurve"
1601 for i, pt in enumerate(contour):
1602 if cFlags[i] & flagOnCurve:
1603 pen.addPoint(pt, segmentType=segmentType)
1604 segmentType = "line"
1605 else:
1606 pen.addPoint(pt)
1607 segmentType = "curve" if cFlags[i] & flagCubic else "qcurve"
1608 pen.endPath()
1609
1610 def __eq__(self, other):
1611 if type(self) != type(other):
1612 return NotImplemented
1613 return self.__dict__ == other.__dict__
1614
1615 def __ne__(self, other):
1616 result = self.__eq__(other)
1617 return result if result is NotImplemented else not result
1618
1619
1620# Vector.__round__ uses the built-in (Banker's) `round` but we want
1621# to use otRound below
1622_roundv = partial(Vector.__round__, round=otRound)
1623
1624
1625def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool:
1626 # True if p1 is in the middle of p0 and p2, either before or after rounding
1627 p0 = Vector(p0)
1628 p1 = Vector(p1)
1629 p2 = Vector(p2)
1630 return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2
1631
1632
1633def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
1634 """Drop impliable on-curve points from the (simple) glyph or glyphs.
1635
1636 In TrueType glyf outlines, on-curve points can be implied when they are located at
1637 the midpoint of the line connecting two consecutive off-curve points.
1638
1639 If more than one glyphs are passed, these are assumed to be interpolatable masters
1640 of the same glyph impliable, and thus only the on-curve points that are impliable
1641 for all of them will actually be implied.
1642 Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more
1643 contours are considered.
1644 The input glyph(s) is/are modified in-place.
1645
1646 Args:
1647 interpolatable_glyphs: The glyph or glyphs to modify in-place.
1648
1649 Returns:
1650 The set of point indices that were dropped if any.
1651
1652 Raises:
1653 ValueError if simple glyphs are not in fact interpolatable because they have
1654 different point flags or number of contours.
1655
1656 Reference:
1657 https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
1658 """
1659 staticAttributes = SimpleNamespace(
1660 numberOfContours=None, flags=None, endPtsOfContours=None
1661 )
1662 drop = None
1663 simple_glyphs = []
1664 for i, glyph in enumerate(interpolatable_glyphs):
1665 if glyph.numberOfContours < 1:
1666 # ignore composite or empty glyphs
1667 continue
1668
1669 for attr in staticAttributes.__dict__:
1670 expected = getattr(staticAttributes, attr)
1671 found = getattr(glyph, attr)
1672 if expected is None:
1673 setattr(staticAttributes, attr, found)
1674 elif expected != found:
1675 raise ValueError(
1676 f"Incompatible {attr} for glyph at master index {i}: "
1677 f"expected {expected}, found {found}"
1678 )
1679
1680 may_drop = set()
1681 start = 0
1682 coords = glyph.coordinates
1683 flags = staticAttributes.flags
1684 endPtsOfContours = staticAttributes.endPtsOfContours
1685 for last in endPtsOfContours:
1686 for i in range(start, last + 1):
1687 if not (flags[i] & flagOnCurve):
1688 continue
1689 prv = i - 1 if i > start else last
1690 nxt = i + 1 if i < last else start
1691 if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
1692 continue
1693 # we may drop the ith on-curve if halfway between previous/next off-curves
1694 if not _is_mid_point(coords[prv], coords[i], coords[nxt]):
1695 continue
1696
1697 may_drop.add(i)
1698 start = last + 1
1699 # we only want to drop if ALL interpolatable glyphs have the same implied oncurves
1700 if drop is None:
1701 drop = may_drop
1702 else:
1703 drop.intersection_update(may_drop)
1704
1705 simple_glyphs.append(glyph)
1706
1707 if drop:
1708 # Do the actual dropping
1709 flags = staticAttributes.flags
1710 assert flags is not None
1711 newFlags = array.array(
1712 "B", (flags[i] for i in range(len(flags)) if i not in drop)
1713 )
1714
1715 endPts = staticAttributes.endPtsOfContours
1716 assert endPts is not None
1717 newEndPts = []
1718 i = 0
1719 delta = 0
1720 for d in sorted(drop):
1721 while d > endPts[i]:
1722 newEndPts.append(endPts[i] - delta)
1723 i += 1
1724 delta += 1
1725 while i < len(endPts):
1726 newEndPts.append(endPts[i] - delta)
1727 i += 1
1728
1729 for glyph in simple_glyphs:
1730 coords = glyph.coordinates
1731 glyph.coordinates = GlyphCoordinates(
1732 coords[i] for i in range(len(coords)) if i not in drop
1733 )
1734 glyph.flags = newFlags
1735 glyph.endPtsOfContours = newEndPts
1736
1737 return drop if drop is not None else set()
1738
1739
1740class GlyphComponent(object):
1741 """Represents a component within a composite glyph.
1742
1743 The component is represented internally with four attributes: ``glyphName``,
1744 ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e
1745 no scaling, reflection, or rotation; only translation), the ``transform``
1746 attribute is not present.
1747 """
1748
1749 # The above documentation is not *completely* true, but is *true enough* because
1750 # the rare firstPt/lastPt attributes are not totally supported and nobody seems to
1751 # mind - see below.
1752
1753 def __init__(self):
1754 pass
1755
1756 def getComponentInfo(self):
1757 """Return information about the component
1758
1759 This method returns a tuple of two values: the glyph name of the component's
1760 base glyph, and a transformation matrix. As opposed to accessing the attributes
1761 directly, ``getComponentInfo`` always returns a six-element tuple of the
1762 component's transformation matrix, even when the two-by-two ``.transform``
1763 matrix is not present.
1764 """
1765 # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
1766 # something equivalent in fontTools.objects.glyph (I'd rather not
1767 # convert it to an absolute offset, since it is valuable information).
1768 # This method will now raise "AttributeError: x" on glyphs that use
1769 # this TT feature.
1770 if hasattr(self, "transform"):
1771 [[xx, xy], [yx, yy]] = self.transform
1772 trans = (xx, xy, yx, yy, self.x, self.y)
1773 else:
1774 trans = (1, 0, 0, 1, self.x, self.y)
1775 return self.glyphName, trans
1776
1777 def decompile(self, data, glyfTable):
1778 flags, glyphID = struct.unpack(">HH", data[:4])
1779 self.flags = int(flags)
1780 glyphID = int(glyphID)
1781 self.glyphName = glyfTable.getGlyphName(int(glyphID))
1782 data = data[4:]
1783
1784 if self.flags & ARG_1_AND_2_ARE_WORDS:
1785 if self.flags & ARGS_ARE_XY_VALUES:
1786 self.x, self.y = struct.unpack(">hh", data[:4])
1787 else:
1788 x, y = struct.unpack(">HH", data[:4])
1789 self.firstPt, self.secondPt = int(x), int(y)
1790 data = data[4:]
1791 else:
1792 if self.flags & ARGS_ARE_XY_VALUES:
1793 self.x, self.y = struct.unpack(">bb", data[:2])
1794 else:
1795 x, y = struct.unpack(">BB", data[:2])
1796 self.firstPt, self.secondPt = int(x), int(y)
1797 data = data[2:]
1798
1799 if self.flags & WE_HAVE_A_SCALE:
1800 (scale,) = struct.unpack(">h", data[:2])
1801 self.transform = [
1802 [fi2fl(scale, 14), 0],
1803 [0, fi2fl(scale, 14)],
1804 ] # fixed 2.14
1805 data = data[2:]
1806 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
1807 xscale, yscale = struct.unpack(">hh", data[:4])
1808 self.transform = [
1809 [fi2fl(xscale, 14), 0],
1810 [0, fi2fl(yscale, 14)],
1811 ] # fixed 2.14
1812 data = data[4:]
1813 elif self.flags & WE_HAVE_A_TWO_BY_TWO:
1814 (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8])
1815 self.transform = [
1816 [fi2fl(xscale, 14), fi2fl(scale01, 14)],
1817 [fi2fl(scale10, 14), fi2fl(yscale, 14)],
1818 ] # fixed 2.14
1819 data = data[8:]
1820 more = self.flags & MORE_COMPONENTS
1821 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
1822 self.flags = self.flags & (
1823 ROUND_XY_TO_GRID
1824 | USE_MY_METRICS
1825 | SCALED_COMPONENT_OFFSET
1826 | UNSCALED_COMPONENT_OFFSET
1827 | NON_OVERLAPPING
1828 | OVERLAP_COMPOUND
1829 )
1830 return more, haveInstructions, data
1831
1832 def compile(self, more, haveInstructions, glyfTable):
1833 data = b""
1834
1835 # reset all flags we will calculate ourselves
1836 flags = self.flags & (
1837 ROUND_XY_TO_GRID
1838 | USE_MY_METRICS
1839 | SCALED_COMPONENT_OFFSET
1840 | UNSCALED_COMPONENT_OFFSET
1841 | NON_OVERLAPPING
1842 | OVERLAP_COMPOUND
1843 )
1844 if more:
1845 flags = flags | MORE_COMPONENTS
1846 if haveInstructions:
1847 flags = flags | WE_HAVE_INSTRUCTIONS
1848
1849 if hasattr(self, "firstPt"):
1850 if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255):
1851 data = data + struct.pack(">BB", self.firstPt, self.secondPt)
1852 else:
1853 data = data + struct.pack(">HH", self.firstPt, self.secondPt)
1854 flags = flags | ARG_1_AND_2_ARE_WORDS
1855 else:
1856 x = otRound(self.x)
1857 y = otRound(self.y)
1858 flags = flags | ARGS_ARE_XY_VALUES
1859 if (-128 <= x <= 127) and (-128 <= y <= 127):
1860 data = data + struct.pack(">bb", x, y)
1861 else:
1862 data = data + struct.pack(">hh", x, y)
1863 flags = flags | ARG_1_AND_2_ARE_WORDS
1864
1865 if hasattr(self, "transform"):
1866 transform = [[fl2fi(x, 14) for x in row] for row in self.transform]
1867 if transform[0][1] or transform[1][0]:
1868 flags = flags | WE_HAVE_A_TWO_BY_TWO
1869 data = data + struct.pack(
1870 ">hhhh",
1871 transform[0][0],
1872 transform[0][1],
1873 transform[1][0],
1874 transform[1][1],
1875 )
1876 elif transform[0][0] != transform[1][1]:
1877 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE
1878 data = data + struct.pack(">hh", transform[0][0], transform[1][1])
1879 else:
1880 flags = flags | WE_HAVE_A_SCALE
1881 data = data + struct.pack(">h", transform[0][0])
1882
1883 glyphID = glyfTable.getGlyphID(self.glyphName)
1884 return struct.pack(">HH", flags, glyphID) + data
1885
1886 def toXML(self, writer, ttFont):
1887 attrs = [("glyphName", self.glyphName)]
1888 if not hasattr(self, "firstPt"):
1889 attrs = attrs + [("x", self.x), ("y", self.y)]
1890 else:
1891 attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)]
1892
1893 if hasattr(self, "transform"):
1894 transform = self.transform
1895 if transform[0][1] or transform[1][0]:
1896 attrs = attrs + [
1897 ("scalex", fl2str(transform[0][0], 14)),
1898 ("scale01", fl2str(transform[0][1], 14)),
1899 ("scale10", fl2str(transform[1][0], 14)),
1900 ("scaley", fl2str(transform[1][1], 14)),
1901 ]
1902 elif transform[0][0] != transform[1][1]:
1903 attrs = attrs + [
1904 ("scalex", fl2str(transform[0][0], 14)),
1905 ("scaley", fl2str(transform[1][1], 14)),
1906 ]
1907 else:
1908 attrs = attrs + [("scale", fl2str(transform[0][0], 14))]
1909 attrs = attrs + [("flags", hex(self.flags))]
1910 writer.simpletag("component", attrs)
1911 writer.newline()
1912
1913 def fromXML(self, name, attrs, content, ttFont):
1914 self.glyphName = attrs["glyphName"]
1915 if "firstPt" in attrs:
1916 self.firstPt = safeEval(attrs["firstPt"])
1917 self.secondPt = safeEval(attrs["secondPt"])
1918 else:
1919 self.x = safeEval(attrs["x"])
1920 self.y = safeEval(attrs["y"])
1921 if "scale01" in attrs:
1922 scalex = str2fl(attrs["scalex"], 14)
1923 scale01 = str2fl(attrs["scale01"], 14)
1924 scale10 = str2fl(attrs["scale10"], 14)
1925 scaley = str2fl(attrs["scaley"], 14)
1926 self.transform = [[scalex, scale01], [scale10, scaley]]
1927 elif "scalex" in attrs:
1928 scalex = str2fl(attrs["scalex"], 14)
1929 scaley = str2fl(attrs["scaley"], 14)
1930 self.transform = [[scalex, 0], [0, scaley]]
1931 elif "scale" in attrs:
1932 scale = str2fl(attrs["scale"], 14)
1933 self.transform = [[scale, 0], [0, scale]]
1934 self.flags = safeEval(attrs["flags"])
1935
1936 def __eq__(self, other):
1937 if type(self) != type(other):
1938 return NotImplemented
1939 return self.__dict__ == other.__dict__
1940
1941 def __ne__(self, other):
1942 result = self.__eq__(other)
1943 return result if result is NotImplemented else not result
1944
1945 def _hasOnlyIntegerTranslate(self):
1946 """Return True if it's a 'simple' component.
1947
1948 That is, it has no anchor points and no transform other than integer translate.
1949 """
1950 return (
1951 not hasattr(self, "firstPt")
1952 and not hasattr(self, "transform")
1953 and float(self.x).is_integer()
1954 and float(self.y).is_integer()
1955 )
1956
1957
1958class GlyphCoordinates(object):
1959 """A list of glyph coordinates.
1960
1961 Unlike an ordinary list, this is a numpy-like matrix object which supports
1962 matrix addition, scalar multiplication and other operations described below.
1963 """
1964
1965 def __init__(self, iterable=[]):
1966 self._a = array.array("d")
1967 self.extend(iterable)
1968
1969 @property
1970 def array(self):
1971 """Returns the underlying array of coordinates"""
1972 return self._a
1973
1974 @staticmethod
1975 def zeros(count):
1976 """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)"""
1977 g = GlyphCoordinates()
1978 g._a.frombytes(bytes(count * 2 * g._a.itemsize))
1979 return g
1980
1981 def copy(self):
1982 """Creates a new ``GlyphCoordinates`` object which is a copy of the current one."""
1983 c = GlyphCoordinates()
1984 c._a.extend(self._a)
1985 return c
1986
1987 def __len__(self):
1988 """Returns the number of coordinates in the array."""
1989 return len(self._a) // 2
1990
1991 def __getitem__(self, k):
1992 """Returns a two element tuple (x,y)"""
1993 a = self._a
1994 if isinstance(k, slice):
1995 indices = range(*k.indices(len(self)))
1996 # Instead of calling ourselves recursively, duplicate code; faster
1997 ret = []
1998 for k in indices:
1999 x = a[2 * k]
2000 y = a[2 * k + 1]
2001 ret.append(
2002 (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2003 )
2004 return ret
2005 x = a[2 * k]
2006 y = a[2 * k + 1]
2007 return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
2008
2009 def __setitem__(self, k, v):
2010 """Sets a point's coordinates to a two element tuple (x,y)"""
2011 if isinstance(k, slice):
2012 indices = range(*k.indices(len(self)))
2013 # XXX This only works if len(v) == len(indices)
2014 for j, i in enumerate(indices):
2015 self[i] = v[j]
2016 return
2017 self._a[2 * k], self._a[2 * k + 1] = v
2018
2019 def __delitem__(self, i):
2020 """Removes a point from the list"""
2021 i = (2 * i) % len(self._a)
2022 del self._a[i]
2023 del self._a[i]
2024
2025 def __repr__(self):
2026 return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])"
2027
2028 def append(self, p):
2029 self._a.extend(tuple(p))
2030
2031 def extend(self, iterable):
2032 for p in iterable:
2033 self._a.extend(p)
2034
2035 def toInt(self, *, round=otRound):
2036 if round is noRound:
2037 return
2038 a = self._a
2039 for i, value in enumerate(a):
2040 a[i] = round(value)
2041
2042 def calcBounds(self):
2043 a = self._a
2044 if not a:
2045 return 0, 0, 0, 0
2046 xs = a[0::2]
2047 ys = a[1::2]
2048 return min(xs), min(ys), max(xs), max(ys)
2049
2050 def calcIntBounds(self, round=otRound):
2051 return tuple(round(v) for v in self.calcBounds())
2052
2053 def relativeToAbsolute(self):
2054 a = self._a
2055 x, y = 0, 0
2056 for i in range(0, len(a), 2):
2057 a[i] = x = a[i] + x
2058 a[i + 1] = y = a[i + 1] + y
2059
2060 def absoluteToRelative(self):
2061 a = self._a
2062 x, y = 0, 0
2063 for i in range(0, len(a), 2):
2064 nx = a[i]
2065 ny = a[i + 1]
2066 a[i] = nx - x
2067 a[i + 1] = ny - y
2068 x = nx
2069 y = ny
2070
2071 def translate(self, p):
2072 """
2073 >>> GlyphCoordinates([(1,2)]).translate((.5,0))
2074 """
2075 x, y = p
2076 if x == 0 and y == 0:
2077 return
2078 a = self._a
2079 for i in range(0, len(a), 2):
2080 a[i] += x
2081 a[i + 1] += y
2082
2083 def scale(self, p):
2084 """
2085 >>> GlyphCoordinates([(1,2)]).scale((.5,0))
2086 """
2087 x, y = p
2088 if x == 1 and y == 1:
2089 return
2090 a = self._a
2091 for i in range(0, len(a), 2):
2092 a[i] *= x
2093 a[i + 1] *= y
2094
2095 def transform(self, t):
2096 """
2097 >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
2098 """
2099 a = self._a
2100 for i in range(0, len(a), 2):
2101 x = a[i]
2102 y = a[i + 1]
2103 px = x * t[0][0] + y * t[1][0]
2104 py = x * t[0][1] + y * t[1][1]
2105 a[i] = px
2106 a[i + 1] = py
2107
2108 def __eq__(self, other):
2109 """
2110 >>> g = GlyphCoordinates([(1,2)])
2111 >>> g2 = GlyphCoordinates([(1.0,2)])
2112 >>> g3 = GlyphCoordinates([(1.5,2)])
2113 >>> g == g2
2114 True
2115 >>> g == g3
2116 False
2117 >>> g2 == g3
2118 False
2119 """
2120 if type(self) != type(other):
2121 return NotImplemented
2122 return self._a == other._a
2123
2124 def __ne__(self, other):
2125 """
2126 >>> g = GlyphCoordinates([(1,2)])
2127 >>> g2 = GlyphCoordinates([(1.0,2)])
2128 >>> g3 = GlyphCoordinates([(1.5,2)])
2129 >>> g != g2
2130 False
2131 >>> g != g3
2132 True
2133 >>> g2 != g3
2134 True
2135 """
2136 result = self.__eq__(other)
2137 return result if result is NotImplemented else not result
2138
2139 # Math operations
2140
2141 def __pos__(self):
2142 """
2143 >>> g = GlyphCoordinates([(1,2)])
2144 >>> g
2145 GlyphCoordinates([(1, 2)])
2146 >>> g2 = +g
2147 >>> g2
2148 GlyphCoordinates([(1, 2)])
2149 >>> g2.translate((1,0))
2150 >>> g2
2151 GlyphCoordinates([(2, 2)])
2152 >>> g
2153 GlyphCoordinates([(1, 2)])
2154 """
2155 return self.copy()
2156
2157 def __neg__(self):
2158 """
2159 >>> g = GlyphCoordinates([(1,2)])
2160 >>> g
2161 GlyphCoordinates([(1, 2)])
2162 >>> g2 = -g
2163 >>> g2
2164 GlyphCoordinates([(-1, -2)])
2165 >>> g
2166 GlyphCoordinates([(1, 2)])
2167 """
2168 r = self.copy()
2169 a = r._a
2170 for i, value in enumerate(a):
2171 a[i] = -value
2172 return r
2173
2174 def __round__(self, *, round=otRound):
2175 r = self.copy()
2176 r.toInt(round=round)
2177 return r
2178
2179 def __add__(self, other):
2180 return self.copy().__iadd__(other)
2181
2182 def __sub__(self, other):
2183 return self.copy().__isub__(other)
2184
2185 def __mul__(self, other):
2186 return self.copy().__imul__(other)
2187
2188 def __truediv__(self, other):
2189 return self.copy().__itruediv__(other)
2190
2191 __radd__ = __add__
2192 __rmul__ = __mul__
2193
2194 def __rsub__(self, other):
2195 return other + (-self)
2196
2197 def __iadd__(self, other):
2198 """
2199 >>> g = GlyphCoordinates([(1,2)])
2200 >>> g += (.5,0)
2201 >>> g
2202 GlyphCoordinates([(1.5, 2)])
2203 >>> g2 = GlyphCoordinates([(3,4)])
2204 >>> g += g2
2205 >>> g
2206 GlyphCoordinates([(4.5, 6)])
2207 """
2208 if isinstance(other, tuple):
2209 assert len(other) == 2
2210 self.translate(other)
2211 return self
2212 if isinstance(other, GlyphCoordinates):
2213 other = other._a
2214 a = self._a
2215 assert len(a) == len(other)
2216 for i, value in enumerate(other):
2217 a[i] += value
2218 return self
2219 return NotImplemented
2220
2221 def __isub__(self, other):
2222 """
2223 >>> g = GlyphCoordinates([(1,2)])
2224 >>> g -= (.5,0)
2225 >>> g
2226 GlyphCoordinates([(0.5, 2)])
2227 >>> g2 = GlyphCoordinates([(3,4)])
2228 >>> g -= g2
2229 >>> g
2230 GlyphCoordinates([(-2.5, -2)])
2231 """
2232 if isinstance(other, tuple):
2233 assert len(other) == 2
2234 self.translate((-other[0], -other[1]))
2235 return self
2236 if isinstance(other, GlyphCoordinates):
2237 other = other._a
2238 a = self._a
2239 assert len(a) == len(other)
2240 for i, value in enumerate(other):
2241 a[i] -= value
2242 return self
2243 return NotImplemented
2244
2245 def __imul__(self, other):
2246 """
2247 >>> g = GlyphCoordinates([(1,2)])
2248 >>> g *= (2,.5)
2249 >>> g *= 2
2250 >>> g
2251 GlyphCoordinates([(4, 2)])
2252 >>> g = GlyphCoordinates([(1,2)])
2253 >>> g *= 2
2254 >>> g
2255 GlyphCoordinates([(2, 4)])
2256 """
2257 if isinstance(other, tuple):
2258 assert len(other) == 2
2259 self.scale(other)
2260 return self
2261 if isinstance(other, Number):
2262 if other == 1:
2263 return self
2264 a = self._a
2265 for i in range(len(a)):
2266 a[i] *= other
2267 return self
2268 return NotImplemented
2269
2270 def __itruediv__(self, other):
2271 """
2272 >>> g = GlyphCoordinates([(1,3)])
2273 >>> g /= (.5,1.5)
2274 >>> g /= 2
2275 >>> g
2276 GlyphCoordinates([(1, 1)])
2277 """
2278 if isinstance(other, Number):
2279 other = (other, other)
2280 if isinstance(other, tuple):
2281 if other == (1, 1):
2282 return self
2283 assert len(other) == 2
2284 self.scale((1.0 / other[0], 1.0 / other[1]))
2285 return self
2286 return NotImplemented
2287
2288 def __bool__(self):
2289 """
2290 >>> g = GlyphCoordinates([])
2291 >>> bool(g)
2292 False
2293 >>> g = GlyphCoordinates([(0,0), (0.,0)])
2294 >>> bool(g)
2295 True
2296 >>> g = GlyphCoordinates([(0,0), (1,0)])
2297 >>> bool(g)
2298 True
2299 >>> g = GlyphCoordinates([(0,.5), (0,0)])
2300 >>> bool(g)
2301 True
2302 """
2303 return bool(self._a)
2304
2305 __nonzero__ = __bool__
2306
2307
2308if __name__ == "__main__":
2309 import doctest, sys
2310
2311 sys.exit(doctest.testmod().failed)