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