otBase.py revision 95f795f40a5d75c2c14c03c1b74cf1ee8816dbf8
1from __future__ import print_function, division 2from fontTools.misc.py23 import * 3from .DefaultTable import DefaultTable 4import struct 5 6class OverflowErrorRecord(object): 7 def __init__(self, overflowTuple): 8 self.tableType = overflowTuple[0] 9 self.LookupListIndex = overflowTuple[1] 10 self.SubTableIndex = overflowTuple[2] 11 self.itemName = overflowTuple[3] 12 self.itemIndex = overflowTuple[4] 13 14 def __repr__(self): 15 return str((self.tableType, "LookupIndex:", self.LookupListIndex, "SubTableIndex:", self.SubTableIndex, "ItemName:", self.itemName, "ItemIndex:", self.itemIndex)) 16 17class OTLOffsetOverflowError(Exception): 18 def __init__(self, overflowErrorRecord): 19 self.value = overflowErrorRecord 20 21 def __str__(self): 22 return repr(self.value) 23 24 25class BaseTTXConverter(DefaultTable): 26 27 """Generic base class for TTX table converters. It functions as an 28 adapter between the TTX (ttLib actually) table model and the model 29 we use for OpenType tables, which is necessarily subtly different. 30 """ 31 32 def decompile(self, data, font): 33 from . import otTables 34 cachingStats = None if True else {} 35 class GlobalState(object): 36 def __init__(self, tableType, cachingStats): 37 self.tableType = tableType 38 self.cachingStats = cachingStats 39 globalState = GlobalState(tableType=self.tableTag, 40 cachingStats=cachingStats) 41 reader = OTTableReader(data, globalState) 42 tableClass = getattr(otTables, self.tableTag) 43 self.table = tableClass() 44 self.table.decompile(reader, font) 45 if cachingStats: 46 stats = sorted([(v, k) for k, v in cachingStats.items()]) 47 stats.reverse() 48 print("cachingsstats for ", self.tableTag) 49 for v, k in stats: 50 if v < 2: 51 break 52 print(v, k) 53 print("---", len(stats)) 54 55 def compile(self, font): 56 """ Create a top-level OTFWriter for the GPOS/GSUB table. 57 Call the compile method for the the table 58 for each 'converter' record in the table converter list 59 call converter's write method for each item in the value. 60 - For simple items, the write method adds a string to the 61 writer's self.items list. 62 - For Struct/Table/Subtable items, it add first adds new writer to the 63 to the writer's self.items, then calls the item's compile method. 64 This creates a tree of writers, rooted at the GUSB/GPOS writer, with 65 each writer representing a table, and the writer.items list containing 66 the child data strings and writers. 67 call the getAllData method 68 call _doneWriting, which removes duplicates 69 call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables 70 Traverse the flat list of tables, calling getDataLength on each to update their position 71 Traverse the flat list of tables again, calling getData each get the data in the table, now that 72 pos's and offset are known. 73 74 If a lookup subtable overflows an offset, we have to start all over. 75 """ 76 class GlobalState(object): 77 def __init__(self, tableType): 78 self.tableType = tableType 79 globalState = GlobalState(tableType=self.tableTag) 80 overflowRecord = None 81 82 while True: 83 try: 84 writer = OTTableWriter(globalState) 85 self.table.compile(writer, font) 86 return writer.getAllData() 87 88 except OTLOffsetOverflowError as e: 89 90 if overflowRecord == e.value: 91 raise # Oh well... 92 93 overflowRecord = e.value 94 print("Attempting to fix OTLOffsetOverflowError", e) 95 lastItem = overflowRecord 96 97 ok = 0 98 if overflowRecord.itemName is None: 99 from .otTables import fixLookupOverFlows 100 ok = fixLookupOverFlows(font, overflowRecord) 101 else: 102 from .otTables import fixSubTableOverFlows 103 ok = fixSubTableOverFlows(font, overflowRecord) 104 if not ok: 105 raise 106 107 def toXML(self, writer, font): 108 self.table.toXML2(writer, font) 109 110 def fromXML(self, name, attrs, content, font): 111 from . import otTables 112 if not hasattr(self, "table"): 113 tableClass = getattr(otTables, self.tableTag) 114 self.table = tableClass() 115 self.table.fromXML(name, attrs, content, font) 116 117 118class OTTableReader(object): 119 120 """Helper class to retrieve data from an OpenType table.""" 121 122 __slots__ = ('data', 'offset', 'pos', 'globalState', 'localState') 123 124 def __init__(self, data, globalState={}, localState=None, offset=0): 125 self.data = data 126 self.offset = offset 127 self.pos = offset 128 self.globalState = globalState 129 self.localState = localState 130 131 def getSubReader(self, offset): 132 offset = self.offset + offset 133 cachingStats = self.globalState.cachingStats 134 if cachingStats is not None: 135 cachingStats[offset] = cachingStats.get(offset, 0) + 1 136 return self.__class__(self.data, self.globalState, self.localState, offset) 137 138 def readUShort(self): 139 pos = self.pos 140 newpos = pos + 2 141 value, = struct.unpack(">H", self.data[pos:newpos]) 142 self.pos = newpos 143 return value 144 145 def readShort(self): 146 pos = self.pos 147 newpos = pos + 2 148 value, = struct.unpack(">h", self.data[pos:newpos]) 149 self.pos = newpos 150 return value 151 152 def readLong(self): 153 pos = self.pos 154 newpos = pos + 4 155 value, = struct.unpack(">l", self.data[pos:newpos]) 156 self.pos = newpos 157 return value 158 159 def readUInt24(self): 160 pos = self.pos 161 newpos = pos + 3 162 value, = struct.unpack(">l", b'\0'+self.data[pos:newpos]) 163 self.pos = newpos 164 return value 165 166 def readULong(self): 167 pos = self.pos 168 newpos = pos + 4 169 value, = struct.unpack(">L", self.data[pos:newpos]) 170 self.pos = newpos 171 return value 172 173 def readTag(self): 174 pos = self.pos 175 newpos = pos + 4 176 value = Tag(self.data[pos:newpos]) 177 assert len(value) == 4 178 self.pos = newpos 179 return value 180 181 def __setitem__(self, name, value): 182 state = self.localState.copy() if self.localState else dict() 183 state[name] = value 184 self.localState = state 185 186 def __getitem__(self, name): 187 return self.localState[name] 188 189 190class OTTableWriter(object): 191 192 """Helper class to gather and assemble data for OpenType tables.""" 193 194 def __init__(self, globalState, localState=None): 195 self.items = [] 196 self.pos = None 197 self.globalState = globalState 198 self.localState = localState 199 self.parent = None 200 201 def __setitem__(self, name, value): 202 state = self.localState.copy() if self.localState else dict() 203 state[name] = value 204 self.localState = state 205 206 def __getitem__(self, name): 207 return self.localState[name] 208 209 # assembler interface 210 211 def getAllData(self): 212 """Assemble all data, including all subtables.""" 213 self._doneWriting() 214 tables, extTables = self._gatherTables() 215 tables.reverse() 216 extTables.reverse() 217 # Gather all data in two passes: the absolute positions of all 218 # subtable are needed before the actual data can be assembled. 219 pos = 0 220 for table in tables: 221 table.pos = pos 222 pos = pos + table.getDataLength() 223 224 for table in extTables: 225 table.pos = pos 226 pos = pos + table.getDataLength() 227 228 229 data = [] 230 for table in tables: 231 tableData = table.getData() 232 data.append(tableData) 233 234 for table in extTables: 235 tableData = table.getData() 236 data.append(tableData) 237 238 return bytesjoin(data) 239 240 def getDataLength(self): 241 """Return the length of this table in bytes, without subtables.""" 242 l = 0 243 for item in self.items: 244 if hasattr(item, "getData") or hasattr(item, "getCountData"): 245 if item.longOffset: 246 l = l + 4 # sizeof(ULong) 247 else: 248 l = l + 2 # sizeof(UShort) 249 else: 250 l = l + len(item) 251 return l 252 253 def getData(self): 254 """Assemble the data for this writer/table, without subtables.""" 255 items = list(self.items) # make a shallow copy 256 pos = self.pos 257 numItems = len(items) 258 for i in range(numItems): 259 item = items[i] 260 261 if hasattr(item, "getData"): 262 if item.longOffset: 263 items[i] = packULong(item.pos - pos) 264 else: 265 try: 266 items[i] = packUShort(item.pos - pos) 267 except struct.error: 268 # provide data to fix overflow problem. 269 # If the overflow is to a lookup, or from a lookup to a subtable, 270 # just report the current item. Otherwise... 271 if self.name not in [ 'LookupList', 'Lookup']: 272 # overflow is within a subTable. Life is more complicated. 273 # If we split the sub-table just before the current item, we may still suffer overflow. 274 # This is because duplicate table merging is done only within an Extension subTable tree; 275 # when we split the subtable in two, some items may no longer be duplicates. 276 # Get worst case by adding up all the item lengths, depth first traversal. 277 # and then report the first item that overflows a short. 278 def getDeepItemLength(table): 279 if hasattr(table, "getDataLength"): 280 length = 0 281 for item in table.items: 282 length = length + getDeepItemLength(item) 283 else: 284 length = len(table) 285 return length 286 287 length = self.getDataLength() 288 if hasattr(self, "sortCoverageLast") and item.name == "Coverage": 289 # Coverage is first in the item list, but last in the table list, 290 # The original overflow is really in the item list. Skip the Coverage 291 # table in the following test. 292 items = items[i+1:] 293 294 for j in range(len(items)): 295 item = items[j] 296 length = length + getDeepItemLength(item) 297 if length > 65535: 298 break 299 overflowErrorRecord = self.getOverflowErrorRecord(item) 300 301 302 raise OTLOffsetOverflowError(overflowErrorRecord) 303 304 return bytesjoin(items) 305 306 def __hash__(self): 307 # only works after self._doneWriting() has been called 308 return hash(self.items) 309 310 def __ne__(self, other): 311 return not self.__eq__(other) 312 def __eq__(self, other): 313 if type(self) != type(other): 314 return NotImplemented 315 return self.items == other.items 316 317 def _doneWriting(self, internedTables=None): 318 # Convert CountData references to data string items 319 # collapse duplicate table references to a unique entry 320 # "tables" are OTTableWriter objects. 321 322 # For Extension Lookup types, we can 323 # eliminate duplicates only within the tree under the Extension Lookup, 324 # as offsets may exceed 64K even between Extension LookupTable subtables. 325 if internedTables is None: 326 internedTables = {} 327 items = self.items 328 iRange = list(range(len(items))) 329 330 if hasattr(self, "Extension"): 331 newTree = 1 332 else: 333 newTree = 0 334 for i in iRange: 335 item = items[i] 336 if hasattr(item, "getCountData"): 337 items[i] = item.getCountData() 338 elif hasattr(item, "getData"): 339 if newTree: 340 item._doneWriting() 341 else: 342 item._doneWriting(internedTables) 343 internedItem = internedTables.get(item) 344 if internedItem: 345 items[i] = item = internedItem 346 else: 347 internedTables[item] = item 348 self.items = tuple(items) 349 350 def _gatherTables(self, tables=None, extTables=None, done=None): 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 if tables is None: # init call for first time. 359 tables = [] 360 extTables = [] 361 done = {} 362 363 done[self] = 1 364 365 numItems = len(self.items) 366 iRange = list(range(numItems)) 367 iRange.reverse() 368 369 if hasattr(self, "Extension"): 370 appendExtensions = 1 371 else: 372 appendExtensions = 0 373 374 # add Coverage table if it is sorted last. 375 sortCoverageLast = 0 376 if hasattr(self, "sortCoverageLast"): 377 # Find coverage table 378 for i in range(numItems): 379 item = self.items[i] 380 if hasattr(item, "name") and (item.name == "Coverage"): 381 sortCoverageLast = 1 382 break 383 if item not in done: 384 item._gatherTables(tables, extTables, done) 385 else: 386 # We're a new parent of item 387 pass 388 389 for i in iRange: 390 item = self.items[i] 391 if not hasattr(item, "getData"): 392 continue 393 394 if sortCoverageLast and (i==1) and item.name == 'Coverage': 395 # we've already 'gathered' it above 396 continue 397 398 if appendExtensions: 399 assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables" 400 newDone = {} 401 item._gatherTables(extTables, None, newDone) 402 403 elif item not in done: 404 item._gatherTables(tables, extTables, done) 405 else: 406 # We're a new parent of item 407 pass 408 409 410 tables.append(self) 411 return tables, extTables 412 413 # interface for gathering data, as used by table.compile() 414 415 def getSubWriter(self): 416 subwriter = self.__class__(self.globalState, self.localState) 417 subwriter.parent = self # because some subtables have idential values, we discard 418 # the duplicates under the getAllData method. Hence some 419 # subtable writers can have more than one parent writer. 420 # But we just care about first one right now. 421 return subwriter 422 423 def writeUShort(self, value): 424 assert 0 <= value < 0x10000 425 self.items.append(struct.pack(">H", value)) 426 427 def writeShort(self, value): 428 self.items.append(struct.pack(">h", value)) 429 430 def writeUInt24(self, value): 431 assert 0 <= value < 0x1000000 432 b = struct.pack(">L", value) 433 self.items.append(b[1:]) 434 435 def writeLong(self, value): 436 self.items.append(struct.pack(">l", value)) 437 438 def writeULong(self, value): 439 self.items.append(struct.pack(">L", value)) 440 441 def writeTag(self, tag): 442 tag = Tag(tag).tobytes() 443 assert len(tag) == 4 444 self.items.append(tag) 445 446 def writeSubTable(self, subWriter): 447 self.items.append(subWriter) 448 449 def writeCountReference(self, table, name): 450 ref = CountReference(table, name) 451 self.items.append(ref) 452 return ref 453 454 def writeStruct(self, format, values): 455 data = struct.pack(*(format,) + values) 456 self.items.append(data) 457 458 def writeData(self, data): 459 self.items.append(data) 460 461 def getOverflowErrorRecord(self, item): 462 LookupListIndex = SubTableIndex = itemName = itemIndex = None 463 if self.name == 'LookupList': 464 LookupListIndex = item.repeatIndex 465 elif self.name == 'Lookup': 466 LookupListIndex = self.repeatIndex 467 SubTableIndex = item.repeatIndex 468 else: 469 itemName = item.name 470 if hasattr(item, 'repeatIndex'): 471 itemIndex = item.repeatIndex 472 if self.name == 'SubTable': 473 LookupListIndex = self.parent.repeatIndex 474 SubTableIndex = self.repeatIndex 475 elif self.name == 'ExtSubTable': 476 LookupListIndex = self.parent.parent.repeatIndex 477 SubTableIndex = self.parent.repeatIndex 478 else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable. 479 itemName = ".".join([self.name, item.name]) 480 p1 = self.parent 481 while p1 and p1.name not in ['ExtSubTable', 'SubTable']: 482 itemName = ".".join([p1.name, item.name]) 483 p1 = p1.parent 484 if p1: 485 if p1.name == 'ExtSubTable': 486 LookupListIndex = p1.parent.parent.repeatIndex 487 SubTableIndex = p1.parent.repeatIndex 488 else: 489 LookupListIndex = p1.parent.repeatIndex 490 SubTableIndex = p1.repeatIndex 491 492 return OverflowErrorRecord( (self.globalState.tableType, LookupListIndex, SubTableIndex, itemName, itemIndex) ) 493 494 495class CountReference(object): 496 """A reference to a Count value, not a count of references.""" 497 def __init__(self, table, name): 498 self.table = table 499 self.name = name 500 def setValue(self, value): 501 table = self.table 502 name = self.name 503 if table[name] is None: 504 table[name] = value 505 else: 506 assert table[name] == value, (name, table[name], value) 507 def getCountData(self): 508 return packUShort(self.table[self.name]) 509 510 511def packUShort(value): 512 return struct.pack(">H", value) 513 514 515def packULong(value): 516 assert 0 <= value < 0x100000000, value 517 return struct.pack(">L", value) 518 519 520class BaseTable(object): 521 522 def __getattr__(self, attr): 523 reader = self.__dict__.get("reader") 524 if reader: 525 del self.reader 526 font = self.font 527 del self.font 528 self.decompile(reader, font) 529 return getattr(self, attr) 530 531 raise AttributeError(attr) 532 533 """Generic base class for all OpenType (sub)tables.""" 534 535 def getConverters(self): 536 return self.converters 537 538 def getConverterByName(self, name): 539 return self.convertersByName[name] 540 541 def decompile(self, reader, font): 542 self.readFormat(reader) 543 table = {} 544 self.__rawTable = table # for debugging 545 converters = self.getConverters() 546 for conv in converters: 547 if conv.name == "SubTable": 548 conv = conv.getConverter(reader.globalState.tableType, 549 table["LookupType"]) 550 if conv.name == "ExtSubTable": 551 conv = conv.getConverter(reader.globalState.tableType, 552 table["ExtensionLookupType"]) 553 if conv.name == "FeatureParams": 554 conv = conv.getConverter(reader["FeatureTag"]) 555 if conv.repeat: 556 l = [] 557 if conv.repeat in table: 558 countValue = table[conv.repeat] 559 else: 560 # conv.repeat is a propagated count 561 countValue = reader[conv.repeat] 562 for i in range(countValue + conv.aux): 563 l.append(conv.read(reader, font, table)) 564 table[conv.name] = l 565 else: 566 if conv.aux and not eval(conv.aux, None, table): 567 continue 568 table[conv.name] = conv.read(reader, font, table) 569 if conv.isPropagated: 570 reader[conv.name] = table[conv.name] 571 572 self.postRead(table, font) 573 574 del self.__rawTable # succeeded, get rid of debugging info 575 576 def ensureDecompiled(self): 577 reader = self.__dict__.get("reader") 578 if reader: 579 del self.reader 580 font = self.font 581 del self.font 582 self.decompile(reader, font) 583 584 def compile(self, writer, font): 585 self.ensureDecompiled() 586 table = self.preWrite(font) 587 588 if hasattr(self, 'sortCoverageLast'): 589 writer.sortCoverageLast = 1 590 591 if hasattr(self.__class__, 'LookupType'): 592 writer['LookupType'].setValue(self.__class__.LookupType) 593 594 self.writeFormat(writer) 595 for conv in self.getConverters(): 596 value = table.get(conv.name) 597 if conv.repeat: 598 if value is None: 599 value = [] 600 countValue = len(value) - conv.aux 601 if conv.repeat in table: 602 CountReference(table, conv.repeat).setValue(countValue) 603 else: 604 # conv.repeat is a propagated count 605 writer[conv.repeat].setValue(countValue) 606 for i in range(len(value)): 607 conv.write(writer, font, table, value[i], i) 608 elif conv.isCount: 609 # Special-case Count values. 610 # Assumption: a Count field will *always* precede 611 # the actual array(s). 612 # We need a default value, as it may be set later by a nested 613 # table. We will later store it here. 614 # We add a reference: by the time the data is assembled 615 # the Count value will be filled in. 616 ref = writer.writeCountReference(table, conv.name) 617 table[conv.name] = None 618 if conv.isPropagated: 619 writer[conv.name] = ref 620 elif conv.isLookupType: 621 ref = writer.writeCountReference(table, conv.name) 622 table[conv.name] = None 623 writer['LookupType'] = ref 624 else: 625 if conv.aux and not eval(conv.aux, None, table): 626 continue 627 conv.write(writer, font, table, value) 628 if conv.isPropagated: 629 writer[conv.name] = value 630 631 def readFormat(self, reader): 632 pass 633 634 def writeFormat(self, writer): 635 pass 636 637 def postRead(self, table, font): 638 self.__dict__.update(table) 639 640 def preWrite(self, font): 641 return self.__dict__.copy() 642 643 def toXML(self, xmlWriter, font, attrs=None, name=None): 644 tableName = name if name else self.__class__.__name__ 645 if attrs is None: 646 attrs = [] 647 if hasattr(self, "Format"): 648 attrs = attrs + [("Format", self.Format)] 649 xmlWriter.begintag(tableName, attrs) 650 xmlWriter.newline() 651 self.toXML2(xmlWriter, font) 652 xmlWriter.endtag(tableName) 653 xmlWriter.newline() 654 655 def toXML2(self, xmlWriter, font): 656 # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB). 657 # This is because in TTX our parent writes our main tag, and in otBase.py we 658 # do it ourselves. I think I'm getting schizophrenic... 659 for conv in self.getConverters(): 660 if conv.repeat: 661 value = getattr(self, conv.name) 662 for i in range(len(value)): 663 item = value[i] 664 conv.xmlWrite(xmlWriter, font, item, conv.name, 665 [("index", i)]) 666 else: 667 if conv.aux and not eval(conv.aux, None, vars(self)): 668 continue 669 value = getattr(self, conv.name) 670 conv.xmlWrite(xmlWriter, font, value, conv.name, []) 671 672 def fromXML(self, name, attrs, content, font): 673 try: 674 conv = self.getConverterByName(name) 675 except KeyError: 676 raise # XXX on KeyError, raise nice error 677 value = conv.xmlRead(attrs, content, font) 678 if conv.repeat: 679 seq = getattr(self, conv.name, None) 680 if seq is None: 681 seq = [] 682 setattr(self, conv.name, seq) 683 seq.append(value) 684 else: 685 setattr(self, conv.name, value) 686 687 def __ne__(self, other): 688 return not self.__eq__(other) 689 def __eq__(self, other): 690 if type(self) != type(other): 691 return NotImplemented 692 693 self.ensureDecompiled() 694 other.ensureDecompiled() 695 696 return self.__dict__ == other.__dict__ 697 698 699class FormatSwitchingBaseTable(BaseTable): 700 701 """Minor specialization of BaseTable, for tables that have multiple 702 formats, eg. CoverageFormat1 vs. CoverageFormat2.""" 703 704 def getConverters(self): 705 return self.converters[self.Format] 706 707 def getConverterByName(self, name): 708 return self.convertersByName[self.Format][name] 709 710 def readFormat(self, reader): 711 self.Format = reader.readUShort() 712 assert self.Format != 0, (self, reader.pos, len(reader.data)) 713 714 def writeFormat(self, writer): 715 writer.writeUShort(self.Format) 716 717 def toXML(self, xmlWriter, font, attrs=None, name=None): 718 BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__) 719 720 721# 722# Support for ValueRecords 723# 724# This data type is so different from all other OpenType data types that 725# it requires quite a bit of code for itself. It even has special support 726# in OTTableReader and OTTableWriter... 727# 728 729valueRecordFormat = [ 730# Mask Name isDevice signed 731 (0x0001, "XPlacement", 0, 1), 732 (0x0002, "YPlacement", 0, 1), 733 (0x0004, "XAdvance", 0, 1), 734 (0x0008, "YAdvance", 0, 1), 735 (0x0010, "XPlaDevice", 1, 0), 736 (0x0020, "YPlaDevice", 1, 0), 737 (0x0040, "XAdvDevice", 1, 0), 738 (0x0080, "YAdvDevice", 1, 0), 739# reserved: 740 (0x0100, "Reserved1", 0, 0), 741 (0x0200, "Reserved2", 0, 0), 742 (0x0400, "Reserved3", 0, 0), 743 (0x0800, "Reserved4", 0, 0), 744 (0x1000, "Reserved5", 0, 0), 745 (0x2000, "Reserved6", 0, 0), 746 (0x4000, "Reserved7", 0, 0), 747 (0x8000, "Reserved8", 0, 0), 748] 749 750def _buildDict(): 751 d = {} 752 for mask, name, isDevice, signed in valueRecordFormat: 753 d[name] = mask, isDevice, signed 754 return d 755 756valueRecordFormatDict = _buildDict() 757 758 759class ValueRecordFactory(object): 760 761 """Given a format code, this object convert ValueRecords.""" 762 763 def __init__(self, valueFormat): 764 format = [] 765 for mask, name, isDevice, signed in valueRecordFormat: 766 if valueFormat & mask: 767 format.append((name, isDevice, signed)) 768 self.format = format 769 770 def readValueRecord(self, reader, font): 771 format = self.format 772 if not format: 773 return None 774 valueRecord = ValueRecord() 775 for name, isDevice, signed in format: 776 if signed: 777 value = reader.readShort() 778 else: 779 value = reader.readUShort() 780 if isDevice: 781 if value: 782 from . import otTables 783 subReader = reader.getSubReader(value) 784 value = getattr(otTables, name)() 785 value.decompile(subReader, font) 786 else: 787 value = None 788 setattr(valueRecord, name, value) 789 return valueRecord 790 791 def writeValueRecord(self, writer, font, valueRecord): 792 for name, isDevice, signed in self.format: 793 value = getattr(valueRecord, name, 0) 794 if isDevice: 795 if value: 796 subWriter = writer.getSubWriter() 797 writer.writeSubTable(subWriter) 798 value.compile(subWriter, font) 799 else: 800 writer.writeUShort(0) 801 elif signed: 802 writer.writeShort(value) 803 else: 804 writer.writeUShort(value) 805 806 807class ValueRecord(object): 808 809 # see ValueRecordFactory 810 811 def getFormat(self): 812 format = 0 813 for name in self.__dict__.keys(): 814 format = format | valueRecordFormatDict[name][0] 815 return format 816 817 def toXML(self, xmlWriter, font, valueName, attrs=None): 818 if attrs is None: 819 simpleItems = [] 820 else: 821 simpleItems = list(attrs) 822 for mask, name, isDevice, format in valueRecordFormat[:4]: # "simple" values 823 if hasattr(self, name): 824 simpleItems.append((name, getattr(self, name))) 825 deviceItems = [] 826 for mask, name, isDevice, format in valueRecordFormat[4:8]: # device records 827 if hasattr(self, name): 828 device = getattr(self, name) 829 if device is not None: 830 deviceItems.append((name, device)) 831 if deviceItems: 832 xmlWriter.begintag(valueName, simpleItems) 833 xmlWriter.newline() 834 for name, deviceRecord in deviceItems: 835 if deviceRecord is not None: 836 deviceRecord.toXML(xmlWriter, font) 837 xmlWriter.endtag(valueName) 838 xmlWriter.newline() 839 else: 840 xmlWriter.simpletag(valueName, simpleItems) 841 xmlWriter.newline() 842 843 def fromXML(self, name, attrs, content, font): 844 from . import otTables 845 for k, v in attrs.items(): 846 setattr(self, k, int(v)) 847 for element in content: 848 if not isinstance(element, tuple): 849 continue 850 name, attrs, content = element 851 value = getattr(otTables, name)() 852 for elem2 in content: 853 if not isinstance(elem2, tuple): 854 continue 855 name2, attrs2, content2 = elem2 856 value.fromXML(name2, attrs2, content2, font) 857 setattr(self, name, value) 858 859 def __ne__(self, other): 860 return not self.__eq__(other) 861 def __eq__(self, other): 862 if type(self) != type(other): 863 return NotImplemented 864 return self.__dict__ == other.__dict__ 865