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