1 """_g_l_y_f.py -- Converter classes for the 'glyf' table.""" 2 3 from __future__ import print_function, division, absolute_import 4 from collections import namedtuple 5 from fontTools.misc.py23 import * 6 from fontTools.misc import sstruct 7 from fontTools import ttLib 8 from fontTools import version 9 from fontTools.misc.textTools import safeEval, pad 10 from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect 11 from fontTools.misc.bezierTools import calcQuadraticBounds 12 from fontTools.misc.fixedTools import ( 13 fixedToFloat as fi2fl, 14 floatToFixed as fl2fi, 15 otRound, 16 ) 17 from numbers import Number 18 from . import DefaultTable 19 from . import ttProgram 20 import sys 21 import struct 22 import array 23 import logging 24 import os 25 from fontTools.misc import xmlWriter 26 from fontTools.misc.filenames import userNameToFileName 27 28 log = 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. 33 version = ".".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 # 45 SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple 46 47 48 class 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 247 glyphHeaderFormat = """ 248 > # big endian 249 numberOfContours: h 250 xMin: h 251 yMin: h 252 xMax: h 253 yMax: h 254 """ 255 256 # flags 257 flagOnCurve = 0x01 258 flagXShort = 0x02 259 flagYShort = 0x04 260 flagRepeat = 0x08 261 flagXsame = 0x10 262 flagYsame = 0x20 263 flagOverlapSimple = 0x40 264 flagReserved = 0x80 265 266 # These flags are kept for XML output after decompiling the coordinates 267 keepFlags = flagOnCurve + flagOverlapSimple 268 269 _flagSignBytes = { 270 0: 2, 271 flagXsame: 0, 272 flagXShort|flagXsame: +1, 273 flagXShort: -1, 274 flagYsame: 0, 275 flagYShort|flagYsame: +1, 276 flagYShort: -1, 277 } 278 279 def flagBest(x, y, onCurve): 280 """For a given x,y delta pair, returns the flag that packs this pair 281 most efficiently, as well as the number of byte cost of such flag.""" 282 283 flag = flagOnCurve if onCurve else 0 284 cost = 0 285 # do x 286 if x == 0: 287 flag = flag | flagXsame 288 elif -255 <= x <= 255: 289 flag = flag | flagXShort 290 if x > 0: 291 flag = flag | flagXsame 292 cost += 1 293 else: 294 cost += 2 295 # do y 296 if y == 0: 297 flag = flag | flagYsame 298 elif -255 <= y <= 255: 299 flag = flag | flagYShort 300 if y > 0: 301 flag = flag | flagYsame 302 cost += 1 303 else: 304 cost += 2 305 return flag, cost 306 307 def flagFits(newFlag, oldFlag, mask): 308 newBytes = _flagSignBytes[newFlag & mask] 309 oldBytes = _flagSignBytes[oldFlag & mask] 310 return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) 311 312 def flagSupports(newFlag, oldFlag): 313 return ((oldFlag & flagOnCurve) == (newFlag & flagOnCurve) and 314 flagFits(newFlag, oldFlag, flagXsame|flagXShort) and 315 flagFits(newFlag, oldFlag, flagYsame|flagYShort)) 316 317 def flagEncodeCoord(flag, mask, coord, coordBytes): 318 byteCount = _flagSignBytes[flag & mask] 319 if byteCount == 1: 320 coordBytes.append(coord) 321 elif byteCount == -1: 322 coordBytes.append(-coord) 323 elif byteCount == 2: 324 coordBytes.append((coord >> 8) & 0xFF) 325 coordBytes.append(coord & 0xFF) 326 327 def flagEncodeCoords(flag, x, y, xBytes, yBytes): 328 flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes) 329 flagEncodeCoord(flag, flagYsame|flagYShort, y, yBytes) 330 331 332 ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes 333 ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points 334 ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true 335 WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 336 NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) 337 MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one 338 WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy 339 WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 340 WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow 341 USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph 342 OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts 343 SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) 344 UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) 345 346 347 CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth']) 348 349 350 class Glyph(object): 351 352 def __init__(self, data=""): 353 if not data: 354 # empty char 355 self.numberOfContours = 0 356 return 357 self.data = data 358 359 def compact(self, glyfTable, recalcBBoxes=True): 360 data = self.compile(glyfTable, recalcBBoxes) 361 self.__dict__.clear() 362 self.data = data 363 364 def expand(self, glyfTable): 365 if not hasattr(self, "data"): 366 # already unpacked 367 return 368 if not self.data: 369 # empty char 370 del self.data 371 self.numberOfContours = 0 372 return 373 dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) 374 del self.data 375 # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in 376 # some glyphs; decompileCoordinates assumes that there's at least 377 # one, so short-circuit here. 378 if self.numberOfContours == 0: 379 return 380 if self.isComposite(): 381 self.decompileComponents(data, glyfTable) 382 else: 383 self.decompileCoordinates(data) 384 385 def compile(self, glyfTable, recalcBBoxes=True): 386 if hasattr(self, "data"): 387 if recalcBBoxes: 388 # must unpack glyph in order to recalculate bounding box 389 self.expand(glyfTable) 390 else: 391 return self.data 392 if self.numberOfContours == 0: 393 return "" 394 if recalcBBoxes: 395 self.recalcBounds(glyfTable) 396 data = sstruct.pack(glyphHeaderFormat, self) 397 if self.isComposite(): 398 data = data + self.compileComponents(glyfTable) 399 else: 400 data = data + self.compileCoordinates() 401 return data 402 403 def toXML(self, writer, ttFont): 404 if self.isComposite(): 405 for compo in self.components: 406 compo.toXML(writer, ttFont) 407 haveInstructions = hasattr(self, "program") 408 else: 409 last = 0 410 for i in range(self.numberOfContours): 411 writer.begintag("contour") 412 writer.newline() 413 for j in range(last, self.endPtsOfContours[i] + 1): 414 attrs = [ 415 ("x", self.coordinates[j][0]), 416 ("y", self.coordinates[j][1]), 417 ("on", self.flags[j] & flagOnCurve), 418 ] 419 if self.flags[j] & flagOverlapSimple: 420 # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours 421 attrs.append(("overlap", 1)) 422 writer.simpletag("pt", attrs) 423 writer.newline() 424 last = self.endPtsOfContours[i] + 1 425 writer.endtag("contour") 426 writer.newline() 427 haveInstructions = self.numberOfContours > 0 428 if haveInstructions: 429 if self.program: 430 writer.begintag("instructions") 431 writer.newline() 432 self.program.toXML(writer, ttFont) 433 writer.endtag("instructions") 434 else: 435 writer.simpletag("instructions") 436 writer.newline() 437 438 def fromXML(self, name, attrs, content, ttFont): 439 if name == "contour": 440 if self.numberOfContours < 0: 441 raise ttLib.TTLibError("can't mix composites and contours in glyph") 442 self.numberOfContours = self.numberOfContours + 1 443 coordinates = GlyphCoordinates() 444 flags = [] 445 for element in content: 446 if not isinstance(element, tuple): 447 continue 448 name, attrs, content = element 449 if name != "pt": 450 continue # ignore anything but "pt" 451 coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"]))) 452 flag = not not safeEval(attrs["on"]) 453 if "overlap" in attrs and bool(safeEval(attrs["overlap"])): 454 flag |= flagOverlapSimple 455 flags.append(flag) 456 flags = array.array("B", flags) 457 if not hasattr(self, "coordinates"): 458 self.coordinates = coordinates 459 self.flags = flags 460 self.endPtsOfContours = [len(coordinates)-1] 461 else: 462 self.coordinates.extend (coordinates) 463 self.flags.extend(flags) 464 self.endPtsOfContours.append(len(self.coordinates)-1) 465 elif name == "component": 466 if self.numberOfContours > 0: 467 raise ttLib.TTLibError("can't mix composites and contours in glyph") 468 self.numberOfContours = -1 469 if not hasattr(self, "components"): 470 self.components = [] 471 component = GlyphComponent() 472 self.components.append(component) 473 component.fromXML(name, attrs, content, ttFont) 474 elif name == "instructions": 475 self.program = ttProgram.Program() 476 for element in content: 477 if not isinstance(element, tuple): 478 continue 479 name, attrs, content = element 480 self.program.fromXML(name, attrs, content, ttFont) 481 482 def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): 483 assert self.isComposite() 484 nContours = 0 485 nPoints = 0 486 for compo in self.components: 487 baseGlyph = glyfTable[compo.glyphName] 488 if baseGlyph.numberOfContours == 0: 489 continue 490 elif baseGlyph.numberOfContours > 0: 491 nP, nC = baseGlyph.getMaxpValues() 492 else: 493 nP, nC, maxComponentDepth = baseGlyph.getCompositeMaxpValues( 494 glyfTable, maxComponentDepth + 1) 495 nPoints = nPoints + nP 496 nContours = nContours + nC 497 return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) 498 499 def getMaxpValues(self): 500 assert self.numberOfContours > 0 501 return len(self.coordinates), len(self.endPtsOfContours) 502 503 def decompileComponents(self, data, glyfTable): 504 self.components = [] 505 more = 1 506 haveInstructions = 0 507 while more: 508 component = GlyphComponent() 509 more, haveInstr, data = component.decompile(data, glyfTable) 510 haveInstructions = haveInstructions | haveInstr 511 self.components.append(component) 512 if haveInstructions: 513 numInstructions, = struct.unpack(">h", data[:2]) 514 data = data[2:] 515 self.program = ttProgram.Program() 516 self.program.fromBytecode(data[:numInstructions]) 517 data = data[numInstructions:] 518 if len(data) >= 4: 519 log.warning( 520 "too much glyph data at the end of composite glyph: %d excess bytes", 521 len(data)) 522 523 def decompileCoordinates(self, data): 524 endPtsOfContours = array.array("h") 525 endPtsOfContours.fromstring(data[:2*self.numberOfContours]) 526 if sys.byteorder != "big": endPtsOfContours.byteswap() 527 self.endPtsOfContours = endPtsOfContours.tolist() 528 529 data = data[2*self.numberOfContours:] 530 531 instructionLength, = struct.unpack(">h", data[:2]) 532 data = data[2:] 533 self.program = ttProgram.Program() 534 self.program.fromBytecode(data[:instructionLength]) 535 data = data[instructionLength:] 536 nCoordinates = self.endPtsOfContours[-1] + 1 537 flags, xCoordinates, yCoordinates = \ 538 self.decompileCoordinatesRaw(nCoordinates, data) 539 540 # fill in repetitions and apply signs 541 self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) 542 xIndex = 0 543 yIndex = 0 544 for i in range(nCoordinates): 545 flag = flags[i] 546 # x coordinate 547 if flag & flagXShort: 548 if flag & flagXsame: 549 x = xCoordinates[xIndex] 550 else: 551 x = -xCoordinates[xIndex] 552 xIndex = xIndex + 1 553 elif flag & flagXsame: 554 x = 0 555 else: 556 x = xCoordinates[xIndex] 557 xIndex = xIndex + 1 558 # y coordinate 559 if flag & flagYShort: 560 if flag & flagYsame: 561 y = yCoordinates[yIndex] 562 else: 563 y = -yCoordinates[yIndex] 564 yIndex = yIndex + 1 565 elif flag & flagYsame: 566 y = 0 567 else: 568 y = yCoordinates[yIndex] 569 yIndex = yIndex + 1 570 coordinates[i] = (x, y) 571 assert xIndex == len(xCoordinates) 572 assert yIndex == len(yCoordinates) 573 coordinates.relativeToAbsolute() 574 # discard all flags except "keepFlags" 575 self.flags = array.array("B", (f & keepFlags for f in flags)) 576 577 def decompileCoordinatesRaw(self, nCoordinates, data): 578 # unpack flags and prepare unpacking of coordinates 579 flags = array.array("B", [0] * nCoordinates) 580 # Warning: deep Python trickery going on. We use the struct module to unpack 581 # the coordinates. We build a format string based on the flags, so we can 582 # unpack the coordinates in one struct.unpack() call. 583 xFormat = ">" # big endian 584 yFormat = ">" # big endian 585 i = j = 0 586 while True: 587 flag = byteord(data[i]) 588 i = i + 1 589 repeat = 1 590 if flag & flagRepeat: 591 repeat = byteord(data[i]) + 1 592 i = i + 1 593 for k in range(repeat): 594 if flag & flagXShort: 595 xFormat = xFormat + 'B' 596 elif not (flag & flagXsame): 597 xFormat = xFormat + 'h' 598 if flag & flagYShort: 599 yFormat = yFormat + 'B' 600 elif not (flag & flagYsame): 601 yFormat = yFormat + 'h' 602 flags[j] = flag 603 j = j + 1 604 if j >= nCoordinates: 605 break 606 assert j == nCoordinates, "bad glyph flags" 607 data = data[i:] 608 # unpack raw coordinates, krrrrrr-tching! 609 xDataLen = struct.calcsize(xFormat) 610 yDataLen = struct.calcsize(yFormat) 611 if len(data) - (xDataLen + yDataLen) >= 4: 612 log.warning( 613 "too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen)) 614 xCoordinates = struct.unpack(xFormat, data[:xDataLen]) 615 yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen]) 616 return flags, xCoordinates, yCoordinates 617 618 def compileComponents(self, glyfTable): 619 data = b"" 620 lastcomponent = len(self.components) - 1 621 more = 1 622 haveInstructions = 0 623 for i in range(len(self.components)): 624 if i == lastcomponent: 625 haveInstructions = hasattr(self, "program") 626 more = 0 627 compo = self.components[i] 628 data = data + compo.compile(more, haveInstructions, glyfTable) 629 if haveInstructions: 630 instructions = self.program.getBytecode() 631 data = data + struct.pack(">h", len(instructions)) + instructions 632 return data 633 634 def compileCoordinates(self): 635 assert len(self.coordinates) == len(self.flags) 636 data = [] 637 endPtsOfContours = array.array("h", self.endPtsOfContours) 638 if sys.byteorder != "big": endPtsOfContours.byteswap() 639 data.append(endPtsOfContours.tostring()) 640 instructions = self.program.getBytecode() 641 data.append(struct.pack(">h", len(instructions))) 642 data.append(instructions) 643 644 deltas = self.coordinates.copy() 645 if deltas.isFloat(): 646 # Warn? 647 deltas.toInt() 648 deltas.absoluteToRelative() 649 650 # TODO(behdad): Add a configuration option for this? 651 deltas = self.compileDeltasGreedy(self.flags, deltas) 652 #deltas = self.compileDeltasOptimal(self.flags, deltas) 653 654 data.extend(deltas) 655 return bytesjoin(data) 656 657 def compileDeltasGreedy(self, flags, deltas): 658 # Implements greedy algorithm for packing coordinate deltas: 659 # uses shortest representation one coordinate at a time. 660 compressedflags = [] 661 xPoints = [] 662 yPoints = [] 663 lastflag = None 664 repeat = 0 665 for flag,(x,y) in zip(flags, deltas): 666 # Oh, the horrors of TrueType 667 # do x 668 if x == 0: 669 flag = flag | flagXsame 670 elif -255 <= x <= 255: 671 flag = flag | flagXShort 672 if x > 0: 673 flag = flag | flagXsame 674 else: 675 x = -x 676 xPoints.append(bytechr(x)) 677 else: 678 xPoints.append(struct.pack(">h", x)) 679 # do y 680 if y == 0: 681 flag = flag | flagYsame 682 elif -255 <= y <= 255: 683 flag = flag | flagYShort 684 if y > 0: 685 flag = flag | flagYsame 686 else: 687 y = -y 688 yPoints.append(bytechr(y)) 689 else: 690 yPoints.append(struct.pack(">h", y)) 691 # handle repeating flags 692 if flag == lastflag and repeat != 255: 693 repeat = repeat + 1 694 if repeat == 1: 695 compressedflags.append(flag) 696 else: 697 compressedflags[-2] = flag | flagRepeat 698 compressedflags[-1] = repeat 699 else: 700 repeat = 0 701 compressedflags.append(flag) 702 lastflag = flag 703 compressedFlags = array.array("B", compressedflags).tostring() 704 compressedXs = bytesjoin(xPoints) 705 compressedYs = bytesjoin(yPoints) 706 return (compressedFlags, compressedXs, compressedYs) 707 708 def compileDeltasOptimal(self, flags, deltas): 709 # Implements optimal, dynaic-programming, algorithm for packing coordinate 710 # deltas. The savings are negligible :(. 711 candidates = [] 712 bestTuple = None 713 bestCost = 0 714 repeat = 0 715 for flag,(x,y) in zip(flags, deltas): 716 # Oh, the horrors of TrueType 717 flag, coordBytes = flagBest(x, y, flag) 718 bestCost += 1 + coordBytes 719 newCandidates = [(bestCost, bestTuple, flag, coordBytes), 720 (bestCost+1, bestTuple, (flag|flagRepeat), coordBytes)] 721 for lastCost,lastTuple,lastFlag,coordBytes in candidates: 722 if lastCost + coordBytes <= bestCost + 1 and (lastFlag & flagRepeat) and (lastFlag < 0xff00) and flagSupports(lastFlag, flag): 723 if (lastFlag & 0xFF) == (flag|flagRepeat) and lastCost == bestCost + 1: 724 continue 725 newCandidates.append((lastCost + coordBytes, lastTuple, lastFlag+256, coordBytes)) 726 candidates = newCandidates 727 bestTuple = min(candidates, key=lambda t:t[0]) 728 bestCost = bestTuple[0] 729 730 flags = [] 731 while bestTuple: 732 cost, bestTuple, flag, coordBytes = bestTuple 733 flags.append(flag) 734 flags.reverse() 735 736 compressedFlags = array.array("B") 737 compressedXs = array.array("B") 738 compressedYs = array.array("B") 739 coords = iter(deltas) 740 ff = [] 741 for flag in flags: 742 repeatCount, flag = flag >> 8, flag & 0xFF 743 compressedFlags.append(flag) 744 if flag & flagRepeat: 745 assert(repeatCount > 0) 746 compressedFlags.append(repeatCount) 747 else: 748 assert(repeatCount == 0) 749 for i in range(1 + repeatCount): 750 x,y = next(coords) 751 flagEncodeCoords(flag, x, y, compressedXs, compressedYs) 752 ff.append(flag) 753 try: 754 next(coords) 755 raise Exception("internal error") 756 except StopIteration: 757 pass 758 compressedFlags = compressedFlags.tostring() 759 compressedXs = compressedXs.tostring() 760 compressedYs = compressedYs.tostring() 761 762 return (compressedFlags, compressedXs, compressedYs) 763 764 def recalcBounds(self, glyfTable): 765 coords, endPts, flags = self.getCoordinates(glyfTable) 766 if len(coords) > 0: 767 if 0: 768 # This branch calculates exact glyph outline bounds 769 # analytically, handling cases without on-curve 770 # extremas, etc. However, the glyf table header 771 # simply says that the bounds should be min/max x/y 772 # "for coordinate data", so I suppose that means no 773 # fancy thing here, just get extremas of all coord 774 # points (on and off). As such, this branch is 775 # disabled. 776 777 # Collect on-curve points 778 onCurveCoords = [coords[j] for j in range(len(coords)) 779 if flags[j] & flagOnCurve] 780 # Add implicit on-curve points 781 start = 0 782 for end in endPts: 783 last = end 784 for j in range(start, end + 1): 785 if not ((flags[j] | flags[last]) & flagOnCurve): 786 x = (coords[last][0] + coords[j][0]) / 2 787 y = (coords[last][1] + coords[j][1]) / 2 788 onCurveCoords.append((x,y)) 789 last = j 790 start = end + 1 791 # Add bounds for curves without an explicit extrema 792 start = 0 793 for end in endPts: 794 last = end 795 for j in range(start, end + 1): 796 if not (flags[j] & flagOnCurve): 797 next = j + 1 if j < end else start 798 bbox = calcBounds([coords[last], coords[next]]) 799 if not pointInRect(coords[j], bbox): 800 # Ouch! 801 log.warning("Outline has curve with implicit extrema.") 802 # Ouch! Find analytical curve bounds. 803 pthis = coords[j] 804 plast = coords[last] 805 if not (flags[last] & flagOnCurve): 806 plast = ((pthis[0]+plast[0])/2, (pthis[1]+plast[1])/2) 807 pnext = coords[next] 808 if not (flags[next] & flagOnCurve): 809 pnext = ((pthis[0]+pnext[0])/2, (pthis[1]+pnext[1])/2) 810 bbox = calcQuadraticBounds(plast, pthis, pnext) 811 onCurveCoords.append((bbox[0],bbox[1])) 812 onCurveCoords.append((bbox[2],bbox[3])) 813 last = j 814 start = end + 1 815 816 self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(onCurveCoords) 817 else: 818 self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) 819 else: 820 self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0) 821 822 def isComposite(self): 823 """Can be called on compact or expanded glyph.""" 824 if hasattr(self, "data") and self.data: 825 return struct.unpack(">h", self.data[:2])[0] == -1 826 else: 827 return self.numberOfContours == -1 828 829 def __getitem__(self, componentIndex): 830 if not self.isComposite(): 831 raise ttLib.TTLibError("can't use glyph as sequence") 832 return self.components[componentIndex] 833 834 def getCoordinates(self, glyfTable): 835 if self.numberOfContours > 0: 836 return self.coordinates, self.endPtsOfContours, self.flags 837 elif self.isComposite(): 838 # it's a composite 839 allCoords = GlyphCoordinates() 840 allFlags = array.array("B") 841 allEndPts = [] 842 for compo in self.components: 843 g = glyfTable[compo.glyphName] 844 try: 845 coordinates, endPts, flags = g.getCoordinates(glyfTable) 846 except RecursionError: 847 raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName) 848 if hasattr(compo, "firstPt"): 849 # move according to two reference points 850 x1,y1 = allCoords[compo.firstPt] 851 x2,y2 = coordinates[compo.secondPt] 852 move = x1-x2, y1-y2 853 else: 854 move = compo.x, compo.y 855 856 coordinates = GlyphCoordinates(coordinates) 857 if not hasattr(compo, "transform"): 858 coordinates.translate(move) 859 else: 860 apple_way = compo.flags & SCALED_COMPONENT_OFFSET 861 ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET 862 assert not (apple_way and ms_way) 863 if not (apple_way or ms_way): 864 scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file 865 else: 866 scale_component_offset = apple_way 867 if scale_component_offset: 868 # the Apple way: first move, then scale (ie. scale the component offset) 869 coordinates.translate(move) 870 coordinates.transform(compo.transform) 871 else: 872 # the MS way: first scale, then move 873 coordinates.transform(compo.transform) 874 coordinates.translate(move) 875 offset = len(allCoords) 876 allEndPts.extend(e + offset for e in endPts) 877 allCoords.extend(coordinates) 878 allFlags.extend(flags) 879 return allCoords, allEndPts, allFlags 880 else: 881 return GlyphCoordinates(), [], array.array("B") 882 883 def getComponentNames(self, glyfTable): 884 if not hasattr(self, "data"): 885 if self.isComposite(): 886 return [c.glyphName for c in self.components] 887 else: 888 return [] 889 890 # Extract components without expanding glyph 891 892 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 893 return [] # Not composite 894 895 data = self.data 896 i = 10 897 components = [] 898 more = 1 899 while more: 900 flags, glyphID = struct.unpack(">HH", data[i:i+4]) 901 i += 4 902 flags = int(flags) 903 components.append(glyfTable.getGlyphName(int(glyphID))) 904 905 if flags & ARG_1_AND_2_ARE_WORDS: i += 4 906 else: i += 2 907 if flags & WE_HAVE_A_SCALE: i += 2 908 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 909 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 910 more = flags & MORE_COMPONENTS 911 912 return components 913 914 def trim(self, remove_hinting=False): 915 """ Remove padding and, if requested, hinting, from a glyph. 916 This works on both expanded and compacted glyphs, without 917 expanding it.""" 918 if not hasattr(self, "data"): 919 if remove_hinting: 920 self.program = ttProgram.Program() 921 self.program.fromBytecode([]) 922 # No padding to trim. 923 return 924 if not self.data: 925 return 926 numContours = struct.unpack(">h", self.data[:2])[0] 927 data = array.array("B", self.data) 928 i = 10 929 if numContours >= 0: 930 i += 2 * numContours # endPtsOfContours 931 nCoordinates = ((data[i-2] << 8) | data[i-1]) + 1 932 instructionLen = (data[i] << 8) | data[i+1] 933 if remove_hinting: 934 # Zero instruction length 935 data[i] = data [i+1] = 0 936 i += 2 937 if instructionLen: 938 # Splice it out 939 data = data[:i] + data[i+instructionLen:] 940 instructionLen = 0 941 else: 942 i += 2 + instructionLen 943 944 coordBytes = 0 945 j = 0 946 while True: 947 flag = data[i] 948 i = i + 1 949 repeat = 1 950 if flag & flagRepeat: 951 repeat = data[i] + 1 952 i = i + 1 953 xBytes = yBytes = 0 954 if flag & flagXShort: 955 xBytes = 1 956 elif not (flag & flagXsame): 957 xBytes = 2 958 if flag & flagYShort: 959 yBytes = 1 960 elif not (flag & flagYsame): 961 yBytes = 2 962 coordBytes += (xBytes + yBytes) * repeat 963 j += repeat 964 if j >= nCoordinates: 965 break 966 assert j == nCoordinates, "bad glyph flags" 967 i += coordBytes 968 # Remove padding 969 data = data[:i] 970 else: 971 more = 1 972 we_have_instructions = False 973 while more: 974 flags =(data[i] << 8) | data[i+1] 975 if remove_hinting: 976 flags &= ~WE_HAVE_INSTRUCTIONS 977 if flags & WE_HAVE_INSTRUCTIONS: 978 we_have_instructions = True 979 data[i+0] = flags >> 8 980 data[i+1] = flags & 0xFF 981 i += 4 982 flags = int(flags) 983 984 if flags & ARG_1_AND_2_ARE_WORDS: i += 4 985 else: i += 2 986 if flags & WE_HAVE_A_SCALE: i += 2 987 elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 988 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 989 more = flags & MORE_COMPONENTS 990 if we_have_instructions: 991 instructionLen = (data[i] << 8) | data[i+1] 992 i += 2 + instructionLen 993 # Remove padding 994 data = data[:i] 995 996 self.data = data.tostring() 997 998 def removeHinting(self): 999 self.trim (remove_hinting=True) 1000 1001 def draw(self, pen, glyfTable, offset=0): 1002 1003 if self.isComposite(): 1004 for component in self.components: 1005 glyphName, transform = component.getComponentInfo() 1006 pen.addComponent(glyphName, transform) 1007 return 1008 1009 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1010 if offset: 1011 coordinates = coordinates.copy() 1012 coordinates.translate((offset, 0)) 1013 start = 0 1014 for end in endPts: 1015 end = end + 1 1016 contour = coordinates[start:end] 1017 cFlags = flags[start:end] 1018 start = end 1019 if 1 not in cFlags: 1020 # There is not a single on-curve point on the curve, 1021 # use pen.qCurveTo's special case by specifying None 1022 # as the on-curve point. 1023 contour.append(None) 1024 pen.qCurveTo(*contour) 1025 else: 1026 # Shuffle the points so that contour the is guaranteed 1027 # to *end* in an on-curve point, which we'll use for 1028 # the moveTo. 1029 firstOnCurve = cFlags.index(1) + 1 1030 contour = contour[firstOnCurve:] + contour[:firstOnCurve] 1031 cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] 1032 pen.moveTo(contour[-1]) 1033 while contour: 1034 nextOnCurve = cFlags.index(1) + 1 1035 if nextOnCurve == 1: 1036 pen.lineTo(contour[0]) 1037 else: 1038 pen.qCurveTo(*contour[:nextOnCurve]) 1039 contour = contour[nextOnCurve:] 1040 cFlags = cFlags[nextOnCurve:] 1041 pen.closePath() 1042 1043 def drawPoints(self, pen, glyfTable, offset=0): 1044 """Draw the glyph using the supplied pointPen. Opposed to Glyph.draw(), 1045 this will not change the point indices. 1046 """ 1047 1048 if self.isComposite(): 1049 for component in self.components: 1050 glyphName, transform = component.getComponentInfo() 1051 pen.addComponent(glyphName, transform) 1052 return 1053 1054 coordinates, endPts, flags = self.getCoordinates(glyfTable) 1055 if offset: 1056 coordinates = coordinates.copy() 1057 coordinates.translate((offset, 0)) 1058 start = 0 1059 for end in endPts: 1060 end = end + 1 1061 contour = coordinates[start:end] 1062 cFlags = flags[start:end] 1063 start = end 1064 pen.beginPath() 1065 # Start with the appropriate segment type based on the final segment 1066 segmentType = "line" if cFlags[-1] == 1 else "qcurve" 1067 for i, pt in enumerate(contour): 1068 if cFlags[i] == 1: 1069 pen.addPoint(pt, segmentType=segmentType) 1070 segmentType = "line" 1071 else: 1072 pen.addPoint(pt) 1073 segmentType = "qcurve" 1074 pen.endPath() 1075 1076 def __eq__(self, other): 1077 if type(self) != type(other): 1078 return NotImplemented 1079 return self.__dict__ == other.__dict__ 1080 1081 def __ne__(self, other): 1082 result = self.__eq__(other) 1083 return result if result is NotImplemented else not result 1084 1085 class GlyphComponent(object): 1086 1087 def __init__(self): 1088 pass 1089 1090 def getComponentInfo(self): 1091 """Return the base glyph name and a transform.""" 1092 # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement 1093 # something equivalent in fontTools.objects.glyph (I'd rather not 1094 # convert it to an absolute offset, since it is valuable information). 1095 # This method will now raise "AttributeError: x" on glyphs that use 1096 # this TT feature. 1097 if hasattr(self, "transform"): 1098 [[xx, xy], [yx, yy]] = self.transform 1099 trans = (xx, xy, yx, yy, self.x, self.y) 1100 else: 1101 trans = (1, 0, 0, 1, self.x, self.y) 1102 return self.glyphName, trans 1103 1104 def decompile(self, data, glyfTable): 1105 flags, glyphID = struct.unpack(">HH", data[:4]) 1106 self.flags = int(flags) 1107 glyphID = int(glyphID) 1108 self.glyphName = glyfTable.getGlyphName(int(glyphID)) 1109 data = data[4:] 1110 1111 if self.flags & ARG_1_AND_2_ARE_WORDS: 1112 if self.flags & ARGS_ARE_XY_VALUES: 1113 self.x, self.y = struct.unpack(">hh", data[:4]) 1114 else: 1115 x, y = struct.unpack(">HH", data[:4]) 1116 self.firstPt, self.secondPt = int(x), int(y) 1117 data = data[4:] 1118 else: 1119 if self.flags & ARGS_ARE_XY_VALUES: 1120 self.x, self.y = struct.unpack(">bb", data[:2]) 1121 else: 1122 x, y = struct.unpack(">BB", data[:2]) 1123 self.firstPt, self.secondPt = int(x), int(y) 1124 data = data[2:] 1125 1126 if self.flags & WE_HAVE_A_SCALE: 1127 scale, = struct.unpack(">h", data[:2]) 1128 self.transform = [[fi2fl(scale,14), 0], [0, fi2fl(scale,14)]] # fixed 2.14 1129 data = data[2:] 1130 elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE: 1131 xscale, yscale = struct.unpack(">hh", data[:4]) 1132 self.transform = [[fi2fl(xscale,14), 0], [0, fi2fl(yscale,14)]] # fixed 2.14 1133 data = data[4:] 1134 elif self.flags & WE_HAVE_A_TWO_BY_TWO: 1135 (xscale, scale01, 1136 scale10, yscale) = struct.unpack(">hhhh", data[:8]) 1137 self.transform = [[fi2fl(xscale,14), fi2fl(scale01,14)], 1138 [fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14 1139 data = data[8:] 1140 more = self.flags & MORE_COMPONENTS 1141 haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS 1142 self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | 1143 SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | 1144 NON_OVERLAPPING | OVERLAP_COMPOUND) 1145 return more, haveInstructions, data 1146 1147 def compile(self, more, haveInstructions, glyfTable): 1148 data = b"" 1149 1150 # reset all flags we will calculate ourselves 1151 flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | 1152 SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | 1153 NON_OVERLAPPING | OVERLAP_COMPOUND) 1154 if more: 1155 flags = flags | MORE_COMPONENTS 1156 if haveInstructions: 1157 flags = flags | WE_HAVE_INSTRUCTIONS 1158 1159 if hasattr(self, "firstPt"): 1160 if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): 1161 data = data + struct.pack(">BB", self.firstPt, self.secondPt) 1162 else: 1163 data = data + struct.pack(">HH", self.firstPt, self.secondPt) 1164 flags = flags | ARG_1_AND_2_ARE_WORDS 1165 else: 1166 x = otRound(self.x) 1167 y = otRound(self.y) 1168 flags = flags | ARGS_ARE_XY_VALUES 1169 if (-128 <= x <= 127) and (-128 <= y <= 127): 1170 data = data + struct.pack(">bb", x, y) 1171 else: 1172 data = data + struct.pack(">hh", x, y) 1173 flags = flags | ARG_1_AND_2_ARE_WORDS 1174 1175 if hasattr(self, "transform"): 1176 transform = [[fl2fi(x,14) for x in row] for row in self.transform] 1177 if transform[0][1] or transform[1][0]: 1178 flags = flags | WE_HAVE_A_TWO_BY_TWO 1179 data = data + struct.pack(">hhhh", 1180 transform[0][0], transform[0][1], 1181 transform[1][0], transform[1][1]) 1182 elif transform[0][0] != transform[1][1]: 1183 flags = flags | WE_HAVE_AN_X_AND_Y_SCALE 1184 data = data + struct.pack(">hh", 1185 transform[0][0], transform[1][1]) 1186 else: 1187 flags = flags | WE_HAVE_A_SCALE 1188 data = data + struct.pack(">h", 1189 transform[0][0]) 1190 1191 glyphID = glyfTable.getGlyphID(self.glyphName) 1192 return struct.pack(">HH", flags, glyphID) + data 1193 1194 def toXML(self, writer, ttFont): 1195 attrs = [("glyphName", self.glyphName)] 1196 if not hasattr(self, "firstPt"): 1197 attrs = attrs + [("x", self.x), ("y", self.y)] 1198 else: 1199 attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] 1200 1201 if hasattr(self, "transform"): 1202 transform = self.transform 1203 if transform[0][1] or transform[1][0]: 1204 attrs = attrs + [ 1205 ("scalex", transform[0][0]), ("scale01", transform[0][1]), 1206 ("scale10", transform[1][0]), ("scaley", transform[1][1]), 1207 ] 1208 elif transform[0][0] != transform[1][1]: 1209 attrs = attrs + [ 1210 ("scalex", transform[0][0]), ("scaley", transform[1][1]), 1211 ] 1212 else: 1213 attrs = attrs + [("scale", transform[0][0])] 1214 attrs = attrs + [("flags", hex(self.flags))] 1215 writer.simpletag("component", attrs) 1216 writer.newline() 1217 1218 def fromXML(self, name, attrs, content, ttFont): 1219 self.glyphName = attrs["glyphName"] 1220 if "firstPt" in attrs: 1221 self.firstPt = safeEval(attrs["firstPt"]) 1222 self.secondPt = safeEval(attrs["secondPt"]) 1223 else: 1224 self.x = safeEval(attrs["x"]) 1225 self.y = safeEval(attrs["y"]) 1226 if "scale01" in attrs: 1227 scalex = safeEval(attrs["scalex"]) 1228 scale01 = safeEval(attrs["scale01"]) 1229 scale10 = safeEval(attrs["scale10"]) 1230 scaley = safeEval(attrs["scaley"]) 1231 self.transform = [[scalex, scale01], [scale10, scaley]] 1232 elif "scalex" in attrs: 1233 scalex = safeEval(attrs["scalex"]) 1234 scaley = safeEval(attrs["scaley"]) 1235 self.transform = [[scalex, 0], [0, scaley]] 1236 elif "scale" in attrs: 1237 scale = safeEval(attrs["scale"]) 1238 self.transform = [[scale, 0], [0, scale]] 1239 self.flags = safeEval(attrs["flags"]) 1240 1241 def __eq__(self, other): 1242 if type(self) != type(other): 1243 return NotImplemented 1244 return self.__dict__ == other.__dict__ 1245 1246 def __ne__(self, other): 1247 result = self.__eq__(other) 1248 return result if result is NotImplemented else not result 1249 1250 class GlyphCoordinates(object): 1251 1252 def __init__(self, iterable=[], typecode="h"): 1253 self._a = array.array(typecode) 1254 self.extend(iterable) 1255 1256 @property 1257 def array(self): 1258 return self._a 1259 1260 def isFloat(self): 1261 return self._a.typecode == 'd' 1262 1263 def _ensureFloat(self): 1264 if self.isFloat(): 1265 return 1266 # The conversion to list() is to work around Jython bug 1267 self._a = array.array("d", list(self._a)) 1268 1269 def _checkFloat(self, p): 1270 if self.isFloat(): 1271 return p 1272 if any(v > 0x7FFF or v < -0x8000 for v in p): 1273 self._ensureFloat() 1274 return p 1275 if any(isinstance(v, float) for v in p): 1276 p = [int(v) if int(v) == v else v for v in p] 1277 if any(isinstance(v, float) for v in p): 1278 self._ensureFloat() 1279 return p 1280 1281 @staticmethod 1282 def zeros(count): 1283 return GlyphCoordinates([(0,0)] * count) 1284 1285 def copy(self): 1286 c = GlyphCoordinates(typecode=self._a.typecode) 1287 c._a.extend(self._a) 1288 return c 1289 1290 def __len__(self): 1291 return len(self._a) // 2 1292 1293 def __getitem__(self, k): 1294 if isinstance(k, slice): 1295 indices = range(*k.indices(len(self))) 1296 return [self[i] for i in indices] 1297 return self._a[2*k],self._a[2*k+1] 1298 1299 def __setitem__(self, k, v): 1300 if isinstance(k, slice): 1301 indices = range(*k.indices(len(self))) 1302 # XXX This only works if len(v) == len(indices) 1303 for j,i in enumerate(indices): 1304 self[i] = v[j] 1305 return 1306 v = self._checkFloat(v) 1307 self._a[2*k],self._a[2*k+1] = v 1308 1309 def __delitem__(self, i): 1310 i = (2*i) % len(self._a) 1311 del self._a[i] 1312 del self._a[i] 1313 1314 def __repr__(self): 1315 return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' 1316 1317 def append(self, p): 1318 p = self._checkFloat(p) 1319 self._a.extend(tuple(p)) 1320 1321 def extend(self, iterable): 1322 for p in iterable: 1323 p = self._checkFloat(p) 1324 self._a.extend(p) 1325 1326 def toInt(self): 1327 if not self.isFloat(): 1328 return 1329 a = array.array("h") 1330 for n in self._a: 1331 a.append(otRound(n)) 1332 self._a = a 1333 1334 def relativeToAbsolute(self): 1335 a = self._a 1336 x,y = 0,0 1337 for i in range(len(a) // 2): 1338 x = a[2*i ] + x 1339 y = a[2*i+1] + y 1340 self[i] = (x, y) 1341 1342 def absoluteToRelative(self): 1343 a = self._a 1344 x,y = 0,0 1345 for i in range(len(a) // 2): 1346 dx = a[2*i ] - x 1347 dy = a[2*i+1] - y 1348 x = a[2*i ] 1349 y = a[2*i+1] 1350 self[i] = (dx, dy) 1351 1352 def translate(self, p): 1353 """ 1354 >>> GlyphCoordinates([(1,2)]).translate((.5,0)) 1355 """ 1356 (x,y) = self._checkFloat(p) 1357 a = self._a 1358 for i in range(len(a) // 2): 1359 self[i] = (a[2*i] + x, a[2*i+1] + y) 1360 1361 def scale(self, p): 1362 """ 1363 >>> GlyphCoordinates([(1,2)]).scale((.5,0)) 1364 """ 1365 (x,y) = self._checkFloat(p) 1366 a = self._a 1367 for i in range(len(a) // 2): 1368 self[i] = (a[2*i] * x, a[2*i+1] * y) 1369 1370 def transform(self, t): 1371 """ 1372 >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) 1373 """ 1374 a = self._a 1375 for i in range(len(a) // 2): 1376 x = a[2*i ] 1377 y = a[2*i+1] 1378 px = x * t[0][0] + y * t[1][0] 1379 py = x * t[0][1] + y * t[1][1] 1380 self[i] = (px, py) 1381 1382 def __eq__(self, other): 1383 """ 1384 >>> g = GlyphCoordinates([(1,2)]) 1385 >>> g2 = GlyphCoordinates([(1.0,2)]) 1386 >>> g3 = GlyphCoordinates([(1.5,2)]) 1387 >>> g == g2 1388 True 1389 >>> g == g3 1390 False 1391 >>> g2 == g3 1392 False 1393 """ 1394 if type(self) != type(other): 1395 return NotImplemented 1396 return self._a == other._a 1397 1398 def __ne__(self, other): 1399 """ 1400 >>> g = GlyphCoordinates([(1,2)]) 1401 >>> g2 = GlyphCoordinates([(1.0,2)]) 1402 >>> g3 = GlyphCoordinates([(1.5,2)]) 1403 >>> g != g2 1404 False 1405 >>> g != g3 1406 True 1407 >>> g2 != g3 1408 True 1409 """ 1410 result = self.__eq__(other) 1411 return result if result is NotImplemented else not result 1412 1413 # Math operations 1414 1415 def __pos__(self): 1416 """ 1417 >>> g = GlyphCoordinates([(1,2)]) 1418 >>> g 1419 GlyphCoordinates([(1, 2)]) 1420 >>> g2 = +g 1421 >>> g2 1422 GlyphCoordinates([(1, 2)]) 1423 >>> g2.translate((1,0)) 1424 >>> g2 1425 GlyphCoordinates([(2, 2)]) 1426 >>> g 1427 GlyphCoordinates([(1, 2)]) 1428 """ 1429 return self.copy() 1430 def __neg__(self): 1431 """ 1432 >>> g = GlyphCoordinates([(1,2)]) 1433 >>> g 1434 GlyphCoordinates([(1, 2)]) 1435 >>> g2 = -g 1436 >>> g2 1437 GlyphCoordinates([(-1, -2)]) 1438 >>> g 1439 GlyphCoordinates([(1, 2)]) 1440 """ 1441 r = self.copy() 1442 a = r._a 1443 for i in range(len(a)): 1444 a[i] = -a[i] 1445 return r 1446 def __round__(self): 1447 """ 1448 Note: This is Python 3 only. Python 2 does not call __round__. 1449 As such, we cannot test this method either. :( 1450 """ 1451 r = self.copy() 1452 r.toInt() 1453 return r 1454 1455 def __add__(self, other): return self.copy().__iadd__(other) 1456 def __sub__(self, other): return self.copy().__isub__(other) 1457 def __mul__(self, other): return self.copy().__imul__(other) 1458 def __truediv__(self, other): return self.copy().__itruediv__(other) 1459 1460 __radd__ = __add__ 1461 __rmul__ = __mul__ 1462 def __rsub__(self, other): return other + (-self) 1463 1464 def __iadd__(self, other): 1465 """ 1466 >>> g = GlyphCoordinates([(1,2)]) 1467 >>> g += (.5,0) 1468 >>> g 1469 GlyphCoordinates([(1.5, 2.0)]) 1470 >>> g2 = GlyphCoordinates([(3,4)]) 1471 >>> g += g2 1472 >>> g 1473 GlyphCoordinates([(4.5, 6.0)]) 1474 """ 1475 if isinstance(other, tuple): 1476 assert len(other) == 2 1477 self.translate(other) 1478 return self 1479 if isinstance(other, GlyphCoordinates): 1480 if other.isFloat(): self._ensureFloat() 1481 other = other._a 1482 a = self._a 1483 assert len(a) == len(other) 1484 for i in range(len(a) // 2): 1485 self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) 1486 return self 1487 return NotImplemented 1488 1489 def __isub__(self, other): 1490 """ 1491 >>> g = GlyphCoordinates([(1,2)]) 1492 >>> g -= (.5,0) 1493 >>> g 1494 GlyphCoordinates([(0.5, 2.0)]) 1495 >>> g2 = GlyphCoordinates([(3,4)]) 1496 >>> g -= g2 1497 >>> g 1498 GlyphCoordinates([(-2.5, -2.0)]) 1499 """ 1500 if isinstance(other, tuple): 1501 assert len(other) == 2 1502 self.translate((-other[0],-other[1])) 1503 return self 1504 if isinstance(other, GlyphCoordinates): 1505 if other.isFloat(): self._ensureFloat() 1506 other = other._a 1507 a = self._a 1508 assert len(a) == len(other) 1509 for i in range(len(a) // 2): 1510 self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) 1511 return self 1512 return NotImplemented 1513 1514 def __imul__(self, other): 1515 """ 1516 >>> g = GlyphCoordinates([(1,2)]) 1517 >>> g *= (2,.5) 1518 >>> g *= 2 1519 >>> g 1520 GlyphCoordinates([(4.0, 2.0)]) 1521 >>> g = GlyphCoordinates([(1,2)]) 1522 >>> g *= 2 1523 >>> g 1524 GlyphCoordinates([(2, 4)]) 1525 """ 1526 if isinstance(other, Number): 1527 other = (other, other) 1528 if isinstance(other, tuple): 1529 if other == (1,1): 1530 return self 1531 assert len(other) == 2 1532 self.scale(other) 1533 return self 1534 return NotImplemented 1535 1536 def __itruediv__(self, other): 1537 """ 1538 >>> g = GlyphCoordinates([(1,3)]) 1539 >>> g /= (.5,1.5) 1540 >>> g /= 2 1541 >>> g 1542 GlyphCoordinates([(1.0, 1.0)]) 1543 """ 1544 if isinstance(other, Number): 1545 other = (other, other) 1546 if isinstance(other, tuple): 1547 if other == (1,1): 1548 return self 1549 assert len(other) == 2 1550 self.scale((1./other[0],1./other[1])) 1551 return self 1552 return NotImplemented 1553 1554 def __bool__(self): 1555 """ 1556 >>> g = GlyphCoordinates([]) 1557 >>> bool(g) 1558 False 1559 >>> g = GlyphCoordinates([(0,0), (0.,0)]) 1560 >>> bool(g) 1561 True 1562 >>> g = GlyphCoordinates([(0,0), (1,0)]) 1563 >>> bool(g) 1564 True 1565 >>> g = GlyphCoordinates([(0,.5), (0,0)]) 1566 >>> bool(g) 1567 True 1568 """ 1569 return bool(self._a) 1570 1571 __nonzero__ = __bool__ 1572 1573 1574 def reprflag(flag): 1575 bin = "" 1576 if isinstance(flag, str): 1577 flag = byteord(flag) 1578 while flag: 1579 if flag & 0x01: 1580 bin = "1" + bin 1581 else: 1582 bin = "0" + bin 1583 flag = flag >> 1 1584 bin = (14 - len(bin)) * "0" + bin 1585 return bin 1586 1587 1588 if __name__ == "__main__": 1589 import doctest, sys 1590 sys.exit(doctest.testmod().failed) 1591