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 elif glyph.isVarComposite(): 428 coords = [] 429 controls = [] 430 431 for component in glyph.components: 432 ( 433 componentCoords, 434 componentControls, 435 ) = component.getCoordinatesAndControls() 436 coords.extend(componentCoords) 437 controls.extend(componentControls) 438 439 coords = GlyphCoordinates(coords) 440 441 controls = _GlyphControls( 442 numberOfContours=glyph.numberOfContours, 443 endPts=list(range(len(coords))), 444 flags=None, 445 components=[ 446 (c.glyphName, getattr(c, "flags", None)) for c in glyph.components 447 ], 448 ) 449 450 else: 451 coords, endPts, flags = glyph.getCoordinates(self) 452 coords = coords.copy() 453 controls = _GlyphControls( 454 numberOfContours=glyph.numberOfContours, 455 endPts=endPts, 456 flags=flags, 457 components=None, 458 ) 459 # Add phantom points for (left, right, top, bottom) positions. 460 phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) 461 coords.extend(phantomPoints) 462 coords.toInt(round=round) 463 return coords, controls 464 465 def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): 466 """Set coordinates and metrics for the given glyph. 467 468 "coord" is an array of GlyphCoordinates which must include the "phantom 469 points" as the last four coordinates. 470 471 Both the horizontal/vertical advances and left/top sidebearings in "hmtx" 472 and "vmtx" tables (if any) are updated from four phantom points and 473 the glyph's bounding boxes. 474 475 The "hMetrics" and vMetrics are used to propagate "phantom points" 476 into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints" 477 method). 478 """ 479 glyph = self[glyphName] 480 481 # Handle phantom points for (left, right, top, bottom) positions. 482 assert len(coord) >= 4 483 leftSideX = coord[-4][0] 484 rightSideX = coord[-3][0] 485 topSideY = coord[-2][1] 486 bottomSideY = coord[-1][1] 487 488 coord = coord[:-4] 489 490 if glyph.isComposite(): 491 assert len(coord) == len(glyph.components) 492 for p, comp in zip(coord, glyph.components): 493 if hasattr(comp, "x"): 494 comp.x, comp.y = p 495 elif glyph.isVarComposite(): 496 for comp in glyph.components: 497 coord = comp.setCoordinates(coord) 498 assert not coord 499 elif glyph.numberOfContours == 0: 500 assert len(coord) == 0 501 else: 502 assert len(coord) == len(glyph.coordinates) 503 glyph.coordinates = GlyphCoordinates(coord) 504 505 glyph.recalcBounds(self, boundsDone=set()) 506 507 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 508 if horizontalAdvanceWidth < 0: 509 # unlikely, but it can happen, see: 510 # https://github.com/fonttools/fonttools/pull/1198 511 horizontalAdvanceWidth = 0 512 leftSideBearing = otRound(glyph.xMin - leftSideX) 513 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 514 515 if vMetrics is not None: 516 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 517 if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal 518 verticalAdvanceWidth = 0 519 topSideBearing = otRound(topSideY - glyph.yMax) 520 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 521 522 # Deprecated 523 524 def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin): 525 """This method is wrong and deprecated. 526 For rationale see: 527 https://github.com/fonttools/fonttools/pull/2266/files#r613569473 528 """ 529 vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) 530 if vMetrics is None: 531 verticalAdvanceWidth = ttFont["head"].unitsPerEm 532 topSideY = getattr(ttFont.get("hhea"), "ascent", None) 533 if topSideY is None: 534 if defaultVerticalOrigin is not None: 535 topSideY = defaultVerticalOrigin 536 else: 537 topSideY = verticalAdvanceWidth 538 glyph = self[glyphName] 539 glyph.recalcBounds(self) 540 topSideBearing = otRound(topSideY - glyph.yMax) 541 vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)} 542 return vMetrics 543 544 @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning) 545 def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): 546 """Old public name for self._getPhantomPoints(). 547 See: https://github.com/fonttools/fonttools/pull/2266""" 548 hMetrics = ttFont["hmtx"].metrics 549 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 550 return self._getPhantomPoints(glyphName, hMetrics, vMetrics) 551 552 @deprecateFunction( 553 "use '_getCoordinatesAndControls' instead", category=DeprecationWarning 554 ) 555 def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): 556 """Old public name for self._getCoordinatesAndControls(). 557 See: https://github.com/fonttools/fonttools/pull/2266""" 558 hMetrics = ttFont["hmtx"].metrics 559 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 560 return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics) 561 562 @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning) 563 def setCoordinates(self, glyphName, ttFont): 564 """Old public name for self._setCoordinates(). 565 See: https://github.com/fonttools/fonttools/pull/2266""" 566 hMetrics = ttFont["hmtx"].metrics 567 vMetrics = getattr(ttFont.get("vmtx"), "metrics", None) 568 self._setCoordinates(glyphName, hMetrics, vMetrics) 569 570 571_GlyphControls = namedtuple( 572 "_GlyphControls", "numberOfContours endPts flags components" 573) 574 575 576glyphHeaderFormat = """ 577 > # big endian 578 numberOfContours: h 579 xMin: h 580 yMin: h 581 xMax: h 582 yMax: h 583""" 584 585# flags 586flagOnCurve = 0x01 587flagXShort = 0x02 588flagYShort = 0x04 589flagRepeat = 0x08 590flagXsame = 0x10 591flagYsame = 0x20 592flagOverlapSimple = 0x40 593flagCubic = 0x80 594 595# These flags are kept for XML output after decompiling the coordinates 596keepFlags = flagOnCurve + flagOverlapSimple + flagCubic 597 598_flagSignBytes = { 599 0: 2, 600 flagXsame: 0, 601 flagXShort | flagXsame: +1, 602 flagXShort: -1, 603 flagYsame: 0, 604 flagYShort | flagYsame: +1, 605 flagYShort: -1, 606} 607 608 609def flagBest(x, y, onCurve): 610 """For a given x,y delta pair, returns the flag that packs this pair 611 most efficiently, as well as the number of byte cost of such flag.""" 612 613 flag = flagOnCurve if onCurve else 0 614 cost = 0 615 # do x 616 if x == 0: 617 flag = flag | flagXsame 618 elif -255 <= x <= 255: 619 flag = flag | flagXShort 620 if x > 0: 621 flag = flag | flagXsame 622 cost += 1 623 else: 624 cost += 2 625 # do y 626 if y == 0: 627 flag = flag | flagYsame 628 elif -255 <= y <= 255: 629 flag = flag | flagYShort 630 if y > 0: 631 flag = flag | flagYsame 632 cost += 1 633 else: 634 cost += 2 635 return flag, cost 636 637 638def flagFits(newFlag, oldFlag, mask): 639 newBytes = _flagSignBytes[newFlag & mask] 640 oldBytes = _flagSignBytes[oldFlag & mask] 641 return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) 642 643 644def flagSupports(newFlag, oldFlag): 645 return ( 646 (oldFlag & flagOnCurve) == (newFlag & flagOnCurve) 647 and flagFits(newFlag, oldFlag, flagXsame | flagXShort) 648 and flagFits(newFlag, oldFlag, flagYsame | flagYShort) 649 ) 650 651 652def flagEncodeCoord(flag, mask, coord, coordBytes): 653 byteCount = _flagSignBytes[flag & mask] 654 if byteCount == 1: 655 coordBytes.append(coord) 656 elif byteCount == -1: 657 coordBytes.append(-coord) 658 elif byteCount == 2: 659 coordBytes.extend(struct.pack(">h", coord)) 660 661 662def flagEncodeCoords(flag, x, y, xBytes, yBytes): 663 flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes) 664 flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes) 665 666 667ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes 668ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points 669ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true 670WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 671NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) 672MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one 673WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 674WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 675WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 676USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph 677OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts 678SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) 679UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) 680 681 682CompositeMaxpValues = namedtuple( 683 "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"] 684) 685 686 687class Glyph(object): 688 """This class represents an individual TrueType glyph. 689 690 TrueType glyph objects come in two flavours: simple and composite. Simple 691 glyph objects contain contours, represented via the ``.coordinates``, 692 ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes; 693 composite glyphs contain components, available through the ``.components`` 694 attributes. 695 696 Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned 697 above) is only set on simple glyphs and the ``.components`` attribute is only 698 set on composite glyphs, it is necessary to use the :py:meth:`isComposite` 699 method to test whether a glyph is simple or composite before attempting to 700 access its data. 701 702 For a composite glyph, the components can also be accessed via array-like access:: 703 704 >> assert(font["glyf"]["Aacute"].isComposite()) 705 >> font["glyf"]["Aacute"][0] 706 <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0> 707 708 """ 709 710 def __init__(self, data=b""): 711 if not data: 712 # empty char 713 self.numberOfContours = 0 714 return 715 self.data = data 716 717 def compact(self, glyfTable, recalcBBoxes=True): 718 data = self.compile(glyfTable, recalcBBoxes) 719 self.__dict__.clear() 720 self.data = data 721 722 def expand(self, glyfTable): 723 if not hasattr(self, "data"): 724 # already unpacked 725 return 726 if not self.data: 727 # empty char 728 del self.data 729 self.numberOfContours = 0 730 return 731 dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) 732 del self.data 733 # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in 734 # some glyphs; decompileCoordinates assumes that there's at least 735 # one, so short-circuit here. 736 if self.numberOfContours == 0: 737 return 738 if self.isComposite(): 739 self.decompileComponents(data, glyfTable) 740 elif self.isVarComposite(): 741 self.decompileVarComponents(data, glyfTable) 742 else: 743 self.decompileCoordinates(data) 744 745 def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): 746 if hasattr(self, "data"): 747 if recalcBBoxes: 748 # must unpack glyph in order to recalculate bounding box 749 self.expand(glyfTable) 750 else: 751 return self.data 752 if self.numberOfContours == 0: 753 return b"" 754 755 if recalcBBoxes: 756 self.recalcBounds(glyfTable, boundsDone=boundsDone) 757 758 data = sstruct.pack(glyphHeaderFormat, self) 759 if self.isComposite(): 760 data = data + self.compileComponents(glyfTable) 761 elif self.isVarComposite(): 762 data = data + self.compileVarComponents(glyfTable) 763 else: 764 data = data + self.compileCoordinates() 765 return data 766 767 def toXML(self, writer, ttFont): 768 if self.isComposite(): 769 for compo in self.components: 770 compo.toXML(writer, ttFont) 771 haveInstructions = hasattr(self, "program") 772 elif self.isVarComposite(): 773 for compo in self.components: 774 compo.toXML(writer, ttFont) 775 haveInstructions = False 776 else: 777 last = 0 778 for i in range(self.numberOfContours): 779 writer.begintag("contour") 780 writer.newline() 781 for j in range(last, self.endPtsOfContours[i] + 1): 782 attrs = [ 783 ("x", self.coordinates[j][0]), 784 ("y", self.coordinates[j][1]), 785 ("on", self.flags[j] & flagOnCurve), 786 ] 787 if self.flags[j] & flagOverlapSimple: 788 # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours 789 attrs.append(("overlap", 1)) 790 if self.flags[j] & flagCubic: 791 attrs.append(("cubic", 1)) 792 writer.simpletag("pt", attrs) 793 writer.newline() 794 last = self.endPtsOfContours[i] + 1 795 writer.endtag("contour") 796 writer.newline() 797 haveInstructions = self.numberOfContours > 0 798 if haveInstructions: 799 if self.program: 800 writer.begintag("instructions") 801 writer.newline() 802 self.program.toXML(writer, ttFont) 803 writer.endtag("instructions") 804 else: 805 writer.simpletag("instructions") 806 writer.newline() 807 808 def fromXML(self, name, attrs, content, ttFont): 809 if name == "contour": 810 if self.numberOfContours < 0: 811 raise ttLib.TTLibError("can't mix composites and contours in glyph") 812 self.numberOfContours = self.numberOfContours + 1 813 coordinates = GlyphCoordinates() 814 flags = bytearray() 815 for element in content: 816 if not isinstance(element, tuple): 817 continue 818 name, attrs, content = element 819 if name != "pt": 820 continue # ignore anything but "pt" 821 coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) 822 flag = bool(safeEval(attrs["on"])) 823 if "overlap" in attrs and bool(safeEval(attrs["overlap"])): 824 flag |= flagOverlapSimple 825 if "cubic" in attrs and bool(safeEval(attrs["cubic"])): 826 flag |= flagCubic 827 flags.append(flag) 828 if not hasattr(self, "coordinates"): 829 self.coordinates = coordinates 830 self.flags = flags 831 self.endPtsOfContours = [len(coordinates) - 1] 832 else: 833 self.coordinates.extend(coordinates) 834 self.flags.extend(flags) 835 self.endPtsOfContours.append(len(self.coordinates) - 1) 836 elif name == "component": 837 if self.numberOfContours > 0: 838 raise ttLib.TTLibError("can't mix composites and contours in glyph") 839 self.numberOfContours = -1 840 if not hasattr(self, "components"): 841 self.components = [] 842 component = GlyphComponent() 843 self.components.append(component) 844 component.fromXML(name, attrs, content, ttFont) 845 elif name == "varComponent": 846 if self.numberOfContours > 0: 847 raise ttLib.TTLibError("can't mix composites and contours in glyph") 848 self.numberOfContours = -2 849 if not hasattr(self, "components"): 850 self.components = [] 851 component = GlyphVarComponent() 852 self.components.append(component) 853 component.fromXML(name, attrs, content, ttFont) 854 elif name == "instructions": 855 self.program = ttProgram.Program() 856 for element in content: 857 if not isinstance(element, tuple): 858 continue 859 name, attrs, content = element 860 self.program.fromXML(name, attrs, content, ttFont) 861 862 def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): 863 assert self.isComposite() or self.isVarComposite() 864 nContours = 0 865 nPoints = 0 866 initialMaxComponentDepth = maxComponentDepth 867 for compo in self.components: 868 baseGlyph = glyfTable[compo.glyphName] 869 if baseGlyph.numberOfContours == 0: 870 continue 871 elif baseGlyph.numberOfContours > 0: 872 nP, nC = baseGlyph.getMaxpValues() 873 else: 874 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 875 glyfTable, initialMaxComponentDepth + 1 876 ) 877 maxComponentDepth = max(maxComponentDepth, componentDepth) 878 nPoints = nPoints + nP 879 nContours = nContours + nC 880 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 881 882 def getMaxpValues(self): 883 assert self.numberOfContours > 0 884 return len(self.coordinates), len(self.endPtsOfContours) 885 886 def decompileComponents(self, data, glyfTable): 887 self.components = [] 888 more = 1 889 haveInstructions = 0 890 while more: 891 component = GlyphComponent() 892 more, haveInstr, data = component.decompile(data, glyfTable) 893 haveInstructions = haveInstructions | haveInstr 894 self.components.append(component) 895 if haveInstructions: 896 (numInstructions,) = struct.unpack(">h", data[:2]) 897 data = data[2:] 898 self.program = ttProgram.Program() 899 self.program.fromBytecode(data[:numInstructions]) 900 data = data[numInstructions:] 901 if len(data) >= 4: 902 log.warning( 903 "too much glyph data at the end of composite glyph: %d excess bytes", 904 len(data), 905 ) 906 907 def decompileVarComponents(self, data, glyfTable): 908 self.components = [] 909 while len(data) >= GlyphVarComponent.MIN_SIZE: 910 component = GlyphVarComponent() 911 data = component.decompile(data, glyfTable) 912 self.components.append(component) 913 914 def decompileCoordinates(self, data): 915 endPtsOfContours = array.array("H") 916 endPtsOfContours.frombytes(data[: 2 * self.numberOfContours]) 917 if sys.byteorder != "big": 918 endPtsOfContours.byteswap() 919 self.endPtsOfContours = endPtsOfContours.tolist() 920 921 pos = 2 * self.numberOfContours 922 (instructionLength,) = struct.unpack(">h", data[pos : pos + 2]) 923 self.program = ttProgram.Program() 924 self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength]) 925 pos += 2 + instructionLength 926 nCoordinates = self.endPtsOfContours[-1] + 1 927 flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw( 928 nCoordinates, data, pos 929 ) 930 931 # fill in repetitions and apply signs 932 self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) 933 xIndex = 0 934 yIndex = 0 935 for i in range(nCoordinates): 936 flag = flags[i] 937 # x coordinate 938 if flag & flagXShort: 939 if flag & flagXsame: 940 x = xCoordinates[xIndex] 941 else: 942 x = -xCoordinates[xIndex] 943 xIndex = xIndex + 1 944 elif flag & flagXsame: 945 x = 0 946 else: 947 x = xCoordinates[xIndex] 948 xIndex = xIndex + 1 949 # y coordinate 950 if flag & flagYShort: 951 if flag & flagYsame: 952 y = yCoordinates[yIndex] 953 else: 954 y = -yCoordinates[yIndex] 955 yIndex = yIndex + 1 956 elif flag & flagYsame: 957 y = 0 958 else: 959 y = yCoordinates[yIndex] 960 yIndex = yIndex + 1 961 coordinates[i] = (x, y) 962 assert xIndex == len(xCoordinates) 963 assert yIndex == len(yCoordinates) 964 coordinates.relativeToAbsolute() 965 # discard all flags except "keepFlags" 966 for i in range(len(flags)): 967 flags[i] &= keepFlags 968 self.flags = flags 969 970 def decompileCoordinatesRaw(self, nCoordinates, data, pos=0): 971 # unpack flags and prepare unpacking of coordinates 972 flags = bytearray(nCoordinates) 973 # Warning: deep Python trickery going on. We use the struct module to unpack 974 # the coordinates. We build a format string based on the flags, so we can 975 # unpack the coordinates in one struct.unpack() call. 976 xFormat = ">" # big endian 977 yFormat = ">" # big endian 978 j = 0 979 while True: 980 flag = data[pos] 981 pos += 1 982 repeat = 1 983 if flag & flagRepeat: 984 repeat = data[pos] + 1 985 pos += 1 986 for k in range(repeat): 987 if flag & flagXShort: 988 xFormat = xFormat + "B" 989 elif not (flag & flagXsame): 990 xFormat = xFormat + "h" 991 if flag & flagYShort: 992 yFormat = yFormat + "B" 993 elif not (flag & flagYsame): 994 yFormat = yFormat + "h" 995 flags[j] = flag 996 j = j + 1 997 if j >= nCoordinates: 998 break 999 assert j == nCoordinates, "bad glyph flags" 1000 # unpack raw coordinates, krrrrrr-tching! 1001 xDataLen = struct.calcsize(xFormat) 1002 yDataLen = struct.calcsize(yFormat) 1003 if len(data) - pos - (xDataLen + yDataLen) >= 4: 1004 log.warning( 1005 "too much glyph data: %d excess bytes", 1006 len(data) - pos - (xDataLen + yDataLen), 1007 ) 1008 xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen]) 1009 yCoordinates = struct.unpack( 1010 yFormat, data[pos + xDataLen : pos + xDataLen + yDataLen] 1011 ) 1012 return flags, xCoordinates, yCoordinates 1013 1014 def compileComponents(self, glyfTable): 1015 data = b"" 1016 lastcomponent = len(self.components) - 1 1017 more = 1 1018 haveInstructions = 0 1019 for i in range(len(self.components)): 1020 if i == lastcomponent: 1021 haveInstructions = hasattr(self, "program") 1022 more = 0 1023 compo = self.components[i] 1024 data = data + compo.compile(more, haveInstructions, glyfTable) 1025 if haveInstructions: 1026 instructions = self.program.getBytecode() 1027 data = data + struct.pack(">h", len(instructions)) + instructions 1028 return data 1029 1030 def compileVarComponents(self, glyfTable): 1031 return b"".join(c.compile(glyfTable) for c in self.components) 1032 1033 def compileCoordinates(self): 1034 assert len(self.coordinates) == len(self.flags) 1035 data = [] 1036 endPtsOfContours = array.array("H", self.endPtsOfContours) 1037 if sys.byteorder != "big": 1038 endPtsOfContours.byteswap() 1039 data.append(endPtsOfContours.tobytes()) 1040 instructions = self.program.getBytecode() 1041 data.append(struct.pack(">h", len(instructions))) 1042 data.append(instructions) 1043 1044 deltas = self.coordinates.copy() 1045 deltas.toInt() 1046 deltas.absoluteToRelative() 1047 1048 # TODO(behdad): Add a configuration option for this? 1049 deltas = self.compileDeltasGreedy(self.flags, deltas) 1050 # deltas = self.compileDeltasOptimal(self.flags, deltas) 1051 1052 data.extend(deltas) 1053 return b"".join(data) 1054 1055 def compileDeltasGreedy(self, flags, deltas): 1056 # Implements greedy algorithm for packing coordinate deltas: 1057 # uses shortest representation one coordinate at a time. 1058 compressedFlags = bytearray() 1059 compressedXs = bytearray() 1060 compressedYs = bytearray() 1061 lastflag = None 1062 repeat = 0 1063 for flag, (x, y) in zip(flags, deltas): 1064 # Oh, the horrors of TrueType 1065 # do x 1066 if x == 0: 1067 flag = flag | flagXsame 1068 elif -255 <= x <= 255: 1069 flag = flag | flagXShort 1070 if x > 0: 1071 flag = flag | flagXsame 1072 else: 1073 x = -x 1074 compressedXs.append(x) 1075 else: 1076 compressedXs.extend(struct.pack(">h", x)) 1077 # do y 1078 if y == 0: 1079 flag = flag | flagYsame 1080 elif -255 <= y <= 255: 1081 flag = flag | flagYShort 1082 if y > 0: 1083 flag = flag | flagYsame 1084 else: 1085 y = -y 1086 compressedYs.append(y) 1087 else: 1088 compressedYs.extend(struct.pack(">h", y)) 1089 # handle repeating flags 1090 if flag == lastflag and repeat != 255: 1091 repeat = repeat + 1 1092 if repeat == 1: 1093 compressedFlags.append(flag) 1094 else: 1095 compressedFlags[-2] = flag | flagRepeat 1096 compressedFlags[-1] = repeat 1097 else: 1098 repeat = 0 1099 compressedFlags.append(flag) 1100 lastflag = flag 1101 return (compressedFlags, compressedXs, compressedYs) 1102 1103 def compileDeltasOptimal(self, flags, deltas): 1104 # Implements optimal, dynaic-programming, algorithm for packing coordinate 1105 # deltas. The savings are negligible :(. 1106 candidates = [] 1107 bestTuple = None 1108 bestCost = 0 1109 repeat = 0 1110 for flag, (x, y) in zip(flags, deltas): 1111 # Oh, the horrors of TrueType 1112 flag, coordBytes = flagBest(x, y, flag) 1113 bestCost += 1 + coordBytes 1114 newCandidates = [ 1115 (bestCost, bestTuple, flag, coordBytes), 1116 (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes), 1117 ] 1118 for lastCost, lastTuple, lastFlag, coordBytes in candidates: 1119 if ( 1120 lastCost + coordBytes <= bestCost + 1 1121 and (lastFlag & flagRepeat) 1122 and (lastFlag < 0xFF00) 1123 and flagSupports(lastFlag, flag) 1124 ): 1125 if (lastFlag & 0xFF) == ( 1126 flag | flagRepeat 1127 ) and lastCost == bestCost + 1: 1128 continue 1129 newCandidates.append( 1130 (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes) 1131 ) 1132 candidates = newCandidates 1133 bestTuple = min(candidates, key=lambda t: t[0]) 1134 bestCost = bestTuple[0] 1135 1136 flags = [] 1137 while bestTuple: 1138 cost, bestTuple, flag, coordBytes = bestTuple 1139 flags.append(flag) 1140 flags.reverse() 1141 1142 compressedFlags = bytearray() 1143 compressedXs = bytearray() 1144 compressedYs = bytearray() 1145 coords = iter(deltas) 1146 ff = [] 1147 for flag in flags: 1148 repeatCount, flag = flag >> 8, flag & 0xFF 1149 compressedFlags.append(flag) 1150 if flag & flagRepeat: 1151 assert repeatCount > 0 1152 compressedFlags.append(repeatCount) 1153 else: 1154 assert repeatCount == 0 1155 for i in range(1 + repeatCount): 1156 x, y = next(coords) 1157 flagEncodeCoords(flag, x, y, compressedXs, compressedYs) 1158 ff.append(flag) 1159 try: 1160 next(coords) 1161 raise Exception("internal error") 1162 except StopIteration: 1163 pass 1164 1165 return (compressedFlags, compressedXs, compressedYs) 1166 1167 def recalcBounds(self, glyfTable, *, boundsDone=None): 1168 """Recalculates the bounds of the glyph. 1169 1170 Each glyph object stores its bounding box in the 1171 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be 1172 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds 1173 must be provided to resolve component bounds. 1174 """ 1175 if self.isComposite() and self.tryRecalcBoundsComposite( 1176 glyfTable, boundsDone=boundsDone 1177 ): 1178 return 1179 try: 1180 coords, endPts, flags = self.getCoordinates(glyfTable) 1181 self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds() 1182 except NotImplementedError: 1183 pass 1184 1185 def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): 1186 """Try recalculating the bounds of a composite glyph that has 1187 certain constrained properties. Namely, none of the components 1188 have a transform other than an integer translate, and none 1189 uses the anchor points. 1190 1191 Each glyph object stores its bounding box in the 1192 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be 1193 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds 1194 must be provided to resolve component bounds. 1195 1196 Return True if bounds were calculated, False otherwise. 1197 """ 1198 for compo in self.components: 1199 if hasattr(compo, "firstPt") or hasattr(compo, "transform"): 1200 return False 1201 if not float(compo.x).is_integer() or not float(compo.y).is_integer(): 1202 return False 1203 1204 # All components are untransformed and have an integer x/y translate 1205 bounds = None 1206 for compo in self.components: 1207 glyphName = compo.glyphName 1208 g = glyfTable[glyphName] 1209 1210 if boundsDone is None or glyphName not in boundsDone: 1211 g.recalcBounds(glyfTable, boundsDone=boundsDone) 1212 if boundsDone is not None: 1213 boundsDone.add(glyphName) 1214 # empty components shouldn't update the bounds of the parent glyph 1215 if g.numberOfContours == 0: 1216 continue 1217 1218 x, y = compo.x, compo.y 1219 bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y)) 1220 bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y)) 1221 1222 if bounds is None: 1223 bounds = (0, 0, 0, 0) 1224 self.xMin, self.yMin, self.xMax, self.yMax = bounds 1225 return True 1226 1227 def isComposite(self): 1228 """Test whether a glyph has components""" 1229 if hasattr(self, "data"): 1230 return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False 1231 else: 1232 return self.numberOfContours == -1 1233 1234 def isVarComposite(self): 1235 """Test whether a glyph has variable components""" 1236 if hasattr(self, "data"): 1237 return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False 1238 else: 1239 return self.numberOfContours == -2 1240 1241 def getCoordinates(self, glyfTable): 1242 """Return the coordinates, end points and flags 1243 1244 This method returns three values: A :py:class:`GlyphCoordinates` object, 1245 a list of the indexes of the final points of each contour (allowing you 1246 to split up the coordinates list into contours) and a list of flags. 1247 1248 On simple glyphs, this method returns information from the glyph's own 1249 contours; on composite glyphs, it "flattens" all components recursively 1250 to return a list of coordinates representing all the components involved 1251 in the glyph. 1252 1253 To interpret the flags for each point, see the "Simple Glyph Flags" 1254 section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`. 1255 """ 1256 1257 if self.numberOfContours > 0: 1258 return self.coordinates, self.endPtsOfContours, self.flags 1259 elif self.isComposite(): 1260 # it's a composite 1261 allCoords = GlyphCoordinates() 1262 allFlags = bytearray() 1263 allEndPts = [] 1264 for compo in self.components: 1265 g = glyfTable[compo.glyphName] 1266 try: 1267 coordinates, endPts, flags = g.getCoordinates(glyfTable) 1268 except RecursionError: 1269 raise ttLib.TTLibError( 1270 "glyph '%s' contains a recursive component reference" 1271 % compo.glyphName 1272 ) 1273 coordinates = GlyphCoordinates(coordinates) 1274 if hasattr(compo, "firstPt"): 1275 # component uses two reference points: we apply the transform _before_ 1276 # computing the offset between the points 1277 if hasattr(compo, "transform"): 1278 coordinates.transform(compo.transform) 1279 x1, y1 = allCoords[compo.firstPt] 1280 x2, y2 = coordinates[compo.secondPt] 1281 move = x1 - x2, y1 - y2 1282 coordinates.translate(move) 1283 else: 1284 # component uses XY offsets 1285 move = compo.x, compo.y 1286 if not hasattr(compo, "transform"): 1287 coordinates.translate(move) 1288 else: 1289 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 1290 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 1291 assert not (apple_way and ms_way) 1292 if not (apple_way or ms_way): 1293 scale_component_offset = ( 1294 SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 1295 ) 1296 else: 1297 scale_component_offset = apple_way 1298 if scale_component_offset: 1299 # the Apple way: first move, then scale (ie. scale the component offset) 1300 coordinates.translate(move) 1301 coordinates.transform(compo.transform) 1302 else: 1303 # the MS way: first scale, then move 1304 coordinates.transform(compo.transform) 1305 coordinates.translate(move) 1306 offset = len(allCoords) 1307 allEndPts.extend(e + offset for e in endPts) 1308 allCoords.extend(coordinates) 1309 allFlags.extend(flags) 1310 return allCoords, allEndPts, allFlags 1311 elif self.isVarComposite(): 1312 raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs") 1313 else: 1314 return GlyphCoordinates(), [], bytearray() 1315 1316 def getComponentNames(self, glyfTable): 1317 """Returns a list of names of component glyphs used in this glyph 1318 1319 This method can be used on simple glyphs (in which case it returns an 1320 empty list) or composite glyphs. 1321 """ 1322 if hasattr(self, "data") and self.isVarComposite(): 1323 # TODO(VarComposite) Add implementation without expanding glyph 1324 self.expand(glyfTable) 1325 1326 if not hasattr(self, "data"): 1327 if self.isComposite() or self.isVarComposite(): 1328 return [c.glyphName for c in self.components] 1329 else: 1330 return [] 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 elif self.isVarComposite(): 1371 pass # Doesn't have hinting 1372 else: 1373 self.program = ttProgram.Program() 1374 self.program.fromBytecode([]) 1375 # No padding to trim. 1376 return 1377 if not self.data: 1378 return 1379 numContours = struct.unpack(">h", self.data[:2])[0] 1380 data = bytearray(self.data) 1381 i = 10 1382 if numContours >= 0: 1383 i += 2 * numContours # endPtsOfContours 1384 nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1 1385 instructionLen = (data[i] << 8) | data[i + 1] 1386 if remove_hinting: 1387 # Zero instruction length 1388 data[i] = data[i + 1] = 0 1389 i += 2 1390 if instructionLen: 1391 # Splice it out 1392 data = data[:i] + data[i + instructionLen :] 1393 instructionLen = 0 1394 else: 1395 i += 2 + instructionLen 1396 1397 coordBytes = 0 1398 j = 0 1399 while True: 1400 flag = data[i] 1401 i = i + 1 1402 repeat = 1 1403 if flag & flagRepeat: 1404 repeat = data[i] + 1 1405 i = i + 1 1406 xBytes = yBytes = 0 1407 if flag & flagXShort: 1408 xBytes = 1 1409 elif not (flag & flagXsame): 1410 xBytes = 2 1411 if flag & flagYShort: 1412 yBytes = 1 1413 elif not (flag & flagYsame): 1414 yBytes = 2 1415 coordBytes += (xBytes + yBytes) * repeat 1416 j += repeat 1417 if j >= nCoordinates: 1418 break 1419 assert j == nCoordinates, "bad glyph flags" 1420 i += coordBytes 1421 # Remove padding 1422 data = data[:i] 1423 elif self.isComposite(): 1424 more = 1 1425 we_have_instructions = False 1426 while more: 1427 flags = (data[i] << 8) | data[i + 1] 1428 if remove_hinting: 1429 flags &= ~WE_HAVE_INSTRUCTIONS 1430 if flags & WE_HAVE_INSTRUCTIONS: 1431 we_have_instructions = True 1432 data[i + 0] = flags >> 8 1433 data[i + 1] = flags & 0xFF 1434 i += 4 1435 flags = int(flags) 1436 1437 if flags & ARG_1_AND_2_ARE_WORDS: 1438 i += 4 1439 else: 1440 i += 2 1441 if flags & WE_HAVE_A_SCALE: 1442 i += 2 1443 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: 1444 i += 4 1445 elif flags & WE_HAVE_A_TWO_BY_TWO: 1446 i += 8 1447 more = flags & MORE_COMPONENTS 1448 if we_have_instructions: 1449 instructionLen = (data[i] << 8) | data[i + 1] 1450 i += 2 + instructionLen 1451 # Remove padding 1452 data = data[:i] 1453 elif self.isVarComposite(): 1454 i = 0 1455 MIN_SIZE = GlyphVarComponent.MIN_SIZE 1456 while len(data[i : i + MIN_SIZE]) >= MIN_SIZE: 1457 size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE]) 1458 i += size 1459 data = data[:i] 1460 1461 self.data = data 1462 1463 def removeHinting(self): 1464 """Removes TrueType hinting instructions from the glyph.""" 1465 self.trim(remove_hinting=True) 1466 1467 def draw(self, pen, glyfTable, offset=0): 1468 """Draws the glyph using the supplied pen object. 1469 1470 Arguments: 1471 pen: An object conforming to the pen protocol. 1472 glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components. 1473 offset (int): A horizontal offset. If provided, all coordinates are 1474 translated by this offset. 1475 """ 1476 1477 if self.isComposite(): 1478 for component in self.components: 1479 glyphName, transform = component.getComponentInfo() 1480 pen.addComponent(glyphName, transform) 1481 return 1482 1483 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1484 if offset: 1485 coordinates = coordinates.copy() 1486 coordinates.translate((offset, 0)) 1487 start = 0 1488 maybeInt = lambda v: int(v) if v == int(v) else v 1489 for end in endPts: 1490 end = end + 1 1491 contour = coordinates[start:end] 1492 cFlags = [flagOnCurve & f for f in flags[start:end]] 1493 cuFlags = [flagCubic & f for f in flags[start:end]] 1494 start = end 1495 if 1 not in cFlags: 1496 assert all(cuFlags) or not any(cuFlags) 1497 cubic = all(cuFlags) 1498 if cubic: 1499 count = len(contour) 1500 assert count % 2 == 0, "Odd number of cubic off-curves undefined" 1501 l = contour[-1] 1502 f = contour[0] 1503 p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5)) 1504 pen.moveTo(p0) 1505 for i in range(0, count, 2): 1506 p1 = contour[i] 1507 p2 = contour[i + 1] 1508 p4 = contour[i + 2 if i + 2 < count else 0] 1509 p3 = ( 1510 maybeInt((p2[0] + p4[0]) * 0.5), 1511 maybeInt((p2[1] + p4[1]) * 0.5), 1512 ) 1513 pen.curveTo(p1, p2, p3) 1514 else: 1515 # There is not a single on-curve point on the curve, 1516 # use pen.qCurveTo's special case by specifying None 1517 # as the on-curve point. 1518 contour.append(None) 1519 pen.qCurveTo(*contour) 1520 else: 1521 # Shuffle the points so that the contour is guaranteed 1522 # to *end* in an on-curve point, which we'll use for 1523 # the moveTo. 1524 firstOnCurve = cFlags.index(1) + 1 1525 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 1526 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 1527 cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve] 1528 pen.moveTo(contour[-1]) 1529 while contour: 1530 nextOnCurve = cFlags.index(1) + 1 1531 if nextOnCurve == 1: 1532 # Skip a final lineTo(), as it is implied by 1533 # pen.closePath() 1534 if len(contour) > 1: 1535 pen.lineTo(contour[0]) 1536 else: 1537 cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]] 1538 assert all(cubicFlags) or not any(cubicFlags) 1539 cubic = any(cubicFlags) 1540 if cubic: 1541 assert all( 1542 cubicFlags 1543 ), "Mixed cubic and quadratic segment undefined" 1544 1545 count = nextOnCurve 1546 assert ( 1547 count >= 3 1548 ), "At least two cubic off-curve points required" 1549 assert ( 1550 count - 1 1551 ) % 2 == 0, "Odd number of cubic off-curves undefined" 1552 for i in range(0, count - 3, 2): 1553 p1 = contour[i] 1554 p2 = contour[i + 1] 1555 p4 = contour[i + 2] 1556 p3 = ( 1557 maybeInt((p2[0] + p4[0]) * 0.5), 1558 maybeInt((p2[1] + p4[1]) * 0.5), 1559 ) 1560 lastOnCurve = p3 1561 pen.curveTo(p1, p2, p3) 1562 pen.curveTo(*contour[count - 3 : count]) 1563 else: 1564 pen.qCurveTo(*contour[:nextOnCurve]) 1565 contour = contour[nextOnCurve:] 1566 cFlags = cFlags[nextOnCurve:] 1567 cuFlags = cuFlags[nextOnCurve:] 1568 pen.closePath() 1569 1570 def drawPoints(self, pen, glyfTable, offset=0): 1571 """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(), 1572 this will not change the point indices. 1573 """ 1574 1575 if self.isComposite(): 1576 for component in self.components: 1577 glyphName, transform = component.getComponentInfo() 1578 pen.addComponent(glyphName, transform) 1579 return 1580 1581 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1582 if offset: 1583 coordinates = coordinates.copy() 1584 coordinates.translate((offset, 0)) 1585 start = 0 1586 for end in endPts: 1587 end = end + 1 1588 contour = coordinates[start:end] 1589 cFlags = flags[start:end] 1590 start = end 1591 pen.beginPath() 1592 # Start with the appropriate segment type based on the final segment 1593 1594 if cFlags[-1] & flagOnCurve: 1595 segmentType = "line" 1596 elif cFlags[-1] & flagCubic: 1597 segmentType = "curve" 1598 else: 1599 segmentType = "qcurve" 1600 for i, pt in enumerate(contour): 1601 if cFlags[i] & flagOnCurve: 1602 pen.addPoint(pt, segmentType=segmentType) 1603 segmentType = "line" 1604 else: 1605 pen.addPoint(pt) 1606 segmentType = "curve" if cFlags[i] & flagCubic else "qcurve" 1607 pen.endPath() 1608 1609 def __eq__(self, other): 1610 if type(self) != type(other): 1611 return NotImplemented 1612 return self.__dict__ == other.__dict__ 1613 1614 def __ne__(self, other): 1615 result = self.__eq__(other) 1616 return result if result is NotImplemented else not result 1617 1618 1619# Vector.__round__ uses the built-in (Banker's) `round` but we want 1620# to use otRound below 1621_roundv = partial(Vector.__round__, round=otRound) 1622 1623 1624def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool: 1625 # True if p1 is in the middle of p0 and p2, either before or after rounding 1626 p0 = Vector(p0) 1627 p1 = Vector(p1) 1628 p2 = Vector(p2) 1629 return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2 1630 1631 1632def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]: 1633 """Drop impliable on-curve points from the (simple) glyph or glyphs. 1634 1635 In TrueType glyf outlines, on-curve points can be implied when they are located at 1636 the midpoint of the line connecting two consecutive off-curve points. 1637 1638 If more than one glyphs are passed, these are assumed to be interpolatable masters 1639 of the same glyph impliable, and thus only the on-curve points that are impliable 1640 for all of them will actually be implied. 1641 Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more 1642 contours are considered. 1643 The input glyph(s) is/are modified in-place. 1644 1645 Args: 1646 interpolatable_glyphs: The glyph or glyphs to modify in-place. 1647 1648 Returns: 1649 The set of point indices that were dropped if any. 1650 1651 Raises: 1652 ValueError if simple glyphs are not in fact interpolatable because they have 1653 different point flags or number of contours. 1654 1655 Reference: 1656 https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html 1657 """ 1658 staticAttributes = SimpleNamespace( 1659 numberOfContours=None, flags=None, endPtsOfContours=None 1660 ) 1661 drop = None 1662 simple_glyphs = [] 1663 for i, glyph in enumerate(interpolatable_glyphs): 1664 if glyph.numberOfContours < 1: 1665 # ignore composite or empty glyphs 1666 continue 1667 1668 for attr in staticAttributes.__dict__: 1669 expected = getattr(staticAttributes, attr) 1670 found = getattr(glyph, attr) 1671 if expected is None: 1672 setattr(staticAttributes, attr, found) 1673 elif expected != found: 1674 raise ValueError( 1675 f"Incompatible {attr} for glyph at master index {i}: " 1676 f"expected {expected}, found {found}" 1677 ) 1678 1679 may_drop = set() 1680 start = 0 1681 coords = glyph.coordinates 1682 flags = staticAttributes.flags 1683 endPtsOfContours = staticAttributes.endPtsOfContours 1684 for last in endPtsOfContours: 1685 for i in range(start, last + 1): 1686 if not (flags[i] & flagOnCurve): 1687 continue 1688 prv = i - 1 if i > start else last 1689 nxt = i + 1 if i < last else start 1690 if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]: 1691 continue 1692 # we may drop the ith on-curve if halfway between previous/next off-curves 1693 if not _is_mid_point(coords[prv], coords[i], coords[nxt]): 1694 continue 1695 1696 may_drop.add(i) 1697 start = last + 1 1698 # we only want to drop if ALL interpolatable glyphs have the same implied oncurves 1699 if drop is None: 1700 drop = may_drop 1701 else: 1702 drop.intersection_update(may_drop) 1703 1704 simple_glyphs.append(glyph) 1705 1706 if drop: 1707 # Do the actual dropping 1708 flags = staticAttributes.flags 1709 assert flags is not None 1710 newFlags = array.array( 1711 "B", (flags[i] for i in range(len(flags)) if i not in drop) 1712 ) 1713 1714 endPts = staticAttributes.endPtsOfContours 1715 assert endPts is not None 1716 newEndPts = [] 1717 i = 0 1718 delta = 0 1719 for d in sorted(drop): 1720 while d > endPts[i]: 1721 newEndPts.append(endPts[i] - delta) 1722 i += 1 1723 delta += 1 1724 while i < len(endPts): 1725 newEndPts.append(endPts[i] - delta) 1726 i += 1 1727 1728 for glyph in simple_glyphs: 1729 coords = glyph.coordinates 1730 glyph.coordinates = GlyphCoordinates( 1731 coords[i] for i in range(len(coords)) if i not in drop 1732 ) 1733 glyph.flags = newFlags 1734 glyph.endPtsOfContours = newEndPts 1735 1736 return drop if drop is not None else set() 1737 1738 1739class GlyphComponent(object): 1740 """Represents a component within a composite glyph. 1741 1742 The component is represented internally with four attributes: ``glyphName``, 1743 ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e 1744 no scaling, reflection, or rotation; only translation), the ``transform`` 1745 attribute is not present. 1746 """ 1747 1748 # The above documentation is not *completely* true, but is *true enough* because 1749 # the rare firstPt/lastPt attributes are not totally supported and nobody seems to 1750 # mind - see below. 1751 1752 def __init__(self): 1753 pass 1754 1755 def getComponentInfo(self): 1756 """Return information about the component 1757 1758 This method returns a tuple of two values: the glyph name of the component's 1759 base glyph, and a transformation matrix. As opposed to accessing the attributes 1760 directly, ``getComponentInfo`` always returns a six-element tuple of the 1761 component's transformation matrix, even when the two-by-two ``.transform`` 1762 matrix is not present. 1763 """ 1764 # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement 1765 # something equivalent in fontTools.objects.glyph (I'd rather not 1766 # convert it to an absolute offset, since it is valuable information). 1767 # This method will now raise "AttributeError: x" on glyphs that use 1768 # this TT feature. 1769 if hasattr(self, "transform"): 1770 [[xx, xy], [yx, yy]] = self.transform 1771 trans = (xx, xy, yx, yy, self.x, self.y) 1772 else: 1773 trans = (1, 0, 0, 1, self.x, self.y) 1774 return self.glyphName, trans 1775 1776 def decompile(self, data, glyfTable): 1777 flags, glyphID = struct.unpack(">HH", data[:4]) 1778 self.flags = int(flags) 1779 glyphID = int(glyphID) 1780 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 1781 data = data[4:] 1782 1783 if self.flags & ARG_1_AND_2_ARE_WORDS: 1784 if self.flags & ARGS_ARE_XY_VALUES: 1785 self.x, self.y = struct.unpack(">hh", data[:4]) 1786 else: 1787 x, y = struct.unpack(">HH", data[:4]) 1788 self.firstPt, self.secondPt = int(x), int(y) 1789 data = data[4:] 1790 else: 1791 if self.flags & ARGS_ARE_XY_VALUES: 1792 self.x, self.y = struct.unpack(">bb", data[:2]) 1793 else: 1794 x, y = struct.unpack(">BB", data[:2]) 1795 self.firstPt, self.secondPt = int(x), int(y) 1796 data = data[2:] 1797 1798 if self.flags & WE_HAVE_A_SCALE: 1799 (scale,) = struct.unpack(">h", data[:2]) 1800 self.transform = [ 1801 [fi2fl(scale, 14), 0], 1802 [0, fi2fl(scale, 14)], 1803 ] # fixed 2.14 1804 data = data[2:] 1805 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 1806 xscale, yscale = struct.unpack(">hh", data[:4]) 1807 self.transform = [ 1808 [fi2fl(xscale, 14), 0], 1809 [0, fi2fl(yscale, 14)], 1810 ] # fixed 2.14 1811 data = data[4:] 1812 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 1813 (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8]) 1814 self.transform = [ 1815 [fi2fl(xscale, 14), fi2fl(scale01, 14)], 1816 [fi2fl(scale10, 14), fi2fl(yscale, 14)], 1817 ] # fixed 2.14 1818 data = data[8:] 1819 more = self.flags & MORE_COMPONENTS 1820 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 1821 self.flags = self.flags & ( 1822 ROUND_XY_TO_GRID 1823 | USE_MY_METRICS 1824 | SCALED_COMPONENT_OFFSET 1825 | UNSCALED_COMPONENT_OFFSET 1826 | NON_OVERLAPPING 1827 | OVERLAP_COMPOUND 1828 ) 1829 return more, haveInstructions, data 1830 1831 def compile(self, more, haveInstructions, glyfTable): 1832 data = b"" 1833 1834 # reset all flags we will calculate ourselves 1835 flags = self.flags & ( 1836 ROUND_XY_TO_GRID 1837 | USE_MY_METRICS 1838 | SCALED_COMPONENT_OFFSET 1839 | UNSCALED_COMPONENT_OFFSET 1840 | NON_OVERLAPPING 1841 | OVERLAP_COMPOUND 1842 ) 1843 if more: 1844 flags = flags | MORE_COMPONENTS 1845 if haveInstructions: 1846 flags = flags | WE_HAVE_INSTRUCTIONS 1847 1848 if hasattr(self, "firstPt"): 1849 if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): 1850 data = data + struct.pack(">BB", self.firstPt, self.secondPt) 1851 else: 1852 data = data + struct.pack(">HH", self.firstPt, self.secondPt) 1853 flags = flags | ARG_1_AND_2_ARE_WORDS 1854 else: 1855 x = otRound(self.x) 1856 y = otRound(self.y) 1857 flags = flags | ARGS_ARE_XY_VALUES 1858 if (-128 <= x <= 127) and (-128 <= y <= 127): 1859 data = data + struct.pack(">bb", x, y) 1860 else: 1861 data = data + struct.pack(">hh", x, y) 1862 flags = flags | ARG_1_AND_2_ARE_WORDS 1863 1864 if hasattr(self, "transform"): 1865 transform = [[fl2fi(x, 14) for x in row] for row in self.transform] 1866 if transform[0][1] or transform[1][0]: 1867 flags = flags | WE_HAVE_A_TWO_BY_TWO 1868 data = data + struct.pack( 1869 ">hhhh", 1870 transform[0][0], 1871 transform[0][1], 1872 transform[1][0], 1873 transform[1][1], 1874 ) 1875 elif transform[0][0] != transform[1][1]: 1876 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 1877 data = data + struct.pack(">hh", transform[0][0], transform[1][1]) 1878 else: 1879 flags = flags | WE_HAVE_A_SCALE 1880 data = data + struct.pack(">h", transform[0][0]) 1881 1882 glyphID = glyfTable.getGlyphID(self.glyphName) 1883 return struct.pack(">HH", flags, glyphID) + data 1884 1885 def toXML(self, writer, ttFont): 1886 attrs = [("glyphName", self.glyphName)] 1887 if not hasattr(self, "firstPt"): 1888 attrs = attrs + [("x", self.x), ("y", self.y)] 1889 else: 1890 attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] 1891 1892 if hasattr(self, "transform"): 1893 transform = self.transform 1894 if transform[0][1] or transform[1][0]: 1895 attrs = attrs + [ 1896 ("scalex", fl2str(transform[0][0], 14)), 1897 ("scale01", fl2str(transform[0][1], 14)), 1898 ("scale10", fl2str(transform[1][0], 14)), 1899 ("scaley", fl2str(transform[1][1], 14)), 1900 ] 1901 elif transform[0][0] != transform[1][1]: 1902 attrs = attrs + [ 1903 ("scalex", fl2str(transform[0][0], 14)), 1904 ("scaley", fl2str(transform[1][1], 14)), 1905 ] 1906 else: 1907 attrs = attrs + [("scale", fl2str(transform[0][0], 14))] 1908 attrs = attrs + [("flags", hex(self.flags))] 1909 writer.simpletag("component", attrs) 1910 writer.newline() 1911 1912 def fromXML(self, name, attrs, content, ttFont): 1913 self.glyphName = attrs["glyphName"] 1914 if "firstPt" in attrs: 1915 self.firstPt = safeEval(attrs["firstPt"]) 1916 self.secondPt = safeEval(attrs["secondPt"]) 1917 else: 1918 self.x = safeEval(attrs["x"]) 1919 self.y = safeEval(attrs["y"]) 1920 if "scale01" in attrs: 1921 scalex = str2fl(attrs["scalex"], 14) 1922 scale01 = str2fl(attrs["scale01"], 14) 1923 scale10 = str2fl(attrs["scale10"], 14) 1924 scaley = str2fl(attrs["scaley"], 14) 1925 self.transform = [[scalex, scale01], [scale10, scaley]] 1926 elif "scalex" in attrs: 1927 scalex = str2fl(attrs["scalex"], 14) 1928 scaley = str2fl(attrs["scaley"], 14) 1929 self.transform = [[scalex, 0], [0, scaley]] 1930 elif "scale" in attrs: 1931 scale = str2fl(attrs["scale"], 14) 1932 self.transform = [[scale, 0], [0, scale]] 1933 self.flags = safeEval(attrs["flags"]) 1934 1935 def __eq__(self, other): 1936 if type(self) != type(other): 1937 return NotImplemented 1938 return self.__dict__ == other.__dict__ 1939 1940 def __ne__(self, other): 1941 result = self.__eq__(other) 1942 return result if result is NotImplemented else not result 1943 1944 1945# 1946# Variable Composite glyphs 1947# https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md 1948# 1949 1950 1951class VarComponentFlags(IntFlag): 1952 USE_MY_METRICS = 0x0001 1953 AXIS_INDICES_ARE_SHORT = 0x0002 1954 UNIFORM_SCALE = 0x0004 1955 HAVE_TRANSLATE_X = 0x0008 1956 HAVE_TRANSLATE_Y = 0x0010 1957 HAVE_ROTATION = 0x0020 1958 HAVE_SCALE_X = 0x0040 1959 HAVE_SCALE_Y = 0x0080 1960 HAVE_SKEW_X = 0x0100 1961 HAVE_SKEW_Y = 0x0200 1962 HAVE_TCENTER_X = 0x0400 1963 HAVE_TCENTER_Y = 0x0800 1964 GID_IS_24BIT = 0x1000 1965 AXES_HAVE_VARIATION = 0x2000 1966 RESET_UNSPECIFIED_AXES = 0x4000 1967 1968 1969VarComponentTransformMappingValues = namedtuple( 1970 "VarComponentTransformMappingValues", 1971 ["flag", "fractionalBits", "scale", "defaultValue"], 1972) 1973 1974VAR_COMPONENT_TRANSFORM_MAPPING = { 1975 "translateX": VarComponentTransformMappingValues( 1976 VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0 1977 ), 1978 "translateY": VarComponentTransformMappingValues( 1979 VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0 1980 ), 1981 "rotation": VarComponentTransformMappingValues( 1982 VarComponentFlags.HAVE_ROTATION, 12, 180, 0 1983 ), 1984 "scaleX": VarComponentTransformMappingValues( 1985 VarComponentFlags.HAVE_SCALE_X, 10, 1, 1 1986 ), 1987 "scaleY": VarComponentTransformMappingValues( 1988 VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1 1989 ), 1990 "skewX": VarComponentTransformMappingValues( 1991 VarComponentFlags.HAVE_SKEW_X, 12, -180, 0 1992 ), 1993 "skewY": VarComponentTransformMappingValues( 1994 VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0 1995 ), 1996 "tCenterX": VarComponentTransformMappingValues( 1997 VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0 1998 ), 1999 "tCenterY": VarComponentTransformMappingValues( 2000 VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0 2001 ), 2002} 2003 2004 2005class GlyphVarComponent(object): 2006 MIN_SIZE = 5 2007 2008 def __init__(self): 2009 self.location = {} 2010 self.transform = DecomposedTransform() 2011 2012 @staticmethod 2013 def getSize(data): 2014 size = 5 2015 flags = struct.unpack(">H", data[:2])[0] 2016 numAxes = int(data[2]) 2017 2018 if flags & VarComponentFlags.GID_IS_24BIT: 2019 size += 1 2020 2021 size += numAxes 2022 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 2023 size += 2 * numAxes 2024 else: 2025 axisIndices = array.array("B", data[:numAxes]) 2026 size += numAxes 2027 2028 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2029 if flags & mapping_values.flag: 2030 size += 2 2031 2032 return size 2033 2034 def decompile(self, data, glyfTable): 2035 flags = struct.unpack(">H", data[:2])[0] 2036 self.flags = int(flags) 2037 data = data[2:] 2038 2039 numAxes = int(data[0]) 2040 data = data[1:] 2041 2042 if flags & VarComponentFlags.GID_IS_24BIT: 2043 glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0]) 2044 data = data[3:] 2045 flags ^= VarComponentFlags.GID_IS_24BIT 2046 else: 2047 glyphID = int(struct.unpack(">H", data[:2])[0]) 2048 data = data[2:] 2049 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 2050 2051 if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT: 2052 axisIndices = array.array("H", data[: 2 * numAxes]) 2053 if sys.byteorder != "big": 2054 axisIndices.byteswap() 2055 data = data[2 * numAxes :] 2056 flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT 2057 else: 2058 axisIndices = array.array("B", data[:numAxes]) 2059 data = data[numAxes:] 2060 assert len(axisIndices) == numAxes 2061 axisIndices = list(axisIndices) 2062 2063 axisValues = array.array("h", data[: 2 * numAxes]) 2064 if sys.byteorder != "big": 2065 axisValues.byteswap() 2066 data = data[2 * numAxes :] 2067 assert len(axisValues) == numAxes 2068 axisValues = [fi2fl(v, 14) for v in axisValues] 2069 2070 self.location = { 2071 glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues) 2072 } 2073 2074 def read_transform_component(data, values): 2075 if flags & values.flag: 2076 return ( 2077 data[2:], 2078 fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits) 2079 * values.scale, 2080 ) 2081 else: 2082 return data, values.defaultValue 2083 2084 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2085 data, value = read_transform_component(data, mapping_values) 2086 setattr(self.transform, attr_name, value) 2087 2088 if flags & VarComponentFlags.UNIFORM_SCALE: 2089 if flags & VarComponentFlags.HAVE_SCALE_X and not ( 2090 flags & VarComponentFlags.HAVE_SCALE_Y 2091 ): 2092 self.transform.scaleY = self.transform.scaleX 2093 flags |= VarComponentFlags.HAVE_SCALE_Y 2094 flags ^= VarComponentFlags.UNIFORM_SCALE 2095 2096 return data 2097 2098 def compile(self, glyfTable): 2099 data = b"" 2100 2101 if not hasattr(self, "flags"): 2102 flags = 0 2103 # Calculate optimal transform component flags 2104 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2105 value = getattr(self.transform, attr_name) 2106 if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi( 2107 mapping.defaultValue / mapping.scale, mapping.fractionalBits 2108 ): 2109 flags |= mapping.flag 2110 else: 2111 flags = self.flags 2112 2113 if ( 2114 flags & VarComponentFlags.HAVE_SCALE_X 2115 and flags & VarComponentFlags.HAVE_SCALE_Y 2116 and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10) 2117 ): 2118 flags |= VarComponentFlags.UNIFORM_SCALE 2119 flags ^= VarComponentFlags.HAVE_SCALE_Y 2120 2121 numAxes = len(self.location) 2122 2123 data = data + struct.pack(">B", numAxes) 2124 2125 glyphID = glyfTable.getGlyphID(self.glyphName) 2126 if glyphID > 65535: 2127 flags |= VarComponentFlags.GID_IS_24BIT 2128 data = data + struct.pack(">L", glyphID)[1:] 2129 else: 2130 data = data + struct.pack(">H", glyphID) 2131 2132 axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()] 2133 if all(a <= 255 for a in axisIndices): 2134 axisIndices = array.array("B", axisIndices) 2135 else: 2136 axisIndices = array.array("H", axisIndices) 2137 if sys.byteorder != "big": 2138 axisIndices.byteswap() 2139 flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT 2140 data = data + bytes(axisIndices) 2141 2142 axisValues = self.location.values() 2143 axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues)) 2144 if sys.byteorder != "big": 2145 axisValues.byteswap() 2146 data = data + bytes(axisValues) 2147 2148 def write_transform_component(data, value, values): 2149 if flags & values.flag: 2150 return data + struct.pack( 2151 ">h", fl2fi(value / values.scale, values.fractionalBits) 2152 ) 2153 else: 2154 return data 2155 2156 for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2157 value = getattr(self.transform, attr_name) 2158 data = write_transform_component(data, value, mapping_values) 2159 2160 return struct.pack(">H", flags) + data 2161 2162 def toXML(self, writer, ttFont): 2163 attrs = [("glyphName", self.glyphName)] 2164 2165 if hasattr(self, "flags"): 2166 attrs = attrs + [("flags", hex(self.flags))] 2167 2168 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2169 v = getattr(self.transform, attr_name) 2170 if v != mapping.defaultValue: 2171 attrs.append((attr_name, fl2str(v, mapping.fractionalBits))) 2172 2173 writer.begintag("varComponent", attrs) 2174 writer.newline() 2175 2176 writer.begintag("location") 2177 writer.newline() 2178 for tag, v in self.location.items(): 2179 writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))]) 2180 writer.newline() 2181 writer.endtag("location") 2182 writer.newline() 2183 2184 writer.endtag("varComponent") 2185 writer.newline() 2186 2187 def fromXML(self, name, attrs, content, ttFont): 2188 self.glyphName = attrs["glyphName"] 2189 2190 if "flags" in attrs: 2191 self.flags = safeEval(attrs["flags"]) 2192 2193 for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items(): 2194 if attr_name not in attrs: 2195 continue 2196 v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits) 2197 setattr(self.transform, attr_name, v) 2198 2199 for c in content: 2200 if not isinstance(c, tuple): 2201 continue 2202 name, attrs, content = c 2203 if name != "location": 2204 continue 2205 for c in content: 2206 if not isinstance(c, tuple): 2207 continue 2208 name, attrs, content = c 2209 assert name == "axis" 2210 assert not content 2211 self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14) 2212 2213 def getPointCount(self): 2214 assert hasattr(self, "flags"), "VarComponent with variations must have flags" 2215 2216 count = 0 2217 2218 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 2219 count += len(self.location) 2220 2221 if self.flags & ( 2222 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 2223 ): 2224 count += 1 2225 if self.flags & VarComponentFlags.HAVE_ROTATION: 2226 count += 1 2227 if self.flags & ( 2228 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 2229 ): 2230 count += 1 2231 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 2232 count += 1 2233 if self.flags & ( 2234 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 2235 ): 2236 count += 1 2237 2238 return count 2239 2240 def getCoordinatesAndControls(self): 2241 coords = [] 2242 controls = [] 2243 2244 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 2245 for tag, v in self.location.items(): 2246 controls.append(tag) 2247 coords.append((fl2fi(v, 14), 0)) 2248 2249 if self.flags & ( 2250 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 2251 ): 2252 controls.append("translate") 2253 coords.append((self.transform.translateX, self.transform.translateY)) 2254 if self.flags & VarComponentFlags.HAVE_ROTATION: 2255 controls.append("rotation") 2256 coords.append((fl2fi(self.transform.rotation / 180, 12), 0)) 2257 if self.flags & ( 2258 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 2259 ): 2260 controls.append("scale") 2261 coords.append( 2262 (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10)) 2263 ) 2264 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 2265 controls.append("skew") 2266 coords.append( 2267 ( 2268 fl2fi(self.transform.skewX / -180, 12), 2269 fl2fi(self.transform.skewY / 180, 12), 2270 ) 2271 ) 2272 if self.flags & ( 2273 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 2274 ): 2275 controls.append("tCenter") 2276 coords.append((self.transform.tCenterX, self.transform.tCenterY)) 2277 2278 return coords, controls 2279 2280 def setCoordinates(self, coords): 2281 i = 0 2282 2283 if self.flags & VarComponentFlags.AXES_HAVE_VARIATION: 2284 newLocation = {} 2285 for tag in self.location: 2286 newLocation[tag] = fi2fl(coords[i][0], 14) 2287 i += 1 2288 self.location = newLocation 2289 2290 self.transform = DecomposedTransform() 2291 if self.flags & ( 2292 VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y 2293 ): 2294 self.transform.translateX, self.transform.translateY = coords[i] 2295 i += 1 2296 if self.flags & VarComponentFlags.HAVE_ROTATION: 2297 self.transform.rotation = fi2fl(coords[i][0], 12) * 180 2298 i += 1 2299 if self.flags & ( 2300 VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y 2301 ): 2302 self.transform.scaleX, self.transform.scaleY = fi2fl( 2303 coords[i][0], 10 2304 ), fi2fl(coords[i][1], 10) 2305 i += 1 2306 if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y): 2307 self.transform.skewX, self.transform.skewY = ( 2308 fi2fl(coords[i][0], 12) * -180, 2309 fi2fl(coords[i][1], 12) * 180, 2310 ) 2311 i += 1 2312 if self.flags & ( 2313 VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y 2314 ): 2315 self.transform.tCenterX, self.transform.tCenterY = coords[i] 2316 i += 1 2317 2318 return coords[i:] 2319 2320 def __eq__(self, other): 2321 if type(self) != type(other): 2322 return NotImplemented 2323 return self.__dict__ == other.__dict__ 2324 2325 def __ne__(self, other): 2326 result = self.__eq__(other) 2327 return result if result is NotImplemented else not result 2328 2329 2330class GlyphCoordinates(object): 2331 """A list of glyph coordinates. 2332 2333 Unlike an ordinary list, this is a numpy-like matrix object which supports 2334 matrix addition, scalar multiplication and other operations described below. 2335 """ 2336 2337 def __init__(self, iterable=[]): 2338 self._a = array.array("d") 2339 self.extend(iterable) 2340 2341 @property 2342 def array(self): 2343 """Returns the underlying array of coordinates""" 2344 return self._a 2345 2346 @staticmethod 2347 def zeros(count): 2348 """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)""" 2349 g = GlyphCoordinates() 2350 g._a.frombytes(bytes(count * 2 * g._a.itemsize)) 2351 return g 2352 2353 def copy(self): 2354 """Creates a new ``GlyphCoordinates`` object which is a copy of the current one.""" 2355 c = GlyphCoordinates() 2356 c._a.extend(self._a) 2357 return c 2358 2359 def __len__(self): 2360 """Returns the number of coordinates in the array.""" 2361 return len(self._a) // 2 2362 2363 def __getitem__(self, k): 2364 """Returns a two element tuple (x,y)""" 2365 a = self._a 2366 if isinstance(k, slice): 2367 indices = range(*k.indices(len(self))) 2368 # Instead of calling ourselves recursively, duplicate code; faster 2369 ret = [] 2370 for k in indices: 2371 x = a[2 * k] 2372 y = a[2 * k + 1] 2373 ret.append( 2374 (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) 2375 ) 2376 return ret 2377 x = a[2 * k] 2378 y = a[2 * k + 1] 2379 return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y) 2380 2381 def __setitem__(self, k, v): 2382 """Sets a point's coordinates to a two element tuple (x,y)""" 2383 if isinstance(k, slice): 2384 indices = range(*k.indices(len(self))) 2385 # XXX This only works if len(v) == len(indices) 2386 for j, i in enumerate(indices): 2387 self[i] = v[j] 2388 return 2389 self._a[2 * k], self._a[2 * k + 1] = v 2390 2391 def __delitem__(self, i): 2392 """Removes a point from the list""" 2393 i = (2 * i) % len(self._a) 2394 del self._a[i] 2395 del self._a[i] 2396 2397 def __repr__(self): 2398 return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])" 2399 2400 def append(self, p): 2401 self._a.extend(tuple(p)) 2402 2403 def extend(self, iterable): 2404 for p in iterable: 2405 self._a.extend(p) 2406 2407 def toInt(self, *, round=otRound): 2408 if round is noRound: 2409 return 2410 a = self._a 2411 for i in range(len(a)): 2412 a[i] = round(a[i]) 2413 2414 def calcBounds(self): 2415 a = self._a 2416 if not a: 2417 return 0, 0, 0, 0 2418 xs = a[0::2] 2419 ys = a[1::2] 2420 return min(xs), min(ys), max(xs), max(ys) 2421 2422 def calcIntBounds(self, round=otRound): 2423 return tuple(round(v) for v in self.calcBounds()) 2424 2425 def relativeToAbsolute(self): 2426 a = self._a 2427 x, y = 0, 0 2428 for i in range(0, len(a), 2): 2429 a[i] = x = a[i] + x 2430 a[i + 1] = y = a[i + 1] + y 2431 2432 def absoluteToRelative(self): 2433 a = self._a 2434 x, y = 0, 0 2435 for i in range(0, len(a), 2): 2436 nx = a[i] 2437 ny = a[i + 1] 2438 a[i] = nx - x 2439 a[i + 1] = ny - y 2440 x = nx 2441 y = ny 2442 2443 def translate(self, p): 2444 """ 2445 >>> GlyphCoordinates([(1,2)]).translate((.5,0)) 2446 """ 2447 x, y = p 2448 if x == 0 and y == 0: 2449 return 2450 a = self._a 2451 for i in range(0, len(a), 2): 2452 a[i] += x 2453 a[i + 1] += y 2454 2455 def scale(self, p): 2456 """ 2457 >>> GlyphCoordinates([(1,2)]).scale((.5,0)) 2458 """ 2459 x, y = p 2460 if x == 1 and y == 1: 2461 return 2462 a = self._a 2463 for i in range(0, len(a), 2): 2464 a[i] *= x 2465 a[i + 1] *= y 2466 2467 def transform(self, t): 2468 """ 2469 >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) 2470 """ 2471 a = self._a 2472 for i in range(0, len(a), 2): 2473 x = a[i] 2474 y = a[i + 1] 2475 px = x * t[0][0] + y * t[1][0] 2476 py = x * t[0][1] + y * t[1][1] 2477 a[i] = px 2478 a[i + 1] = py 2479 2480 def __eq__(self, other): 2481 """ 2482 >>> g = GlyphCoordinates([(1,2)]) 2483 >>> g2 = GlyphCoordinates([(1.0,2)]) 2484 >>> g3 = GlyphCoordinates([(1.5,2)]) 2485 >>> g == g2 2486 True 2487 >>> g == g3 2488 False 2489 >>> g2 == g3 2490 False 2491 """ 2492 if type(self) != type(other): 2493 return NotImplemented 2494 return self._a == other._a 2495 2496 def __ne__(self, other): 2497 """ 2498 >>> g = GlyphCoordinates([(1,2)]) 2499 >>> g2 = GlyphCoordinates([(1.0,2)]) 2500 >>> g3 = GlyphCoordinates([(1.5,2)]) 2501 >>> g != g2 2502 False 2503 >>> g != g3 2504 True 2505 >>> g2 != g3 2506 True 2507 """ 2508 result = self.__eq__(other) 2509 return result if result is NotImplemented else not result 2510 2511 # Math operations 2512 2513 def __pos__(self): 2514 """ 2515 >>> g = GlyphCoordinates([(1,2)]) 2516 >>> g 2517 GlyphCoordinates([(1, 2)]) 2518 >>> g2 = +g 2519 >>> g2 2520 GlyphCoordinates([(1, 2)]) 2521 >>> g2.translate((1,0)) 2522 >>> g2 2523 GlyphCoordinates([(2, 2)]) 2524 >>> g 2525 GlyphCoordinates([(1, 2)]) 2526 """ 2527 return self.copy() 2528 2529 def __neg__(self): 2530 """ 2531 >>> g = GlyphCoordinates([(1,2)]) 2532 >>> g 2533 GlyphCoordinates([(1, 2)]) 2534 >>> g2 = -g 2535 >>> g2 2536 GlyphCoordinates([(-1, -2)]) 2537 >>> g 2538 GlyphCoordinates([(1, 2)]) 2539 """ 2540 r = self.copy() 2541 a = r._a 2542 for i in range(len(a)): 2543 a[i] = -a[i] 2544 return r 2545 2546 def __round__(self, *, round=otRound): 2547 r = self.copy() 2548 r.toInt(round=round) 2549 return r 2550 2551 def __add__(self, other): 2552 return self.copy().__iadd__(other) 2553 2554 def __sub__(self, other): 2555 return self.copy().__isub__(other) 2556 2557 def __mul__(self, other): 2558 return self.copy().__imul__(other) 2559 2560 def __truediv__(self, other): 2561 return self.copy().__itruediv__(other) 2562 2563 __radd__ = __add__ 2564 __rmul__ = __mul__ 2565 2566 def __rsub__(self, other): 2567 return other + (-self) 2568 2569 def __iadd__(self, other): 2570 """ 2571 >>> g = GlyphCoordinates([(1,2)]) 2572 >>> g += (.5,0) 2573 >>> g 2574 GlyphCoordinates([(1.5, 2)]) 2575 >>> g2 = GlyphCoordinates([(3,4)]) 2576 >>> g += g2 2577 >>> g 2578 GlyphCoordinates([(4.5, 6)]) 2579 """ 2580 if isinstance(other, tuple): 2581 assert len(other) == 2 2582 self.translate(other) 2583 return self 2584 if isinstance(other, GlyphCoordinates): 2585 other = other._a 2586 a = self._a 2587 assert len(a) == len(other) 2588 for i in range(len(a)): 2589 a[i] += other[i] 2590 return self 2591 return NotImplemented 2592 2593 def __isub__(self, other): 2594 """ 2595 >>> g = GlyphCoordinates([(1,2)]) 2596 >>> g -= (.5,0) 2597 >>> g 2598 GlyphCoordinates([(0.5, 2)]) 2599 >>> g2 = GlyphCoordinates([(3,4)]) 2600 >>> g -= g2 2601 >>> g 2602 GlyphCoordinates([(-2.5, -2)]) 2603 """ 2604 if isinstance(other, tuple): 2605 assert len(other) == 2 2606 self.translate((-other[0], -other[1])) 2607 return self 2608 if isinstance(other, GlyphCoordinates): 2609 other = other._a 2610 a = self._a 2611 assert len(a) == len(other) 2612 for i in range(len(a)): 2613 a[i] -= other[i] 2614 return self 2615 return NotImplemented 2616 2617 def __imul__(self, other): 2618 """ 2619 >>> g = GlyphCoordinates([(1,2)]) 2620 >>> g *= (2,.5) 2621 >>> g *= 2 2622 >>> g 2623 GlyphCoordinates([(4, 2)]) 2624 >>> g = GlyphCoordinates([(1,2)]) 2625 >>> g *= 2 2626 >>> g 2627 GlyphCoordinates([(2, 4)]) 2628 """ 2629 if isinstance(other, tuple): 2630 assert len(other) == 2 2631 self.scale(other) 2632 return self 2633 if isinstance(other, Number): 2634 if other == 1: 2635 return self 2636 a = self._a 2637 for i in range(len(a)): 2638 a[i] *= other 2639 return self 2640 return NotImplemented 2641 2642 def __itruediv__(self, other): 2643 """ 2644 >>> g = GlyphCoordinates([(1,3)]) 2645 >>> g /= (.5,1.5) 2646 >>> g /= 2 2647 >>> g 2648 GlyphCoordinates([(1, 1)]) 2649 """ 2650 if isinstance(other, Number): 2651 other = (other, other) 2652 if isinstance(other, tuple): 2653 if other == (1, 1): 2654 return self 2655 assert len(other) == 2 2656 self.scale((1.0 / other[0], 1.0 / other[1])) 2657 return self 2658 return NotImplemented 2659 2660 def __bool__(self): 2661 """ 2662 >>> g = GlyphCoordinates([]) 2663 >>> bool(g) 2664 False 2665 >>> g = GlyphCoordinates([(0,0), (0.,0)]) 2666 >>> bool(g) 2667 True 2668 >>> g = GlyphCoordinates([(0,0), (1,0)]) 2669 >>> bool(g) 2670 True 2671 >>> g = GlyphCoordinates([(0,.5), (0,0)]) 2672 >>> bool(g) 2673 True 2674 """ 2675 return bool(self._a) 2676 2677 __nonzero__ = __bool__ 2678 2679 2680if __name__ == "__main__": 2681 import doctest, sys 2682 2683 sys.exit(doctest.testmod().failed) 2684