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