1from fontTools.misc.textTools import Tag, bytesjoin 2from .DefaultTable import DefaultTable 3import sys 4import array 5import struct 6import logging 7from typing import Iterator, NamedTuple, Optional 8 9log = logging.getLogger(__name__) 10 11class OverflowErrorRecord(object): 12 def __init__(self, overflowTuple): 13 self.tableType = overflowTuple[0] 14 self.LookupListIndex = overflowTuple[1] 15 self.SubTableIndex = overflowTuple[2] 16 self.itemName = overflowTuple[3] 17 self.itemIndex = overflowTuple[4] 18 19 def __repr__(self): 20 return str((self.tableType, "LookupIndex:", self.LookupListIndex, "SubTableIndex:", self.SubTableIndex, "ItemName:", self.itemName, "ItemIndex:", self.itemIndex)) 21 22class OTLOffsetOverflowError(Exception): 23 def __init__(self, overflowErrorRecord): 24 self.value = overflowErrorRecord 25 26 def __str__(self): 27 return repr(self.value) 28 29 30class BaseTTXConverter(DefaultTable): 31 32 """Generic base class for TTX table converters. It functions as an 33 adapter between the TTX (ttLib actually) table model and the model 34 we use for OpenType tables, which is necessarily subtly different. 35 """ 36 37 def decompile(self, data, font): 38 """Create an object from the binary data. Called automatically on access.""" 39 from . import otTables 40 reader = OTTableReader(data, tableTag=self.tableTag) 41 tableClass = getattr(otTables, self.tableTag) 42 self.table = tableClass() 43 self.table.decompile(reader, font) 44 45 def compile(self, font): 46 """Compiles the table into binary. Called automatically on save.""" 47 48 # General outline: 49 # Create a top-level OTTableWriter for the GPOS/GSUB table. 50 # Call the compile method for the the table 51 # for each 'converter' record in the table converter list 52 # call converter's write method for each item in the value. 53 # - For simple items, the write method adds a string to the 54 # writer's self.items list. 55 # - For Struct/Table/Subtable items, it add first adds new writer to the 56 # to the writer's self.items, then calls the item's compile method. 57 # This creates a tree of writers, rooted at the GUSB/GPOS writer, with 58 # each writer representing a table, and the writer.items list containing 59 # the child data strings and writers. 60 # call the getAllData method 61 # call _doneWriting, which removes duplicates 62 # call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables 63 # Traverse the flat list of tables, calling getDataLength on each to update their position 64 # Traverse the flat list of tables again, calling getData each get the data in the table, now that 65 # pos's and offset are known. 66 67 # If a lookup subtable overflows an offset, we have to start all over. 68 overflowRecord = None 69 70 while True: 71 try: 72 writer = OTTableWriter(tableTag=self.tableTag) 73 self.table.compile(writer, font) 74 return writer.getAllData() 75 76 except OTLOffsetOverflowError as e: 77 78 if overflowRecord == e.value: 79 raise # Oh well... 80 81 overflowRecord = e.value 82 log.info("Attempting to fix OTLOffsetOverflowError %s", e) 83 lastItem = overflowRecord 84 85 ok = 0 86 if overflowRecord.itemName is None: 87 from .otTables import fixLookupOverFlows 88 ok = fixLookupOverFlows(font, overflowRecord) 89 else: 90 from .otTables import fixSubTableOverFlows 91 ok = fixSubTableOverFlows(font, overflowRecord) 92 if not ok: 93 # Try upgrading lookup to Extension and hope 94 # that cross-lookup sharing not happening would 95 # fix overflow... 96 from .otTables import fixLookupOverFlows 97 ok = fixLookupOverFlows(font, overflowRecord) 98 if not ok: 99 raise 100 101 def toXML(self, writer, font): 102 self.table.toXML2(writer, font) 103 104 def fromXML(self, name, attrs, content, font): 105 from . import otTables 106 if not hasattr(self, "table"): 107 tableClass = getattr(otTables, self.tableTag) 108 self.table = tableClass() 109 self.table.fromXML(name, attrs, content, font) 110 self.table.populateDefaults() 111 112 def ensureDecompiled(self): 113 self.table.ensureDecompiled(recurse=True) 114 115 116# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928 117assert len(struct.pack('i', 0)) == 4 118assert array.array('i').itemsize == 4, "Oops, file a bug against fonttools." 119 120class OTTableReader(object): 121 122 """Helper class to retrieve data from an OpenType table.""" 123 124 __slots__ = ('data', 'offset', 'pos', 'localState', 'tableTag') 125 126 def __init__(self, data, localState=None, offset=0, tableTag=None): 127 self.data = data 128 self.offset = offset 129 self.pos = offset 130 self.localState = localState 131 self.tableTag = tableTag 132 133 def advance(self, count): 134 self.pos += count 135 136 def seek(self, pos): 137 self.pos = pos 138 139 def copy(self): 140 other = self.__class__(self.data, self.localState, self.offset, self.tableTag) 141 other.pos = self.pos 142 return other 143 144 def getSubReader(self, offset): 145 offset = self.offset + offset 146 return self.__class__(self.data, self.localState, offset, self.tableTag) 147 148 def readValue(self, typecode, staticSize): 149 pos = self.pos 150 newpos = pos + staticSize 151 value, = struct.unpack(f">{typecode}", self.data[pos:newpos]) 152 self.pos = newpos 153 return value 154 def readArray(self, typecode, staticSize, count): 155 pos = self.pos 156 newpos = pos + count * staticSize 157 value = array.array(typecode, self.data[pos:newpos]) 158 if sys.byteorder != "big": value.byteswap() 159 self.pos = newpos 160 return value.tolist() 161 162 def readInt8(self): 163 return self.readValue("b", staticSize=1) 164 def readInt8Array(self, count): 165 return self.readArray("b", staticSize=1, count=count) 166 167 def readShort(self): 168 return self.readValue("h", staticSize=2) 169 def readShortArray(self, count): 170 return self.readArray("h", staticSize=2, count=count) 171 172 def readLong(self): 173 return self.readValue("i", staticSize=4) 174 def readLongArray(self, count): 175 return self.readArray("i", staticSize=4, count=count) 176 177 def readUInt8(self): 178 return self.readValue("B", staticSize=1) 179 def readUInt8Array(self, count): 180 return self.readArray("B", staticSize=1, count=count) 181 182 def readUShort(self): 183 return self.readValue("H", staticSize=2) 184 def readUShortArray(self, count): 185 return self.readArray("H", staticSize=2, count=count) 186 187 def readULong(self): 188 return self.readValue("I", staticSize=4) 189 def readULongArray(self, count): 190 return self.readArray("I", staticSize=4, count=count) 191 192 def readUInt24(self): 193 pos = self.pos 194 newpos = pos + 3 195 value, = struct.unpack(">l", b'\0'+self.data[pos:newpos]) 196 self.pos = newpos 197 return value 198 def readUInt24Array(self, count): 199 return [self.readUInt24() for _ in range(count)] 200 201 def readTag(self): 202 pos = self.pos 203 newpos = pos + 4 204 value = Tag(self.data[pos:newpos]) 205 assert len(value) == 4, value 206 self.pos = newpos 207 return value 208 209 def readData(self, count): 210 pos = self.pos 211 newpos = pos + count 212 value = self.data[pos:newpos] 213 self.pos = newpos 214 return value 215 216 def __setitem__(self, name, value): 217 state = self.localState.copy() if self.localState else dict() 218 state[name] = value 219 self.localState = state 220 221 def __getitem__(self, name): 222 return self.localState and self.localState[name] 223 224 def __contains__(self, name): 225 return self.localState and name in self.localState 226 227 228class OTTableWriter(object): 229 230 """Helper class to gather and assemble data for OpenType tables.""" 231 232 def __init__(self, localState=None, tableTag=None, offsetSize=2): 233 self.items = [] 234 self.pos = None 235 self.localState = localState 236 self.tableTag = tableTag 237 self.offsetSize = offsetSize 238 self.parent = None 239 240 # DEPRECATED: 'longOffset' is kept as a property for backward compat with old code. 241 # You should use 'offsetSize' instead (2, 3 or 4 bytes). 242 @property 243 def longOffset(self): 244 return self.offsetSize == 4 245 246 @longOffset.setter 247 def longOffset(self, value): 248 self.offsetSize = 4 if value else 2 249 250 def __setitem__(self, name, value): 251 state = self.localState.copy() if self.localState else dict() 252 state[name] = value 253 self.localState = state 254 255 def __getitem__(self, name): 256 return self.localState[name] 257 258 def __delitem__(self, name): 259 del self.localState[name] 260 261 # assembler interface 262 263 def getDataLength(self): 264 """Return the length of this table in bytes, without subtables.""" 265 l = 0 266 for item in self.items: 267 if hasattr(item, "getCountData"): 268 l += item.size 269 elif hasattr(item, "getData"): 270 l += item.offsetSize 271 else: 272 l = l + len(item) 273 return l 274 275 def getData(self): 276 """Assemble the data for this writer/table, without subtables.""" 277 items = list(self.items) # make a shallow copy 278 pos = self.pos 279 numItems = len(items) 280 for i in range(numItems): 281 item = items[i] 282 283 if hasattr(item, "getData"): 284 if item.offsetSize == 4: 285 items[i] = packULong(item.pos - pos) 286 elif item.offsetSize == 2: 287 try: 288 items[i] = packUShort(item.pos - pos) 289 except struct.error: 290 # provide data to fix overflow problem. 291 overflowErrorRecord = self.getOverflowErrorRecord(item) 292 293 raise OTLOffsetOverflowError(overflowErrorRecord) 294 elif item.offsetSize == 3: 295 items[i] = packUInt24(item.pos - pos) 296 else: 297 raise ValueError(item.offsetSize) 298 299 return bytesjoin(items) 300 301 def __hash__(self): 302 # only works after self._doneWriting() has been called 303 return hash(self.items) 304 305 def __ne__(self, other): 306 result = self.__eq__(other) 307 return result if result is NotImplemented else not result 308 309 def __eq__(self, other): 310 if type(self) != type(other): 311 return NotImplemented 312 return self.offsetSize == other.offsetSize and self.items == other.items 313 314 def _doneWriting(self, internedTables): 315 # Convert CountData references to data string items 316 # collapse duplicate table references to a unique entry 317 # "tables" are OTTableWriter objects. 318 319 # For Extension Lookup types, we can 320 # eliminate duplicates only within the tree under the Extension Lookup, 321 # as offsets may exceed 64K even between Extension LookupTable subtables. 322 isExtension = hasattr(self, "Extension") 323 324 # Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level 325 # arrays (ScriptList, FeatureList, LookupList) point to the same, possibly 326 # empty, array. So, we don't share those. 327 # See: https://github.com/fonttools/fonttools/issues/518 328 dontShare = hasattr(self, 'DontShare') 329 330 if isExtension: 331 internedTables = {} 332 333 items = self.items 334 for i in range(len(items)): 335 item = items[i] 336 if hasattr(item, "getCountData"): 337 items[i] = item.getCountData() 338 elif hasattr(item, "getData"): 339 item._doneWriting(internedTables) 340 # At this point, all subwriters are hashable based on their items. 341 # (See hash and comparison magic methods above.) So the ``setdefault`` 342 # call here will return the first writer object we've seen with 343 # equal content, or store it in the dictionary if it's not been 344 # seen yet. We therefore replace the subwriter object with an equivalent 345 # object, which deduplicates the tree. 346 if not dontShare: 347 items[i] = item = internedTables.setdefault(item, item) 348 self.items = tuple(items) 349 350 def _gatherTables(self, tables, extTables, done): 351 # Convert table references in self.items tree to a flat 352 # list of tables in depth-first traversal order. 353 # "tables" are OTTableWriter objects. 354 # We do the traversal in reverse order at each level, in order to 355 # resolve duplicate references to be the last reference in the list of tables. 356 # For extension lookups, duplicate references can be merged only within the 357 # writer tree under the extension lookup. 358 359 done[id(self)] = True 360 361 numItems = len(self.items) 362 iRange = list(range(numItems)) 363 iRange.reverse() 364 365 isExtension = hasattr(self, "Extension") 366 367 selfTables = tables 368 369 if isExtension: 370 assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables" 371 tables, extTables, done = extTables, None, {} 372 373 # add Coverage table if it is sorted last. 374 sortCoverageLast = False 375 if hasattr(self, "sortCoverageLast"): 376 # Find coverage table 377 for i in range(numItems): 378 item = self.items[i] 379 if getattr(item, 'name', None) == "Coverage": 380 sortCoverageLast = True 381 break 382 if id(item) not in done: 383 item._gatherTables(tables, extTables, done) 384 else: 385 # We're a new parent of item 386 pass 387 388 for i in iRange: 389 item = self.items[i] 390 if not hasattr(item, "getData"): 391 continue 392 393 if sortCoverageLast and (i==1) and getattr(item, 'name', None) == 'Coverage': 394 # we've already 'gathered' it above 395 continue 396 397 if id(item) not in done: 398 item._gatherTables(tables, extTables, done) 399 else: 400 # Item is already written out by other parent 401 pass 402 403 selfTables.append(self) 404 405 def getAllData(self): 406 """Assemble all data, including all subtables.""" 407 internedTables = {} 408 self._doneWriting(internedTables) 409 tables = [] 410 extTables = [] 411 done = {} 412 self._gatherTables(tables, extTables, done) 413 tables.reverse() 414 extTables.reverse() 415 # Gather all data in two passes: the absolute positions of all 416 # subtable are needed before the actual data can be assembled. 417 pos = 0 418 for table in tables: 419 table.pos = pos 420 pos = pos + table.getDataLength() 421 422 for table in extTables: 423 table.pos = pos 424 pos = pos + table.getDataLength() 425 426 data = [] 427 for table in tables: 428 tableData = table.getData() 429 data.append(tableData) 430 431 for table in extTables: 432 tableData = table.getData() 433 data.append(tableData) 434 435 return bytesjoin(data) 436 437 # interface for gathering data, as used by table.compile() 438 439 def getSubWriter(self, offsetSize=2): 440 subwriter = self.__class__(self.localState, self.tableTag, offsetSize=offsetSize) 441 subwriter.parent = self # because some subtables have idential values, we discard 442 # the duplicates under the getAllData method. Hence some 443 # subtable writers can have more than one parent writer. 444 # But we just care about first one right now. 445 return subwriter 446 447 def writeValue(self, typecode, value): 448 self.items.append(struct.pack(f">{typecode}", value)) 449 def writeArray(self, typecode, values): 450 a = array.array(typecode, values) 451 if sys.byteorder != "big": a.byteswap() 452 self.items.append(a.tobytes()) 453 454 def writeInt8(self, value): 455 assert -128 <= value < 128, value 456 self.items.append(struct.pack(">b", value)) 457 def writeInt8Array(self, values): 458 self.writeArray('b', values) 459 460 def writeShort(self, value): 461 assert -32768 <= value < 32768, value 462 self.items.append(struct.pack(">h", value)) 463 def writeShortArray(self, values): 464 self.writeArray('h', values) 465 466 def writeLong(self, value): 467 self.items.append(struct.pack(">i", value)) 468 def writeLongArray(self, values): 469 self.writeArray('i', values) 470 471 def writeUInt8(self, value): 472 assert 0 <= value < 256, value 473 self.items.append(struct.pack(">B", value)) 474 def writeUInt8Array(self, values): 475 self.writeArray('B', values) 476 477 def writeUShort(self, value): 478 assert 0 <= value < 0x10000, value 479 self.items.append(struct.pack(">H", value)) 480 def writeUShortArray(self, values): 481 self.writeArray('H', values) 482 483 def writeULong(self, value): 484 self.items.append(struct.pack(">I", value)) 485 def writeULongArray(self, values): 486 self.writeArray('I', values) 487 488 def writeUInt24(self, value): 489 assert 0 <= value < 0x1000000, value 490 b = struct.pack(">L", value) 491 self.items.append(b[1:]) 492 def writeUInt24Array(self, values): 493 for value in values: 494 self.writeUInt24(value) 495 496 def writeTag(self, tag): 497 tag = Tag(tag).tobytes() 498 assert len(tag) == 4, tag 499 self.items.append(tag) 500 501 def writeSubTable(self, subWriter): 502 self.items.append(subWriter) 503 504 def writeCountReference(self, table, name, size=2, value=None): 505 ref = CountReference(table, name, size=size, value=value) 506 self.items.append(ref) 507 return ref 508 509 def writeStruct(self, format, values): 510 data = struct.pack(*(format,) + values) 511 self.items.append(data) 512 513 def writeData(self, data): 514 self.items.append(data) 515 516 def getOverflowErrorRecord(self, item): 517 LookupListIndex = SubTableIndex = itemName = itemIndex = None 518 if self.name == 'LookupList': 519 LookupListIndex = item.repeatIndex 520 elif self.name == 'Lookup': 521 LookupListIndex = self.repeatIndex 522 SubTableIndex = item.repeatIndex 523 else: 524 itemName = getattr(item, 'name', '<none>') 525 if hasattr(item, 'repeatIndex'): 526 itemIndex = item.repeatIndex 527 if self.name == 'SubTable': 528 LookupListIndex = self.parent.repeatIndex 529 SubTableIndex = self.repeatIndex 530 elif self.name == 'ExtSubTable': 531 LookupListIndex = self.parent.parent.repeatIndex 532 SubTableIndex = self.parent.repeatIndex 533 else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable. 534 itemName = ".".join([self.name, itemName]) 535 p1 = self.parent 536 while p1 and p1.name not in ['ExtSubTable', 'SubTable']: 537 itemName = ".".join([p1.name, itemName]) 538 p1 = p1.parent 539 if p1: 540 if p1.name == 'ExtSubTable': 541 LookupListIndex = p1.parent.parent.repeatIndex 542 SubTableIndex = p1.parent.repeatIndex 543 else: 544 LookupListIndex = p1.parent.repeatIndex 545 SubTableIndex = p1.repeatIndex 546 547 return OverflowErrorRecord( (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) ) 548 549 550class CountReference(object): 551 """A reference to a Count value, not a count of references.""" 552 def __init__(self, table, name, size=None, value=None): 553 self.table = table 554 self.name = name 555 self.size = size 556 if value is not None: 557 self.setValue(value) 558 def setValue(self, value): 559 table = self.table 560 name = self.name 561 if table[name] is None: 562 table[name] = value 563 else: 564 assert table[name] == value, (name, table[name], value) 565 def getValue(self): 566 return self.table[self.name] 567 def getCountData(self): 568 v = self.table[self.name] 569 if v is None: v = 0 570 return {1:packUInt8, 2:packUShort, 4:packULong}[self.size](v) 571 572 573def packUInt8 (value): 574 return struct.pack(">B", value) 575 576def packUShort(value): 577 return struct.pack(">H", value) 578 579def packULong(value): 580 assert 0 <= value < 0x100000000, value 581 return struct.pack(">I", value) 582 583def packUInt24(value): 584 assert 0 <= value < 0x1000000, value 585 return struct.pack(">I", value)[1:] 586 587 588class BaseTable(object): 589 590 """Generic base class for all OpenType (sub)tables.""" 591 592 def __getattr__(self, attr): 593 reader = self.__dict__.get("reader") 594 if reader: 595 del self.reader 596 font = self.font 597 del self.font 598 self.decompile(reader, font) 599 return getattr(self, attr) 600 601 raise AttributeError(attr) 602 603 def ensureDecompiled(self, recurse=False): 604 reader = self.__dict__.get("reader") 605 if reader: 606 del self.reader 607 font = self.font 608 del self.font 609 self.decompile(reader, font) 610 if recurse: 611 for subtable in self.iterSubTables(): 612 subtable.value.ensureDecompiled(recurse) 613 614 @classmethod 615 def getRecordSize(cls, reader): 616 totalSize = 0 617 for conv in cls.converters: 618 size = conv.getRecordSize(reader) 619 if size is NotImplemented: return NotImplemented 620 countValue = 1 621 if conv.repeat: 622 if conv.repeat in reader: 623 countValue = reader[conv.repeat] + conv.aux 624 else: 625 return NotImplemented 626 totalSize += size * countValue 627 return totalSize 628 629 def getConverters(self): 630 return self.converters 631 632 def getConverterByName(self, name): 633 return self.convertersByName[name] 634 635 def populateDefaults(self, propagator=None): 636 for conv in self.getConverters(): 637 if conv.repeat: 638 if not hasattr(self, conv.name): 639 setattr(self, conv.name, []) 640 countValue = len(getattr(self, conv.name)) - conv.aux 641 try: 642 count_conv = self.getConverterByName(conv.repeat) 643 setattr(self, conv.repeat, countValue) 644 except KeyError: 645 # conv.repeat is a propagated count 646 if propagator and conv.repeat in propagator: 647 propagator[conv.repeat].setValue(countValue) 648 else: 649 if conv.aux and not eval(conv.aux, None, self.__dict__): 650 continue 651 if hasattr(self, conv.name): 652 continue # Warn if it should NOT be present?! 653 if hasattr(conv, 'writeNullOffset'): 654 setattr(self, conv.name, None) # Warn? 655 #elif not conv.isCount: 656 # # Warn? 657 # pass 658 659 def decompile(self, reader, font): 660 self.readFormat(reader) 661 table = {} 662 self.__rawTable = table # for debugging 663 for conv in self.getConverters(): 664 if conv.name == "SubTable": 665 conv = conv.getConverter(reader.tableTag, 666 table["LookupType"]) 667 if conv.name == "ExtSubTable": 668 conv = conv.getConverter(reader.tableTag, 669 table["ExtensionLookupType"]) 670 if conv.name == "FeatureParams": 671 conv = conv.getConverter(reader["FeatureTag"]) 672 if conv.name == "SubStruct": 673 conv = conv.getConverter(reader.tableTag, 674 table["MorphType"]) 675 try: 676 if conv.repeat: 677 if isinstance(conv.repeat, int): 678 countValue = conv.repeat 679 elif conv.repeat in table: 680 countValue = table[conv.repeat] 681 else: 682 # conv.repeat is a propagated count 683 countValue = reader[conv.repeat] 684 countValue += conv.aux 685 table[conv.name] = conv.readArray(reader, font, table, countValue) 686 else: 687 if conv.aux and not eval(conv.aux, None, table): 688 continue 689 table[conv.name] = conv.read(reader, font, table) 690 if conv.isPropagated: 691 reader[conv.name] = table[conv.name] 692 except Exception as e: 693 name = conv.name 694 e.args = e.args + (name,) 695 raise 696 697 if hasattr(self, 'postRead'): 698 self.postRead(table, font) 699 else: 700 self.__dict__.update(table) 701 702 del self.__rawTable # succeeded, get rid of debugging info 703 704 def compile(self, writer, font): 705 self.ensureDecompiled() 706 # TODO Following hack to be removed by rewriting how FormatSwitching tables 707 # are handled. 708 # https://github.com/fonttools/fonttools/pull/2238#issuecomment-805192631 709 if hasattr(self, 'preWrite'): 710 deleteFormat = not hasattr(self, 'Format') 711 table = self.preWrite(font) 712 deleteFormat = deleteFormat and hasattr(self, 'Format') 713 else: 714 deleteFormat = False 715 table = self.__dict__.copy() 716 717 # some count references may have been initialized in a custom preWrite; we set 718 # these in the writer's state beforehand (instead of sequentially) so they will 719 # be propagated to all nested subtables even if the count appears in the current 720 # table only *after* the offset to the subtable that it is counting. 721 for conv in self.getConverters(): 722 if conv.isCount and conv.isPropagated: 723 value = table.get(conv.name) 724 if isinstance(value, CountReference): 725 writer[conv.name] = value 726 727 if hasattr(self, 'sortCoverageLast'): 728 writer.sortCoverageLast = 1 729 730 if hasattr(self, 'DontShare'): 731 writer.DontShare = True 732 733 if hasattr(self.__class__, 'LookupType'): 734 writer['LookupType'].setValue(self.__class__.LookupType) 735 736 self.writeFormat(writer) 737 for conv in self.getConverters(): 738 value = table.get(conv.name) # TODO Handle defaults instead of defaulting to None! 739 if conv.repeat: 740 if value is None: 741 value = [] 742 countValue = len(value) - conv.aux 743 if isinstance(conv.repeat, int): 744 assert len(value) == conv.repeat, 'expected %d values, got %d' % (conv.repeat, len(value)) 745 elif conv.repeat in table: 746 CountReference(table, conv.repeat, value=countValue) 747 else: 748 # conv.repeat is a propagated count 749 writer[conv.repeat].setValue(countValue) 750 try: 751 conv.writeArray(writer, font, table, value) 752 except Exception as e: 753 e.args = e.args + (conv.name+'[]',) 754 raise 755 elif conv.isCount: 756 # Special-case Count values. 757 # Assumption: a Count field will *always* precede 758 # the actual array(s). 759 # We need a default value, as it may be set later by a nested 760 # table. We will later store it here. 761 # We add a reference: by the time the data is assembled 762 # the Count value will be filled in. 763 # We ignore the current count value since it will be recomputed, 764 # unless it's a CountReference that was already initialized in a custom preWrite. 765 if isinstance(value, CountReference): 766 ref = value 767 ref.size = conv.staticSize 768 writer.writeData(ref) 769 table[conv.name] = ref.getValue() 770 else: 771 ref = writer.writeCountReference(table, conv.name, conv.staticSize) 772 table[conv.name] = None 773 if conv.isPropagated: 774 writer[conv.name] = ref 775 elif conv.isLookupType: 776 # We make sure that subtables have the same lookup type, 777 # and that the type is the same as the one set on the 778 # Lookup object, if any is set. 779 if conv.name not in table: 780 table[conv.name] = None 781 ref = writer.writeCountReference(table, conv.name, conv.staticSize, table[conv.name]) 782 writer['LookupType'] = ref 783 else: 784 if conv.aux and not eval(conv.aux, None, table): 785 continue 786 try: 787 conv.write(writer, font, table, value) 788 except Exception as e: 789 name = value.__class__.__name__ if value is not None else conv.name 790 e.args = e.args + (name,) 791 raise 792 if conv.isPropagated: 793 writer[conv.name] = value 794 795 if deleteFormat: 796 del self.Format 797 798 def readFormat(self, reader): 799 pass 800 801 def writeFormat(self, writer): 802 pass 803 804 def toXML(self, xmlWriter, font, attrs=None, name=None): 805 tableName = name if name else self.__class__.__name__ 806 if attrs is None: 807 attrs = [] 808 if hasattr(self, "Format"): 809 attrs = attrs + [("Format", self.Format)] 810 xmlWriter.begintag(tableName, attrs) 811 xmlWriter.newline() 812 self.toXML2(xmlWriter, font) 813 xmlWriter.endtag(tableName) 814 xmlWriter.newline() 815 816 def toXML2(self, xmlWriter, font): 817 # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB). 818 # This is because in TTX our parent writes our main tag, and in otBase.py we 819 # do it ourselves. I think I'm getting schizophrenic... 820 for conv in self.getConverters(): 821 if conv.repeat: 822 value = getattr(self, conv.name, []) 823 for i in range(len(value)): 824 item = value[i] 825 conv.xmlWrite(xmlWriter, font, item, conv.name, 826 [("index", i)]) 827 else: 828 if conv.aux and not eval(conv.aux, None, vars(self)): 829 continue 830 value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! 831 conv.xmlWrite(xmlWriter, font, value, conv.name, []) 832 833 def fromXML(self, name, attrs, content, font): 834 try: 835 conv = self.getConverterByName(name) 836 except KeyError: 837 raise # XXX on KeyError, raise nice error 838 value = conv.xmlRead(attrs, content, font) 839 if conv.repeat: 840 seq = getattr(self, conv.name, None) 841 if seq is None: 842 seq = [] 843 setattr(self, conv.name, seq) 844 seq.append(value) 845 else: 846 setattr(self, conv.name, value) 847 848 def __ne__(self, other): 849 result = self.__eq__(other) 850 return result if result is NotImplemented else not result 851 852 def __eq__(self, other): 853 if type(self) != type(other): 854 return NotImplemented 855 856 self.ensureDecompiled() 857 other.ensureDecompiled() 858 859 return self.__dict__ == other.__dict__ 860 861 class SubTableEntry(NamedTuple): 862 """See BaseTable.iterSubTables()""" 863 name: str 864 value: "BaseTable" 865 index: Optional[int] = None # index into given array, None for single values 866 867 def iterSubTables(self) -> Iterator[SubTableEntry]: 868 """Yield (name, value, index) namedtuples for all subtables of current table. 869 870 A sub-table is an instance of BaseTable (or subclass thereof) that is a child 871 of self, the current parent table. 872 The tuples also contain the attribute name (str) of the of parent table to get 873 a subtable, and optionally, for lists of subtables (i.e. attributes associated 874 with a converter that has a 'repeat'), an index into the list containing the 875 given subtable value. 876 This method can be useful to traverse trees of otTables. 877 """ 878 for conv in self.getConverters(): 879 name = conv.name 880 value = getattr(self, name, None) 881 if value is None: 882 continue 883 if isinstance(value, BaseTable): 884 yield self.SubTableEntry(name, value) 885 elif isinstance(value, list): 886 yield from ( 887 self.SubTableEntry(name, v, index=i) 888 for i, v in enumerate(value) 889 if isinstance(v, BaseTable) 890 ) 891 892 893class FormatSwitchingBaseTable(BaseTable): 894 895 """Minor specialization of BaseTable, for tables that have multiple 896 formats, eg. CoverageFormat1 vs. CoverageFormat2.""" 897 898 @classmethod 899 def getRecordSize(cls, reader): 900 return NotImplemented 901 902 def getConverters(self): 903 try: 904 fmt = self.Format 905 except AttributeError: 906 # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format' 907 # attribute after fully decompiled, only gain one in preWrite before being 908 # recompiled. In the decompiled state, these hand-coded classes defined in 909 # otTables.py lose their format-specific nature and gain more high-level 910 # attributes that are not tied to converters. 911 return [] 912 return self.converters.get(self.Format, []) 913 914 def getConverterByName(self, name): 915 return self.convertersByName[self.Format][name] 916 917 def readFormat(self, reader): 918 self.Format = reader.readUShort() 919 920 def writeFormat(self, writer): 921 writer.writeUShort(self.Format) 922 923 def toXML(self, xmlWriter, font, attrs=None, name=None): 924 BaseTable.toXML(self, xmlWriter, font, attrs, name) 925 926 927class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable): 928 def readFormat(self, reader): 929 self.Format = reader.readUInt8() 930 931 def writeFormat(self, writer): 932 writer.writeUInt8(self.Format) 933 934 935formatSwitchingBaseTables = { 936 "uint16": FormatSwitchingBaseTable, 937 "uint8": UInt8FormatSwitchingBaseTable, 938} 939 940def getFormatSwitchingBaseTableClass(formatType): 941 try: 942 return formatSwitchingBaseTables[formatType] 943 except KeyError: 944 raise TypeError(f"Unsupported format type: {formatType!r}") 945 946 947# 948# Support for ValueRecords 949# 950# This data type is so different from all other OpenType data types that 951# it requires quite a bit of code for itself. It even has special support 952# in OTTableReader and OTTableWriter... 953# 954 955valueRecordFormat = [ 956# Mask Name isDevice signed 957 (0x0001, "XPlacement", 0, 1), 958 (0x0002, "YPlacement", 0, 1), 959 (0x0004, "XAdvance", 0, 1), 960 (0x0008, "YAdvance", 0, 1), 961 (0x0010, "XPlaDevice", 1, 0), 962 (0x0020, "YPlaDevice", 1, 0), 963 (0x0040, "XAdvDevice", 1, 0), 964 (0x0080, "YAdvDevice", 1, 0), 965# reserved: 966 (0x0100, "Reserved1", 0, 0), 967 (0x0200, "Reserved2", 0, 0), 968 (0x0400, "Reserved3", 0, 0), 969 (0x0800, "Reserved4", 0, 0), 970 (0x1000, "Reserved5", 0, 0), 971 (0x2000, "Reserved6", 0, 0), 972 (0x4000, "Reserved7", 0, 0), 973 (0x8000, "Reserved8", 0, 0), 974] 975 976def _buildDict(): 977 d = {} 978 for mask, name, isDevice, signed in valueRecordFormat: 979 d[name] = mask, isDevice, signed 980 return d 981 982valueRecordFormatDict = _buildDict() 983 984 985class ValueRecordFactory(object): 986 987 """Given a format code, this object convert ValueRecords.""" 988 989 def __init__(self, valueFormat): 990 format = [] 991 for mask, name, isDevice, signed in valueRecordFormat: 992 if valueFormat & mask: 993 format.append((name, isDevice, signed)) 994 self.format = format 995 996 def __len__(self): 997 return len(self.format) 998 999 def readValueRecord(self, reader, font): 1000 format = self.format 1001 if not format: 1002 return None 1003 valueRecord = ValueRecord() 1004 for name, isDevice, signed in format: 1005 if signed: 1006 value = reader.readShort() 1007 else: 1008 value = reader.readUShort() 1009 if isDevice: 1010 if value: 1011 from . import otTables 1012 subReader = reader.getSubReader(value) 1013 value = getattr(otTables, name)() 1014 value.decompile(subReader, font) 1015 else: 1016 value = None 1017 setattr(valueRecord, name, value) 1018 return valueRecord 1019 1020 def writeValueRecord(self, writer, font, valueRecord): 1021 for name, isDevice, signed in self.format: 1022 value = getattr(valueRecord, name, 0) 1023 if isDevice: 1024 if value: 1025 subWriter = writer.getSubWriter() 1026 writer.writeSubTable(subWriter) 1027 value.compile(subWriter, font) 1028 else: 1029 writer.writeUShort(0) 1030 elif signed: 1031 writer.writeShort(value) 1032 else: 1033 writer.writeUShort(value) 1034 1035 1036class ValueRecord(object): 1037 1038 # see ValueRecordFactory 1039 1040 def __init__(self, valueFormat=None, src=None): 1041 if valueFormat is not None: 1042 for mask, name, isDevice, signed in valueRecordFormat: 1043 if valueFormat & mask: 1044 setattr(self, name, None if isDevice else 0) 1045 if src is not None: 1046 for key,val in src.__dict__.items(): 1047 if not hasattr(self, key): 1048 continue 1049 setattr(self, key, val) 1050 elif src is not None: 1051 self.__dict__ = src.__dict__.copy() 1052 1053 def getFormat(self): 1054 format = 0 1055 for name in self.__dict__.keys(): 1056 format = format | valueRecordFormatDict[name][0] 1057 return format 1058 1059 def getEffectiveFormat(self): 1060 format = 0 1061 for name,value in self.__dict__.items(): 1062 if value: 1063 format = format | valueRecordFormatDict[name][0] 1064 return format 1065 1066 def toXML(self, xmlWriter, font, valueName, attrs=None): 1067 if attrs is None: 1068 simpleItems = [] 1069 else: 1070 simpleItems = list(attrs) 1071 for mask, name, isDevice, format in valueRecordFormat[:4]: # "simple" values 1072 if hasattr(self, name): 1073 simpleItems.append((name, getattr(self, name))) 1074 deviceItems = [] 1075 for mask, name, isDevice, format in valueRecordFormat[4:8]: # device records 1076 if hasattr(self, name): 1077 device = getattr(self, name) 1078 if device is not None: 1079 deviceItems.append((name, device)) 1080 if deviceItems: 1081 xmlWriter.begintag(valueName, simpleItems) 1082 xmlWriter.newline() 1083 for name, deviceRecord in deviceItems: 1084 if deviceRecord is not None: 1085 deviceRecord.toXML(xmlWriter, font, name=name) 1086 xmlWriter.endtag(valueName) 1087 xmlWriter.newline() 1088 else: 1089 xmlWriter.simpletag(valueName, simpleItems) 1090 xmlWriter.newline() 1091 1092 def fromXML(self, name, attrs, content, font): 1093 from . import otTables 1094 for k, v in attrs.items(): 1095 setattr(self, k, int(v)) 1096 for element in content: 1097 if not isinstance(element, tuple): 1098 continue 1099 name, attrs, content = element 1100 value = getattr(otTables, name)() 1101 for elem2 in content: 1102 if not isinstance(elem2, tuple): 1103 continue 1104 name2, attrs2, content2 = elem2 1105 value.fromXML(name2, attrs2, content2, font) 1106 setattr(self, name, value) 1107 1108 def __ne__(self, other): 1109 result = self.__eq__(other) 1110 return result if result is NotImplemented else not result 1111 1112 def __eq__(self, other): 1113 if type(self) != type(other): 1114 return NotImplemented 1115 return self.__dict__ == other.__dict__ 1116