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