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