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