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