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