1from fontTools.misc.fixedTools import ( 2 fixedToFloat as fi2fl, 3 floatToFixed as fl2fi, 4 floatToFixedToStr as fl2str, 5 strToFixedToFloat as str2fl, 6 ensureVersionIsLong as fi2ve, 7 versionToFixed as ve2fi, 8) 9from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound 10from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval 11from fontTools.ttLib import getSearchRange 12from .otBase import ( 13 CountReference, 14 FormatSwitchingBaseTable, 15 OTTableReader, 16 OTTableWriter, 17 ValueRecordFactory, 18) 19from .otTables import ( 20 lookupTypes, 21 AATStateTable, 22 AATState, 23 AATAction, 24 ContextualMorphAction, 25 LigatureMorphAction, 26 InsertionMorphAction, 27 MorxSubtable, 28 ExtendMode as _ExtendMode, 29 CompositeMode as _CompositeMode, 30 NO_VARIATION_INDEX, 31) 32from itertools import zip_longest 33from functools import partial 34import re 35import struct 36from typing import Optional 37import logging 38 39 40log = logging.getLogger(__name__) 41istuple = lambda t: isinstance(t, tuple) 42 43 44def buildConverters(tableSpec, tableNamespace): 45 """Given a table spec from otData.py, build a converter object for each 46 field of the table. This is called for each table in otData.py, and 47 the results are assigned to the corresponding class in otTables.py.""" 48 converters = [] 49 convertersByName = {} 50 for tp, name, repeat, aux, descr in tableSpec: 51 tableName = name 52 if name.startswith("ValueFormat"): 53 assert tp == "uint16" 54 converterClass = ValueFormat 55 elif name.endswith("Count") or name in ("StructLength", "MorphType"): 56 converterClass = { 57 "uint8": ComputedUInt8, 58 "uint16": ComputedUShort, 59 "uint32": ComputedULong, 60 }[tp] 61 elif name == "SubTable": 62 converterClass = SubTable 63 elif name == "ExtSubTable": 64 converterClass = ExtSubTable 65 elif name == "SubStruct": 66 converterClass = SubStruct 67 elif name == "FeatureParams": 68 converterClass = FeatureParams 69 elif name in ("CIDGlyphMapping", "GlyphCIDMapping"): 70 converterClass = StructWithLength 71 else: 72 if not tp in converterMapping and "(" not in tp: 73 tableName = tp 74 converterClass = Struct 75 else: 76 converterClass = eval(tp, tableNamespace, converterMapping) 77 78 conv = converterClass(name, repeat, aux, description=descr) 79 80 if conv.tableClass: 81 # A "template" such as OffsetTo(AType) knowss the table class already 82 tableClass = conv.tableClass 83 elif tp in ("MortChain", "MortSubtable", "MorxChain"): 84 tableClass = tableNamespace.get(tp) 85 else: 86 tableClass = tableNamespace.get(tableName) 87 88 if not conv.tableClass: 89 conv.tableClass = tableClass 90 91 if name in ["SubTable", "ExtSubTable", "SubStruct"]: 92 conv.lookupTypes = tableNamespace["lookupTypes"] 93 # also create reverse mapping 94 for t in conv.lookupTypes.values(): 95 for cls in t.values(): 96 convertersByName[cls.__name__] = Table(name, repeat, aux, cls) 97 if name == "FeatureParams": 98 conv.featureParamTypes = tableNamespace["featureParamTypes"] 99 conv.defaultFeatureParams = tableNamespace["FeatureParams"] 100 for cls in conv.featureParamTypes.values(): 101 convertersByName[cls.__name__] = Table(name, repeat, aux, cls) 102 converters.append(conv) 103 assert name not in convertersByName, name 104 convertersByName[name] = conv 105 return converters, convertersByName 106 107 108class _MissingItem(tuple): 109 __slots__ = () 110 111 112try: 113 from collections import UserList 114except ImportError: 115 from UserList import UserList 116 117 118class _LazyList(UserList): 119 def __getslice__(self, i, j): 120 return self.__getitem__(slice(i, j)) 121 122 def __getitem__(self, k): 123 if isinstance(k, slice): 124 indices = range(*k.indices(len(self))) 125 return [self[i] for i in indices] 126 item = self.data[k] 127 if isinstance(item, _MissingItem): 128 self.reader.seek(self.pos + item[0] * self.recordSize) 129 item = self.conv.read(self.reader, self.font, {}) 130 self.data[k] = item 131 return item 132 133 def __add__(self, other): 134 if isinstance(other, _LazyList): 135 other = list(other) 136 elif isinstance(other, list): 137 pass 138 else: 139 return NotImplemented 140 return list(self) + other 141 142 def __radd__(self, other): 143 if not isinstance(other, list): 144 return NotImplemented 145 return other + list(self) 146 147 148class BaseConverter(object): 149 """Base class for converter objects. Apart from the constructor, this 150 is an abstract class.""" 151 152 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 153 self.name = name 154 self.repeat = repeat 155 self.aux = aux 156 self.tableClass = tableClass 157 self.isCount = name.endswith("Count") or name in [ 158 "DesignAxisRecordSize", 159 "ValueRecordSize", 160 ] 161 self.isLookupType = name.endswith("LookupType") or name == "MorphType" 162 self.isPropagated = name in [ 163 "ClassCount", 164 "Class2Count", 165 "FeatureTag", 166 "SettingsCount", 167 "VarRegionCount", 168 "MappingCount", 169 "RegionAxisCount", 170 "DesignAxisCount", 171 "DesignAxisRecordSize", 172 "AxisValueCount", 173 "ValueRecordSize", 174 "AxisCount", 175 "BaseGlyphRecordCount", 176 "LayerRecordCount", 177 ] 178 self.description = description 179 180 def readArray(self, reader, font, tableDict, count): 181 """Read an array of values from the reader.""" 182 lazy = font.lazy and count > 8 183 if lazy: 184 recordSize = self.getRecordSize(reader) 185 if recordSize is NotImplemented: 186 lazy = False 187 if not lazy: 188 l = [] 189 for i in range(count): 190 l.append(self.read(reader, font, tableDict)) 191 return l 192 else: 193 l = _LazyList() 194 l.reader = reader.copy() 195 l.pos = l.reader.pos 196 l.font = font 197 l.conv = self 198 l.recordSize = recordSize 199 l.extend(_MissingItem([i]) for i in range(count)) 200 reader.advance(count * recordSize) 201 return l 202 203 def getRecordSize(self, reader): 204 if hasattr(self, "staticSize"): 205 return self.staticSize 206 return NotImplemented 207 208 def read(self, reader, font, tableDict): 209 """Read a value from the reader.""" 210 raise NotImplementedError(self) 211 212 def writeArray(self, writer, font, tableDict, values): 213 try: 214 for i, value in enumerate(values): 215 self.write(writer, font, tableDict, value, i) 216 except Exception as e: 217 e.args = e.args + (i,) 218 raise 219 220 def write(self, writer, font, tableDict, value, repeatIndex=None): 221 """Write a value to the writer.""" 222 raise NotImplementedError(self) 223 224 def xmlRead(self, attrs, content, font): 225 """Read a value from XML.""" 226 raise NotImplementedError(self) 227 228 def xmlWrite(self, xmlWriter, font, value, name, attrs): 229 """Write a value to XML.""" 230 raise NotImplementedError(self) 231 232 varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)") 233 234 def getVarIndexOffset(self) -> Optional[int]: 235 """If description has `VarIndexBase + {offset}`, return the offset else None.""" 236 m = self.varIndexBasePlusOffsetRE.search(self.description) 237 if not m: 238 return None 239 return int(m.group(1)) 240 241 242class SimpleValue(BaseConverter): 243 @staticmethod 244 def toString(value): 245 return value 246 247 @staticmethod 248 def fromString(value): 249 return value 250 251 def xmlWrite(self, xmlWriter, font, value, name, attrs): 252 xmlWriter.simpletag(name, attrs + [("value", self.toString(value))]) 253 xmlWriter.newline() 254 255 def xmlRead(self, attrs, content, font): 256 return self.fromString(attrs["value"]) 257 258 259class OptionalValue(SimpleValue): 260 DEFAULT = None 261 262 def xmlWrite(self, xmlWriter, font, value, name, attrs): 263 if value != self.DEFAULT: 264 attrs.append(("value", self.toString(value))) 265 xmlWriter.simpletag(name, attrs) 266 xmlWriter.newline() 267 268 def xmlRead(self, attrs, content, font): 269 if "value" in attrs: 270 return self.fromString(attrs["value"]) 271 return self.DEFAULT 272 273 274class IntValue(SimpleValue): 275 @staticmethod 276 def fromString(value): 277 return int(value, 0) 278 279 280class Long(IntValue): 281 staticSize = 4 282 283 def read(self, reader, font, tableDict): 284 return reader.readLong() 285 286 def readArray(self, reader, font, tableDict, count): 287 return reader.readLongArray(count) 288 289 def write(self, writer, font, tableDict, value, repeatIndex=None): 290 writer.writeLong(value) 291 292 def writeArray(self, writer, font, tableDict, values): 293 writer.writeLongArray(values) 294 295 296class ULong(IntValue): 297 staticSize = 4 298 299 def read(self, reader, font, tableDict): 300 return reader.readULong() 301 302 def readArray(self, reader, font, tableDict, count): 303 return reader.readULongArray(count) 304 305 def write(self, writer, font, tableDict, value, repeatIndex=None): 306 writer.writeULong(value) 307 308 def writeArray(self, writer, font, tableDict, values): 309 writer.writeULongArray(values) 310 311 312class Flags32(ULong): 313 @staticmethod 314 def toString(value): 315 return "0x%08X" % value 316 317 318class VarIndex(OptionalValue, ULong): 319 DEFAULT = NO_VARIATION_INDEX 320 321 322class Short(IntValue): 323 staticSize = 2 324 325 def read(self, reader, font, tableDict): 326 return reader.readShort() 327 328 def readArray(self, reader, font, tableDict, count): 329 return reader.readShortArray(count) 330 331 def write(self, writer, font, tableDict, value, repeatIndex=None): 332 writer.writeShort(value) 333 334 def writeArray(self, writer, font, tableDict, values): 335 writer.writeShortArray(values) 336 337 338class UShort(IntValue): 339 staticSize = 2 340 341 def read(self, reader, font, tableDict): 342 return reader.readUShort() 343 344 def readArray(self, reader, font, tableDict, count): 345 return reader.readUShortArray(count) 346 347 def write(self, writer, font, tableDict, value, repeatIndex=None): 348 writer.writeUShort(value) 349 350 def writeArray(self, writer, font, tableDict, values): 351 writer.writeUShortArray(values) 352 353 354class Int8(IntValue): 355 staticSize = 1 356 357 def read(self, reader, font, tableDict): 358 return reader.readInt8() 359 360 def readArray(self, reader, font, tableDict, count): 361 return reader.readInt8Array(count) 362 363 def write(self, writer, font, tableDict, value, repeatIndex=None): 364 writer.writeInt8(value) 365 366 def writeArray(self, writer, font, tableDict, values): 367 writer.writeInt8Array(values) 368 369 370class UInt8(IntValue): 371 staticSize = 1 372 373 def read(self, reader, font, tableDict): 374 return reader.readUInt8() 375 376 def readArray(self, reader, font, tableDict, count): 377 return reader.readUInt8Array(count) 378 379 def write(self, writer, font, tableDict, value, repeatIndex=None): 380 writer.writeUInt8(value) 381 382 def writeArray(self, writer, font, tableDict, values): 383 writer.writeUInt8Array(values) 384 385 386class UInt24(IntValue): 387 staticSize = 3 388 389 def read(self, reader, font, tableDict): 390 return reader.readUInt24() 391 392 def write(self, writer, font, tableDict, value, repeatIndex=None): 393 writer.writeUInt24(value) 394 395 396class ComputedInt(IntValue): 397 def xmlWrite(self, xmlWriter, font, value, name, attrs): 398 if value is not None: 399 xmlWriter.comment("%s=%s" % (name, value)) 400 xmlWriter.newline() 401 402 403class ComputedUInt8(ComputedInt, UInt8): 404 pass 405 406 407class ComputedUShort(ComputedInt, UShort): 408 pass 409 410 411class ComputedULong(ComputedInt, ULong): 412 pass 413 414 415class Tag(SimpleValue): 416 staticSize = 4 417 418 def read(self, reader, font, tableDict): 419 return reader.readTag() 420 421 def write(self, writer, font, tableDict, value, repeatIndex=None): 422 writer.writeTag(value) 423 424 425class GlyphID(SimpleValue): 426 staticSize = 2 427 typecode = "H" 428 429 def readArray(self, reader, font, tableDict, count): 430 return font.getGlyphNameMany( 431 reader.readArray(self.typecode, self.staticSize, count) 432 ) 433 434 def read(self, reader, font, tableDict): 435 return font.getGlyphName(reader.readValue(self.typecode, self.staticSize)) 436 437 def writeArray(self, writer, font, tableDict, values): 438 writer.writeArray(self.typecode, font.getGlyphIDMany(values)) 439 440 def write(self, writer, font, tableDict, value, repeatIndex=None): 441 writer.writeValue(self.typecode, font.getGlyphID(value)) 442 443 444class GlyphID32(GlyphID): 445 staticSize = 4 446 typecode = "L" 447 448 449class NameID(UShort): 450 def xmlWrite(self, xmlWriter, font, value, name, attrs): 451 xmlWriter.simpletag(name, attrs + [("value", value)]) 452 if font and value: 453 nameTable = font.get("name") 454 if nameTable: 455 name = nameTable.getDebugName(value) 456 xmlWriter.write(" ") 457 if name: 458 xmlWriter.comment(name) 459 else: 460 xmlWriter.comment("missing from name table") 461 log.warning("name id %d missing from name table" % value) 462 xmlWriter.newline() 463 464 465class STATFlags(UShort): 466 def xmlWrite(self, xmlWriter, font, value, name, attrs): 467 xmlWriter.simpletag(name, attrs + [("value", value)]) 468 flags = [] 469 if value & 0x01: 470 flags.append("OlderSiblingFontAttribute") 471 if value & 0x02: 472 flags.append("ElidableAxisValueName") 473 if flags: 474 xmlWriter.write(" ") 475 xmlWriter.comment(" ".join(flags)) 476 xmlWriter.newline() 477 478 479class FloatValue(SimpleValue): 480 @staticmethod 481 def fromString(value): 482 return float(value) 483 484 485class DeciPoints(FloatValue): 486 staticSize = 2 487 488 def read(self, reader, font, tableDict): 489 return reader.readUShort() / 10 490 491 def write(self, writer, font, tableDict, value, repeatIndex=None): 492 writer.writeUShort(round(value * 10)) 493 494 495class BaseFixedValue(FloatValue): 496 staticSize = NotImplemented 497 precisionBits = NotImplemented 498 readerMethod = NotImplemented 499 writerMethod = NotImplemented 500 501 def read(self, reader, font, tableDict): 502 return self.fromInt(getattr(reader, self.readerMethod)()) 503 504 def write(self, writer, font, tableDict, value, repeatIndex=None): 505 getattr(writer, self.writerMethod)(self.toInt(value)) 506 507 @classmethod 508 def fromInt(cls, value): 509 return fi2fl(value, cls.precisionBits) 510 511 @classmethod 512 def toInt(cls, value): 513 return fl2fi(value, cls.precisionBits) 514 515 @classmethod 516 def fromString(cls, value): 517 return str2fl(value, cls.precisionBits) 518 519 @classmethod 520 def toString(cls, value): 521 return fl2str(value, cls.precisionBits) 522 523 524class Fixed(BaseFixedValue): 525 staticSize = 4 526 precisionBits = 16 527 readerMethod = "readLong" 528 writerMethod = "writeLong" 529 530 531class F2Dot14(BaseFixedValue): 532 staticSize = 2 533 precisionBits = 14 534 readerMethod = "readShort" 535 writerMethod = "writeShort" 536 537 538class Angle(F2Dot14): 539 # angles are specified in degrees, and encoded as F2Dot14 fractions of half 540 # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc. 541 bias = 0.0 542 factor = 1.0 / (1 << 14) * 180 # 0.010986328125 543 544 @classmethod 545 def fromInt(cls, value): 546 return (super().fromInt(value) + cls.bias) * 180 547 548 @classmethod 549 def toInt(cls, value): 550 return super().toInt((value / 180) - cls.bias) 551 552 @classmethod 553 def fromString(cls, value): 554 # quantize to nearest multiples of minimum fixed-precision angle 555 return otRound(float(value) / cls.factor) * cls.factor 556 557 @classmethod 558 def toString(cls, value): 559 return nearestMultipleShortestRepr(value, cls.factor) 560 561 562class BiasedAngle(Angle): 563 # A bias of 1.0 is used in the representation of start and end angles 564 # of COLRv1 PaintSweepGradients to allow for encoding +360deg 565 bias = 1.0 566 567 568class Version(SimpleValue): 569 staticSize = 4 570 571 def read(self, reader, font, tableDict): 572 value = reader.readLong() 573 return value 574 575 def write(self, writer, font, tableDict, value, repeatIndex=None): 576 value = fi2ve(value) 577 writer.writeLong(value) 578 579 @staticmethod 580 def fromString(value): 581 return ve2fi(value) 582 583 @staticmethod 584 def toString(value): 585 return "0x%08x" % value 586 587 @staticmethod 588 def fromFloat(v): 589 return fl2fi(v, 16) 590 591 592class Char64(SimpleValue): 593 """An ASCII string with up to 64 characters. 594 595 Unused character positions are filled with 0x00 bytes. 596 Used in Apple AAT fonts in the `gcid` table. 597 """ 598 599 staticSize = 64 600 601 def read(self, reader, font, tableDict): 602 data = reader.readData(self.staticSize) 603 zeroPos = data.find(b"\0") 604 if zeroPos >= 0: 605 data = data[:zeroPos] 606 s = tostr(data, encoding="ascii", errors="replace") 607 if s != tostr(data, encoding="ascii", errors="ignore"): 608 log.warning('replaced non-ASCII characters in "%s"' % s) 609 return s 610 611 def write(self, writer, font, tableDict, value, repeatIndex=None): 612 data = tobytes(value, encoding="ascii", errors="replace") 613 if data != tobytes(value, encoding="ascii", errors="ignore"): 614 log.warning('replacing non-ASCII characters in "%s"' % value) 615 if len(data) > self.staticSize: 616 log.warning( 617 'truncating overlong "%s" to %d bytes' % (value, self.staticSize) 618 ) 619 data = (data + b"\0" * self.staticSize)[: self.staticSize] 620 writer.writeData(data) 621 622 623class Struct(BaseConverter): 624 def getRecordSize(self, reader): 625 return self.tableClass and self.tableClass.getRecordSize(reader) 626 627 def read(self, reader, font, tableDict): 628 table = self.tableClass() 629 table.decompile(reader, font) 630 return table 631 632 def write(self, writer, font, tableDict, value, repeatIndex=None): 633 value.compile(writer, font) 634 635 def xmlWrite(self, xmlWriter, font, value, name, attrs): 636 if value is None: 637 if attrs: 638 # If there are attributes (probably index), then 639 # don't drop this even if it's NULL. It will mess 640 # up the array indices of the containing element. 641 xmlWriter.simpletag(name, attrs + [("empty", 1)]) 642 xmlWriter.newline() 643 else: 644 pass # NULL table, ignore 645 else: 646 value.toXML(xmlWriter, font, attrs, name=name) 647 648 def xmlRead(self, attrs, content, font): 649 if "empty" in attrs and safeEval(attrs["empty"]): 650 return None 651 table = self.tableClass() 652 Format = attrs.get("Format") 653 if Format is not None: 654 table.Format = int(Format) 655 656 noPostRead = not hasattr(table, "postRead") 657 if noPostRead: 658 # TODO Cache table.hasPropagated. 659 cleanPropagation = False 660 for conv in table.getConverters(): 661 if conv.isPropagated: 662 cleanPropagation = True 663 if not hasattr(font, "_propagator"): 664 font._propagator = {} 665 propagator = font._propagator 666 assert conv.name not in propagator, (conv.name, propagator) 667 setattr(table, conv.name, None) 668 propagator[conv.name] = CountReference(table.__dict__, conv.name) 669 670 for element in content: 671 if isinstance(element, tuple): 672 name, attrs, content = element 673 table.fromXML(name, attrs, content, font) 674 else: 675 pass 676 677 table.populateDefaults(propagator=getattr(font, "_propagator", None)) 678 679 if noPostRead: 680 if cleanPropagation: 681 for conv in table.getConverters(): 682 if conv.isPropagated: 683 propagator = font._propagator 684 del propagator[conv.name] 685 if not propagator: 686 del font._propagator 687 688 return table 689 690 def __repr__(self): 691 return "Struct of " + repr(self.tableClass) 692 693 694class StructWithLength(Struct): 695 def read(self, reader, font, tableDict): 696 pos = reader.pos 697 table = self.tableClass() 698 table.decompile(reader, font) 699 reader.seek(pos + table.StructLength) 700 return table 701 702 def write(self, writer, font, tableDict, value, repeatIndex=None): 703 for convIndex, conv in enumerate(value.getConverters()): 704 if conv.name == "StructLength": 705 break 706 lengthIndex = len(writer.items) + convIndex 707 if isinstance(value, FormatSwitchingBaseTable): 708 lengthIndex += 1 # implicit Format field 709 deadbeef = {1: 0xDE, 2: 0xDEAD, 4: 0xDEADBEEF}[conv.staticSize] 710 711 before = writer.getDataLength() 712 value.StructLength = deadbeef 713 value.compile(writer, font) 714 length = writer.getDataLength() - before 715 lengthWriter = writer.getSubWriter() 716 conv.write(lengthWriter, font, tableDict, length) 717 assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"[: conv.staticSize] 718 writer.items[lengthIndex] = lengthWriter.getAllData() 719 720 721class Table(Struct): 722 staticSize = 2 723 724 def readOffset(self, reader): 725 return reader.readUShort() 726 727 def writeNullOffset(self, writer): 728 writer.writeUShort(0) 729 730 def read(self, reader, font, tableDict): 731 offset = self.readOffset(reader) 732 if offset == 0: 733 return None 734 table = self.tableClass() 735 reader = reader.getSubReader(offset) 736 if font.lazy: 737 table.reader = reader 738 table.font = font 739 else: 740 table.decompile(reader, font) 741 return table 742 743 def write(self, writer, font, tableDict, value, repeatIndex=None): 744 if value is None: 745 self.writeNullOffset(writer) 746 else: 747 subWriter = writer.getSubWriter() 748 subWriter.name = self.name 749 if repeatIndex is not None: 750 subWriter.repeatIndex = repeatIndex 751 writer.writeSubTable(subWriter, offsetSize=self.staticSize) 752 value.compile(subWriter, font) 753 754 755class LTable(Table): 756 staticSize = 4 757 758 def readOffset(self, reader): 759 return reader.readULong() 760 761 def writeNullOffset(self, writer): 762 writer.writeULong(0) 763 764 765# Table pointed to by a 24-bit, 3-byte long offset 766class Table24(Table): 767 staticSize = 3 768 769 def readOffset(self, reader): 770 return reader.readUInt24() 771 772 def writeNullOffset(self, writer): 773 writer.writeUInt24(0) 774 775 776# TODO Clean / merge the SubTable and SubStruct 777 778 779class SubStruct(Struct): 780 def getConverter(self, tableType, lookupType): 781 tableClass = self.lookupTypes[tableType][lookupType] 782 return self.__class__(self.name, self.repeat, self.aux, tableClass) 783 784 def xmlWrite(self, xmlWriter, font, value, name, attrs): 785 super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs) 786 787 788class SubTable(Table): 789 def getConverter(self, tableType, lookupType): 790 tableClass = self.lookupTypes[tableType][lookupType] 791 return self.__class__(self.name, self.repeat, self.aux, tableClass) 792 793 def xmlWrite(self, xmlWriter, font, value, name, attrs): 794 super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs) 795 796 797class ExtSubTable(LTable, SubTable): 798 def write(self, writer, font, tableDict, value, repeatIndex=None): 799 writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer. 800 Table.write(self, writer, font, tableDict, value, repeatIndex) 801 802 803class FeatureParams(Table): 804 def getConverter(self, featureTag): 805 tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams) 806 return self.__class__(self.name, self.repeat, self.aux, tableClass) 807 808 809class ValueFormat(IntValue): 810 staticSize = 2 811 812 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 813 BaseConverter.__init__( 814 self, name, repeat, aux, tableClass, description=description 815 ) 816 self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1") 817 818 def read(self, reader, font, tableDict): 819 format = reader.readUShort() 820 reader[self.which] = ValueRecordFactory(format) 821 return format 822 823 def write(self, writer, font, tableDict, format, repeatIndex=None): 824 writer.writeUShort(format) 825 writer[self.which] = ValueRecordFactory(format) 826 827 828class ValueRecord(ValueFormat): 829 def getRecordSize(self, reader): 830 return 2 * len(reader[self.which]) 831 832 def read(self, reader, font, tableDict): 833 return reader[self.which].readValueRecord(reader, font) 834 835 def write(self, writer, font, tableDict, value, repeatIndex=None): 836 writer[self.which].writeValueRecord(writer, font, value) 837 838 def xmlWrite(self, xmlWriter, font, value, name, attrs): 839 if value is None: 840 pass # NULL table, ignore 841 else: 842 value.toXML(xmlWriter, font, self.name, attrs) 843 844 def xmlRead(self, attrs, content, font): 845 from .otBase import ValueRecord 846 847 value = ValueRecord() 848 value.fromXML(None, attrs, content, font) 849 return value 850 851 852class AATLookup(BaseConverter): 853 BIN_SEARCH_HEADER_SIZE = 10 854 855 def __init__(self, name, repeat, aux, tableClass, *, description=""): 856 BaseConverter.__init__( 857 self, name, repeat, aux, tableClass, description=description 858 ) 859 if issubclass(self.tableClass, SimpleValue): 860 self.converter = self.tableClass(name="Value", repeat=None, aux=None) 861 else: 862 self.converter = Table( 863 name="Value", repeat=None, aux=None, tableClass=self.tableClass 864 ) 865 866 def read(self, reader, font, tableDict): 867 format = reader.readUShort() 868 if format == 0: 869 return self.readFormat0(reader, font) 870 elif format == 2: 871 return self.readFormat2(reader, font) 872 elif format == 4: 873 return self.readFormat4(reader, font) 874 elif format == 6: 875 return self.readFormat6(reader, font) 876 elif format == 8: 877 return self.readFormat8(reader, font) 878 else: 879 assert False, "unsupported lookup format: %d" % format 880 881 def write(self, writer, font, tableDict, value, repeatIndex=None): 882 values = list( 883 sorted([(font.getGlyphID(glyph), val) for glyph, val in value.items()]) 884 ) 885 # TODO: Also implement format 4. 886 formats = list( 887 sorted( 888 filter( 889 None, 890 [ 891 self.buildFormat0(writer, font, values), 892 self.buildFormat2(writer, font, values), 893 self.buildFormat6(writer, font, values), 894 self.buildFormat8(writer, font, values), 895 ], 896 ) 897 ) 898 ) 899 # We use the format ID as secondary sort key to make the output 900 # deterministic when multiple formats have same encoded size. 901 dataSize, lookupFormat, writeMethod = formats[0] 902 pos = writer.getDataLength() 903 writeMethod() 904 actualSize = writer.getDataLength() - pos 905 assert ( 906 actualSize == dataSize 907 ), "AATLookup format %d claimed to write %d bytes, but wrote %d" % ( 908 lookupFormat, 909 dataSize, 910 actualSize, 911 ) 912 913 @staticmethod 914 def writeBinSearchHeader(writer, numUnits, unitSize): 915 writer.writeUShort(unitSize) 916 writer.writeUShort(numUnits) 917 searchRange, entrySelector, rangeShift = getSearchRange( 918 n=numUnits, itemSize=unitSize 919 ) 920 writer.writeUShort(searchRange) 921 writer.writeUShort(entrySelector) 922 writer.writeUShort(rangeShift) 923 924 def buildFormat0(self, writer, font, values): 925 numGlyphs = len(font.getGlyphOrder()) 926 if len(values) != numGlyphs: 927 return None 928 valueSize = self.converter.staticSize 929 return ( 930 2 + numGlyphs * valueSize, 931 0, 932 lambda: self.writeFormat0(writer, font, values), 933 ) 934 935 def writeFormat0(self, writer, font, values): 936 writer.writeUShort(0) 937 for glyphID_, value in values: 938 self.converter.write( 939 writer, font, tableDict=None, value=value, repeatIndex=None 940 ) 941 942 def buildFormat2(self, writer, font, values): 943 segStart, segValue = values[0] 944 segEnd = segStart 945 segments = [] 946 for glyphID, curValue in values[1:]: 947 if glyphID != segEnd + 1 or curValue != segValue: 948 segments.append((segStart, segEnd, segValue)) 949 segStart = segEnd = glyphID 950 segValue = curValue 951 else: 952 segEnd = glyphID 953 segments.append((segStart, segEnd, segValue)) 954 valueSize = self.converter.staticSize 955 numUnits, unitSize = len(segments) + 1, valueSize + 4 956 return ( 957 2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, 958 2, 959 lambda: self.writeFormat2(writer, font, segments), 960 ) 961 962 def writeFormat2(self, writer, font, segments): 963 writer.writeUShort(2) 964 valueSize = self.converter.staticSize 965 numUnits, unitSize = len(segments), valueSize + 4 966 self.writeBinSearchHeader(writer, numUnits, unitSize) 967 for firstGlyph, lastGlyph, value in segments: 968 writer.writeUShort(lastGlyph) 969 writer.writeUShort(firstGlyph) 970 self.converter.write( 971 writer, font, tableDict=None, value=value, repeatIndex=None 972 ) 973 writer.writeUShort(0xFFFF) 974 writer.writeUShort(0xFFFF) 975 writer.writeData(b"\x00" * valueSize) 976 977 def buildFormat6(self, writer, font, values): 978 valueSize = self.converter.staticSize 979 numUnits, unitSize = len(values), valueSize + 2 980 return ( 981 2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, 982 6, 983 lambda: self.writeFormat6(writer, font, values), 984 ) 985 986 def writeFormat6(self, writer, font, values): 987 writer.writeUShort(6) 988 valueSize = self.converter.staticSize 989 numUnits, unitSize = len(values), valueSize + 2 990 self.writeBinSearchHeader(writer, numUnits, unitSize) 991 for glyphID, value in values: 992 writer.writeUShort(glyphID) 993 self.converter.write( 994 writer, font, tableDict=None, value=value, repeatIndex=None 995 ) 996 writer.writeUShort(0xFFFF) 997 writer.writeData(b"\x00" * valueSize) 998 999 def buildFormat8(self, writer, font, values): 1000 minGlyphID, maxGlyphID = values[0][0], values[-1][0] 1001 if len(values) != maxGlyphID - minGlyphID + 1: 1002 return None 1003 valueSize = self.converter.staticSize 1004 return ( 1005 6 + len(values) * valueSize, 1006 8, 1007 lambda: self.writeFormat8(writer, font, values), 1008 ) 1009 1010 def writeFormat8(self, writer, font, values): 1011 firstGlyphID = values[0][0] 1012 writer.writeUShort(8) 1013 writer.writeUShort(firstGlyphID) 1014 writer.writeUShort(len(values)) 1015 for _, value in values: 1016 self.converter.write( 1017 writer, font, tableDict=None, value=value, repeatIndex=None 1018 ) 1019 1020 def readFormat0(self, reader, font): 1021 numGlyphs = len(font.getGlyphOrder()) 1022 data = self.converter.readArray(reader, font, tableDict=None, count=numGlyphs) 1023 return {font.getGlyphName(k): value for k, value in enumerate(data)} 1024 1025 def readFormat2(self, reader, font): 1026 mapping = {} 1027 pos = reader.pos - 2 # start of table is at UShort for format 1028 unitSize, numUnits = reader.readUShort(), reader.readUShort() 1029 assert unitSize >= 4 + self.converter.staticSize, unitSize 1030 for i in range(numUnits): 1031 reader.seek(pos + i * unitSize + 12) 1032 last = reader.readUShort() 1033 first = reader.readUShort() 1034 value = self.converter.read(reader, font, tableDict=None) 1035 if last != 0xFFFF: 1036 for k in range(first, last + 1): 1037 mapping[font.getGlyphName(k)] = value 1038 return mapping 1039 1040 def readFormat4(self, reader, font): 1041 mapping = {} 1042 pos = reader.pos - 2 # start of table is at UShort for format 1043 unitSize = reader.readUShort() 1044 assert unitSize >= 6, unitSize 1045 for i in range(reader.readUShort()): 1046 reader.seek(pos + i * unitSize + 12) 1047 last = reader.readUShort() 1048 first = reader.readUShort() 1049 offset = reader.readUShort() 1050 if last != 0xFFFF: 1051 dataReader = reader.getSubReader(0) # relative to current position 1052 dataReader.seek(pos + offset) # relative to start of table 1053 data = self.converter.readArray( 1054 dataReader, font, tableDict=None, count=last - first + 1 1055 ) 1056 for k, v in enumerate(data): 1057 mapping[font.getGlyphName(first + k)] = v 1058 return mapping 1059 1060 def readFormat6(self, reader, font): 1061 mapping = {} 1062 pos = reader.pos - 2 # start of table is at UShort for format 1063 unitSize = reader.readUShort() 1064 assert unitSize >= 2 + self.converter.staticSize, unitSize 1065 for i in range(reader.readUShort()): 1066 reader.seek(pos + i * unitSize + 12) 1067 glyphID = reader.readUShort() 1068 value = self.converter.read(reader, font, tableDict=None) 1069 if glyphID != 0xFFFF: 1070 mapping[font.getGlyphName(glyphID)] = value 1071 return mapping 1072 1073 def readFormat8(self, reader, font): 1074 first = reader.readUShort() 1075 count = reader.readUShort() 1076 data = self.converter.readArray(reader, font, tableDict=None, count=count) 1077 return {font.getGlyphName(first + k): value for (k, value) in enumerate(data)} 1078 1079 def xmlRead(self, attrs, content, font): 1080 value = {} 1081 for element in content: 1082 if isinstance(element, tuple): 1083 name, a, eltContent = element 1084 if name == "Lookup": 1085 value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font) 1086 return value 1087 1088 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1089 xmlWriter.begintag(name, attrs) 1090 xmlWriter.newline() 1091 for glyph, value in sorted(value.items()): 1092 self.converter.xmlWrite( 1093 xmlWriter, font, value=value, name="Lookup", attrs=[("glyph", glyph)] 1094 ) 1095 xmlWriter.endtag(name) 1096 xmlWriter.newline() 1097 1098 1099# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup 1100# followed by an offset to a glyph data table. Other than usual, the 1101# offsets in the AATLookup are not relative to the beginning of 1102# the beginning of the 'ankr' table, but relative to the glyph data table. 1103# So, to find the anchor data for a glyph, one needs to add the offset 1104# to the data table to the offset found in the AATLookup, and then use 1105# the sum of these two offsets to find the actual data. 1106class AATLookupWithDataOffset(BaseConverter): 1107 def read(self, reader, font, tableDict): 1108 lookupOffset = reader.readULong() 1109 dataOffset = reader.readULong() 1110 lookupReader = reader.getSubReader(lookupOffset) 1111 lookup = AATLookup("DataOffsets", None, None, UShort) 1112 offsets = lookup.read(lookupReader, font, tableDict) 1113 result = {} 1114 for glyph, offset in offsets.items(): 1115 dataReader = reader.getSubReader(offset + dataOffset) 1116 item = self.tableClass() 1117 item.decompile(dataReader, font) 1118 result[glyph] = item 1119 return result 1120 1121 def write(self, writer, font, tableDict, value, repeatIndex=None): 1122 # We do not work with OTTableWriter sub-writers because 1123 # the offsets in our AATLookup are relative to our data 1124 # table, for which we need to provide an offset value itself. 1125 # It might have been possible to somehow make a kludge for 1126 # performing this indirect offset computation directly inside 1127 # OTTableWriter. But this would have made the internal logic 1128 # of OTTableWriter even more complex than it already is, 1129 # so we decided to roll our own offset computation for the 1130 # contents of the AATLookup and associated data table. 1131 offsetByGlyph, offsetByData, dataLen = {}, {}, 0 1132 compiledData = [] 1133 for glyph in sorted(value, key=font.getGlyphID): 1134 subWriter = OTTableWriter() 1135 value[glyph].compile(subWriter, font) 1136 data = subWriter.getAllData() 1137 offset = offsetByData.get(data, None) 1138 if offset == None: 1139 offset = dataLen 1140 dataLen = dataLen + len(data) 1141 offsetByData[data] = offset 1142 compiledData.append(data) 1143 offsetByGlyph[glyph] = offset 1144 # For calculating the offsets to our AATLookup and data table, 1145 # we can use the regular OTTableWriter infrastructure. 1146 lookupWriter = writer.getSubWriter() 1147 lookup = AATLookup("DataOffsets", None, None, UShort) 1148 lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None) 1149 1150 dataWriter = writer.getSubWriter() 1151 writer.writeSubTable(lookupWriter, offsetSize=4) 1152 writer.writeSubTable(dataWriter, offsetSize=4) 1153 for d in compiledData: 1154 dataWriter.writeData(d) 1155 1156 def xmlRead(self, attrs, content, font): 1157 lookup = AATLookup("DataOffsets", None, None, self.tableClass) 1158 return lookup.xmlRead(attrs, content, font) 1159 1160 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1161 lookup = AATLookup("DataOffsets", None, None, self.tableClass) 1162 lookup.xmlWrite(xmlWriter, font, value, name, attrs) 1163 1164 1165class MorxSubtableConverter(BaseConverter): 1166 _PROCESSING_ORDERS = { 1167 # bits 30 and 28 of morx.CoverageFlags; see morx spec 1168 (False, False): "LayoutOrder", 1169 (True, False): "ReversedLayoutOrder", 1170 (False, True): "LogicalOrder", 1171 (True, True): "ReversedLogicalOrder", 1172 } 1173 1174 _PROCESSING_ORDERS_REVERSED = {val: key for key, val in _PROCESSING_ORDERS.items()} 1175 1176 def __init__(self, name, repeat, aux, tableClass=None, *, description=""): 1177 BaseConverter.__init__( 1178 self, name, repeat, aux, tableClass, description=description 1179 ) 1180 1181 def _setTextDirectionFromCoverageFlags(self, flags, subtable): 1182 if (flags & 0x20) != 0: 1183 subtable.TextDirection = "Any" 1184 elif (flags & 0x80) != 0: 1185 subtable.TextDirection = "Vertical" 1186 else: 1187 subtable.TextDirection = "Horizontal" 1188 1189 def read(self, reader, font, tableDict): 1190 pos = reader.pos 1191 m = MorxSubtable() 1192 m.StructLength = reader.readULong() 1193 flags = reader.readUInt8() 1194 orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0) 1195 m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] 1196 self._setTextDirectionFromCoverageFlags(flags, m) 1197 m.Reserved = reader.readUShort() 1198 m.Reserved |= (flags & 0xF) << 16 1199 m.MorphType = reader.readUInt8() 1200 m.SubFeatureFlags = reader.readULong() 1201 tableClass = lookupTypes["morx"].get(m.MorphType) 1202 if tableClass is None: 1203 assert False, "unsupported 'morx' lookup type %s" % m.MorphType 1204 # To decode AAT ligatures, we need to know the subtable size. 1205 # The easiest way to pass this along is to create a new reader 1206 # that works on just the subtable as its data. 1207 headerLength = reader.pos - pos 1208 data = reader.data[reader.pos : reader.pos + m.StructLength - headerLength] 1209 assert len(data) == m.StructLength - headerLength 1210 subReader = OTTableReader(data=data, tableTag=reader.tableTag) 1211 m.SubStruct = tableClass() 1212 m.SubStruct.decompile(subReader, font) 1213 reader.seek(pos + m.StructLength) 1214 return m 1215 1216 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1217 xmlWriter.begintag(name, attrs) 1218 xmlWriter.newline() 1219 xmlWriter.comment("StructLength=%d" % value.StructLength) 1220 xmlWriter.newline() 1221 xmlWriter.simpletag("TextDirection", value=value.TextDirection) 1222 xmlWriter.newline() 1223 xmlWriter.simpletag("ProcessingOrder", value=value.ProcessingOrder) 1224 xmlWriter.newline() 1225 if value.Reserved != 0: 1226 xmlWriter.simpletag("Reserved", value="0x%04x" % value.Reserved) 1227 xmlWriter.newline() 1228 xmlWriter.comment("MorphType=%d" % value.MorphType) 1229 xmlWriter.newline() 1230 xmlWriter.simpletag("SubFeatureFlags", value="0x%08x" % value.SubFeatureFlags) 1231 xmlWriter.newline() 1232 value.SubStruct.toXML(xmlWriter, font) 1233 xmlWriter.endtag(name) 1234 xmlWriter.newline() 1235 1236 def xmlRead(self, attrs, content, font): 1237 m = MorxSubtable() 1238 covFlags = 0 1239 m.Reserved = 0 1240 for eltName, eltAttrs, eltContent in filter(istuple, content): 1241 if eltName == "CoverageFlags": 1242 # Only in XML from old versions of fonttools. 1243 covFlags = safeEval(eltAttrs["value"]) 1244 orderKey = ((covFlags & 0x40) != 0, (covFlags & 0x10) != 0) 1245 m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] 1246 self._setTextDirectionFromCoverageFlags(covFlags, m) 1247 elif eltName == "ProcessingOrder": 1248 m.ProcessingOrder = eltAttrs["value"] 1249 assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, ( 1250 "unknown ProcessingOrder: %s" % m.ProcessingOrder 1251 ) 1252 elif eltName == "TextDirection": 1253 m.TextDirection = eltAttrs["value"] 1254 assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, ( 1255 "unknown TextDirection %s" % m.TextDirection 1256 ) 1257 elif eltName == "Reserved": 1258 m.Reserved = safeEval(eltAttrs["value"]) 1259 elif eltName == "SubFeatureFlags": 1260 m.SubFeatureFlags = safeEval(eltAttrs["value"]) 1261 elif eltName.endswith("Morph"): 1262 m.fromXML(eltName, eltAttrs, eltContent, font) 1263 else: 1264 assert False, eltName 1265 m.Reserved = (covFlags & 0xF) << 16 | m.Reserved 1266 return m 1267 1268 def write(self, writer, font, tableDict, value, repeatIndex=None): 1269 covFlags = (value.Reserved & 0x000F0000) >> 16 1270 reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[ 1271 value.ProcessingOrder 1272 ] 1273 covFlags |= 0x80 if value.TextDirection == "Vertical" else 0 1274 covFlags |= 0x40 if reverseOrder else 0 1275 covFlags |= 0x20 if value.TextDirection == "Any" else 0 1276 covFlags |= 0x10 if logicalOrder else 0 1277 value.CoverageFlags = covFlags 1278 lengthIndex = len(writer.items) 1279 before = writer.getDataLength() 1280 value.StructLength = 0xDEADBEEF 1281 # The high nibble of value.Reserved is actuallly encoded 1282 # into coverageFlags, so we need to clear it here. 1283 origReserved = value.Reserved # including high nibble 1284 value.Reserved = value.Reserved & 0xFFFF # without high nibble 1285 value.compile(writer, font) 1286 value.Reserved = origReserved # restore original value 1287 assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef" 1288 length = writer.getDataLength() - before 1289 writer.items[lengthIndex] = struct.pack(">L", length) 1290 1291 1292# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader 1293# TODO: Untangle the implementation of the various lookup-specific formats. 1294class STXHeader(BaseConverter): 1295 def __init__(self, name, repeat, aux, tableClass, *, description=""): 1296 BaseConverter.__init__( 1297 self, name, repeat, aux, tableClass, description=description 1298 ) 1299 assert issubclass(self.tableClass, AATAction) 1300 self.classLookup = AATLookup("GlyphClasses", None, None, UShort) 1301 if issubclass(self.tableClass, ContextualMorphAction): 1302 self.perGlyphLookup = AATLookup("PerGlyphLookup", None, None, GlyphID) 1303 else: 1304 self.perGlyphLookup = None 1305 1306 def read(self, reader, font, tableDict): 1307 table = AATStateTable() 1308 pos = reader.pos 1309 classTableReader = reader.getSubReader(0) 1310 stateArrayReader = reader.getSubReader(0) 1311 entryTableReader = reader.getSubReader(0) 1312 actionReader = None 1313 ligaturesReader = None 1314 table.GlyphClassCount = reader.readULong() 1315 classTableReader.seek(pos + reader.readULong()) 1316 stateArrayReader.seek(pos + reader.readULong()) 1317 entryTableReader.seek(pos + reader.readULong()) 1318 if self.perGlyphLookup is not None: 1319 perGlyphTableReader = reader.getSubReader(0) 1320 perGlyphTableReader.seek(pos + reader.readULong()) 1321 if issubclass(self.tableClass, LigatureMorphAction): 1322 actionReader = reader.getSubReader(0) 1323 actionReader.seek(pos + reader.readULong()) 1324 ligComponentReader = reader.getSubReader(0) 1325 ligComponentReader.seek(pos + reader.readULong()) 1326 ligaturesReader = reader.getSubReader(0) 1327 ligaturesReader.seek(pos + reader.readULong()) 1328 numLigComponents = (ligaturesReader.pos - ligComponentReader.pos) // 2 1329 assert numLigComponents >= 0 1330 table.LigComponents = ligComponentReader.readUShortArray(numLigComponents) 1331 table.Ligatures = self._readLigatures(ligaturesReader, font) 1332 elif issubclass(self.tableClass, InsertionMorphAction): 1333 actionReader = reader.getSubReader(0) 1334 actionReader.seek(pos + reader.readULong()) 1335 table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict) 1336 numStates = int( 1337 (entryTableReader.pos - stateArrayReader.pos) / (table.GlyphClassCount * 2) 1338 ) 1339 for stateIndex in range(numStates): 1340 state = AATState() 1341 table.States.append(state) 1342 for glyphClass in range(table.GlyphClassCount): 1343 entryIndex = stateArrayReader.readUShort() 1344 state.Transitions[glyphClass] = self._readTransition( 1345 entryTableReader, entryIndex, font, actionReader 1346 ) 1347 if self.perGlyphLookup is not None: 1348 table.PerGlyphLookups = self._readPerGlyphLookups( 1349 table, perGlyphTableReader, font 1350 ) 1351 return table 1352 1353 def _readTransition(self, reader, entryIndex, font, actionReader): 1354 transition = self.tableClass() 1355 entryReader = reader.getSubReader( 1356 reader.pos + entryIndex * transition.staticSize 1357 ) 1358 transition.decompile(entryReader, font, actionReader) 1359 return transition 1360 1361 def _readLigatures(self, reader, font): 1362 limit = len(reader.data) 1363 numLigatureGlyphs = (limit - reader.pos) // 2 1364 return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs)) 1365 1366 def _countPerGlyphLookups(self, table): 1367 # Somewhat annoyingly, the morx table does not encode 1368 # the size of the per-glyph table. So we need to find 1369 # the maximum value that MorphActions use as index 1370 # into this table. 1371 numLookups = 0 1372 for state in table.States: 1373 for t in state.Transitions.values(): 1374 if isinstance(t, ContextualMorphAction): 1375 if t.MarkIndex != 0xFFFF: 1376 numLookups = max(numLookups, t.MarkIndex + 1) 1377 if t.CurrentIndex != 0xFFFF: 1378 numLookups = max(numLookups, t.CurrentIndex + 1) 1379 return numLookups 1380 1381 def _readPerGlyphLookups(self, table, reader, font): 1382 pos = reader.pos 1383 lookups = [] 1384 for _ in range(self._countPerGlyphLookups(table)): 1385 lookupReader = reader.getSubReader(0) 1386 lookupReader.seek(pos + reader.readULong()) 1387 lookups.append(self.perGlyphLookup.read(lookupReader, font, {})) 1388 return lookups 1389 1390 def write(self, writer, font, tableDict, value, repeatIndex=None): 1391 glyphClassWriter = OTTableWriter() 1392 self.classLookup.write( 1393 glyphClassWriter, font, tableDict, value.GlyphClasses, repeatIndex=None 1394 ) 1395 glyphClassData = pad(glyphClassWriter.getAllData(), 2) 1396 glyphClassCount = max(value.GlyphClasses.values()) + 1 1397 glyphClassTableOffset = 16 # size of STXHeader 1398 if self.perGlyphLookup is not None: 1399 glyphClassTableOffset += 4 1400 1401 glyphClassTableOffset += self.tableClass.actionHeaderSize 1402 actionData, actionIndex = self.tableClass.compileActions(font, value.States) 1403 stateArrayData, entryTableData = self._compileStates( 1404 font, value.States, glyphClassCount, actionIndex 1405 ) 1406 stateArrayOffset = glyphClassTableOffset + len(glyphClassData) 1407 entryTableOffset = stateArrayOffset + len(stateArrayData) 1408 perGlyphOffset = entryTableOffset + len(entryTableData) 1409 perGlyphData = pad(self._compilePerGlyphLookups(value, font), 4) 1410 if actionData is not None: 1411 actionOffset = entryTableOffset + len(entryTableData) 1412 else: 1413 actionOffset = None 1414 1415 ligaturesOffset, ligComponentsOffset = None, None 1416 ligComponentsData = self._compileLigComponents(value, font) 1417 ligaturesData = self._compileLigatures(value, font) 1418 if ligComponentsData is not None: 1419 assert len(perGlyphData) == 0 1420 ligComponentsOffset = actionOffset + len(actionData) 1421 ligaturesOffset = ligComponentsOffset + len(ligComponentsData) 1422 1423 writer.writeULong(glyphClassCount) 1424 writer.writeULong(glyphClassTableOffset) 1425 writer.writeULong(stateArrayOffset) 1426 writer.writeULong(entryTableOffset) 1427 if self.perGlyphLookup is not None: 1428 writer.writeULong(perGlyphOffset) 1429 if actionOffset is not None: 1430 writer.writeULong(actionOffset) 1431 if ligComponentsOffset is not None: 1432 writer.writeULong(ligComponentsOffset) 1433 writer.writeULong(ligaturesOffset) 1434 writer.writeData(glyphClassData) 1435 writer.writeData(stateArrayData) 1436 writer.writeData(entryTableData) 1437 writer.writeData(perGlyphData) 1438 if actionData is not None: 1439 writer.writeData(actionData) 1440 if ligComponentsData is not None: 1441 writer.writeData(ligComponentsData) 1442 if ligaturesData is not None: 1443 writer.writeData(ligaturesData) 1444 1445 def _compileStates(self, font, states, glyphClassCount, actionIndex): 1446 stateArrayWriter = OTTableWriter() 1447 entries, entryIDs = [], {} 1448 for state in states: 1449 for glyphClass in range(glyphClassCount): 1450 transition = state.Transitions[glyphClass] 1451 entryWriter = OTTableWriter() 1452 transition.compile(entryWriter, font, actionIndex) 1453 entryData = entryWriter.getAllData() 1454 assert ( 1455 len(entryData) == transition.staticSize 1456 ), "%s has staticSize %d, " "but actually wrote %d bytes" % ( 1457 repr(transition), 1458 transition.staticSize, 1459 len(entryData), 1460 ) 1461 entryIndex = entryIDs.get(entryData) 1462 if entryIndex is None: 1463 entryIndex = len(entries) 1464 entryIDs[entryData] = entryIndex 1465 entries.append(entryData) 1466 stateArrayWriter.writeUShort(entryIndex) 1467 stateArrayData = pad(stateArrayWriter.getAllData(), 4) 1468 entryTableData = pad(bytesjoin(entries), 4) 1469 return stateArrayData, entryTableData 1470 1471 def _compilePerGlyphLookups(self, table, font): 1472 if self.perGlyphLookup is None: 1473 return b"" 1474 numLookups = self._countPerGlyphLookups(table) 1475 assert len(table.PerGlyphLookups) == numLookups, ( 1476 "len(AATStateTable.PerGlyphLookups) is %d, " 1477 "but the actions inside the table refer to %d" 1478 % (len(table.PerGlyphLookups), numLookups) 1479 ) 1480 writer = OTTableWriter() 1481 for lookup in table.PerGlyphLookups: 1482 lookupWriter = writer.getSubWriter() 1483 self.perGlyphLookup.write(lookupWriter, font, {}, lookup, None) 1484 writer.writeSubTable(lookupWriter, offsetSize=4) 1485 return writer.getAllData() 1486 1487 def _compileLigComponents(self, table, font): 1488 if not hasattr(table, "LigComponents"): 1489 return None 1490 writer = OTTableWriter() 1491 for component in table.LigComponents: 1492 writer.writeUShort(component) 1493 return writer.getAllData() 1494 1495 def _compileLigatures(self, table, font): 1496 if not hasattr(table, "Ligatures"): 1497 return None 1498 writer = OTTableWriter() 1499 for glyphName in table.Ligatures: 1500 writer.writeUShort(font.getGlyphID(glyphName)) 1501 return writer.getAllData() 1502 1503 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1504 xmlWriter.begintag(name, attrs) 1505 xmlWriter.newline() 1506 xmlWriter.comment("GlyphClassCount=%s" % value.GlyphClassCount) 1507 xmlWriter.newline() 1508 for g, klass in sorted(value.GlyphClasses.items()): 1509 xmlWriter.simpletag("GlyphClass", glyph=g, value=klass) 1510 xmlWriter.newline() 1511 for stateIndex, state in enumerate(value.States): 1512 xmlWriter.begintag("State", index=stateIndex) 1513 xmlWriter.newline() 1514 for glyphClass, trans in sorted(state.Transitions.items()): 1515 trans.toXML( 1516 xmlWriter, 1517 font=font, 1518 attrs={"onGlyphClass": glyphClass}, 1519 name="Transition", 1520 ) 1521 xmlWriter.endtag("State") 1522 xmlWriter.newline() 1523 for i, lookup in enumerate(value.PerGlyphLookups): 1524 xmlWriter.begintag("PerGlyphLookup", index=i) 1525 xmlWriter.newline() 1526 for glyph, val in sorted(lookup.items()): 1527 xmlWriter.simpletag("Lookup", glyph=glyph, value=val) 1528 xmlWriter.newline() 1529 xmlWriter.endtag("PerGlyphLookup") 1530 xmlWriter.newline() 1531 if hasattr(value, "LigComponents"): 1532 xmlWriter.begintag("LigComponents") 1533 xmlWriter.newline() 1534 for i, val in enumerate(getattr(value, "LigComponents")): 1535 xmlWriter.simpletag("LigComponent", index=i, value=val) 1536 xmlWriter.newline() 1537 xmlWriter.endtag("LigComponents") 1538 xmlWriter.newline() 1539 self._xmlWriteLigatures(xmlWriter, font, value, name, attrs) 1540 xmlWriter.endtag(name) 1541 xmlWriter.newline() 1542 1543 def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs): 1544 if not hasattr(value, "Ligatures"): 1545 return 1546 xmlWriter.begintag("Ligatures") 1547 xmlWriter.newline() 1548 for i, g in enumerate(getattr(value, "Ligatures")): 1549 xmlWriter.simpletag("Ligature", index=i, glyph=g) 1550 xmlWriter.newline() 1551 xmlWriter.endtag("Ligatures") 1552 xmlWriter.newline() 1553 1554 def xmlRead(self, attrs, content, font): 1555 table = AATStateTable() 1556 for eltName, eltAttrs, eltContent in filter(istuple, content): 1557 if eltName == "GlyphClass": 1558 glyph = eltAttrs["glyph"] 1559 value = eltAttrs["value"] 1560 table.GlyphClasses[glyph] = safeEval(value) 1561 elif eltName == "State": 1562 state = self._xmlReadState(eltAttrs, eltContent, font) 1563 table.States.append(state) 1564 elif eltName == "PerGlyphLookup": 1565 lookup = self.perGlyphLookup.xmlRead(eltAttrs, eltContent, font) 1566 table.PerGlyphLookups.append(lookup) 1567 elif eltName == "LigComponents": 1568 table.LigComponents = self._xmlReadLigComponents( 1569 eltAttrs, eltContent, font 1570 ) 1571 elif eltName == "Ligatures": 1572 table.Ligatures = self._xmlReadLigatures(eltAttrs, eltContent, font) 1573 table.GlyphClassCount = max(table.GlyphClasses.values()) + 1 1574 return table 1575 1576 def _xmlReadState(self, attrs, content, font): 1577 state = AATState() 1578 for eltName, eltAttrs, eltContent in filter(istuple, content): 1579 if eltName == "Transition": 1580 glyphClass = safeEval(eltAttrs["onGlyphClass"]) 1581 transition = self.tableClass() 1582 transition.fromXML(eltName, eltAttrs, eltContent, font) 1583 state.Transitions[glyphClass] = transition 1584 return state 1585 1586 def _xmlReadLigComponents(self, attrs, content, font): 1587 ligComponents = [] 1588 for eltName, eltAttrs, _eltContent in filter(istuple, content): 1589 if eltName == "LigComponent": 1590 ligComponents.append(safeEval(eltAttrs["value"])) 1591 return ligComponents 1592 1593 def _xmlReadLigatures(self, attrs, content, font): 1594 ligs = [] 1595 for eltName, eltAttrs, _eltContent in filter(istuple, content): 1596 if eltName == "Ligature": 1597 ligs.append(eltAttrs["glyph"]) 1598 return ligs 1599 1600 1601class CIDGlyphMap(BaseConverter): 1602 def read(self, reader, font, tableDict): 1603 numCIDs = reader.readUShort() 1604 result = {} 1605 for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): 1606 if glyphID != 0xFFFF: 1607 result[cid] = font.getGlyphName(glyphID) 1608 return result 1609 1610 def write(self, writer, font, tableDict, value, repeatIndex=None): 1611 items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()} 1612 count = max(items) + 1 if items else 0 1613 writer.writeUShort(count) 1614 for cid in range(count): 1615 writer.writeUShort(items.get(cid, 0xFFFF)) 1616 1617 def xmlRead(self, attrs, content, font): 1618 result = {} 1619 for eName, eAttrs, _eContent in filter(istuple, content): 1620 if eName == "CID": 1621 result[safeEval(eAttrs["cid"])] = eAttrs["glyph"].strip() 1622 return result 1623 1624 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1625 xmlWriter.begintag(name, attrs) 1626 xmlWriter.newline() 1627 for cid, glyph in sorted(value.items()): 1628 if glyph is not None and glyph != 0xFFFF: 1629 xmlWriter.simpletag("CID", cid=cid, glyph=glyph) 1630 xmlWriter.newline() 1631 xmlWriter.endtag(name) 1632 xmlWriter.newline() 1633 1634 1635class GlyphCIDMap(BaseConverter): 1636 def read(self, reader, font, tableDict): 1637 glyphOrder = font.getGlyphOrder() 1638 count = reader.readUShort() 1639 cids = reader.readUShortArray(count) 1640 if count > len(glyphOrder): 1641 log.warning( 1642 "GlyphCIDMap has %d elements, " 1643 "but the font has only %d glyphs; " 1644 "ignoring the rest" % (count, len(glyphOrder)) 1645 ) 1646 result = {} 1647 for glyphID in range(min(len(cids), len(glyphOrder))): 1648 cid = cids[glyphID] 1649 if cid != 0xFFFF: 1650 result[glyphOrder[glyphID]] = cid 1651 return result 1652 1653 def write(self, writer, font, tableDict, value, repeatIndex=None): 1654 items = { 1655 font.getGlyphID(g): cid 1656 for g, cid in value.items() 1657 if cid is not None and cid != 0xFFFF 1658 } 1659 count = max(items) + 1 if items else 0 1660 writer.writeUShort(count) 1661 for glyphID in range(count): 1662 writer.writeUShort(items.get(glyphID, 0xFFFF)) 1663 1664 def xmlRead(self, attrs, content, font): 1665 result = {} 1666 for eName, eAttrs, _eContent in filter(istuple, content): 1667 if eName == "CID": 1668 result[eAttrs["glyph"]] = safeEval(eAttrs["value"]) 1669 return result 1670 1671 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1672 xmlWriter.begintag(name, attrs) 1673 xmlWriter.newline() 1674 for glyph, cid in sorted(value.items()): 1675 if cid is not None and cid != 0xFFFF: 1676 xmlWriter.simpletag("CID", glyph=glyph, value=cid) 1677 xmlWriter.newline() 1678 xmlWriter.endtag(name) 1679 xmlWriter.newline() 1680 1681 1682class DeltaValue(BaseConverter): 1683 def read(self, reader, font, tableDict): 1684 StartSize = tableDict["StartSize"] 1685 EndSize = tableDict["EndSize"] 1686 DeltaFormat = tableDict["DeltaFormat"] 1687 assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" 1688 nItems = EndSize - StartSize + 1 1689 nBits = 1 << DeltaFormat 1690 minusOffset = 1 << nBits 1691 mask = (1 << nBits) - 1 1692 signMask = 1 << (nBits - 1) 1693 1694 DeltaValue = [] 1695 tmp, shift = 0, 0 1696 for i in range(nItems): 1697 if shift == 0: 1698 tmp, shift = reader.readUShort(), 16 1699 shift = shift - nBits 1700 value = (tmp >> shift) & mask 1701 if value & signMask: 1702 value = value - minusOffset 1703 DeltaValue.append(value) 1704 return DeltaValue 1705 1706 def write(self, writer, font, tableDict, value, repeatIndex=None): 1707 StartSize = tableDict["StartSize"] 1708 EndSize = tableDict["EndSize"] 1709 DeltaFormat = tableDict["DeltaFormat"] 1710 DeltaValue = value 1711 assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" 1712 nItems = EndSize - StartSize + 1 1713 nBits = 1 << DeltaFormat 1714 assert len(DeltaValue) == nItems 1715 mask = (1 << nBits) - 1 1716 1717 tmp, shift = 0, 16 1718 for value in DeltaValue: 1719 shift = shift - nBits 1720 tmp = tmp | ((value & mask) << shift) 1721 if shift == 0: 1722 writer.writeUShort(tmp) 1723 tmp, shift = 0, 16 1724 if shift != 16: 1725 writer.writeUShort(tmp) 1726 1727 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1728 xmlWriter.simpletag(name, attrs + [("value", value)]) 1729 xmlWriter.newline() 1730 1731 def xmlRead(self, attrs, content, font): 1732 return safeEval(attrs["value"]) 1733 1734 1735class VarIdxMapValue(BaseConverter): 1736 def read(self, reader, font, tableDict): 1737 fmt = tableDict["EntryFormat"] 1738 nItems = tableDict["MappingCount"] 1739 1740 innerBits = 1 + (fmt & 0x000F) 1741 innerMask = (1 << innerBits) - 1 1742 outerMask = 0xFFFFFFFF - innerMask 1743 outerShift = 16 - innerBits 1744 1745 entrySize = 1 + ((fmt & 0x0030) >> 4) 1746 readArray = { 1747 1: reader.readUInt8Array, 1748 2: reader.readUShortArray, 1749 3: reader.readUInt24Array, 1750 4: reader.readULongArray, 1751 }[entrySize] 1752 1753 return [ 1754 (((raw & outerMask) << outerShift) | (raw & innerMask)) 1755 for raw in readArray(nItems) 1756 ] 1757 1758 def write(self, writer, font, tableDict, value, repeatIndex=None): 1759 fmt = tableDict["EntryFormat"] 1760 mapping = value 1761 writer["MappingCount"].setValue(len(mapping)) 1762 1763 innerBits = 1 + (fmt & 0x000F) 1764 innerMask = (1 << innerBits) - 1 1765 outerShift = 16 - innerBits 1766 1767 entrySize = 1 + ((fmt & 0x0030) >> 4) 1768 writeArray = { 1769 1: writer.writeUInt8Array, 1770 2: writer.writeUShortArray, 1771 3: writer.writeUInt24Array, 1772 4: writer.writeULongArray, 1773 }[entrySize] 1774 1775 writeArray( 1776 [ 1777 (((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)) 1778 for idx in mapping 1779 ] 1780 ) 1781 1782 1783class VarDataValue(BaseConverter): 1784 def read(self, reader, font, tableDict): 1785 values = [] 1786 1787 regionCount = tableDict["VarRegionCount"] 1788 wordCount = tableDict["NumShorts"] 1789 1790 # https://github.com/fonttools/fonttools/issues/2279 1791 longWords = bool(wordCount & 0x8000) 1792 wordCount = wordCount & 0x7FFF 1793 1794 if longWords: 1795 readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray 1796 else: 1797 readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array 1798 1799 n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) 1800 values.extend(readBigArray(n1)) 1801 values.extend(readSmallArray(n2 - n1)) 1802 if n2 > regionCount: # Padding 1803 del values[regionCount:] 1804 1805 return values 1806 1807 def write(self, writer, font, tableDict, values, repeatIndex=None): 1808 regionCount = tableDict["VarRegionCount"] 1809 wordCount = tableDict["NumShorts"] 1810 1811 # https://github.com/fonttools/fonttools/issues/2279 1812 longWords = bool(wordCount & 0x8000) 1813 wordCount = wordCount & 0x7FFF 1814 1815 (writeBigArray, writeSmallArray) = { 1816 False: (writer.writeShortArray, writer.writeInt8Array), 1817 True: (writer.writeLongArray, writer.writeShortArray), 1818 }[longWords] 1819 1820 n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) 1821 writeBigArray(values[:n1]) 1822 writeSmallArray(values[n1:regionCount]) 1823 if n2 > regionCount: # Padding 1824 writer.writeSmallArray([0] * (n2 - regionCount)) 1825 1826 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1827 xmlWriter.simpletag(name, attrs + [("value", value)]) 1828 xmlWriter.newline() 1829 1830 def xmlRead(self, attrs, content, font): 1831 return safeEval(attrs["value"]) 1832 1833 1834class LookupFlag(UShort): 1835 def xmlWrite(self, xmlWriter, font, value, name, attrs): 1836 xmlWriter.simpletag(name, attrs + [("value", value)]) 1837 flags = [] 1838 if value & 0x01: 1839 flags.append("rightToLeft") 1840 if value & 0x02: 1841 flags.append("ignoreBaseGlyphs") 1842 if value & 0x04: 1843 flags.append("ignoreLigatures") 1844 if value & 0x08: 1845 flags.append("ignoreMarks") 1846 if value & 0x10: 1847 flags.append("useMarkFilteringSet") 1848 if value & 0xFF00: 1849 flags.append("markAttachmentType[%i]" % (value >> 8)) 1850 if flags: 1851 xmlWriter.comment(" ".join(flags)) 1852 xmlWriter.newline() 1853 1854 1855class _UInt8Enum(UInt8): 1856 enumClass = NotImplemented 1857 1858 def read(self, reader, font, tableDict): 1859 return self.enumClass(super().read(reader, font, tableDict)) 1860 1861 @classmethod 1862 def fromString(cls, value): 1863 return getattr(cls.enumClass, value.upper()) 1864 1865 @classmethod 1866 def toString(cls, value): 1867 return cls.enumClass(value).name.lower() 1868 1869 1870class ExtendMode(_UInt8Enum): 1871 enumClass = _ExtendMode 1872 1873 1874class CompositeMode(_UInt8Enum): 1875 enumClass = _CompositeMode 1876 1877 1878converterMapping = { 1879 # type class 1880 "int8": Int8, 1881 "int16": Short, 1882 "uint8": UInt8, 1883 "uint16": UShort, 1884 "uint24": UInt24, 1885 "uint32": ULong, 1886 "char64": Char64, 1887 "Flags32": Flags32, 1888 "VarIndex": VarIndex, 1889 "Version": Version, 1890 "Tag": Tag, 1891 "GlyphID": GlyphID, 1892 "GlyphID32": GlyphID32, 1893 "NameID": NameID, 1894 "DeciPoints": DeciPoints, 1895 "Fixed": Fixed, 1896 "F2Dot14": F2Dot14, 1897 "Angle": Angle, 1898 "BiasedAngle": BiasedAngle, 1899 "struct": Struct, 1900 "Offset": Table, 1901 "LOffset": LTable, 1902 "Offset24": Table24, 1903 "ValueRecord": ValueRecord, 1904 "DeltaValue": DeltaValue, 1905 "VarIdxMapValue": VarIdxMapValue, 1906 "VarDataValue": VarDataValue, 1907 "LookupFlag": LookupFlag, 1908 "ExtendMode": ExtendMode, 1909 "CompositeMode": CompositeMode, 1910 "STATFlags": STATFlags, 1911 # AAT 1912 "CIDGlyphMap": CIDGlyphMap, 1913 "GlyphCIDMap": GlyphCIDMap, 1914 "MortChain": StructWithLength, 1915 "MortSubtable": StructWithLength, 1916 "MorxChain": StructWithLength, 1917 "MorxSubtable": MorxSubtableConverter, 1918 # "Template" types 1919 "AATLookup": lambda C: partial(AATLookup, tableClass=C), 1920 "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C), 1921 "STXHeader": lambda C: partial(STXHeader, tableClass=C), 1922 "OffsetTo": lambda C: partial(Table, tableClass=C), 1923 "LOffsetTo": lambda C: partial(LTable, tableClass=C), 1924 "LOffset24To": lambda C: partial(Table24, tableClass=C), 1925} 1926