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