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.textTools import tostr, safeEval, pad 8from fontTools.misc.arrayTools import calcIntBounds, pointInRect 9from fontTools.misc.bezierTools import calcQuadraticBounds 10from fontTools.misc.fixedTools import ( 11 fixedToFloat as fi2fl, 12 floatToFixed as fl2fi, 13 floatToFixedToStr as fl2str, 14 strToFixedToFloat as str2fl, 15 otRound, 16) 17from numbers import Number 18from . import DefaultTable 19from . import ttProgram 20import sys 21import struct 22import array 23import logging 24import os 25from fontTools.misc import xmlWriter 26from fontTools.misc.filenames import userNameToFileName 27from fontTools.misc.loggingTools import deprecateFunction 28 29log = logging.getLogger(__name__) 30 31# We compute the version the same as is computed in ttlib/__init__ 32# so that we can write 'ttLibVersion' attribute of the glyf TTX files 33# when glyf is written to separate files. 34version = ".".join(version.split('.')[:2]) 35 36# 37# The Apple and MS rasterizers behave differently for 38# scaled composite components: one does scale first and then translate 39# and the other does it vice versa. MS defined some flags to indicate 40# the difference, but it seems nobody actually _sets_ those flags. 41# 42# Funny thing: Apple seems to _only_ do their thing in the 43# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE 44# (eg. Charcoal)... 45# 46SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple 47 48 49class table__g_l_y_f(DefaultTable.DefaultTable): 50 """Glyph Data Table 51 52 This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_ 53 table, which contains outlines for glyphs in TrueType format. In many cases, 54 it is easier to access and manipulate glyph outlines through the ``GlyphSet`` 55 object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`:: 56 57 >> from fontTools.pens.boundsPen import BoundsPen 58 >> glyphset = font.getGlyphSet() 59 >> bp = BoundsPen(glyphset) 60 >> glyphset["A"].draw(bp) 61 >> bp.bounds 62 (19, 0, 633, 716) 63 64 However, this class can be used for low-level access to the ``glyf`` table data. 65 Objects of this class support dictionary-like access, mapping glyph names to 66 :py:class:`Glyph` objects:: 67 68 >> glyf = font["glyf"] 69 >> len(glyf["Aacute"].components) 70 2 71 72 Note that when adding glyphs to the font via low-level access to the ``glyf`` 73 table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table:: 74 75 >> font["glyf"]["divisionslash"] = Glyph() 76 >> font["hmtx"]["divisionslash"] = (640, 0) 77 78 """ 79 80 # this attribute controls the amount of padding applied to glyph data upon compile. 81 # Glyph lenghts are aligned to multiples of the specified value. 82 # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means 83 # no padding, except for when padding would allow to use short loca offsets. 84 padding = 1 85 86 def decompile(self, data, ttFont): 87 loca = ttFont['loca'] 88 pos = int(loca[0]) 89 nextPos = 0 90 noname = 0 91 self.glyphs = {} 92 self.glyphOrder = glyphOrder = ttFont.getGlyphOrder() 93 for i in range(0, len(loca)-1): 94 try: 95 glyphName = glyphOrder[i] 96 except IndexError: 97 noname = noname + 1 98 glyphName = 'ttxautoglyph%s' % i 99 nextPos = int(loca[i+1]) 100 glyphdata = data[pos:nextPos] 101 if len(glyphdata) != (nextPos - pos): 102 raise ttLib.TTLibError("not enough 'glyf' table data") 103 glyph = Glyph(glyphdata) 104 self.glyphs[glyphName] = glyph 105 pos = nextPos 106 if len(data) - nextPos >= 4: 107 log.warning( 108 "too much 'glyf' table data: expected %d, received %d bytes", 109 nextPos, len(data)) 110 if noname: 111 log.warning('%s glyphs have no name', noname) 112 if ttFont.lazy is False: # Be lazy for None and True 113 self.ensureDecompiled() 114 115 def ensureDecompiled(self, recurse=False): 116 # The recurse argument is unused, but part of the signature of 117 # ensureDecompiled across the library. 118 for glyph in self.glyphs.values(): 119 glyph.expand(self) 120 121 def compile(self, ttFont): 122 if not hasattr(self, "glyphOrder"): 123 self.glyphOrder = ttFont.getGlyphOrder() 124 padding = self.padding 125 assert padding in (0, 1, 2, 4) 126 locations = [] 127 currentLocation = 0 128 dataList = [] 129 recalcBBoxes = ttFont.recalcBBoxes 130 for glyphName in self.glyphOrder: 131 glyph = self.glyphs[glyphName] 132 glyphData = glyph.compile(self, recalcBBoxes) 133 if padding > 1: 134 glyphData = pad(glyphData, size=padding) 135 locations.append(currentLocation) 136 currentLocation = currentLocation + len(glyphData) 137 dataList.append(glyphData) 138 locations.append(currentLocation) 139 140 if padding == 1 and currentLocation < 0x20000: 141 # See if we can pad any odd-lengthed glyphs to allow loca 142 # table to use the short offsets. 143 indices = [i for i,glyphData in enumerate(dataList) if len(glyphData) % 2 == 1] 144 if indices and currentLocation + len(indices) < 0x20000: 145 # It fits. Do it. 146 for i in indices: 147 dataList[i] += b'\0' 148 currentLocation = 0 149 for i,glyphData in enumerate(dataList): 150 locations[i] = currentLocation 151 currentLocation += len(glyphData) 152 locations[len(dataList)] = currentLocation 153 154 data = b''.join(dataList) 155 if 'loca' in ttFont: 156 ttFont['loca'].set(locations) 157 if 'maxp' in ttFont: 158 ttFont['maxp'].numGlyphs = len(self.glyphs) 159 if not data: 160 # As a special case when all glyph in the font are empty, add a zero byte 161 # to the table, so that OTS doesn’t reject it, and to make the table work 162 # on Windows as well. 163 # See https://github.com/khaledhosny/ots/issues/52 164 data = b"\0" 165 return data 166 167 def toXML(self, writer, ttFont, splitGlyphs=False): 168 notice = ( 169 "The xMin, yMin, xMax and yMax values\n" 170 "will be recalculated by the compiler.") 171 glyphNames = ttFont.getGlyphNames() 172 if not splitGlyphs: 173 writer.newline() 174 writer.comment(notice) 175 writer.newline() 176 writer.newline() 177 numGlyphs = len(glyphNames) 178 if splitGlyphs: 179 path, ext = os.path.splitext(writer.file.name) 180 existingGlyphFiles = set() 181 for glyphName in glyphNames: 182 glyph = self.get(glyphName) 183 if glyph is None: 184 log.warning("glyph '%s' does not exist in glyf table", glyphName) 185 continue 186 if glyph.numberOfContours: 187 if splitGlyphs: 188 glyphPath = userNameToFileName( 189 tostr(glyphName, 'utf-8'), 190 existingGlyphFiles, 191 prefix=path + ".", 192 suffix=ext) 193 existingGlyphFiles.add(glyphPath.lower()) 194 glyphWriter = xmlWriter.XMLWriter( 195 glyphPath, idlefunc=writer.idlefunc, 196 newlinestr=writer.newlinestr) 197 glyphWriter.begintag("ttFont", ttLibVersion=version) 198 glyphWriter.newline() 199 glyphWriter.begintag("glyf") 200 glyphWriter.newline() 201 glyphWriter.comment(notice) 202 glyphWriter.newline() 203 writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) 204 else: 205 glyphWriter = writer 206 glyphWriter.begintag('TTGlyph', [ 207 ("name", glyphName), 208 ("xMin", glyph.xMin), 209 ("yMin", glyph.yMin), 210 ("xMax", glyph.xMax), 211 ("yMax", glyph.yMax), 212 ]) 213 glyphWriter.newline() 214 glyph.toXML(glyphWriter, ttFont) 215 glyphWriter.endtag('TTGlyph') 216 glyphWriter.newline() 217 if splitGlyphs: 218 glyphWriter.endtag("glyf") 219 glyphWriter.newline() 220 glyphWriter.endtag("ttFont") 221 glyphWriter.newline() 222 glyphWriter.close() 223 else: 224 writer.simpletag('TTGlyph', name=glyphName) 225 writer.comment("contains no outline data") 226 if not splitGlyphs: 227 writer.newline() 228 writer.newline() 229 230 def fromXML(self, name, attrs, content, ttFont): 231 if name != "TTGlyph": 232 return 233 if not hasattr(self, "glyphs"): 234 self.glyphs = {} 235 if not hasattr(self, "glyphOrder"): 236 self.glyphOrder = ttFont.getGlyphOrder() 237 glyphName = attrs["name"] 238 log.debug("unpacking glyph '%s'", glyphName) 239 glyph = Glyph() 240 for attr in ['xMin', 'yMin', 'xMax', 'yMax']: 241 setattr(glyph, attr, safeEval(attrs.get(attr, '0'))) 242 self.glyphs[glyphName] = glyph 243 for element in content: 244 if not isinstance(element, tuple): 245 continue 246 name, attrs, content = element 247 glyph.fromXML(name, attrs, content, ttFont) 248 if not ttFont.recalcBBoxes: 249 glyph.compact(self, 0) 250 251 def setGlyphOrder(self, glyphOrder): 252 """Sets the glyph order 253 254 Args: 255 glyphOrder ([str]): List of glyph names in order. 256 """ 257 self.glyphOrder = glyphOrder 258 259 def getGlyphName(self, glyphID): 260 """Returns the name for the glyph with the given ID. 261 262 Raises a ``KeyError`` if the glyph name is not found in the font. 263 """ 264 return self.glyphOrder[glyphID] 265 266 def getGlyphID(self, glyphName): 267 """Returns the ID of the glyph with the given name. 268 269 Raises a ``ValueError`` if the glyph is not found in the font. 270 """ 271 # XXX optimize with reverse dict!!! 272 return self.glyphOrder.index(glyphName) 273 274 def removeHinting(self): 275 """Removes TrueType hints from all glyphs in the glyphset. 276 277 See :py:meth:`Glyph.removeHinting`. 278 """ 279 for glyph in self.glyphs.values(): 280 glyph.removeHinting() 281 282 def keys(self): 283 return self.glyphs.keys() 284 285 def has_key(self, glyphName): 286 return glyphName in self.glyphs 287 288 __contains__ = has_key 289 290 def get(self, glyphName, default=None): 291 glyph = self.glyphs.get(glyphName, default) 292 if glyph is not None: 293 glyph.expand(self) 294 return glyph 295 296 def __getitem__(self, glyphName): 297 glyph = self.glyphs[glyphName] 298 glyph.expand(self) 299 return glyph 300 301 def __setitem__(self, glyphName, glyph): 302 self.glyphs[glyphName] = glyph 303 if glyphName not in self.glyphOrder: 304 self.glyphOrder.append(glyphName) 305 306 def __delitem__(self, glyphName): 307 del self.glyphs[glyphName] 308 self.glyphOrder.remove(glyphName) 309 310 def __len__(self): 311 assert len(self.glyphOrder) == len(self.glyphs) 312 return len(self.glyphs) 313 314 def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None): 315 """Compute the four "phantom points" for the given glyph from its bounding box 316 and the horizontal and vertical advance widths and sidebearings stored in the 317 ttFont's "hmtx" and "vmtx" tables. 318 319 'hMetrics' should be ttFont['hmtx'].metrics. 320 321 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise. 322 If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate. 323 324 https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms 325 """ 326 glyph = self[glyphName] 327 if not hasattr(glyph, 'xMin'): 328 glyph.recalcBounds(self) 329 330 horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName] 331 leftSideX = glyph.xMin - leftSideBearing 332 rightSideX = leftSideX + horizontalAdvanceWidth 333 334 if vMetrics: 335 verticalAdvanceWidth, topSideBearing = vMetrics[glyphName] 336 topSideY = topSideBearing + glyph.yMax 337 bottomSideY = topSideY - verticalAdvanceWidth 338 else: 339 bottomSideY = topSideY = 0 340 341 return [ 342 (leftSideX, 0), 343 (rightSideX, 0), 344 (0, topSideY), 345 (0, bottomSideY), 346 ] 347 348 def _getCoordinatesAndControls(self, glyphName, hMetrics, vMetrics=None): 349 """Return glyph coordinates and controls as expected by "gvar" table. 350 351 The coordinates includes four "phantom points" for the glyph metrics, 352 as mandated by the "gvar" spec. 353 354 The glyph controls is a namedtuple with the following attributes: 355 - numberOfContours: -1 for composite glyphs. 356 - endPts: list of indices of end points for each contour in simple 357 glyphs, or component indices in composite glyphs (used for IUP 358 optimization). 359 - flags: array of contour point flags for simple glyphs (None for 360 composite glyphs). 361 - components: list of base glyph names (str) for each component in 362 composite glyphs (None for simple glyphs). 363 364 The "hMetrics" and vMetrics are used to compute the "phantom points" (see 365 the "_getPhantomPoints" method). 366 367 Return None if the requested glyphName is not present. 368 """ 369 glyph = self.get(glyphName) 370 if glyph is None: 371 return None 372 if glyph.isComposite(): 373 coords = GlyphCoordinates( 374 [(getattr(c, 'x', 0), getattr(c, 'y', 0)) for c in glyph.components] 375 ) 376 controls = _GlyphControls( 377 numberOfContours=glyph.numberOfContours, 378 endPts=list(range(len(glyph.components))), 379 flags=None, 380 components=[c.glyphName for c in glyph.components], 381 ) 382 else: 383 coords, endPts, flags = glyph.getCoordinates(self) 384 coords = coords.copy() 385 controls = _GlyphControls( 386 numberOfContours=glyph.numberOfContours, 387 endPts=endPts, 388 flags=flags, 389 components=None, 390 ) 391 # Add phantom points for (left, right, top, bottom) positions. 392 phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics) 393 coords.extend(phantomPoints) 394 return coords, controls 395 396 def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None): 397 """Set coordinates and metrics for the given glyph. 398 399 "coord" is an array of GlyphCoordinates which must include the "phantom 400 points" as the last four coordinates. 401 402 Both the horizontal/vertical advances and left/top sidebearings in "hmtx" 403 and "vmtx" tables (if any) are updated from four phantom points and 404 the glyph's bounding boxes. 405 406 The "hMetrics" and vMetrics are used to propagate "phantom points" 407 into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints" 408 method). 409 """ 410 glyph = self[glyphName] 411 412 # Handle phantom points for (left, right, top, bottom) positions. 413 assert len(coord) >= 4 414 leftSideX = coord[-4][0] 415 rightSideX = coord[-3][0] 416 topSideY = coord[-2][1] 417 bottomSideY = coord[-1][1] 418 419 coord = coord[:-4] 420 421 if glyph.isComposite(): 422 assert len(coord) == len(glyph.components) 423 for p, comp in zip(coord, glyph.components): 424 if hasattr(comp, 'x'): 425 comp.x, comp.y = p 426 elif glyph.numberOfContours == 0: 427 assert len(coord) == 0 428 else: 429 assert len(coord) == len(glyph.coordinates) 430 glyph.coordinates = GlyphCoordinates(coord) 431 432 glyph.recalcBounds(self) 433 434 horizontalAdvanceWidth = otRound(rightSideX - leftSideX) 435 if horizontalAdvanceWidth < 0: 436 # unlikely, but it can happen, see: 437 # https://github.com/fonttools/fonttools/pull/1198 438 horizontalAdvanceWidth = 0 439 leftSideBearing = otRound(glyph.xMin - leftSideX) 440 hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing 441 442 if vMetrics is not None: 443 verticalAdvanceWidth = otRound(topSideY - bottomSideY) 444 if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal 445 verticalAdvanceWidth = 0 446 topSideBearing = otRound(topSideY - glyph.yMax) 447 vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing 448 449 450 # Deprecated 451 452 def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin): 453 """This method is wrong and deprecated. 454 For rationale see: 455 https://github.com/fonttools/fonttools/pull/2266/files#r613569473 456 """ 457 vMetrics = getattr(ttFont.get('vmtx'), 'metrics', None) 458 if vMetrics is None: 459 verticalAdvanceWidth = ttFont["head"].unitsPerEm 460 topSideY = getattr(ttFont.get('hhea'), 'ascent', None) 461 if topSideY is None: 462 if defaultVerticalOrigin is not None: 463 topSideY = defaultVerticalOrigin 464 else: 465 topSideY = verticalAdvanceWidth 466 glyph = self[glyphName] 467 glyph.recalcBounds(self) 468 topSideBearing = otRound(topSideY - glyph.yMax) 469 vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)} 470 return vMetrics 471 472 @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning) 473 def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None): 474 """Old public name for self._getPhantomPoints(). 475 See: https://github.com/fonttools/fonttools/pull/2266""" 476 hMetrics = ttFont['hmtx'].metrics 477 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 478 return self._getPhantomPoints(glyphName, hMetrics, vMetrics) 479 480 @deprecateFunction("use '_getCoordinatesAndControls' instead", category=DeprecationWarning) 481 def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None): 482 """Old public name for self._getCoordinatesAndControls(). 483 See: https://github.com/fonttools/fonttools/pull/2266""" 484 hMetrics = ttFont['hmtx'].metrics 485 vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin) 486 return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics) 487 488 @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning) 489 def setCoordinates(self, glyphName, ttFont): 490 """Old public name for self._setCoordinates(). 491 See: https://github.com/fonttools/fonttools/pull/2266""" 492 hMetrics = ttFont['hmtx'].metrics 493 vMetrics = getattr(ttFont.get('vmtx'), 'metrics', None) 494 self._setCoordinates(glyphName, hMetrics, vMetrics) 495 496 497_GlyphControls = namedtuple( 498 "_GlyphControls", "numberOfContours endPts flags components" 499) 500 501 502glyphHeaderFormat = """ 503 > # big endian 504 numberOfContours: h 505 xMin: h 506 yMin: h 507 xMax: h 508 yMax: h 509""" 510 511# flags 512flagOnCurve = 0x01 513flagXShort = 0x02 514flagYShort = 0x04 515flagRepeat = 0x08 516flagXsame = 0x10 517flagYsame = 0x20 518flagOverlapSimple = 0x40 519flagReserved = 0x80 520 521# These flags are kept for XML output after decompiling the coordinates 522keepFlags = flagOnCurve + flagOverlapSimple 523 524_flagSignBytes = { 525 0: 2, 526 flagXsame: 0, 527 flagXShort|flagXsame: +1, 528 flagXShort: -1, 529 flagYsame: 0, 530 flagYShort|flagYsame: +1, 531 flagYShort: -1, 532} 533 534def flagBest(x, y, onCurve): 535 """For a given x,y delta pair, returns the flag that packs this pair 536 most efficiently, as well as the number of byte cost of such flag.""" 537 538 flag = flagOnCurve if onCurve else 0 539 cost = 0 540 # do x 541 if x == 0: 542 flag = flag | flagXsame 543 elif -255 <= x <= 255: 544 flag = flag | flagXShort 545 if x > 0: 546 flag = flag | flagXsame 547 cost += 1 548 else: 549 cost += 2 550 # do y 551 if y == 0: 552 flag = flag | flagYsame 553 elif -255 <= y <= 255: 554 flag = flag | flagYShort 555 if y > 0: 556 flag = flag | flagYsame 557 cost += 1 558 else: 559 cost += 2 560 return flag, cost 561 562def flagFits(newFlag, oldFlag, mask): 563 newBytes = _flagSignBytes[newFlag & mask] 564 oldBytes = _flagSignBytes[oldFlag & mask] 565 return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) 566 567def flagSupports(newFlag, oldFlag): 568 return ((oldFlag & flagOnCurve) == (newFlag & flagOnCurve) and 569 flagFits(newFlag, oldFlag, flagXsame|flagXShort) and 570 flagFits(newFlag, oldFlag, flagYsame|flagYShort)) 571 572def flagEncodeCoord(flag, mask, coord, coordBytes): 573 byteCount = _flagSignBytes[flag & mask] 574 if byteCount == 1: 575 coordBytes.append(coord) 576 elif byteCount == -1: 577 coordBytes.append(-coord) 578 elif byteCount == 2: 579 coordBytes.extend(struct.pack('>h', coord)) 580 581def flagEncodeCoords(flag, x, y, xBytes, yBytes): 582 flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes) 583 flagEncodeCoord(flag, flagYsame|flagYShort, y, yBytes) 584 585 586ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes 587ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points 588ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true 589WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 590NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) 591MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one 592WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 593WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 594WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 595USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph 596OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts 597SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) 598UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) 599 600 601CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth']) 602 603 604class Glyph(object): 605 """This class represents an individual TrueType glyph. 606 607 TrueType glyph objects come in two flavours: simple and composite. Simple 608 glyph objects contain contours, represented via the ``.coordinates``, 609 ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes; 610 composite glyphs contain components, available through the ``.components`` 611 attributes. 612 613 Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned 614 above) is only set on simple glyphs and the ``.components`` attribute is only 615 set on composite glyphs, it is necessary to use the :py:meth:`isComposite` 616 method to test whether a glyph is simple or composite before attempting to 617 access its data. 618 619 For a composite glyph, the components can also be accessed via array-like access:: 620 621 >> assert(font["glyf"]["Aacute"].isComposite()) 622 >> font["glyf"]["Aacute"][0] 623 <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0> 624 625 """ 626 627 def __init__(self, data=b""): 628 if not data: 629 # empty char 630 self.numberOfContours = 0 631 return 632 self.data = data 633 634 def compact(self, glyfTable, recalcBBoxes=True): 635 data = self.compile(glyfTable, recalcBBoxes) 636 self.__dict__.clear() 637 self.data = data 638 639 def expand(self, glyfTable): 640 if not hasattr(self, "data"): 641 # already unpacked 642 return 643 if not self.data: 644 # empty char 645 del self.data 646 self.numberOfContours = 0 647 return 648 dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) 649 del self.data 650 # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in 651 # some glyphs; decompileCoordinates assumes that there's at least 652 # one, so short-circuit here. 653 if self.numberOfContours == 0: 654 return 655 if self.isComposite(): 656 self.decompileComponents(data, glyfTable) 657 else: 658 self.decompileCoordinates(data) 659 660 def compile(self, glyfTable, recalcBBoxes=True): 661 if hasattr(self, "data"): 662 if recalcBBoxes: 663 # must unpack glyph in order to recalculate bounding box 664 self.expand(glyfTable) 665 else: 666 return self.data 667 if self.numberOfContours == 0: 668 return b'' 669 if recalcBBoxes: 670 self.recalcBounds(glyfTable) 671 data = sstruct.pack(glyphHeaderFormat, self) 672 if self.isComposite(): 673 data = data + self.compileComponents(glyfTable) 674 else: 675 data = data + self.compileCoordinates() 676 return data 677 678 def toXML(self, writer, ttFont): 679 if self.isComposite(): 680 for compo in self.components: 681 compo.toXML(writer, ttFont) 682 haveInstructions = hasattr(self, "program") 683 else: 684 last = 0 685 for i in range(self.numberOfContours): 686 writer.begintag("contour") 687 writer.newline() 688 for j in range(last, self.endPtsOfContours[i] + 1): 689 attrs = [ 690 ("x", self.coordinates[j][0]), 691 ("y", self.coordinates[j][1]), 692 ("on", self.flags[j] & flagOnCurve), 693 ] 694 if self.flags[j] & flagOverlapSimple: 695 # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours 696 attrs.append(("overlap", 1)) 697 writer.simpletag("pt", attrs) 698 writer.newline() 699 last = self.endPtsOfContours[i] + 1 700 writer.endtag("contour") 701 writer.newline() 702 haveInstructions = self.numberOfContours > 0 703 if haveInstructions: 704 if self.program: 705 writer.begintag("instructions") 706 writer.newline() 707 self.program.toXML(writer, ttFont) 708 writer.endtag("instructions") 709 else: 710 writer.simpletag("instructions") 711 writer.newline() 712 713 def fromXML(self, name, attrs, content, ttFont): 714 if name == "contour": 715 if self.numberOfContours < 0: 716 raise ttLib.TTLibError("can't mix composites and contours in glyph") 717 self.numberOfContours = self.numberOfContours + 1 718 coordinates = GlyphCoordinates() 719 flags = bytearray() 720 for element in content: 721 if not isinstance(element, tuple): 722 continue 723 name, attrs, content = element 724 if name != "pt": 725 continue # ignore anything but "pt" 726 coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) 727 flag = bool(safeEval(attrs["on"])) 728 if "overlap" in attrs and bool(safeEval(attrs["overlap"])): 729 flag |= flagOverlapSimple 730 flags.append(flag) 731 if not hasattr(self, "coordinates"): 732 self.coordinates = coordinates 733 self.flags = flags 734 self.endPtsOfContours = [len(coordinates)-1] 735 else: 736 self.coordinates.extend (coordinates) 737 self.flags.extend(flags) 738 self.endPtsOfContours.append(len(self.coordinates)-1) 739 elif name == "component": 740 if self.numberOfContours > 0: 741 raise ttLib.TTLibError("can't mix composites and contours in glyph") 742 self.numberOfContours = -1 743 if not hasattr(self, "components"): 744 self.components = [] 745 component = GlyphComponent() 746 self.components.append(component) 747 component.fromXML(name, attrs, content, ttFont) 748 elif name == "instructions": 749 self.program = ttProgram.Program() 750 for element in content: 751 if not isinstance(element, tuple): 752 continue 753 name, attrs, content = element 754 self.program.fromXML(name, attrs, content, ttFont) 755 756 def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): 757 assert self.isComposite() 758 nContours = 0 759 nPoints = 0 760 initialMaxComponentDepth = maxComponentDepth 761 for compo in self.components: 762 baseGlyph = glyfTable[compo.glyphName] 763 if baseGlyph.numberOfContours == 0: 764 continue 765 elif baseGlyph.numberOfContours > 0: 766 nP, nC = baseGlyph.getMaxpValues() 767 else: 768 nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues( 769 glyfTable, initialMaxComponentDepth + 1) 770 maxComponentDepth = max(maxComponentDepth, componentDepth) 771 nPoints = nPoints + nP 772 nContours = nContours + nC 773 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 774 775 def getMaxpValues(self): 776 assert self.numberOfContours > 0 777 return len(self.coordinates), len(self.endPtsOfContours) 778 779 def decompileComponents(self, data, glyfTable): 780 self.components = [] 781 more = 1 782 haveInstructions = 0 783 while more: 784 component = GlyphComponent() 785 more, haveInstr, data = component.decompile(data, glyfTable) 786 haveInstructions = haveInstructions | haveInstr 787 self.components.append(component) 788 if haveInstructions: 789 numInstructions, = struct.unpack(">h", data[:2]) 790 data = data[2:] 791 self.program = ttProgram.Program() 792 self.program.fromBytecode(data[:numInstructions]) 793 data = data[numInstructions:] 794 if len(data) >= 4: 795 log.warning( 796 "too much glyph data at the end of composite glyph: %d excess bytes", 797 len(data)) 798 799 def decompileCoordinates(self, data): 800 endPtsOfContours = array.array("h") 801 endPtsOfContours.frombytes(data[:2*self.numberOfContours]) 802 if sys.byteorder != "big": endPtsOfContours.byteswap() 803 self.endPtsOfContours = endPtsOfContours.tolist() 804 805 pos = 2*self.numberOfContours 806 instructionLength, = struct.unpack(">h", data[pos:pos+2]) 807 self.program = ttProgram.Program() 808 self.program.fromBytecode(data[pos+2:pos+2+instructionLength]) 809 pos += 2 + instructionLength 810 nCoordinates = self.endPtsOfContours[-1] + 1 811 flags, xCoordinates, yCoordinates = \ 812 self.decompileCoordinatesRaw(nCoordinates, data, pos) 813 814 # fill in repetitions and apply signs 815 self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) 816 xIndex = 0 817 yIndex = 0 818 for i in range(nCoordinates): 819 flag = flags[i] 820 # x coordinate 821 if flag & flagXShort: 822 if flag & flagXsame: 823 x = xCoordinates[xIndex] 824 else: 825 x = -xCoordinates[xIndex] 826 xIndex = xIndex + 1 827 elif flag & flagXsame: 828 x = 0 829 else: 830 x = xCoordinates[xIndex] 831 xIndex = xIndex + 1 832 # y coordinate 833 if flag & flagYShort: 834 if flag & flagYsame: 835 y = yCoordinates[yIndex] 836 else: 837 y = -yCoordinates[yIndex] 838 yIndex = yIndex + 1 839 elif flag & flagYsame: 840 y = 0 841 else: 842 y = yCoordinates[yIndex] 843 yIndex = yIndex + 1 844 coordinates[i] = (x, y) 845 assert xIndex == len(xCoordinates) 846 assert yIndex == len(yCoordinates) 847 coordinates.relativeToAbsolute() 848 # discard all flags except "keepFlags" 849 for i in range(len(flags)): 850 flags[i] &= keepFlags 851 self.flags = flags 852 853 def decompileCoordinatesRaw(self, nCoordinates, data, pos=0): 854 # unpack flags and prepare unpacking of coordinates 855 flags = bytearray(nCoordinates) 856 # Warning: deep Python trickery going on. We use the struct module to unpack 857 # the coordinates. We build a format string based on the flags, so we can 858 # unpack the coordinates in one struct.unpack() call. 859 xFormat = ">" # big endian 860 yFormat = ">" # big endian 861 j = 0 862 while True: 863 flag = data[pos] 864 pos += 1 865 repeat = 1 866 if flag & flagRepeat: 867 repeat = data[pos] + 1 868 pos += 1 869 for k in range(repeat): 870 if flag & flagXShort: 871 xFormat = xFormat + 'B' 872 elif not (flag & flagXsame): 873 xFormat = xFormat + 'h' 874 if flag & flagYShort: 875 yFormat = yFormat + 'B' 876 elif not (flag & flagYsame): 877 yFormat = yFormat + 'h' 878 flags[j] = flag 879 j = j + 1 880 if j >= nCoordinates: 881 break 882 assert j == nCoordinates, "bad glyph flags" 883 # unpack raw coordinates, krrrrrr-tching! 884 xDataLen = struct.calcsize(xFormat) 885 yDataLen = struct.calcsize(yFormat) 886 if len(data) - pos - (xDataLen + yDataLen) >= 4: 887 log.warning( 888 "too much glyph data: %d excess bytes", len(data) - pos - (xDataLen + yDataLen)) 889 xCoordinates = struct.unpack(xFormat, data[pos:pos+xDataLen]) 890 yCoordinates = struct.unpack(yFormat, data[pos+xDataLen:pos+xDataLen+yDataLen]) 891 return flags, xCoordinates, yCoordinates 892 893 def compileComponents(self, glyfTable): 894 data = b"" 895 lastcomponent = len(self.components) - 1 896 more = 1 897 haveInstructions = 0 898 for i in range(len(self.components)): 899 if i == lastcomponent: 900 haveInstructions = hasattr(self, "program") 901 more = 0 902 compo = self.components[i] 903 data = data + compo.compile(more, haveInstructions, glyfTable) 904 if haveInstructions: 905 instructions = self.program.getBytecode() 906 data = data + struct.pack(">h", len(instructions)) + instructions 907 return data 908 909 def compileCoordinates(self): 910 assert len(self.coordinates) == len(self.flags) 911 data = [] 912 endPtsOfContours = array.array("h", self.endPtsOfContours) 913 if sys.byteorder != "big": endPtsOfContours.byteswap() 914 data.append(endPtsOfContours.tobytes()) 915 instructions = self.program.getBytecode() 916 data.append(struct.pack(">h", len(instructions))) 917 data.append(instructions) 918 919 deltas = self.coordinates.copy() 920 deltas.toInt() 921 deltas.absoluteToRelative() 922 923 # TODO(behdad): Add a configuration option for this? 924 deltas = self.compileDeltasGreedy(self.flags, deltas) 925 #deltas = self.compileDeltasOptimal(self.flags, deltas) 926 927 data.extend(deltas) 928 return b''.join(data) 929 930 def compileDeltasGreedy(self, flags, deltas): 931 # Implements greedy algorithm for packing coordinate deltas: 932 # uses shortest representation one coordinate at a time. 933 compressedFlags = bytearray() 934 compressedXs = bytearray() 935 compressedYs = bytearray() 936 lastflag = None 937 repeat = 0 938 for flag,(x,y) in zip(flags, deltas): 939 # Oh, the horrors of TrueType 940 # do x 941 if x == 0: 942 flag = flag | flagXsame 943 elif -255 <= x <= 255: 944 flag = flag | flagXShort 945 if x > 0: 946 flag = flag | flagXsame 947 else: 948 x = -x 949 compressedXs.append(x) 950 else: 951 compressedXs.extend(struct.pack('>h', x)) 952 # do y 953 if y == 0: 954 flag = flag | flagYsame 955 elif -255 <= y <= 255: 956 flag = flag | flagYShort 957 if y > 0: 958 flag = flag | flagYsame 959 else: 960 y = -y 961 compressedYs.append(y) 962 else: 963 compressedYs.extend(struct.pack('>h', y)) 964 # handle repeating flags 965 if flag == lastflag and repeat != 255: 966 repeat = repeat + 1 967 if repeat == 1: 968 compressedFlags.append(flag) 969 else: 970 compressedFlags[-2] = flag | flagRepeat 971 compressedFlags[-1] = repeat 972 else: 973 repeat = 0 974 compressedFlags.append(flag) 975 lastflag = flag 976 return (compressedFlags, compressedXs, compressedYs) 977 978 def compileDeltasOptimal(self, flags, deltas): 979 # Implements optimal, dynaic-programming, algorithm for packing coordinate 980 # deltas. The savings are negligible :(. 981 candidates = [] 982 bestTuple = None 983 bestCost = 0 984 repeat = 0 985 for flag,(x,y) in zip(flags, deltas): 986 # Oh, the horrors of TrueType 987 flag, coordBytes = flagBest(x, y, flag) 988 bestCost += 1 + coordBytes 989 newCandidates = [(bestCost, bestTuple, flag, coordBytes), 990 (bestCost+1, bestTuple, (flag|flagRepeat), coordBytes)] 991 for lastCost,lastTuple,lastFlag,coordBytes in candidates: 992 if lastCost + coordBytes <= bestCost + 1 and (lastFlag & flagRepeat) and (lastFlag < 0xff00) and flagSupports(lastFlag, flag): 993 if (lastFlag & 0xFF) == (flag|flagRepeat) and lastCost == bestCost + 1: 994 continue 995 newCandidates.append((lastCost + coordBytes, lastTuple, lastFlag+256, coordBytes)) 996 candidates = newCandidates 997 bestTuple = min(candidates, key=lambda t:t[0]) 998 bestCost = bestTuple[0] 999 1000 flags = [] 1001 while bestTuple: 1002 cost, bestTuple, flag, coordBytes = bestTuple 1003 flags.append(flag) 1004 flags.reverse() 1005 1006 compressedFlags = bytearray() 1007 compressedXs = bytearray() 1008 compressedYs = bytearray() 1009 coords = iter(deltas) 1010 ff = [] 1011 for flag in flags: 1012 repeatCount, flag = flag >> 8, flag & 0xFF 1013 compressedFlags.append(flag) 1014 if flag & flagRepeat: 1015 assert(repeatCount > 0) 1016 compressedFlags.append(repeatCount) 1017 else: 1018 assert(repeatCount == 0) 1019 for i in range(1 + repeatCount): 1020 x,y = next(coords) 1021 flagEncodeCoords(flag, x, y, compressedXs, compressedYs) 1022 ff.append(flag) 1023 try: 1024 next(coords) 1025 raise Exception("internal error") 1026 except StopIteration: 1027 pass 1028 1029 return (compressedFlags, compressedXs, compressedYs) 1030 1031 def recalcBounds(self, glyfTable): 1032 """Recalculates the bounds of the glyph. 1033 1034 Each glyph object stores its bounding box in the 1035 ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be 1036 recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds 1037 must be provided to resolve component bounds. 1038 """ 1039 coords, endPts, flags = self.getCoordinates(glyfTable) 1040 self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) 1041 1042 def isComposite(self): 1043 """Test whether a glyph has components""" 1044 if hasattr(self, "data") and self.data: 1045 return struct.unpack(">h", self.data[:2])[0] == -1 1046 else: 1047 return self.numberOfContours == -1 1048 1049 def __getitem__(self, componentIndex): 1050 if not self.isComposite(): 1051 raise ttLib.TTLibError("can't use glyph as sequence") 1052 return self.components[componentIndex] 1053 1054 def getCoordinates(self, glyfTable): 1055 """Return the coordinates, end points and flags 1056 1057 This method returns three values: A :py:class:`GlyphCoordinates` object, 1058 a list of the indexes of the final points of each contour (allowing you 1059 to split up the coordinates list into contours) and a list of flags. 1060 1061 On simple glyphs, this method returns information from the glyph's own 1062 contours; on composite glyphs, it "flattens" all components recursively 1063 to return a list of coordinates representing all the components involved 1064 in the glyph. 1065 1066 To interpret the flags for each point, see the "Simple Glyph Flags" 1067 section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`. 1068 """ 1069 1070 if self.numberOfContours > 0: 1071 return self.coordinates, self.endPtsOfContours, self.flags 1072 elif self.isComposite(): 1073 # it's a composite 1074 allCoords = GlyphCoordinates() 1075 allFlags = bytearray() 1076 allEndPts = [] 1077 for compo in self.components: 1078 g = glyfTable[compo.glyphName] 1079 try: 1080 coordinates, endPts, flags = g.getCoordinates(glyfTable) 1081 except RecursionError: 1082 raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName) 1083 coordinates = GlyphCoordinates(coordinates) 1084 if hasattr(compo, "firstPt"): 1085 # component uses two reference points: we apply the transform _before_ 1086 # computing the offset between the points 1087 if hasattr(compo, "transform"): 1088 coordinates.transform(compo.transform) 1089 x1,y1 = allCoords[compo.firstPt] 1090 x2,y2 = coordinates[compo.secondPt] 1091 move = x1-x2, y1-y2 1092 coordinates.translate(move) 1093 else: 1094 # component uses XY offsets 1095 move = compo.x, compo.y 1096 if not hasattr(compo, "transform"): 1097 coordinates.translate(move) 1098 else: 1099 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 1100 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 1101 assert not (apple_way and ms_way) 1102 if not (apple_way or ms_way): 1103 scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 1104 else: 1105 scale_component_offset = apple_way 1106 if scale_component_offset: 1107 # the Apple way: first move, then scale (ie. scale the component offset) 1108 coordinates.translate(move) 1109 coordinates.transform(compo.transform) 1110 else: 1111 # the MS way: first scale, then move 1112 coordinates.transform(compo.transform) 1113 coordinates.translate(move) 1114 offset = len(allCoords) 1115 allEndPts.extend(e + offset for e in endPts) 1116 allCoords.extend(coordinates) 1117 allFlags.extend(flags) 1118 return allCoords, allEndPts, allFlags 1119 else: 1120 return GlyphCoordinates(), [], bytearray() 1121 1122 def getComponentNames(self, glyfTable): 1123 """Returns a list of names of component glyphs used in this glyph 1124 1125 This method can be used on simple glyphs (in which case it returns an 1126 empty list) or composite glyphs. 1127 """ 1128 if not hasattr(self, "data"): 1129 if self.isComposite(): 1130 return [c.glyphName for c in self.components] 1131 else: 1132 return [] 1133 1134 # Extract components without expanding glyph 1135 1136 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 1137 return [] # Not composite 1138 1139 data = self.data 1140 i = 10 1141 components = [] 1142 more = 1 1143 while more: 1144 flags, glyphID = struct.unpack(">HH", data[i:i+4]) 1145 i += 4 1146 flags = int(flags) 1147 components.append(glyfTable.getGlyphName(int(glyphID))) 1148 1149 if flags & ARG_1_AND_2_ARE_WORDS: i += 4 1150 else: i += 2 1151 if flags & WE_HAVE_A_SCALE: i += 2 1152 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 1153 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 1154 more = flags & MORE_COMPONENTS 1155 1156 return components 1157 1158 def trim(self, remove_hinting=False): 1159 """ Remove padding and, if requested, hinting, from a glyph. 1160 This works on both expanded and compacted glyphs, without 1161 expanding it.""" 1162 if not hasattr(self, "data"): 1163 if remove_hinting: 1164 if self.isComposite(): 1165 if hasattr(self, "program"): 1166 del self.program 1167 else: 1168 self.program = ttProgram.Program() 1169 self.program.fromBytecode([]) 1170 # No padding to trim. 1171 return 1172 if not self.data: 1173 return 1174 numContours = struct.unpack(">h", self.data[:2])[0] 1175 data = bytearray(self.data) 1176 i = 10 1177 if numContours >= 0: 1178 i += 2 * numContours # endPtsOfContours 1179 nCoordinates = ((data[i-2] << 8) | data[i-1]) + 1 1180 instructionLen = (data[i] << 8) | data[i+1] 1181 if remove_hinting: 1182 # Zero instruction length 1183 data[i] = data [i+1] = 0 1184 i += 2 1185 if instructionLen: 1186 # Splice it out 1187 data = data[:i] + data[i+instructionLen:] 1188 instructionLen = 0 1189 else: 1190 i += 2 + instructionLen 1191 1192 coordBytes = 0 1193 j = 0 1194 while True: 1195 flag = data[i] 1196 i = i + 1 1197 repeat = 1 1198 if flag & flagRepeat: 1199 repeat = data[i] + 1 1200 i = i + 1 1201 xBytes = yBytes = 0 1202 if flag & flagXShort: 1203 xBytes = 1 1204 elif not (flag & flagXsame): 1205 xBytes = 2 1206 if flag & flagYShort: 1207 yBytes = 1 1208 elif not (flag & flagYsame): 1209 yBytes = 2 1210 coordBytes += (xBytes + yBytes) * repeat 1211 j += repeat 1212 if j >= nCoordinates: 1213 break 1214 assert j == nCoordinates, "bad glyph flags" 1215 i += coordBytes 1216 # Remove padding 1217 data = data[:i] 1218 else: 1219 more = 1 1220 we_have_instructions = False 1221 while more: 1222 flags =(data[i] << 8) | data[i+1] 1223 if remove_hinting: 1224 flags &= ~WE_HAVE_INSTRUCTIONS 1225 if flags & WE_HAVE_INSTRUCTIONS: 1226 we_have_instructions = True 1227 data[i+0] = flags >> 8 1228 data[i+1] = flags & 0xFF 1229 i += 4 1230 flags = int(flags) 1231 1232 if flags & ARG_1_AND_2_ARE_WORDS: i += 4 1233 else: i += 2 1234 if flags & WE_HAVE_A_SCALE: i += 2 1235 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 1236 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 1237 more = flags & MORE_COMPONENTS 1238 if we_have_instructions: 1239 instructionLen = (data[i] << 8) | data[i+1] 1240 i += 2 + instructionLen 1241 # Remove padding 1242 data = data[:i] 1243 1244 self.data = data 1245 1246 def removeHinting(self): 1247 """Removes TrueType hinting instructions from the glyph.""" 1248 self.trim (remove_hinting=True) 1249 1250 def draw(self, pen, glyfTable, offset=0): 1251 """Draws the glyph using the supplied pen object. 1252 1253 Arguments: 1254 pen: An object conforming to the pen protocol. 1255 glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components. 1256 offset (int): A horizontal offset. If provided, all coordinates are 1257 translated by this offset. 1258 """ 1259 1260 if self.isComposite(): 1261 for component in self.components: 1262 glyphName, transform = component.getComponentInfo() 1263 pen.addComponent(glyphName, transform) 1264 return 1265 1266 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1267 if offset: 1268 coordinates = coordinates.copy() 1269 coordinates.translate((offset, 0)) 1270 start = 0 1271 for end in endPts: 1272 end = end + 1 1273 contour = coordinates[start:end] 1274 cFlags = [flagOnCurve & f for f in flags[start:end]] 1275 start = end 1276 if 1 not in cFlags: 1277 # There is not a single on-curve point on the curve, 1278 # use pen.qCurveTo's special case by specifying None 1279 # as the on-curve point. 1280 contour.append(None) 1281 pen.qCurveTo(*contour) 1282 else: 1283 # Shuffle the points so that contour the is guaranteed 1284 # to *end* in an on-curve point, which we'll use for 1285 # the moveTo. 1286 firstOnCurve = cFlags.index(1) + 1 1287 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 1288 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 1289 pen.moveTo(contour[-1]) 1290 while contour: 1291 nextOnCurve = cFlags.index(1) + 1 1292 if nextOnCurve == 1: 1293 # Skip a final lineTo(), as it is implied by 1294 # pen.closePath() 1295 if len(contour) > 1: 1296 pen.lineTo(contour[0]) 1297 else: 1298 pen.qCurveTo(*contour[:nextOnCurve]) 1299 contour = contour[nextOnCurve:] 1300 cFlags = cFlags[nextOnCurve:] 1301 pen.closePath() 1302 1303 def drawPoints(self, pen, glyfTable, offset=0): 1304 """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(), 1305 this will not change the point indices. 1306 """ 1307 1308 if self.isComposite(): 1309 for component in self.components: 1310 glyphName, transform = component.getComponentInfo() 1311 pen.addComponent(glyphName, transform) 1312 return 1313 1314 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1315 if offset: 1316 coordinates = coordinates.copy() 1317 coordinates.translate((offset, 0)) 1318 start = 0 1319 for end in endPts: 1320 end = end + 1 1321 contour = coordinates[start:end] 1322 cFlags = flags[start:end] 1323 start = end 1324 pen.beginPath() 1325 # Start with the appropriate segment type based on the final segment 1326 segmentType = "line" if cFlags[-1] == 1 else "qcurve" 1327 for i, pt in enumerate(contour): 1328 if cFlags[i] & flagOnCurve == 1: 1329 pen.addPoint(pt, segmentType=segmentType) 1330 segmentType = "line" 1331 else: 1332 pen.addPoint(pt) 1333 segmentType = "qcurve" 1334 pen.endPath() 1335 1336 def __eq__(self, other): 1337 if type(self) != type(other): 1338 return NotImplemented 1339 return self.__dict__ == other.__dict__ 1340 1341 def __ne__(self, other): 1342 result = self.__eq__(other) 1343 return result if result is NotImplemented else not result 1344 1345class GlyphComponent(object): 1346 """Represents a component within a composite glyph. 1347 1348 The component is represented internally with four attributes: ``glyphName``, 1349 ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e 1350 no scaling, reflection, or rotation; only translation), the ``transform`` 1351 attribute is not present. 1352 """ 1353 # The above documentation is not *completely* true, but is *true enough* because 1354 # the rare firstPt/lastPt attributes are not totally supported and nobody seems to 1355 # mind - see below. 1356 1357 def __init__(self): 1358 pass 1359 1360 def getComponentInfo(self): 1361 """Return information about the component 1362 1363 This method returns a tuple of two values: the glyph name of the component's 1364 base glyph, and a transformation matrix. As opposed to accessing the attributes 1365 directly, ``getComponentInfo`` always returns a six-element tuple of the 1366 component's transformation matrix, even when the two-by-two ``.transform`` 1367 matrix is not present. 1368 """ 1369 # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement 1370 # something equivalent in fontTools.objects.glyph (I'd rather not 1371 # convert it to an absolute offset, since it is valuable information). 1372 # This method will now raise "AttributeError: x" on glyphs that use 1373 # this TT feature. 1374 if hasattr(self, "transform"): 1375 [[xx, xy], [yx, yy]] = self.transform 1376 trans = (xx, xy, yx, yy, self.x, self.y) 1377 else: 1378 trans = (1, 0, 0, 1, self.x, self.y) 1379 return self.glyphName, trans 1380 1381 def decompile(self, data, glyfTable): 1382 flags, glyphID = struct.unpack(">HH", data[:4]) 1383 self.flags = int(flags) 1384 glyphID = int(glyphID) 1385 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 1386 data = data[4:] 1387 1388 if self.flags & ARG_1_AND_2_ARE_WORDS: 1389 if self.flags & ARGS_ARE_XY_VALUES: 1390 self.x, self.y = struct.unpack(">hh", data[:4]) 1391 else: 1392 x, y = struct.unpack(">HH", data[:4]) 1393 self.firstPt, self.secondPt = int(x), int(y) 1394 data = data[4:] 1395 else: 1396 if self.flags & ARGS_ARE_XY_VALUES: 1397 self.x, self.y = struct.unpack(">bb", data[:2]) 1398 else: 1399 x, y = struct.unpack(">BB", data[:2]) 1400 self.firstPt, self.secondPt = int(x), int(y) 1401 data = data[2:] 1402 1403 if self.flags & WE_HAVE_A_SCALE: 1404 scale, = struct.unpack(">h", data[:2]) 1405 self.transform = [[fi2fl(scale,14), 0], [0, fi2fl(scale,14)]] # fixed 2.14 1406 data = data[2:] 1407 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 1408 xscale, yscale = struct.unpack(">hh", data[:4]) 1409 self.transform = [[fi2fl(xscale,14), 0], [0, fi2fl(yscale,14)]] # fixed 2.14 1410 data = data[4:] 1411 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 1412 (xscale, scale01, 1413 scale10, yscale) = struct.unpack(">hhhh", data[:8]) 1414 self.transform = [[fi2fl(xscale,14), fi2fl(scale01,14)], 1415 [fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14 1416 data = data[8:] 1417 more = self.flags & MORE_COMPONENTS 1418 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 1419 self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | 1420 SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | 1421 NON_OVERLAPPING | OVERLAP_COMPOUND) 1422 return more, haveInstructions, data 1423 1424 def compile(self, more, haveInstructions, glyfTable): 1425 data = b"" 1426 1427 # reset all flags we will calculate ourselves 1428 flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | 1429 SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | 1430 NON_OVERLAPPING | OVERLAP_COMPOUND) 1431 if more: 1432 flags = flags | MORE_COMPONENTS 1433 if haveInstructions: 1434 flags = flags | WE_HAVE_INSTRUCTIONS 1435 1436 if hasattr(self, "firstPt"): 1437 if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): 1438 data = data + struct.pack(">BB", self.firstPt, self.secondPt) 1439 else: 1440 data = data + struct.pack(">HH", self.firstPt, self.secondPt) 1441 flags = flags | ARG_1_AND_2_ARE_WORDS 1442 else: 1443 x = otRound(self.x) 1444 y = otRound(self.y) 1445 flags = flags | ARGS_ARE_XY_VALUES 1446 if (-128 <= x <= 127) and (-128 <= y <= 127): 1447 data = data + struct.pack(">bb", x, y) 1448 else: 1449 data = data + struct.pack(">hh", x, y) 1450 flags = flags | ARG_1_AND_2_ARE_WORDS 1451 1452 if hasattr(self, "transform"): 1453 transform = [[fl2fi(x,14) for x in row] for row in self.transform] 1454 if transform[0][1] or transform[1][0]: 1455 flags = flags | WE_HAVE_A_TWO_BY_TWO 1456 data = data + struct.pack(">hhhh", 1457 transform[0][0], transform[0][1], 1458 transform[1][0], transform[1][1]) 1459 elif transform[0][0] != transform[1][1]: 1460 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 1461 data = data + struct.pack(">hh", 1462 transform[0][0], transform[1][1]) 1463 else: 1464 flags = flags | WE_HAVE_A_SCALE 1465 data = data + struct.pack(">h", 1466 transform[0][0]) 1467 1468 glyphID = glyfTable.getGlyphID(self.glyphName) 1469 return struct.pack(">HH", flags, glyphID) + data 1470 1471 def toXML(self, writer, ttFont): 1472 attrs = [("glyphName", self.glyphName)] 1473 if not hasattr(self, "firstPt"): 1474 attrs = attrs + [("x", self.x), ("y", self.y)] 1475 else: 1476 attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] 1477 1478 if hasattr(self, "transform"): 1479 transform = self.transform 1480 if transform[0][1] or transform[1][0]: 1481 attrs = attrs + [ 1482 ("scalex", fl2str(transform[0][0], 14)), 1483 ("scale01", fl2str(transform[0][1], 14)), 1484 ("scale10", fl2str(transform[1][0], 14)), 1485 ("scaley", fl2str(transform[1][1], 14)), 1486 ] 1487 elif transform[0][0] != transform[1][1]: 1488 attrs = attrs + [ 1489 ("scalex", fl2str(transform[0][0], 14)), 1490 ("scaley", fl2str(transform[1][1], 14)), 1491 ] 1492 else: 1493 attrs = attrs + [("scale", fl2str(transform[0][0], 14))] 1494 attrs = attrs + [("flags", hex(self.flags))] 1495 writer.simpletag("component", attrs) 1496 writer.newline() 1497 1498 def fromXML(self, name, attrs, content, ttFont): 1499 self.glyphName = attrs["glyphName"] 1500 if "firstPt" in attrs: 1501 self.firstPt = safeEval(attrs["firstPt"]) 1502 self.secondPt = safeEval(attrs["secondPt"]) 1503 else: 1504 self.x = safeEval(attrs["x"]) 1505 self.y = safeEval(attrs["y"]) 1506 if "scale01" in attrs: 1507 scalex = str2fl(attrs["scalex"], 14) 1508 scale01 = str2fl(attrs["scale01"], 14) 1509 scale10 = str2fl(attrs["scale10"], 14) 1510 scaley = str2fl(attrs["scaley"], 14) 1511 self.transform = [[scalex, scale01], [scale10, scaley]] 1512 elif "scalex" in attrs: 1513 scalex = str2fl(attrs["scalex"], 14) 1514 scaley = str2fl(attrs["scaley"], 14) 1515 self.transform = [[scalex, 0], [0, scaley]] 1516 elif "scale" in attrs: 1517 scale = str2fl(attrs["scale"], 14) 1518 self.transform = [[scale, 0], [0, scale]] 1519 self.flags = safeEval(attrs["flags"]) 1520 1521 def __eq__(self, other): 1522 if type(self) != type(other): 1523 return NotImplemented 1524 return self.__dict__ == other.__dict__ 1525 1526 def __ne__(self, other): 1527 result = self.__eq__(other) 1528 return result if result is NotImplemented else not result 1529 1530class GlyphCoordinates(object): 1531 """A list of glyph coordinates. 1532 1533 Unlike an ordinary list, this is a numpy-like matrix object which supports 1534 matrix addition, scalar multiplication and other operations described below. 1535 """ 1536 def __init__(self, iterable=[]): 1537 self._a = array.array('d') 1538 self.extend(iterable) 1539 1540 @property 1541 def array(self): 1542 """Returns the underlying array of coordinates""" 1543 return self._a 1544 1545 @staticmethod 1546 def zeros(count): 1547 """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)""" 1548 g = GlyphCoordinates() 1549 g._a.frombytes(bytes(count * 2 * g._a.itemsize)) 1550 return g 1551 1552 def copy(self): 1553 """Creates a new ``GlyphCoordinates`` object which is a copy of the current one.""" 1554 c = GlyphCoordinates() 1555 c._a.extend(self._a) 1556 return c 1557 1558 def __len__(self): 1559 """Returns the number of coordinates in the array.""" 1560 return len(self._a) // 2 1561 1562 def __getitem__(self, k): 1563 """Returns a two element tuple (x,y)""" 1564 if isinstance(k, slice): 1565 indices = range(*k.indices(len(self))) 1566 return [self[i] for i in indices] 1567 a = self._a 1568 x = a[2*k] 1569 y = a[2*k+1] 1570 return (int(x) if x.is_integer() else x, 1571 int(y) if y.is_integer() else y) 1572 1573 def __setitem__(self, k, v): 1574 """Sets a point's coordinates to a two element tuple (x,y)""" 1575 if isinstance(k, slice): 1576 indices = range(*k.indices(len(self))) 1577 # XXX This only works if len(v) == len(indices) 1578 for j,i in enumerate(indices): 1579 self[i] = v[j] 1580 return 1581 self._a[2*k],self._a[2*k+1] = v 1582 1583 def __delitem__(self, i): 1584 """Removes a point from the list""" 1585 i = (2*i) % len(self._a) 1586 del self._a[i] 1587 del self._a[i] 1588 1589 def __repr__(self): 1590 return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' 1591 1592 def append(self, p): 1593 self._a.extend(tuple(p)) 1594 1595 def extend(self, iterable): 1596 for p in iterable: 1597 self._a.extend(p) 1598 1599 def toInt(self, *, round=otRound): 1600 a = self._a 1601 for i in range(len(a)): 1602 a[i] = round(a[i]) 1603 1604 def relativeToAbsolute(self): 1605 a = self._a 1606 x,y = 0,0 1607 for i in range(0, len(a), 2): 1608 a[i ] = x = a[i ] + x 1609 a[i+1] = y = a[i+1] + y 1610 1611 def absoluteToRelative(self): 1612 a = self._a 1613 x,y = 0,0 1614 for i in range(0, len(a), 2): 1615 nx = a[i ] 1616 ny = a[i+1] 1617 a[i] = nx - x 1618 a[i+1] = ny - y 1619 x = nx 1620 y = ny 1621 1622 def translate(self, p): 1623 """ 1624 >>> GlyphCoordinates([(1,2)]).translate((.5,0)) 1625 """ 1626 x,y = p 1627 if x == 0 and y == 0: 1628 return 1629 a = self._a 1630 for i in range(0, len(a), 2): 1631 a[i] += x 1632 a[i+1] += y 1633 1634 def scale(self, p): 1635 """ 1636 >>> GlyphCoordinates([(1,2)]).scale((.5,0)) 1637 """ 1638 x,y = p 1639 if x == 1 and y == 1: 1640 return 1641 a = self._a 1642 for i in range(0, len(a), 2): 1643 a[i] *= x 1644 a[i+1] *= y 1645 1646 def transform(self, t): 1647 """ 1648 >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) 1649 """ 1650 a = self._a 1651 for i in range(0, len(a), 2): 1652 x = a[i ] 1653 y = a[i+1] 1654 px = x * t[0][0] + y * t[1][0] 1655 py = x * t[0][1] + y * t[1][1] 1656 a[i] = px 1657 a[i+1] = py 1658 1659 def __eq__(self, other): 1660 """ 1661 >>> g = GlyphCoordinates([(1,2)]) 1662 >>> g2 = GlyphCoordinates([(1.0,2)]) 1663 >>> g3 = GlyphCoordinates([(1.5,2)]) 1664 >>> g == g2 1665 True 1666 >>> g == g3 1667 False 1668 >>> g2 == g3 1669 False 1670 """ 1671 if type(self) != type(other): 1672 return NotImplemented 1673 return self._a == other._a 1674 1675 def __ne__(self, other): 1676 """ 1677 >>> g = GlyphCoordinates([(1,2)]) 1678 >>> g2 = GlyphCoordinates([(1.0,2)]) 1679 >>> g3 = GlyphCoordinates([(1.5,2)]) 1680 >>> g != g2 1681 False 1682 >>> g != g3 1683 True 1684 >>> g2 != g3 1685 True 1686 """ 1687 result = self.__eq__(other) 1688 return result if result is NotImplemented else not result 1689 1690 # Math operations 1691 1692 def __pos__(self): 1693 """ 1694 >>> g = GlyphCoordinates([(1,2)]) 1695 >>> g 1696 GlyphCoordinates([(1, 2)]) 1697 >>> g2 = +g 1698 >>> g2 1699 GlyphCoordinates([(1, 2)]) 1700 >>> g2.translate((1,0)) 1701 >>> g2 1702 GlyphCoordinates([(2, 2)]) 1703 >>> g 1704 GlyphCoordinates([(1, 2)]) 1705 """ 1706 return self.copy() 1707 def __neg__(self): 1708 """ 1709 >>> g = GlyphCoordinates([(1,2)]) 1710 >>> g 1711 GlyphCoordinates([(1, 2)]) 1712 >>> g2 = -g 1713 >>> g2 1714 GlyphCoordinates([(-1, -2)]) 1715 >>> g 1716 GlyphCoordinates([(1, 2)]) 1717 """ 1718 r = self.copy() 1719 a = r._a 1720 for i in range(len(a)): 1721 a[i] = -a[i] 1722 return r 1723 def __round__(self, *, round=otRound): 1724 r = self.copy() 1725 r.toInt(round=round) 1726 return r 1727 1728 def __add__(self, other): return self.copy().__iadd__(other) 1729 def __sub__(self, other): return self.copy().__isub__(other) 1730 def __mul__(self, other): return self.copy().__imul__(other) 1731 def __truediv__(self, other): return self.copy().__itruediv__(other) 1732 1733 __radd__ = __add__ 1734 __rmul__ = __mul__ 1735 def __rsub__(self, other): return other + (-self) 1736 1737 def __iadd__(self, other): 1738 """ 1739 >>> g = GlyphCoordinates([(1,2)]) 1740 >>> g += (.5,0) 1741 >>> g 1742 GlyphCoordinates([(1.5, 2)]) 1743 >>> g2 = GlyphCoordinates([(3,4)]) 1744 >>> g += g2 1745 >>> g 1746 GlyphCoordinates([(4.5, 6)]) 1747 """ 1748 if isinstance(other, tuple): 1749 assert len(other) == 2 1750 self.translate(other) 1751 return self 1752 if isinstance(other, GlyphCoordinates): 1753 other = other._a 1754 a = self._a 1755 assert len(a) == len(other) 1756 for i in range(len(a)): 1757 a[i] += other[i] 1758 return self 1759 return NotImplemented 1760 1761 def __isub__(self, other): 1762 """ 1763 >>> g = GlyphCoordinates([(1,2)]) 1764 >>> g -= (.5,0) 1765 >>> g 1766 GlyphCoordinates([(0.5, 2)]) 1767 >>> g2 = GlyphCoordinates([(3,4)]) 1768 >>> g -= g2 1769 >>> g 1770 GlyphCoordinates([(-2.5, -2)]) 1771 """ 1772 if isinstance(other, tuple): 1773 assert len(other) == 2 1774 self.translate((-other[0],-other[1])) 1775 return self 1776 if isinstance(other, GlyphCoordinates): 1777 other = other._a 1778 a = self._a 1779 assert len(a) == len(other) 1780 for i in range(len(a)): 1781 a[i] -= other[i] 1782 return self 1783 return NotImplemented 1784 1785 def __imul__(self, other): 1786 """ 1787 >>> g = GlyphCoordinates([(1,2)]) 1788 >>> g *= (2,.5) 1789 >>> g *= 2 1790 >>> g 1791 GlyphCoordinates([(4, 2)]) 1792 >>> g = GlyphCoordinates([(1,2)]) 1793 >>> g *= 2 1794 >>> g 1795 GlyphCoordinates([(2, 4)]) 1796 """ 1797 if isinstance(other, tuple): 1798 assert len(other) == 2 1799 self.scale(other) 1800 return self 1801 if isinstance(other, Number): 1802 if other == 1: 1803 return self 1804 a = self._a 1805 for i in range(len(a)): 1806 a[i] *= other 1807 return self 1808 return NotImplemented 1809 1810 def __itruediv__(self, other): 1811 """ 1812 >>> g = GlyphCoordinates([(1,3)]) 1813 >>> g /= (.5,1.5) 1814 >>> g /= 2 1815 >>> g 1816 GlyphCoordinates([(1, 1)]) 1817 """ 1818 if isinstance(other, Number): 1819 other = (other, other) 1820 if isinstance(other, tuple): 1821 if other == (1,1): 1822 return self 1823 assert len(other) == 2 1824 self.scale((1./other[0],1./other[1])) 1825 return self 1826 return NotImplemented 1827 1828 def __bool__(self): 1829 """ 1830 >>> g = GlyphCoordinates([]) 1831 >>> bool(g) 1832 False 1833 >>> g = GlyphCoordinates([(0,0), (0.,0)]) 1834 >>> bool(g) 1835 True 1836 >>> g = GlyphCoordinates([(0,0), (1,0)]) 1837 >>> bool(g) 1838 True 1839 >>> g = GlyphCoordinates([(0,.5), (0,0)]) 1840 >>> bool(g) 1841 True 1842 """ 1843 return bool(self._a) 1844 1845 __nonzero__ = __bool__ 1846 1847 1848if __name__ == "__main__": 1849 import doctest, sys 1850 sys.exit(doctest.testmod().failed) 1851