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