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