diff options
Diffstat (limited to 'Lib/fontTools/ttLib/tables/otConverters.py')
-rw-r--r-- | Lib/fontTools/ttLib/tables/otConverters.py | 3339 |
1 files changed, 1727 insertions, 1612 deletions
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index b08f1f19..390f1660 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1,22 +1,34 @@ from fontTools.misc.fixedTools import ( - fixedToFloat as fi2fl, - floatToFixed as fl2fi, - floatToFixedToStr as fl2str, - strToFixedToFloat as str2fl, - ensureVersionIsLong as fi2ve, - versionToFixed as ve2fi, + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + floatToFixedToStr as fl2str, + strToFixedToFloat as str2fl, + ensureVersionIsLong as fi2ve, + versionToFixed as ve2fi, ) from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval from fontTools.ttLib import getSearchRange -from .otBase import (CountReference, FormatSwitchingBaseTable, - OTTableReader, OTTableWriter, ValueRecordFactory) -from .otTables import (lookupTypes, AATStateTable, AATState, AATAction, - ContextualMorphAction, LigatureMorphAction, - InsertionMorphAction, MorxSubtable, - ExtendMode as _ExtendMode, - CompositeMode as _CompositeMode, - NO_VARIATION_INDEX) +from .otBase import ( + CountReference, + FormatSwitchingBaseTable, + OTTableReader, + OTTableWriter, + ValueRecordFactory, +) +from .otTables import ( + lookupTypes, + AATStateTable, + AATState, + AATAction, + ContextualMorphAction, + LigatureMorphAction, + InsertionMorphAction, + MorxSubtable, + ExtendMode as _ExtendMode, + CompositeMode as _CompositeMode, + NO_VARIATION_INDEX, +) from itertools import zip_longest from functools import partial import re @@ -30,947 +42,1059 @@ istuple = lambda t: isinstance(t, tuple) def buildConverters(tableSpec, tableNamespace): - """Given a table spec from otData.py, build a converter object for each - field of the table. This is called for each table in otData.py, and - the results are assigned to the corresponding class in otTables.py.""" - converters = [] - convertersByName = {} - for tp, name, repeat, aux, descr in tableSpec: - tableName = name - if name.startswith("ValueFormat"): - assert tp == "uint16" - converterClass = ValueFormat - elif name.endswith("Count") or name in ("StructLength", "MorphType"): - converterClass = { - "uint8": ComputedUInt8, - "uint16": ComputedUShort, - "uint32": ComputedULong, - }[tp] - elif name == "SubTable": - converterClass = SubTable - elif name == "ExtSubTable": - converterClass = ExtSubTable - elif name == "SubStruct": - converterClass = SubStruct - elif name == "FeatureParams": - converterClass = FeatureParams - elif name in ("CIDGlyphMapping", "GlyphCIDMapping"): - converterClass = StructWithLength - else: - if not tp in converterMapping and '(' not in tp: - tableName = tp - converterClass = Struct - else: - converterClass = eval(tp, tableNamespace, converterMapping) - - conv = converterClass(name, repeat, aux, description=descr) - - if conv.tableClass: - # A "template" such as OffsetTo(AType) knowss the table class already - tableClass = conv.tableClass - elif tp in ('MortChain', 'MortSubtable', 'MorxChain'): - tableClass = tableNamespace.get(tp) - else: - tableClass = tableNamespace.get(tableName) - - if not conv.tableClass: - conv.tableClass = tableClass - - if name in ["SubTable", "ExtSubTable", "SubStruct"]: - conv.lookupTypes = tableNamespace['lookupTypes'] - # also create reverse mapping - for t in conv.lookupTypes.values(): - for cls in t.values(): - convertersByName[cls.__name__] = Table(name, repeat, aux, cls) - if name == "FeatureParams": - conv.featureParamTypes = tableNamespace['featureParamTypes'] - conv.defaultFeatureParams = tableNamespace['FeatureParams'] - for cls in conv.featureParamTypes.values(): - convertersByName[cls.__name__] = Table(name, repeat, aux, cls) - converters.append(conv) - assert name not in convertersByName, name - convertersByName[name] = conv - return converters, convertersByName + """Given a table spec from otData.py, build a converter object for each + field of the table. This is called for each table in otData.py, and + the results are assigned to the corresponding class in otTables.py.""" + converters = [] + convertersByName = {} + for tp, name, repeat, aux, descr in tableSpec: + tableName = name + if name.startswith("ValueFormat"): + assert tp == "uint16" + converterClass = ValueFormat + elif name.endswith("Count") or name in ("StructLength", "MorphType"): + converterClass = { + "uint8": ComputedUInt8, + "uint16": ComputedUShort, + "uint32": ComputedULong, + }[tp] + elif name == "SubTable": + converterClass = SubTable + elif name == "ExtSubTable": + converterClass = ExtSubTable + elif name == "SubStruct": + converterClass = SubStruct + elif name == "FeatureParams": + converterClass = FeatureParams + elif name in ("CIDGlyphMapping", "GlyphCIDMapping"): + converterClass = StructWithLength + else: + if not tp in converterMapping and "(" not in tp: + tableName = tp + converterClass = Struct + else: + converterClass = eval(tp, tableNamespace, converterMapping) + + conv = converterClass(name, repeat, aux, description=descr) + + if conv.tableClass: + # A "template" such as OffsetTo(AType) knowss the table class already + tableClass = conv.tableClass + elif tp in ("MortChain", "MortSubtable", "MorxChain"): + tableClass = tableNamespace.get(tp) + else: + tableClass = tableNamespace.get(tableName) + + if not conv.tableClass: + conv.tableClass = tableClass + + if name in ["SubTable", "ExtSubTable", "SubStruct"]: + conv.lookupTypes = tableNamespace["lookupTypes"] + # also create reverse mapping + for t in conv.lookupTypes.values(): + for cls in t.values(): + convertersByName[cls.__name__] = Table(name, repeat, aux, cls) + if name == "FeatureParams": + conv.featureParamTypes = tableNamespace["featureParamTypes"] + conv.defaultFeatureParams = tableNamespace["FeatureParams"] + for cls in conv.featureParamTypes.values(): + convertersByName[cls.__name__] = Table(name, repeat, aux, cls) + converters.append(conv) + assert name not in convertersByName, name + convertersByName[name] = conv + return converters, convertersByName class _MissingItem(tuple): - __slots__ = () + __slots__ = () try: - from collections import UserList + from collections import UserList except ImportError: - from UserList import UserList + from UserList import UserList class _LazyList(UserList): - - def __getslice__(self, i, j): - return self.__getitem__(slice(i, j)) - - def __getitem__(self, k): - if isinstance(k, slice): - indices = range(*k.indices(len(self))) - return [self[i] for i in indices] - item = self.data[k] - if isinstance(item, _MissingItem): - self.reader.seek(self.pos + item[0] * self.recordSize) - item = self.conv.read(self.reader, self.font, {}) - self.data[k] = item - return item - - def __add__(self, other): - if isinstance(other, _LazyList): - other = list(other) - elif isinstance(other, list): - pass - else: - return NotImplemented - return list(self) + other - - def __radd__(self, other): - if not isinstance(other, list): - return NotImplemented - return other + list(self) + def __getslice__(self, i, j): + return self.__getitem__(slice(i, j)) + + def __getitem__(self, k): + if isinstance(k, slice): + indices = range(*k.indices(len(self))) + return [self[i] for i in indices] + item = self.data[k] + if isinstance(item, _MissingItem): + self.reader.seek(self.pos + item[0] * self.recordSize) + item = self.conv.read(self.reader, self.font, {}) + self.data[k] = item + return item + + def __add__(self, other): + if isinstance(other, _LazyList): + other = list(other) + elif isinstance(other, list): + pass + else: + return NotImplemented + return list(self) + other + + def __radd__(self, other): + if not isinstance(other, list): + return NotImplemented + return other + list(self) class BaseConverter(object): - """Base class for converter objects. Apart from the constructor, this - is an abstract class.""" - - def __init__(self, name, repeat, aux, tableClass=None, *, description=""): - self.name = name - self.repeat = repeat - self.aux = aux - self.tableClass = tableClass - self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize'] - self.isLookupType = name.endswith("LookupType") or name == "MorphType" - self.isPropagated = name in [ - "ClassCount", - "Class2Count", - "FeatureTag", - "SettingsCount", - "VarRegionCount", - "MappingCount", - "RegionAxisCount", - "DesignAxisCount", - "DesignAxisRecordSize", - "AxisValueCount", - "ValueRecordSize", - "AxisCount", - "BaseGlyphRecordCount", - "LayerRecordCount", - ] - self.description = description - - def readArray(self, reader, font, tableDict, count): - """Read an array of values from the reader.""" - lazy = font.lazy and count > 8 - if lazy: - recordSize = self.getRecordSize(reader) - if recordSize is NotImplemented: - lazy = False - if not lazy: - l = [] - for i in range(count): - l.append(self.read(reader, font, tableDict)) - return l - else: - l = _LazyList() - l.reader = reader.copy() - l.pos = l.reader.pos - l.font = font - l.conv = self - l.recordSize = recordSize - l.extend(_MissingItem([i]) for i in range(count)) - reader.advance(count * recordSize) - return l - - def getRecordSize(self, reader): - if hasattr(self, 'staticSize'): return self.staticSize - return NotImplemented - - def read(self, reader, font, tableDict): - """Read a value from the reader.""" - raise NotImplementedError(self) - - def writeArray(self, writer, font, tableDict, values): - try: - for i, value in enumerate(values): - self.write(writer, font, tableDict, value, i) - except Exception as e: - e.args = e.args + (i,) - raise - - def write(self, writer, font, tableDict, value, repeatIndex=None): - """Write a value to the writer.""" - raise NotImplementedError(self) - - def xmlRead(self, attrs, content, font): - """Read a value from XML.""" - raise NotImplementedError(self) - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - """Write a value to XML.""" - raise NotImplementedError(self) - - varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)") - - def getVarIndexOffset(self) -> Optional[int]: - """If description has `VarIndexBase + {offset}`, return the offset else None.""" - m = self.varIndexBasePlusOffsetRE.search(self.description) - if not m: - return None - return int(m.group(1)) + """Base class for converter objects. Apart from the constructor, this + is an abstract class.""" + + def __init__(self, name, repeat, aux, tableClass=None, *, description=""): + self.name = name + self.repeat = repeat + self.aux = aux + self.tableClass = tableClass + self.isCount = name.endswith("Count") or name in [ + "DesignAxisRecordSize", + "ValueRecordSize", + ] + self.isLookupType = name.endswith("LookupType") or name == "MorphType" + self.isPropagated = name in [ + "ClassCount", + "Class2Count", + "FeatureTag", + "SettingsCount", + "VarRegionCount", + "MappingCount", + "RegionAxisCount", + "DesignAxisCount", + "DesignAxisRecordSize", + "AxisValueCount", + "ValueRecordSize", + "AxisCount", + "BaseGlyphRecordCount", + "LayerRecordCount", + ] + self.description = description + + def readArray(self, reader, font, tableDict, count): + """Read an array of values from the reader.""" + lazy = font.lazy and count > 8 + if lazy: + recordSize = self.getRecordSize(reader) + if recordSize is NotImplemented: + lazy = False + if not lazy: + l = [] + for i in range(count): + l.append(self.read(reader, font, tableDict)) + return l + else: + l = _LazyList() + l.reader = reader.copy() + l.pos = l.reader.pos + l.font = font + l.conv = self + l.recordSize = recordSize + l.extend(_MissingItem([i]) for i in range(count)) + reader.advance(count * recordSize) + return l + + def getRecordSize(self, reader): + if hasattr(self, "staticSize"): + return self.staticSize + return NotImplemented + + def read(self, reader, font, tableDict): + """Read a value from the reader.""" + raise NotImplementedError(self) + + def writeArray(self, writer, font, tableDict, values): + try: + for i, value in enumerate(values): + self.write(writer, font, tableDict, value, i) + except Exception as e: + e.args = e.args + (i,) + raise + + def write(self, writer, font, tableDict, value, repeatIndex=None): + """Write a value to the writer.""" + raise NotImplementedError(self) + + def xmlRead(self, attrs, content, font): + """Read a value from XML.""" + raise NotImplementedError(self) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + """Write a value to XML.""" + raise NotImplementedError(self) + + varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)") + + def getVarIndexOffset(self) -> Optional[int]: + """If description has `VarIndexBase + {offset}`, return the offset else None.""" + m = self.varIndexBasePlusOffsetRE.search(self.description) + if not m: + return None + return int(m.group(1)) class SimpleValue(BaseConverter): - @staticmethod - def toString(value): - return value - @staticmethod - def fromString(value): - return value - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", self.toString(value))]) - xmlWriter.newline() - def xmlRead(self, attrs, content, font): - return self.fromString(attrs["value"]) + @staticmethod + def toString(value): + return value + + @staticmethod + def fromString(value): + return value + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", self.toString(value))]) + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + return self.fromString(attrs["value"]) + class OptionalValue(SimpleValue): - DEFAULT = None - def xmlWrite(self, xmlWriter, font, value, name, attrs): - if value != self.DEFAULT: - attrs.append(("value", self.toString(value))) - xmlWriter.simpletag(name, attrs) - xmlWriter.newline() - def xmlRead(self, attrs, content, font): - if "value" in attrs: - return self.fromString(attrs["value"]) - return self.DEFAULT + DEFAULT = None + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if value != self.DEFAULT: + attrs.append(("value", self.toString(value))) + xmlWriter.simpletag(name, attrs) + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + if "value" in attrs: + return self.fromString(attrs["value"]) + return self.DEFAULT + class IntValue(SimpleValue): - @staticmethod - def fromString(value): - return int(value, 0) + @staticmethod + def fromString(value): + return int(value, 0) + class Long(IntValue): - staticSize = 4 - def read(self, reader, font, tableDict): - return reader.readLong() - def readArray(self, reader, font, tableDict, count): - return reader.readLongArray(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeLong(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeLongArray(values) + staticSize = 4 + + def read(self, reader, font, tableDict): + return reader.readLong() + + def readArray(self, reader, font, tableDict, count): + return reader.readLongArray(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeLong(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeLongArray(values) + class ULong(IntValue): - staticSize = 4 - def read(self, reader, font, tableDict): - return reader.readULong() - def readArray(self, reader, font, tableDict, count): - return reader.readULongArray(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeULong(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeULongArray(values) + staticSize = 4 + + def read(self, reader, font, tableDict): + return reader.readULong() + + def readArray(self, reader, font, tableDict, count): + return reader.readULongArray(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeULong(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeULongArray(values) + class Flags32(ULong): - @staticmethod - def toString(value): - return "0x%08X" % value + @staticmethod + def toString(value): + return "0x%08X" % value + class VarIndex(OptionalValue, ULong): - DEFAULT = NO_VARIATION_INDEX + DEFAULT = NO_VARIATION_INDEX + class Short(IntValue): - staticSize = 2 - def read(self, reader, font, tableDict): - return reader.readShort() - def readArray(self, reader, font, tableDict, count): - return reader.readShortArray(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeShort(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeShortArray(values) + staticSize = 2 + + def read(self, reader, font, tableDict): + return reader.readShort() + + def readArray(self, reader, font, tableDict, count): + return reader.readShortArray(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeShort(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeShortArray(values) + class UShort(IntValue): - staticSize = 2 - def read(self, reader, font, tableDict): - return reader.readUShort() - def readArray(self, reader, font, tableDict, count): - return reader.readUShortArray(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeUShort(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeUShortArray(values) + staticSize = 2 + + def read(self, reader, font, tableDict): + return reader.readUShort() + + def readArray(self, reader, font, tableDict, count): + return reader.readUShortArray(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeUShort(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeUShortArray(values) + class Int8(IntValue): - staticSize = 1 - def read(self, reader, font, tableDict): - return reader.readInt8() - def readArray(self, reader, font, tableDict, count): - return reader.readInt8Array(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeInt8(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeInt8Array(values) + staticSize = 1 + + def read(self, reader, font, tableDict): + return reader.readInt8() + + def readArray(self, reader, font, tableDict, count): + return reader.readInt8Array(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeInt8(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeInt8Array(values) + class UInt8(IntValue): - staticSize = 1 - def read(self, reader, font, tableDict): - return reader.readUInt8() - def readArray(self, reader, font, tableDict, count): - return reader.readUInt8Array(count) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeUInt8(value) - def writeArray(self, writer, font, tableDict, values): - writer.writeUInt8Array(values) + staticSize = 1 + + def read(self, reader, font, tableDict): + return reader.readUInt8() + + def readArray(self, reader, font, tableDict, count): + return reader.readUInt8Array(count) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeUInt8(value) + + def writeArray(self, writer, font, tableDict, values): + writer.writeUInt8Array(values) + class UInt24(IntValue): - staticSize = 3 - def read(self, reader, font, tableDict): - return reader.readUInt24() - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeUInt24(value) + staticSize = 3 + + def read(self, reader, font, tableDict): + return reader.readUInt24() + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeUInt24(value) + class ComputedInt(IntValue): - def xmlWrite(self, xmlWriter, font, value, name, attrs): - if value is not None: - xmlWriter.comment("%s=%s" % (name, value)) - xmlWriter.newline() + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if value is not None: + xmlWriter.comment("%s=%s" % (name, value)) + xmlWriter.newline() + class ComputedUInt8(ComputedInt, UInt8): - pass + pass + + class ComputedUShort(ComputedInt, UShort): - pass + pass + + class ComputedULong(ComputedInt, ULong): - pass + pass + class Tag(SimpleValue): - staticSize = 4 - def read(self, reader, font, tableDict): - return reader.readTag() - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeTag(value) + staticSize = 4 + + def read(self, reader, font, tableDict): + return reader.readTag() + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeTag(value) + class GlyphID(SimpleValue): - staticSize = 2 - typecode = "H" - def readArray(self, reader, font, tableDict, count): - return font.getGlyphNameMany(reader.readArray(self.typecode, self.staticSize, count)) - def read(self, reader, font, tableDict): - return font.getGlyphName(reader.readValue(self.typecode, self.staticSize)) - def writeArray(self, writer, font, tableDict, values): - writer.writeArray(self.typecode, font.getGlyphIDMany(values)) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeValue(self.typecode, font.getGlyphID(value)) + staticSize = 2 + typecode = "H" + + def readArray(self, reader, font, tableDict, count): + return font.getGlyphNameMany( + reader.readArray(self.typecode, self.staticSize, count) + ) + + def read(self, reader, font, tableDict): + return font.getGlyphName(reader.readValue(self.typecode, self.staticSize)) + + def writeArray(self, writer, font, tableDict, values): + writer.writeArray(self.typecode, font.getGlyphIDMany(values)) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeValue(self.typecode, font.getGlyphID(value)) class GlyphID32(GlyphID): - staticSize = 4 - typecode = "L" + staticSize = 4 + typecode = "L" class NameID(UShort): - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", value)]) - if font and value: - nameTable = font.get("name") - if nameTable: - name = nameTable.getDebugName(value) - xmlWriter.write(" ") - if name: - xmlWriter.comment(name) - else: - xmlWriter.comment("missing from name table") - log.warning("name id %d missing from name table" % value) - xmlWriter.newline() + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + if font and value: + nameTable = font.get("name") + if nameTable: + name = nameTable.getDebugName(value) + xmlWriter.write(" ") + if name: + xmlWriter.comment(name) + else: + xmlWriter.comment("missing from name table") + log.warning("name id %d missing from name table" % value) + xmlWriter.newline() + class STATFlags(UShort): - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", value)]) - flags = [] - if value & 0x01: - flags.append("OlderSiblingFontAttribute") - if value & 0x02: - flags.append("ElidableAxisValueName") - if flags: - xmlWriter.write(" ") - xmlWriter.comment(" ".join(flags)) - xmlWriter.newline() + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + flags = [] + if value & 0x01: + flags.append("OlderSiblingFontAttribute") + if value & 0x02: + flags.append("ElidableAxisValueName") + if flags: + xmlWriter.write(" ") + xmlWriter.comment(" ".join(flags)) + xmlWriter.newline() + class FloatValue(SimpleValue): - @staticmethod - def fromString(value): - return float(value) + @staticmethod + def fromString(value): + return float(value) + class DeciPoints(FloatValue): - staticSize = 2 - def read(self, reader, font, tableDict): - return reader.readUShort() / 10 + staticSize = 2 + + def read(self, reader, font, tableDict): + return reader.readUShort() / 10 + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeUShort(round(value * 10)) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeUShort(round(value * 10)) class BaseFixedValue(FloatValue): - staticSize = NotImplemented - precisionBits = NotImplemented - readerMethod = NotImplemented - writerMethod = NotImplemented - def read(self, reader, font, tableDict): - return self.fromInt(getattr(reader, self.readerMethod)()) - def write(self, writer, font, tableDict, value, repeatIndex=None): - getattr(writer, self.writerMethod)(self.toInt(value)) - @classmethod - def fromInt(cls, value): - return fi2fl(value, cls.precisionBits) - @classmethod - def toInt(cls, value): - return fl2fi(value, cls.precisionBits) - @classmethod - def fromString(cls, value): - return str2fl(value, cls.precisionBits) - @classmethod - def toString(cls, value): - return fl2str(value, cls.precisionBits) + staticSize = NotImplemented + precisionBits = NotImplemented + readerMethod = NotImplemented + writerMethod = NotImplemented + + def read(self, reader, font, tableDict): + return self.fromInt(getattr(reader, self.readerMethod)()) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + getattr(writer, self.writerMethod)(self.toInt(value)) + + @classmethod + def fromInt(cls, value): + return fi2fl(value, cls.precisionBits) + + @classmethod + def toInt(cls, value): + return fl2fi(value, cls.precisionBits) + + @classmethod + def fromString(cls, value): + return str2fl(value, cls.precisionBits) + + @classmethod + def toString(cls, value): + return fl2str(value, cls.precisionBits) + class Fixed(BaseFixedValue): - staticSize = 4 - precisionBits = 16 - readerMethod = "readLong" - writerMethod = "writeLong" + staticSize = 4 + precisionBits = 16 + readerMethod = "readLong" + writerMethod = "writeLong" + class F2Dot14(BaseFixedValue): - staticSize = 2 - precisionBits = 14 - readerMethod = "readShort" - writerMethod = "writeShort" + staticSize = 2 + precisionBits = 14 + readerMethod = "readShort" + writerMethod = "writeShort" + class Angle(F2Dot14): - # angles are specified in degrees, and encoded as F2Dot14 fractions of half - # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc. - bias = 0.0 - factor = 1.0/(1<<14) * 180 # 0.010986328125 - @classmethod - def fromInt(cls, value): - return (super().fromInt(value) + cls.bias) * 180 - @classmethod - def toInt(cls, value): - return super().toInt((value / 180) - cls.bias) - @classmethod - def fromString(cls, value): - # quantize to nearest multiples of minimum fixed-precision angle - return otRound(float(value) / cls.factor) * cls.factor - @classmethod - def toString(cls, value): - return nearestMultipleShortestRepr(value, cls.factor) + # angles are specified in degrees, and encoded as F2Dot14 fractions of half + # circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc. + bias = 0.0 + factor = 1.0 / (1 << 14) * 180 # 0.010986328125 + + @classmethod + def fromInt(cls, value): + return (super().fromInt(value) + cls.bias) * 180 + + @classmethod + def toInt(cls, value): + return super().toInt((value / 180) - cls.bias) + + @classmethod + def fromString(cls, value): + # quantize to nearest multiples of minimum fixed-precision angle + return otRound(float(value) / cls.factor) * cls.factor + + @classmethod + def toString(cls, value): + return nearestMultipleShortestRepr(value, cls.factor) + class BiasedAngle(Angle): - # A bias of 1.0 is used in the representation of start and end angles - # of COLRv1 PaintSweepGradients to allow for encoding +360deg - bias = 1.0 + # A bias of 1.0 is used in the representation of start and end angles + # of COLRv1 PaintSweepGradients to allow for encoding +360deg + bias = 1.0 + class Version(SimpleValue): - staticSize = 4 - def read(self, reader, font, tableDict): - value = reader.readLong() - assert (value >> 16) == 1, "Unsupported version 0x%08x" % value - return value - def write(self, writer, font, tableDict, value, repeatIndex=None): - value = fi2ve(value) - assert (value >> 16) == 1, "Unsupported version 0x%08x" % value - writer.writeLong(value) - @staticmethod - def fromString(value): - return ve2fi(value) - @staticmethod - def toString(value): - return "0x%08x" % value - @staticmethod - def fromFloat(v): - return fl2fi(v, 16) + staticSize = 4 + + def read(self, reader, font, tableDict): + value = reader.readLong() + return value + + def write(self, writer, font, tableDict, value, repeatIndex=None): + value = fi2ve(value) + writer.writeLong(value) + + @staticmethod + def fromString(value): + return ve2fi(value) + + @staticmethod + def toString(value): + return "0x%08x" % value + + @staticmethod + def fromFloat(v): + return fl2fi(v, 16) class Char64(SimpleValue): - """An ASCII string with up to 64 characters. - - Unused character positions are filled with 0x00 bytes. - Used in Apple AAT fonts in the `gcid` table. - """ - staticSize = 64 - - def read(self, reader, font, tableDict): - data = reader.readData(self.staticSize) - zeroPos = data.find(b"\0") - if zeroPos >= 0: - data = data[:zeroPos] - s = tostr(data, encoding="ascii", errors="replace") - if s != tostr(data, encoding="ascii", errors="ignore"): - log.warning('replaced non-ASCII characters in "%s"' % - s) - return s - - def write(self, writer, font, tableDict, value, repeatIndex=None): - data = tobytes(value, encoding="ascii", errors="replace") - if data != tobytes(value, encoding="ascii", errors="ignore"): - log.warning('replacing non-ASCII characters in "%s"' % - value) - if len(data) > self.staticSize: - log.warning('truncating overlong "%s" to %d bytes' % - (value, self.staticSize)) - data = (data + b"\0" * self.staticSize)[:self.staticSize] - writer.writeData(data) + """An ASCII string with up to 64 characters. + + Unused character positions are filled with 0x00 bytes. + Used in Apple AAT fonts in the `gcid` table. + """ + + staticSize = 64 + + def read(self, reader, font, tableDict): + data = reader.readData(self.staticSize) + zeroPos = data.find(b"\0") + if zeroPos >= 0: + data = data[:zeroPos] + s = tostr(data, encoding="ascii", errors="replace") + if s != tostr(data, encoding="ascii", errors="ignore"): + log.warning('replaced non-ASCII characters in "%s"' % s) + return s + + def write(self, writer, font, tableDict, value, repeatIndex=None): + data = tobytes(value, encoding="ascii", errors="replace") + if data != tobytes(value, encoding="ascii", errors="ignore"): + log.warning('replacing non-ASCII characters in "%s"' % value) + if len(data) > self.staticSize: + log.warning( + 'truncating overlong "%s" to %d bytes' % (value, self.staticSize) + ) + data = (data + b"\0" * self.staticSize)[: self.staticSize] + writer.writeData(data) class Struct(BaseConverter): - - def getRecordSize(self, reader): - return self.tableClass and self.tableClass.getRecordSize(reader) - - def read(self, reader, font, tableDict): - table = self.tableClass() - table.decompile(reader, font) - return table - - def write(self, writer, font, tableDict, value, repeatIndex=None): - value.compile(writer, font) - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - if value is None: - if attrs: - # If there are attributes (probably index), then - # don't drop this even if it's NULL. It will mess - # up the array indices of the containing element. - xmlWriter.simpletag(name, attrs + [("empty", 1)]) - xmlWriter.newline() - else: - pass # NULL table, ignore - else: - value.toXML(xmlWriter, font, attrs, name=name) - - def xmlRead(self, attrs, content, font): - if "empty" in attrs and safeEval(attrs["empty"]): - return None - table = self.tableClass() - Format = attrs.get("Format") - if Format is not None: - table.Format = int(Format) - - noPostRead = not hasattr(table, 'postRead') - if noPostRead: - # TODO Cache table.hasPropagated. - cleanPropagation = False - for conv in table.getConverters(): - if conv.isPropagated: - cleanPropagation = True - if not hasattr(font, '_propagator'): - font._propagator = {} - propagator = font._propagator - assert conv.name not in propagator, (conv.name, propagator) - setattr(table, conv.name, None) - propagator[conv.name] = CountReference(table.__dict__, conv.name) - - for element in content: - if isinstance(element, tuple): - name, attrs, content = element - table.fromXML(name, attrs, content, font) - else: - pass - - table.populateDefaults(propagator=getattr(font, '_propagator', None)) - - if noPostRead: - if cleanPropagation: - for conv in table.getConverters(): - if conv.isPropagated: - propagator = font._propagator - del propagator[conv.name] - if not propagator: - del font._propagator - - return table - - def __repr__(self): - return "Struct of " + repr(self.tableClass) + def getRecordSize(self, reader): + return self.tableClass and self.tableClass.getRecordSize(reader) + + def read(self, reader, font, tableDict): + table = self.tableClass() + table.decompile(reader, font) + return table + + def write(self, writer, font, tableDict, value, repeatIndex=None): + value.compile(writer, font) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if value is None: + if attrs: + # If there are attributes (probably index), then + # don't drop this even if it's NULL. It will mess + # up the array indices of the containing element. + xmlWriter.simpletag(name, attrs + [("empty", 1)]) + xmlWriter.newline() + else: + pass # NULL table, ignore + else: + value.toXML(xmlWriter, font, attrs, name=name) + + def xmlRead(self, attrs, content, font): + if "empty" in attrs and safeEval(attrs["empty"]): + return None + table = self.tableClass() + Format = attrs.get("Format") + if Format is not None: + table.Format = int(Format) + + noPostRead = not hasattr(table, "postRead") + if noPostRead: + # TODO Cache table.hasPropagated. + cleanPropagation = False + for conv in table.getConverters(): + if conv.isPropagated: + cleanPropagation = True + if not hasattr(font, "_propagator"): + font._propagator = {} + propagator = font._propagator + assert conv.name not in propagator, (conv.name, propagator) + setattr(table, conv.name, None) + propagator[conv.name] = CountReference(table.__dict__, conv.name) + + for element in content: + if isinstance(element, tuple): + name, attrs, content = element + table.fromXML(name, attrs, content, font) + else: + pass + + table.populateDefaults(propagator=getattr(font, "_propagator", None)) + + if noPostRead: + if cleanPropagation: + for conv in table.getConverters(): + if conv.isPropagated: + propagator = font._propagator + del propagator[conv.name] + if not propagator: + del font._propagator + + return table + + def __repr__(self): + return "Struct of " + repr(self.tableClass) class StructWithLength(Struct): - def read(self, reader, font, tableDict): - pos = reader.pos - table = self.tableClass() - table.decompile(reader, font) - reader.seek(pos + table.StructLength) - return table - - def write(self, writer, font, tableDict, value, repeatIndex=None): - for convIndex, conv in enumerate(value.getConverters()): - if conv.name == "StructLength": - break - lengthIndex = len(writer.items) + convIndex - if isinstance(value, FormatSwitchingBaseTable): - lengthIndex += 1 # implicit Format field - deadbeef = {1:0xDE, 2:0xDEAD, 4:0xDEADBEEF}[conv.staticSize] - - before = writer.getDataLength() - value.StructLength = deadbeef - value.compile(writer, font) - length = writer.getDataLength() - before - lengthWriter = writer.getSubWriter() - conv.write(lengthWriter, font, tableDict, length) - assert(writer.items[lengthIndex] == - b"\xde\xad\xbe\xef"[:conv.staticSize]) - writer.items[lengthIndex] = lengthWriter.getAllData() + def read(self, reader, font, tableDict): + pos = reader.pos + table = self.tableClass() + table.decompile(reader, font) + reader.seek(pos + table.StructLength) + return table + + def write(self, writer, font, tableDict, value, repeatIndex=None): + for convIndex, conv in enumerate(value.getConverters()): + if conv.name == "StructLength": + break + lengthIndex = len(writer.items) + convIndex + if isinstance(value, FormatSwitchingBaseTable): + lengthIndex += 1 # implicit Format field + deadbeef = {1: 0xDE, 2: 0xDEAD, 4: 0xDEADBEEF}[conv.staticSize] + + before = writer.getDataLength() + value.StructLength = deadbeef + value.compile(writer, font) + length = writer.getDataLength() - before + lengthWriter = writer.getSubWriter() + conv.write(lengthWriter, font, tableDict, length) + assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef"[: conv.staticSize] + writer.items[lengthIndex] = lengthWriter.getAllData() class Table(Struct): + staticSize = 2 + + def readOffset(self, reader): + return reader.readUShort() + + def writeNullOffset(self, writer): + writer.writeUShort(0) + + def read(self, reader, font, tableDict): + offset = self.readOffset(reader) + if offset == 0: + return None + table = self.tableClass() + reader = reader.getSubReader(offset) + if font.lazy: + table.reader = reader + table.font = font + else: + table.decompile(reader, font) + return table + + def write(self, writer, font, tableDict, value, repeatIndex=None): + if value is None: + self.writeNullOffset(writer) + else: + subWriter = writer.getSubWriter() + subWriter.name = self.name + if repeatIndex is not None: + subWriter.repeatIndex = repeatIndex + writer.writeSubTable(subWriter, offsetSize=self.staticSize) + value.compile(subWriter, font) - staticSize = 2 - - def readOffset(self, reader): - return reader.readUShort() - - def writeNullOffset(self, writer): - writer.writeUShort(0) - - def read(self, reader, font, tableDict): - offset = self.readOffset(reader) - if offset == 0: - return None - table = self.tableClass() - reader = reader.getSubReader(offset) - if font.lazy: - table.reader = reader - table.font = font - else: - table.decompile(reader, font) - return table - - def write(self, writer, font, tableDict, value, repeatIndex=None): - if value is None: - self.writeNullOffset(writer) - else: - subWriter = writer.getSubWriter(offsetSize=self.staticSize) - subWriter.name = self.name - if repeatIndex is not None: - subWriter.repeatIndex = repeatIndex - writer.writeSubTable(subWriter) - value.compile(subWriter, font) class LTable(Table): + staticSize = 4 - staticSize = 4 + def readOffset(self, reader): + return reader.readULong() - def readOffset(self, reader): - return reader.readULong() - - def writeNullOffset(self, writer): - writer.writeULong(0) + def writeNullOffset(self, writer): + writer.writeULong(0) # Table pointed to by a 24-bit, 3-byte long offset class Table24(Table): + staticSize = 3 - staticSize = 3 - - def readOffset(self, reader): - return reader.readUInt24() + def readOffset(self, reader): + return reader.readUInt24() - def writeNullOffset(self, writer): - writer.writeUInt24(0) + def writeNullOffset(self, writer): + writer.writeUInt24(0) # TODO Clean / merge the SubTable and SubStruct + class SubStruct(Struct): - def getConverter(self, tableType, lookupType): - tableClass = self.lookupTypes[tableType][lookupType] - return self.__class__(self.name, self.repeat, self.aux, tableClass) + def getConverter(self, tableType, lookupType): + tableClass = self.lookupTypes[tableType][lookupType] + return self.__class__(self.name, self.repeat, self.aux, tableClass) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs) - def xmlWrite(self, xmlWriter, font, value, name, attrs): - super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs) class SubTable(Table): - def getConverter(self, tableType, lookupType): - tableClass = self.lookupTypes[tableType][lookupType] - return self.__class__(self.name, self.repeat, self.aux, tableClass) + def getConverter(self, tableType, lookupType): + tableClass = self.lookupTypes[tableType][lookupType] + return self.__class__(self.name, self.repeat, self.aux, tableClass) - def xmlWrite(self, xmlWriter, font, value, name, attrs): - super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs) + def xmlWrite(self, xmlWriter, font, value, name, attrs): + super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs) -class ExtSubTable(LTable, SubTable): - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer. - Table.write(self, writer, font, tableDict, value, repeatIndex) +class ExtSubTable(LTable, SubTable): + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer. + Table.write(self, writer, font, tableDict, value, repeatIndex) class FeatureParams(Table): - def getConverter(self, featureTag): - tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams) - return self.__class__(self.name, self.repeat, self.aux, tableClass) + def getConverter(self, featureTag): + tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams) + return self.__class__(self.name, self.repeat, self.aux, tableClass) class ValueFormat(IntValue): - staticSize = 2 - def __init__(self, name, repeat, aux, tableClass=None, *, description=""): - BaseConverter.__init__( - self, name, repeat, aux, tableClass, description=description - ) - self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1") - def read(self, reader, font, tableDict): - format = reader.readUShort() - reader[self.which] = ValueRecordFactory(format) - return format - def write(self, writer, font, tableDict, format, repeatIndex=None): - writer.writeUShort(format) - writer[self.which] = ValueRecordFactory(format) + staticSize = 2 + + def __init__(self, name, repeat, aux, tableClass=None, *, description=""): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1") + + def read(self, reader, font, tableDict): + format = reader.readUShort() + reader[self.which] = ValueRecordFactory(format) + return format + + def write(self, writer, font, tableDict, format, repeatIndex=None): + writer.writeUShort(format) + writer[self.which] = ValueRecordFactory(format) class ValueRecord(ValueFormat): - def getRecordSize(self, reader): - return 2 * len(reader[self.which]) - def read(self, reader, font, tableDict): - return reader[self.which].readValueRecord(reader, font) - def write(self, writer, font, tableDict, value, repeatIndex=None): - writer[self.which].writeValueRecord(writer, font, value) - def xmlWrite(self, xmlWriter, font, value, name, attrs): - if value is None: - pass # NULL table, ignore - else: - value.toXML(xmlWriter, font, self.name, attrs) - def xmlRead(self, attrs, content, font): - from .otBase import ValueRecord - value = ValueRecord() - value.fromXML(None, attrs, content, font) - return value + def getRecordSize(self, reader): + return 2 * len(reader[self.which]) + + def read(self, reader, font, tableDict): + return reader[self.which].readValueRecord(reader, font) + + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer[self.which].writeValueRecord(writer, font, value) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + if value is None: + pass # NULL table, ignore + else: + value.toXML(xmlWriter, font, self.name, attrs) + + def xmlRead(self, attrs, content, font): + from .otBase import ValueRecord + + value = ValueRecord() + value.fromXML(None, attrs, content, font) + return value class AATLookup(BaseConverter): - BIN_SEARCH_HEADER_SIZE = 10 - - def __init__(self, name, repeat, aux, tableClass, *, description=""): - BaseConverter.__init__( - self, name, repeat, aux, tableClass, description=description - ) - if issubclass(self.tableClass, SimpleValue): - self.converter = self.tableClass(name='Value', repeat=None, aux=None) - else: - self.converter = Table(name='Value', repeat=None, aux=None, tableClass=self.tableClass) - - def read(self, reader, font, tableDict): - format = reader.readUShort() - if format == 0: - return self.readFormat0(reader, font) - elif format == 2: - return self.readFormat2(reader, font) - elif format == 4: - return self.readFormat4(reader, font) - elif format == 6: - return self.readFormat6(reader, font) - elif format == 8: - return self.readFormat8(reader, font) - else: - assert False, "unsupported lookup format: %d" % format - - def write(self, writer, font, tableDict, value, repeatIndex=None): - values = list(sorted([(font.getGlyphID(glyph), val) - for glyph, val in value.items()])) - # TODO: Also implement format 4. - formats = list(sorted(filter(None, [ - self.buildFormat0(writer, font, values), - self.buildFormat2(writer, font, values), - self.buildFormat6(writer, font, values), - self.buildFormat8(writer, font, values), - ]))) - # We use the format ID as secondary sort key to make the output - # deterministic when multiple formats have same encoded size. - dataSize, lookupFormat, writeMethod = formats[0] - pos = writer.getDataLength() - writeMethod() - actualSize = writer.getDataLength() - pos - assert actualSize == dataSize, ( - "AATLookup format %d claimed to write %d bytes, but wrote %d" % - (lookupFormat, dataSize, actualSize)) - - @staticmethod - def writeBinSearchHeader(writer, numUnits, unitSize): - writer.writeUShort(unitSize) - writer.writeUShort(numUnits) - searchRange, entrySelector, rangeShift = \ - getSearchRange(n=numUnits, itemSize=unitSize) - writer.writeUShort(searchRange) - writer.writeUShort(entrySelector) - writer.writeUShort(rangeShift) - - def buildFormat0(self, writer, font, values): - numGlyphs = len(font.getGlyphOrder()) - if len(values) != numGlyphs: - return None - valueSize = self.converter.staticSize - return (2 + numGlyphs * valueSize, 0, - lambda: self.writeFormat0(writer, font, values)) - - def writeFormat0(self, writer, font, values): - writer.writeUShort(0) - for glyphID_, value in values: - self.converter.write( - writer, font, tableDict=None, - value=value, repeatIndex=None) - - def buildFormat2(self, writer, font, values): - segStart, segValue = values[0] - segEnd = segStart - segments = [] - for glyphID, curValue in values[1:]: - if glyphID != segEnd + 1 or curValue != segValue: - segments.append((segStart, segEnd, segValue)) - segStart = segEnd = glyphID - segValue = curValue - else: - segEnd = glyphID - segments.append((segStart, segEnd, segValue)) - valueSize = self.converter.staticSize - numUnits, unitSize = len(segments) + 1, valueSize + 4 - return (2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, 2, - lambda: self.writeFormat2(writer, font, segments)) - - def writeFormat2(self, writer, font, segments): - writer.writeUShort(2) - valueSize = self.converter.staticSize - numUnits, unitSize = len(segments), valueSize + 4 - self.writeBinSearchHeader(writer, numUnits, unitSize) - for firstGlyph, lastGlyph, value in segments: - writer.writeUShort(lastGlyph) - writer.writeUShort(firstGlyph) - self.converter.write( - writer, font, tableDict=None, - value=value, repeatIndex=None) - writer.writeUShort(0xFFFF) - writer.writeUShort(0xFFFF) - writer.writeData(b'\x00' * valueSize) - - def buildFormat6(self, writer, font, values): - valueSize = self.converter.staticSize - numUnits, unitSize = len(values), valueSize + 2 - return (2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, 6, - lambda: self.writeFormat6(writer, font, values)) - - def writeFormat6(self, writer, font, values): - writer.writeUShort(6) - valueSize = self.converter.staticSize - numUnits, unitSize = len(values), valueSize + 2 - self.writeBinSearchHeader(writer, numUnits, unitSize) - for glyphID, value in values: - writer.writeUShort(glyphID) - self.converter.write( - writer, font, tableDict=None, - value=value, repeatIndex=None) - writer.writeUShort(0xFFFF) - writer.writeData(b'\x00' * valueSize) - - def buildFormat8(self, writer, font, values): - minGlyphID, maxGlyphID = values[0][0], values[-1][0] - if len(values) != maxGlyphID - minGlyphID + 1: - return None - valueSize = self.converter.staticSize - return (6 + len(values) * valueSize, 8, - lambda: self.writeFormat8(writer, font, values)) - - def writeFormat8(self, writer, font, values): - firstGlyphID = values[0][0] - writer.writeUShort(8) - writer.writeUShort(firstGlyphID) - writer.writeUShort(len(values)) - for _, value in values: - self.converter.write( - writer, font, tableDict=None, - value=value, repeatIndex=None) - - def readFormat0(self, reader, font): - numGlyphs = len(font.getGlyphOrder()) - data = self.converter.readArray( - reader, font, tableDict=None, count=numGlyphs) - return {font.getGlyphName(k): value - for k, value in enumerate(data)} - - def readFormat2(self, reader, font): - mapping = {} - pos = reader.pos - 2 # start of table is at UShort for format - unitSize, numUnits = reader.readUShort(), reader.readUShort() - assert unitSize >= 4 + self.converter.staticSize, unitSize - for i in range(numUnits): - reader.seek(pos + i * unitSize + 12) - last = reader.readUShort() - first = reader.readUShort() - value = self.converter.read(reader, font, tableDict=None) - if last != 0xFFFF: - for k in range(first, last + 1): - mapping[font.getGlyphName(k)] = value - return mapping - - def readFormat4(self, reader, font): - mapping = {} - pos = reader.pos - 2 # start of table is at UShort for format - unitSize = reader.readUShort() - assert unitSize >= 6, unitSize - for i in range(reader.readUShort()): - reader.seek(pos + i * unitSize + 12) - last = reader.readUShort() - first = reader.readUShort() - offset = reader.readUShort() - if last != 0xFFFF: - dataReader = reader.getSubReader(0) # relative to current position - dataReader.seek(pos + offset) # relative to start of table - data = self.converter.readArray( - dataReader, font, tableDict=None, - count=last - first + 1) - for k, v in enumerate(data): - mapping[font.getGlyphName(first + k)] = v - return mapping - - def readFormat6(self, reader, font): - mapping = {} - pos = reader.pos - 2 # start of table is at UShort for format - unitSize = reader.readUShort() - assert unitSize >= 2 + self.converter.staticSize, unitSize - for i in range(reader.readUShort()): - reader.seek(pos + i * unitSize + 12) - glyphID = reader.readUShort() - value = self.converter.read( - reader, font, tableDict=None) - if glyphID != 0xFFFF: - mapping[font.getGlyphName(glyphID)] = value - return mapping - - def readFormat8(self, reader, font): - first = reader.readUShort() - count = reader.readUShort() - data = self.converter.readArray( - reader, font, tableDict=None, count=count) - return {font.getGlyphName(first + k): value - for (k, value) in enumerate(data)} - - def xmlRead(self, attrs, content, font): - value = {} - for element in content: - if isinstance(element, tuple): - name, a, eltContent = element - if name == "Lookup": - value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font) - return value - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.begintag(name, attrs) - xmlWriter.newline() - for glyph, value in sorted(value.items()): - self.converter.xmlWrite( - xmlWriter, font, value=value, - name="Lookup", attrs=[("glyph", glyph)]) - xmlWriter.endtag(name) - xmlWriter.newline() + BIN_SEARCH_HEADER_SIZE = 10 + + def __init__(self, name, repeat, aux, tableClass, *, description=""): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + if issubclass(self.tableClass, SimpleValue): + self.converter = self.tableClass(name="Value", repeat=None, aux=None) + else: + self.converter = Table( + name="Value", repeat=None, aux=None, tableClass=self.tableClass + ) + + def read(self, reader, font, tableDict): + format = reader.readUShort() + if format == 0: + return self.readFormat0(reader, font) + elif format == 2: + return self.readFormat2(reader, font) + elif format == 4: + return self.readFormat4(reader, font) + elif format == 6: + return self.readFormat6(reader, font) + elif format == 8: + return self.readFormat8(reader, font) + else: + assert False, "unsupported lookup format: %d" % format + + def write(self, writer, font, tableDict, value, repeatIndex=None): + values = list( + sorted([(font.getGlyphID(glyph), val) for glyph, val in value.items()]) + ) + # TODO: Also implement format 4. + formats = list( + sorted( + filter( + None, + [ + self.buildFormat0(writer, font, values), + self.buildFormat2(writer, font, values), + self.buildFormat6(writer, font, values), + self.buildFormat8(writer, font, values), + ], + ) + ) + ) + # We use the format ID as secondary sort key to make the output + # deterministic when multiple formats have same encoded size. + dataSize, lookupFormat, writeMethod = formats[0] + pos = writer.getDataLength() + writeMethod() + actualSize = writer.getDataLength() - pos + assert ( + actualSize == dataSize + ), "AATLookup format %d claimed to write %d bytes, but wrote %d" % ( + lookupFormat, + dataSize, + actualSize, + ) + + @staticmethod + def writeBinSearchHeader(writer, numUnits, unitSize): + writer.writeUShort(unitSize) + writer.writeUShort(numUnits) + searchRange, entrySelector, rangeShift = getSearchRange( + n=numUnits, itemSize=unitSize + ) + writer.writeUShort(searchRange) + writer.writeUShort(entrySelector) + writer.writeUShort(rangeShift) + + def buildFormat0(self, writer, font, values): + numGlyphs = len(font.getGlyphOrder()) + if len(values) != numGlyphs: + return None + valueSize = self.converter.staticSize + return ( + 2 + numGlyphs * valueSize, + 0, + lambda: self.writeFormat0(writer, font, values), + ) + + def writeFormat0(self, writer, font, values): + writer.writeUShort(0) + for glyphID_, value in values: + self.converter.write( + writer, font, tableDict=None, value=value, repeatIndex=None + ) + + def buildFormat2(self, writer, font, values): + segStart, segValue = values[0] + segEnd = segStart + segments = [] + for glyphID, curValue in values[1:]: + if glyphID != segEnd + 1 or curValue != segValue: + segments.append((segStart, segEnd, segValue)) + segStart = segEnd = glyphID + segValue = curValue + else: + segEnd = glyphID + segments.append((segStart, segEnd, segValue)) + valueSize = self.converter.staticSize + numUnits, unitSize = len(segments) + 1, valueSize + 4 + return ( + 2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, + 2, + lambda: self.writeFormat2(writer, font, segments), + ) + + def writeFormat2(self, writer, font, segments): + writer.writeUShort(2) + valueSize = self.converter.staticSize + numUnits, unitSize = len(segments), valueSize + 4 + self.writeBinSearchHeader(writer, numUnits, unitSize) + for firstGlyph, lastGlyph, value in segments: + writer.writeUShort(lastGlyph) + writer.writeUShort(firstGlyph) + self.converter.write( + writer, font, tableDict=None, value=value, repeatIndex=None + ) + writer.writeUShort(0xFFFF) + writer.writeUShort(0xFFFF) + writer.writeData(b"\x00" * valueSize) + + def buildFormat6(self, writer, font, values): + valueSize = self.converter.staticSize + numUnits, unitSize = len(values), valueSize + 2 + return ( + 2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, + 6, + lambda: self.writeFormat6(writer, font, values), + ) + + def writeFormat6(self, writer, font, values): + writer.writeUShort(6) + valueSize = self.converter.staticSize + numUnits, unitSize = len(values), valueSize + 2 + self.writeBinSearchHeader(writer, numUnits, unitSize) + for glyphID, value in values: + writer.writeUShort(glyphID) + self.converter.write( + writer, font, tableDict=None, value=value, repeatIndex=None + ) + writer.writeUShort(0xFFFF) + writer.writeData(b"\x00" * valueSize) + + def buildFormat8(self, writer, font, values): + minGlyphID, maxGlyphID = values[0][0], values[-1][0] + if len(values) != maxGlyphID - minGlyphID + 1: + return None + valueSize = self.converter.staticSize + return ( + 6 + len(values) * valueSize, + 8, + lambda: self.writeFormat8(writer, font, values), + ) + + def writeFormat8(self, writer, font, values): + firstGlyphID = values[0][0] + writer.writeUShort(8) + writer.writeUShort(firstGlyphID) + writer.writeUShort(len(values)) + for _, value in values: + self.converter.write( + writer, font, tableDict=None, value=value, repeatIndex=None + ) + + def readFormat0(self, reader, font): + numGlyphs = len(font.getGlyphOrder()) + data = self.converter.readArray(reader, font, tableDict=None, count=numGlyphs) + return {font.getGlyphName(k): value for k, value in enumerate(data)} + + def readFormat2(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize, numUnits = reader.readUShort(), reader.readUShort() + assert unitSize >= 4 + self.converter.staticSize, unitSize + for i in range(numUnits): + reader.seek(pos + i * unitSize + 12) + last = reader.readUShort() + first = reader.readUShort() + value = self.converter.read(reader, font, tableDict=None) + if last != 0xFFFF: + for k in range(first, last + 1): + mapping[font.getGlyphName(k)] = value + return mapping + + def readFormat4(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize = reader.readUShort() + assert unitSize >= 6, unitSize + for i in range(reader.readUShort()): + reader.seek(pos + i * unitSize + 12) + last = reader.readUShort() + first = reader.readUShort() + offset = reader.readUShort() + if last != 0xFFFF: + dataReader = reader.getSubReader(0) # relative to current position + dataReader.seek(pos + offset) # relative to start of table + data = self.converter.readArray( + dataReader, font, tableDict=None, count=last - first + 1 + ) + for k, v in enumerate(data): + mapping[font.getGlyphName(first + k)] = v + return mapping + + def readFormat6(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize = reader.readUShort() + assert unitSize >= 2 + self.converter.staticSize, unitSize + for i in range(reader.readUShort()): + reader.seek(pos + i * unitSize + 12) + glyphID = reader.readUShort() + value = self.converter.read(reader, font, tableDict=None) + if glyphID != 0xFFFF: + mapping[font.getGlyphName(glyphID)] = value + return mapping + + def readFormat8(self, reader, font): + first = reader.readUShort() + count = reader.readUShort() + data = self.converter.readArray(reader, font, tableDict=None, count=count) + return {font.getGlyphName(first + k): value for (k, value) in enumerate(data)} + + def xmlRead(self, attrs, content, font): + value = {} + for element in content: + if isinstance(element, tuple): + name, a, eltContent = element + if name == "Lookup": + value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font) + return value + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for glyph, value in sorted(value.items()): + self.converter.xmlWrite( + xmlWriter, font, value=value, name="Lookup", attrs=[("glyph", glyph)] + ) + xmlWriter.endtag(name) + xmlWriter.newline() # The AAT 'ankr' table has an unusual structure: An offset to an AATLookup @@ -981,831 +1105,822 @@ class AATLookup(BaseConverter): # to the data table to the offset found in the AATLookup, and then use # the sum of these two offsets to find the actual data. class AATLookupWithDataOffset(BaseConverter): - def read(self, reader, font, tableDict): - lookupOffset = reader.readULong() - dataOffset = reader.readULong() - lookupReader = reader.getSubReader(lookupOffset) - lookup = AATLookup('DataOffsets', None, None, UShort) - offsets = lookup.read(lookupReader, font, tableDict) - result = {} - for glyph, offset in offsets.items(): - dataReader = reader.getSubReader(offset + dataOffset) - item = self.tableClass() - item.decompile(dataReader, font) - result[glyph] = item - return result - - def write(self, writer, font, tableDict, value, repeatIndex=None): - # We do not work with OTTableWriter sub-writers because - # the offsets in our AATLookup are relative to our data - # table, for which we need to provide an offset value itself. - # It might have been possible to somehow make a kludge for - # performing this indirect offset computation directly inside - # OTTableWriter. But this would have made the internal logic - # of OTTableWriter even more complex than it already is, - # so we decided to roll our own offset computation for the - # contents of the AATLookup and associated data table. - offsetByGlyph, offsetByData, dataLen = {}, {}, 0 - compiledData = [] - for glyph in sorted(value, key=font.getGlyphID): - subWriter = OTTableWriter() - value[glyph].compile(subWriter, font) - data = subWriter.getAllData() - offset = offsetByData.get(data, None) - if offset == None: - offset = dataLen - dataLen = dataLen + len(data) - offsetByData[data] = offset - compiledData.append(data) - offsetByGlyph[glyph] = offset - # For calculating the offsets to our AATLookup and data table, - # we can use the regular OTTableWriter infrastructure. - lookupWriter = writer.getSubWriter(offsetSize=4) - lookup = AATLookup('DataOffsets', None, None, UShort) - lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None) - - dataWriter = writer.getSubWriter(offsetSize=4) - writer.writeSubTable(lookupWriter) - writer.writeSubTable(dataWriter) - for d in compiledData: - dataWriter.writeData(d) - - def xmlRead(self, attrs, content, font): - lookup = AATLookup('DataOffsets', None, None, self.tableClass) - return lookup.xmlRead(attrs, content, font) - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - lookup = AATLookup('DataOffsets', None, None, self.tableClass) - lookup.xmlWrite(xmlWriter, font, value, name, attrs) + def read(self, reader, font, tableDict): + lookupOffset = reader.readULong() + dataOffset = reader.readULong() + lookupReader = reader.getSubReader(lookupOffset) + lookup = AATLookup("DataOffsets", None, None, UShort) + offsets = lookup.read(lookupReader, font, tableDict) + result = {} + for glyph, offset in offsets.items(): + dataReader = reader.getSubReader(offset + dataOffset) + item = self.tableClass() + item.decompile(dataReader, font) + result[glyph] = item + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + # We do not work with OTTableWriter sub-writers because + # the offsets in our AATLookup are relative to our data + # table, for which we need to provide an offset value itself. + # It might have been possible to somehow make a kludge for + # performing this indirect offset computation directly inside + # OTTableWriter. But this would have made the internal logic + # of OTTableWriter even more complex than it already is, + # so we decided to roll our own offset computation for the + # contents of the AATLookup and associated data table. + offsetByGlyph, offsetByData, dataLen = {}, {}, 0 + compiledData = [] + for glyph in sorted(value, key=font.getGlyphID): + subWriter = OTTableWriter() + value[glyph].compile(subWriter, font) + data = subWriter.getAllData() + offset = offsetByData.get(data, None) + if offset == None: + offset = dataLen + dataLen = dataLen + len(data) + offsetByData[data] = offset + compiledData.append(data) + offsetByGlyph[glyph] = offset + # For calculating the offsets to our AATLookup and data table, + # we can use the regular OTTableWriter infrastructure. + lookupWriter = writer.getSubWriter() + lookup = AATLookup("DataOffsets", None, None, UShort) + lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None) + + dataWriter = writer.getSubWriter() + writer.writeSubTable(lookupWriter, offsetSize=4) + writer.writeSubTable(dataWriter, offsetSize=4) + for d in compiledData: + dataWriter.writeData(d) + + def xmlRead(self, attrs, content, font): + lookup = AATLookup("DataOffsets", None, None, self.tableClass) + return lookup.xmlRead(attrs, content, font) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + lookup = AATLookup("DataOffsets", None, None, self.tableClass) + lookup.xmlWrite(xmlWriter, font, value, name, attrs) class MorxSubtableConverter(BaseConverter): - _PROCESSING_ORDERS = { - # bits 30 and 28 of morx.CoverageFlags; see morx spec - (False, False): "LayoutOrder", - (True, False): "ReversedLayoutOrder", - (False, True): "LogicalOrder", - (True, True): "ReversedLogicalOrder", - } - - _PROCESSING_ORDERS_REVERSED = { - val: key for key, val in _PROCESSING_ORDERS.items() - } - - def __init__(self, name, repeat, aux, tableClass=None, *, description=""): - BaseConverter.__init__( - self, name, repeat, aux, tableClass, description=description - ) - - def _setTextDirectionFromCoverageFlags(self, flags, subtable): - if (flags & 0x20) != 0: - subtable.TextDirection = "Any" - elif (flags & 0x80) != 0: - subtable.TextDirection = "Vertical" - else: - subtable.TextDirection = "Horizontal" - - def read(self, reader, font, tableDict): - pos = reader.pos - m = MorxSubtable() - m.StructLength = reader.readULong() - flags = reader.readUInt8() - orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0) - m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] - self._setTextDirectionFromCoverageFlags(flags, m) - m.Reserved = reader.readUShort() - m.Reserved |= (flags & 0xF) << 16 - m.MorphType = reader.readUInt8() - m.SubFeatureFlags = reader.readULong() - tableClass = lookupTypes["morx"].get(m.MorphType) - if tableClass is None: - assert False, ("unsupported 'morx' lookup type %s" % - m.MorphType) - # To decode AAT ligatures, we need to know the subtable size. - # The easiest way to pass this along is to create a new reader - # that works on just the subtable as its data. - headerLength = reader.pos - pos - data = reader.data[ - reader.pos - : reader.pos + m.StructLength - headerLength] - assert len(data) == m.StructLength - headerLength - subReader = OTTableReader(data=data, tableTag=reader.tableTag) - m.SubStruct = tableClass() - m.SubStruct.decompile(subReader, font) - reader.seek(pos + m.StructLength) - return m - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.begintag(name, attrs) - xmlWriter.newline() - xmlWriter.comment("StructLength=%d" % value.StructLength) - xmlWriter.newline() - xmlWriter.simpletag("TextDirection", value=value.TextDirection) - xmlWriter.newline() - xmlWriter.simpletag("ProcessingOrder", - value=value.ProcessingOrder) - xmlWriter.newline() - if value.Reserved != 0: - xmlWriter.simpletag("Reserved", - value="0x%04x" % value.Reserved) - xmlWriter.newline() - xmlWriter.comment("MorphType=%d" % value.MorphType) - xmlWriter.newline() - xmlWriter.simpletag("SubFeatureFlags", - value="0x%08x" % value.SubFeatureFlags) - xmlWriter.newline() - value.SubStruct.toXML(xmlWriter, font) - xmlWriter.endtag(name) - xmlWriter.newline() - - def xmlRead(self, attrs, content, font): - m = MorxSubtable() - covFlags = 0 - m.Reserved = 0 - for eltName, eltAttrs, eltContent in filter(istuple, content): - if eltName == "CoverageFlags": - # Only in XML from old versions of fonttools. - covFlags = safeEval(eltAttrs["value"]) - orderKey = ((covFlags & 0x40) != 0, - (covFlags & 0x10) != 0) - m.ProcessingOrder = self._PROCESSING_ORDERS[ - orderKey] - self._setTextDirectionFromCoverageFlags( - covFlags, m) - elif eltName == "ProcessingOrder": - m.ProcessingOrder = eltAttrs["value"] - assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, "unknown ProcessingOrder: %s" % m.ProcessingOrder - elif eltName == "TextDirection": - m.TextDirection = eltAttrs["value"] - assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, "unknown TextDirection %s" % m.TextDirection - elif eltName == "Reserved": - m.Reserved = safeEval(eltAttrs["value"]) - elif eltName == "SubFeatureFlags": - m.SubFeatureFlags = safeEval(eltAttrs["value"]) - elif eltName.endswith("Morph"): - m.fromXML(eltName, eltAttrs, eltContent, font) - else: - assert False, eltName - m.Reserved = (covFlags & 0xF) << 16 | m.Reserved - return m - - def write(self, writer, font, tableDict, value, repeatIndex=None): - covFlags = (value.Reserved & 0x000F0000) >> 16 - reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[ - value.ProcessingOrder] - covFlags |= 0x80 if value.TextDirection == "Vertical" else 0 - covFlags |= 0x40 if reverseOrder else 0 - covFlags |= 0x20 if value.TextDirection == "Any" else 0 - covFlags |= 0x10 if logicalOrder else 0 - value.CoverageFlags = covFlags - lengthIndex = len(writer.items) - before = writer.getDataLength() - value.StructLength = 0xdeadbeef - # The high nibble of value.Reserved is actuallly encoded - # into coverageFlags, so we need to clear it here. - origReserved = value.Reserved # including high nibble - value.Reserved = value.Reserved & 0xFFFF # without high nibble - value.compile(writer, font) - value.Reserved = origReserved # restore original value - assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef" - length = writer.getDataLength() - before - writer.items[lengthIndex] = struct.pack(">L", length) + _PROCESSING_ORDERS = { + # bits 30 and 28 of morx.CoverageFlags; see morx spec + (False, False): "LayoutOrder", + (True, False): "ReversedLayoutOrder", + (False, True): "LogicalOrder", + (True, True): "ReversedLogicalOrder", + } + + _PROCESSING_ORDERS_REVERSED = {val: key for key, val in _PROCESSING_ORDERS.items()} + + def __init__(self, name, repeat, aux, tableClass=None, *, description=""): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + + def _setTextDirectionFromCoverageFlags(self, flags, subtable): + if (flags & 0x20) != 0: + subtable.TextDirection = "Any" + elif (flags & 0x80) != 0: + subtable.TextDirection = "Vertical" + else: + subtable.TextDirection = "Horizontal" + + def read(self, reader, font, tableDict): + pos = reader.pos + m = MorxSubtable() + m.StructLength = reader.readULong() + flags = reader.readUInt8() + orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0) + m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] + self._setTextDirectionFromCoverageFlags(flags, m) + m.Reserved = reader.readUShort() + m.Reserved |= (flags & 0xF) << 16 + m.MorphType = reader.readUInt8() + m.SubFeatureFlags = reader.readULong() + tableClass = lookupTypes["morx"].get(m.MorphType) + if tableClass is None: + assert False, "unsupported 'morx' lookup type %s" % m.MorphType + # To decode AAT ligatures, we need to know the subtable size. + # The easiest way to pass this along is to create a new reader + # that works on just the subtable as its data. + headerLength = reader.pos - pos + data = reader.data[reader.pos : reader.pos + m.StructLength - headerLength] + assert len(data) == m.StructLength - headerLength + subReader = OTTableReader(data=data, tableTag=reader.tableTag) + m.SubStruct = tableClass() + m.SubStruct.decompile(subReader, font) + reader.seek(pos + m.StructLength) + return m + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + xmlWriter.comment("StructLength=%d" % value.StructLength) + xmlWriter.newline() + xmlWriter.simpletag("TextDirection", value=value.TextDirection) + xmlWriter.newline() + xmlWriter.simpletag("ProcessingOrder", value=value.ProcessingOrder) + xmlWriter.newline() + if value.Reserved != 0: + xmlWriter.simpletag("Reserved", value="0x%04x" % value.Reserved) + xmlWriter.newline() + xmlWriter.comment("MorphType=%d" % value.MorphType) + xmlWriter.newline() + xmlWriter.simpletag("SubFeatureFlags", value="0x%08x" % value.SubFeatureFlags) + xmlWriter.newline() + value.SubStruct.toXML(xmlWriter, font) + xmlWriter.endtag(name) + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + m = MorxSubtable() + covFlags = 0 + m.Reserved = 0 + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "CoverageFlags": + # Only in XML from old versions of fonttools. + covFlags = safeEval(eltAttrs["value"]) + orderKey = ((covFlags & 0x40) != 0, (covFlags & 0x10) != 0) + m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] + self._setTextDirectionFromCoverageFlags(covFlags, m) + elif eltName == "ProcessingOrder": + m.ProcessingOrder = eltAttrs["value"] + assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, ( + "unknown ProcessingOrder: %s" % m.ProcessingOrder + ) + elif eltName == "TextDirection": + m.TextDirection = eltAttrs["value"] + assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, ( + "unknown TextDirection %s" % m.TextDirection + ) + elif eltName == "Reserved": + m.Reserved = safeEval(eltAttrs["value"]) + elif eltName == "SubFeatureFlags": + m.SubFeatureFlags = safeEval(eltAttrs["value"]) + elif eltName.endswith("Morph"): + m.fromXML(eltName, eltAttrs, eltContent, font) + else: + assert False, eltName + m.Reserved = (covFlags & 0xF) << 16 | m.Reserved + return m + + def write(self, writer, font, tableDict, value, repeatIndex=None): + covFlags = (value.Reserved & 0x000F0000) >> 16 + reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[ + value.ProcessingOrder + ] + covFlags |= 0x80 if value.TextDirection == "Vertical" else 0 + covFlags |= 0x40 if reverseOrder else 0 + covFlags |= 0x20 if value.TextDirection == "Any" else 0 + covFlags |= 0x10 if logicalOrder else 0 + value.CoverageFlags = covFlags + lengthIndex = len(writer.items) + before = writer.getDataLength() + value.StructLength = 0xDEADBEEF + # The high nibble of value.Reserved is actuallly encoded + # into coverageFlags, so we need to clear it here. + origReserved = value.Reserved # including high nibble + value.Reserved = value.Reserved & 0xFFFF # without high nibble + value.compile(writer, font) + value.Reserved = origReserved # restore original value + assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef" + length = writer.getDataLength() - before + writer.items[lengthIndex] = struct.pack(">L", length) # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader # TODO: Untangle the implementation of the various lookup-specific formats. class STXHeader(BaseConverter): - def __init__(self, name, repeat, aux, tableClass, *, description=""): - BaseConverter.__init__( - self, name, repeat, aux, tableClass, description=description - ) - assert issubclass(self.tableClass, AATAction) - self.classLookup = AATLookup("GlyphClasses", None, None, UShort) - if issubclass(self.tableClass, ContextualMorphAction): - self.perGlyphLookup = AATLookup("PerGlyphLookup", - None, None, GlyphID) - else: - self.perGlyphLookup = None - - def read(self, reader, font, tableDict): - table = AATStateTable() - pos = reader.pos - classTableReader = reader.getSubReader(0) - stateArrayReader = reader.getSubReader(0) - entryTableReader = reader.getSubReader(0) - actionReader = None - ligaturesReader = None - table.GlyphClassCount = reader.readULong() - classTableReader.seek(pos + reader.readULong()) - stateArrayReader.seek(pos + reader.readULong()) - entryTableReader.seek(pos + reader.readULong()) - if self.perGlyphLookup is not None: - perGlyphTableReader = reader.getSubReader(0) - perGlyphTableReader.seek(pos + reader.readULong()) - if issubclass(self.tableClass, LigatureMorphAction): - actionReader = reader.getSubReader(0) - actionReader.seek(pos + reader.readULong()) - ligComponentReader = reader.getSubReader(0) - ligComponentReader.seek(pos + reader.readULong()) - ligaturesReader = reader.getSubReader(0) - ligaturesReader.seek(pos + reader.readULong()) - numLigComponents = (ligaturesReader.pos - - ligComponentReader.pos) // 2 - assert numLigComponents >= 0 - table.LigComponents = \ - ligComponentReader.readUShortArray(numLigComponents) - table.Ligatures = self._readLigatures(ligaturesReader, font) - elif issubclass(self.tableClass, InsertionMorphAction): - actionReader = reader.getSubReader(0) - actionReader.seek(pos + reader.readULong()) - table.GlyphClasses = self.classLookup.read(classTableReader, - font, tableDict) - numStates = int((entryTableReader.pos - stateArrayReader.pos) - / (table.GlyphClassCount * 2)) - for stateIndex in range(numStates): - state = AATState() - table.States.append(state) - for glyphClass in range(table.GlyphClassCount): - entryIndex = stateArrayReader.readUShort() - state.Transitions[glyphClass] = \ - self._readTransition(entryTableReader, - entryIndex, font, - actionReader) - if self.perGlyphLookup is not None: - table.PerGlyphLookups = self._readPerGlyphLookups( - table, perGlyphTableReader, font) - return table - - def _readTransition(self, reader, entryIndex, font, actionReader): - transition = self.tableClass() - entryReader = reader.getSubReader( - reader.pos + entryIndex * transition.staticSize) - transition.decompile(entryReader, font, actionReader) - return transition - - def _readLigatures(self, reader, font): - limit = len(reader.data) - numLigatureGlyphs = (limit - reader.pos) // 2 - return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs)) - - def _countPerGlyphLookups(self, table): - # Somewhat annoyingly, the morx table does not encode - # the size of the per-glyph table. So we need to find - # the maximum value that MorphActions use as index - # into this table. - numLookups = 0 - for state in table.States: - for t in state.Transitions.values(): - if isinstance(t, ContextualMorphAction): - if t.MarkIndex != 0xFFFF: - numLookups = max( - numLookups, - t.MarkIndex + 1) - if t.CurrentIndex != 0xFFFF: - numLookups = max( - numLookups, - t.CurrentIndex + 1) - return numLookups - - def _readPerGlyphLookups(self, table, reader, font): - pos = reader.pos - lookups = [] - for _ in range(self._countPerGlyphLookups(table)): - lookupReader = reader.getSubReader(0) - lookupReader.seek(pos + reader.readULong()) - lookups.append( - self.perGlyphLookup.read(lookupReader, font, {})) - return lookups - - def write(self, writer, font, tableDict, value, repeatIndex=None): - glyphClassWriter = OTTableWriter() - self.classLookup.write(glyphClassWriter, font, tableDict, - value.GlyphClasses, repeatIndex=None) - glyphClassData = pad(glyphClassWriter.getAllData(), 2) - glyphClassCount = max(value.GlyphClasses.values()) + 1 - glyphClassTableOffset = 16 # size of STXHeader - if self.perGlyphLookup is not None: - glyphClassTableOffset += 4 - - glyphClassTableOffset += self.tableClass.actionHeaderSize - actionData, actionIndex = \ - self.tableClass.compileActions(font, value.States) - stateArrayData, entryTableData = self._compileStates( - font, value.States, glyphClassCount, actionIndex) - stateArrayOffset = glyphClassTableOffset + len(glyphClassData) - entryTableOffset = stateArrayOffset + len(stateArrayData) - perGlyphOffset = entryTableOffset + len(entryTableData) - perGlyphData = \ - pad(self._compilePerGlyphLookups(value, font), 4) - if actionData is not None: - actionOffset = entryTableOffset + len(entryTableData) - else: - actionOffset = None - - ligaturesOffset, ligComponentsOffset = None, None - ligComponentsData = self._compileLigComponents(value, font) - ligaturesData = self._compileLigatures(value, font) - if ligComponentsData is not None: - assert len(perGlyphData) == 0 - ligComponentsOffset = actionOffset + len(actionData) - ligaturesOffset = ligComponentsOffset + len(ligComponentsData) - - writer.writeULong(glyphClassCount) - writer.writeULong(glyphClassTableOffset) - writer.writeULong(stateArrayOffset) - writer.writeULong(entryTableOffset) - if self.perGlyphLookup is not None: - writer.writeULong(perGlyphOffset) - if actionOffset is not None: - writer.writeULong(actionOffset) - if ligComponentsOffset is not None: - writer.writeULong(ligComponentsOffset) - writer.writeULong(ligaturesOffset) - writer.writeData(glyphClassData) - writer.writeData(stateArrayData) - writer.writeData(entryTableData) - writer.writeData(perGlyphData) - if actionData is not None: - writer.writeData(actionData) - if ligComponentsData is not None: - writer.writeData(ligComponentsData) - if ligaturesData is not None: - writer.writeData(ligaturesData) - - def _compileStates(self, font, states, glyphClassCount, actionIndex): - stateArrayWriter = OTTableWriter() - entries, entryIDs = [], {} - for state in states: - for glyphClass in range(glyphClassCount): - transition = state.Transitions[glyphClass] - entryWriter = OTTableWriter() - transition.compile(entryWriter, font, - actionIndex) - entryData = entryWriter.getAllData() - assert len(entryData) == transition.staticSize, ( \ - "%s has staticSize %d, " - "but actually wrote %d bytes" % ( - repr(transition), - transition.staticSize, - len(entryData))) - entryIndex = entryIDs.get(entryData) - if entryIndex is None: - entryIndex = len(entries) - entryIDs[entryData] = entryIndex - entries.append(entryData) - stateArrayWriter.writeUShort(entryIndex) - stateArrayData = pad(stateArrayWriter.getAllData(), 4) - entryTableData = pad(bytesjoin(entries), 4) - return stateArrayData, entryTableData - - def _compilePerGlyphLookups(self, table, font): - if self.perGlyphLookup is None: - return b"" - numLookups = self._countPerGlyphLookups(table) - assert len(table.PerGlyphLookups) == numLookups, ( - "len(AATStateTable.PerGlyphLookups) is %d, " - "but the actions inside the table refer to %d" % - (len(table.PerGlyphLookups), numLookups)) - writer = OTTableWriter() - for lookup in table.PerGlyphLookups: - lookupWriter = writer.getSubWriter(offsetSize=4) - self.perGlyphLookup.write(lookupWriter, font, - {}, lookup, None) - writer.writeSubTable(lookupWriter) - return writer.getAllData() - - def _compileLigComponents(self, table, font): - if not hasattr(table, "LigComponents"): - return None - writer = OTTableWriter() - for component in table.LigComponents: - writer.writeUShort(component) - return writer.getAllData() - - def _compileLigatures(self, table, font): - if not hasattr(table, "Ligatures"): - return None - writer = OTTableWriter() - for glyphName in table.Ligatures: - writer.writeUShort(font.getGlyphID(glyphName)) - return writer.getAllData() - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.begintag(name, attrs) - xmlWriter.newline() - xmlWriter.comment("GlyphClassCount=%s" %value.GlyphClassCount) - xmlWriter.newline() - for g, klass in sorted(value.GlyphClasses.items()): - xmlWriter.simpletag("GlyphClass", glyph=g, value=klass) - xmlWriter.newline() - for stateIndex, state in enumerate(value.States): - xmlWriter.begintag("State", index=stateIndex) - xmlWriter.newline() - for glyphClass, trans in sorted(state.Transitions.items()): - trans.toXML(xmlWriter, font=font, - attrs={"onGlyphClass": glyphClass}, - name="Transition") - xmlWriter.endtag("State") - xmlWriter.newline() - for i, lookup in enumerate(value.PerGlyphLookups): - xmlWriter.begintag("PerGlyphLookup", index=i) - xmlWriter.newline() - for glyph, val in sorted(lookup.items()): - xmlWriter.simpletag("Lookup", glyph=glyph, - value=val) - xmlWriter.newline() - xmlWriter.endtag("PerGlyphLookup") - xmlWriter.newline() - if hasattr(value, "LigComponents"): - xmlWriter.begintag("LigComponents") - xmlWriter.newline() - for i, val in enumerate(getattr(value, "LigComponents")): - xmlWriter.simpletag("LigComponent", index=i, - value=val) - xmlWriter.newline() - xmlWriter.endtag("LigComponents") - xmlWriter.newline() - self._xmlWriteLigatures(xmlWriter, font, value, name, attrs) - xmlWriter.endtag(name) - xmlWriter.newline() - - def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs): - if not hasattr(value, "Ligatures"): - return - xmlWriter.begintag("Ligatures") - xmlWriter.newline() - for i, g in enumerate(getattr(value, "Ligatures")): - xmlWriter.simpletag("Ligature", index=i, glyph=g) - xmlWriter.newline() - xmlWriter.endtag("Ligatures") - xmlWriter.newline() - - def xmlRead(self, attrs, content, font): - table = AATStateTable() - for eltName, eltAttrs, eltContent in filter(istuple, content): - if eltName == "GlyphClass": - glyph = eltAttrs["glyph"] - value = eltAttrs["value"] - table.GlyphClasses[glyph] = safeEval(value) - elif eltName == "State": - state = self._xmlReadState(eltAttrs, eltContent, font) - table.States.append(state) - elif eltName == "PerGlyphLookup": - lookup = self.perGlyphLookup.xmlRead( - eltAttrs, eltContent, font) - table.PerGlyphLookups.append(lookup) - elif eltName == "LigComponents": - table.LigComponents = \ - self._xmlReadLigComponents( - eltAttrs, eltContent, font) - elif eltName == "Ligatures": - table.Ligatures = \ - self._xmlReadLigatures( - eltAttrs, eltContent, font) - table.GlyphClassCount = max(table.GlyphClasses.values()) + 1 - return table - - def _xmlReadState(self, attrs, content, font): - state = AATState() - for eltName, eltAttrs, eltContent in filter(istuple, content): - if eltName == "Transition": - glyphClass = safeEval(eltAttrs["onGlyphClass"]) - transition = self.tableClass() - transition.fromXML(eltName, eltAttrs, - eltContent, font) - state.Transitions[glyphClass] = transition - return state - - def _xmlReadLigComponents(self, attrs, content, font): - ligComponents = [] - for eltName, eltAttrs, _eltContent in filter(istuple, content): - if eltName == "LigComponent": - ligComponents.append( - safeEval(eltAttrs["value"])) - return ligComponents - - def _xmlReadLigatures(self, attrs, content, font): - ligs = [] - for eltName, eltAttrs, _eltContent in filter(istuple, content): - if eltName == "Ligature": - ligs.append(eltAttrs["glyph"]) - return ligs + def __init__(self, name, repeat, aux, tableClass, *, description=""): + BaseConverter.__init__( + self, name, repeat, aux, tableClass, description=description + ) + assert issubclass(self.tableClass, AATAction) + self.classLookup = AATLookup("GlyphClasses", None, None, UShort) + if issubclass(self.tableClass, ContextualMorphAction): + self.perGlyphLookup = AATLookup("PerGlyphLookup", None, None, GlyphID) + else: + self.perGlyphLookup = None + + def read(self, reader, font, tableDict): + table = AATStateTable() + pos = reader.pos + classTableReader = reader.getSubReader(0) + stateArrayReader = reader.getSubReader(0) + entryTableReader = reader.getSubReader(0) + actionReader = None + ligaturesReader = None + table.GlyphClassCount = reader.readULong() + classTableReader.seek(pos + reader.readULong()) + stateArrayReader.seek(pos + reader.readULong()) + entryTableReader.seek(pos + reader.readULong()) + if self.perGlyphLookup is not None: + perGlyphTableReader = reader.getSubReader(0) + perGlyphTableReader.seek(pos + reader.readULong()) + if issubclass(self.tableClass, LigatureMorphAction): + actionReader = reader.getSubReader(0) + actionReader.seek(pos + reader.readULong()) + ligComponentReader = reader.getSubReader(0) + ligComponentReader.seek(pos + reader.readULong()) + ligaturesReader = reader.getSubReader(0) + ligaturesReader.seek(pos + reader.readULong()) + numLigComponents = (ligaturesReader.pos - ligComponentReader.pos) // 2 + assert numLigComponents >= 0 + table.LigComponents = ligComponentReader.readUShortArray(numLigComponents) + table.Ligatures = self._readLigatures(ligaturesReader, font) + elif issubclass(self.tableClass, InsertionMorphAction): + actionReader = reader.getSubReader(0) + actionReader.seek(pos + reader.readULong()) + table.GlyphClasses = self.classLookup.read(classTableReader, font, tableDict) + numStates = int( + (entryTableReader.pos - stateArrayReader.pos) / (table.GlyphClassCount * 2) + ) + for stateIndex in range(numStates): + state = AATState() + table.States.append(state) + for glyphClass in range(table.GlyphClassCount): + entryIndex = stateArrayReader.readUShort() + state.Transitions[glyphClass] = self._readTransition( + entryTableReader, entryIndex, font, actionReader + ) + if self.perGlyphLookup is not None: + table.PerGlyphLookups = self._readPerGlyphLookups( + table, perGlyphTableReader, font + ) + return table + + def _readTransition(self, reader, entryIndex, font, actionReader): + transition = self.tableClass() + entryReader = reader.getSubReader( + reader.pos + entryIndex * transition.staticSize + ) + transition.decompile(entryReader, font, actionReader) + return transition + + def _readLigatures(self, reader, font): + limit = len(reader.data) + numLigatureGlyphs = (limit - reader.pos) // 2 + return font.getGlyphNameMany(reader.readUShortArray(numLigatureGlyphs)) + + def _countPerGlyphLookups(self, table): + # Somewhat annoyingly, the morx table does not encode + # the size of the per-glyph table. So we need to find + # the maximum value that MorphActions use as index + # into this table. + numLookups = 0 + for state in table.States: + for t in state.Transitions.values(): + if isinstance(t, ContextualMorphAction): + if t.MarkIndex != 0xFFFF: + numLookups = max(numLookups, t.MarkIndex + 1) + if t.CurrentIndex != 0xFFFF: + numLookups = max(numLookups, t.CurrentIndex + 1) + return numLookups + + def _readPerGlyphLookups(self, table, reader, font): + pos = reader.pos + lookups = [] + for _ in range(self._countPerGlyphLookups(table)): + lookupReader = reader.getSubReader(0) + lookupReader.seek(pos + reader.readULong()) + lookups.append(self.perGlyphLookup.read(lookupReader, font, {})) + return lookups + + def write(self, writer, font, tableDict, value, repeatIndex=None): + glyphClassWriter = OTTableWriter() + self.classLookup.write( + glyphClassWriter, font, tableDict, value.GlyphClasses, repeatIndex=None + ) + glyphClassData = pad(glyphClassWriter.getAllData(), 2) + glyphClassCount = max(value.GlyphClasses.values()) + 1 + glyphClassTableOffset = 16 # size of STXHeader + if self.perGlyphLookup is not None: + glyphClassTableOffset += 4 + + glyphClassTableOffset += self.tableClass.actionHeaderSize + actionData, actionIndex = self.tableClass.compileActions(font, value.States) + stateArrayData, entryTableData = self._compileStates( + font, value.States, glyphClassCount, actionIndex + ) + stateArrayOffset = glyphClassTableOffset + len(glyphClassData) + entryTableOffset = stateArrayOffset + len(stateArrayData) + perGlyphOffset = entryTableOffset + len(entryTableData) + perGlyphData = pad(self._compilePerGlyphLookups(value, font), 4) + if actionData is not None: + actionOffset = entryTableOffset + len(entryTableData) + else: + actionOffset = None + + ligaturesOffset, ligComponentsOffset = None, None + ligComponentsData = self._compileLigComponents(value, font) + ligaturesData = self._compileLigatures(value, font) + if ligComponentsData is not None: + assert len(perGlyphData) == 0 + ligComponentsOffset = actionOffset + len(actionData) + ligaturesOffset = ligComponentsOffset + len(ligComponentsData) + + writer.writeULong(glyphClassCount) + writer.writeULong(glyphClassTableOffset) + writer.writeULong(stateArrayOffset) + writer.writeULong(entryTableOffset) + if self.perGlyphLookup is not None: + writer.writeULong(perGlyphOffset) + if actionOffset is not None: + writer.writeULong(actionOffset) + if ligComponentsOffset is not None: + writer.writeULong(ligComponentsOffset) + writer.writeULong(ligaturesOffset) + writer.writeData(glyphClassData) + writer.writeData(stateArrayData) + writer.writeData(entryTableData) + writer.writeData(perGlyphData) + if actionData is not None: + writer.writeData(actionData) + if ligComponentsData is not None: + writer.writeData(ligComponentsData) + if ligaturesData is not None: + writer.writeData(ligaturesData) + + def _compileStates(self, font, states, glyphClassCount, actionIndex): + stateArrayWriter = OTTableWriter() + entries, entryIDs = [], {} + for state in states: + for glyphClass in range(glyphClassCount): + transition = state.Transitions[glyphClass] + entryWriter = OTTableWriter() + transition.compile(entryWriter, font, actionIndex) + entryData = entryWriter.getAllData() + assert ( + len(entryData) == transition.staticSize + ), "%s has staticSize %d, " "but actually wrote %d bytes" % ( + repr(transition), + transition.staticSize, + len(entryData), + ) + entryIndex = entryIDs.get(entryData) + if entryIndex is None: + entryIndex = len(entries) + entryIDs[entryData] = entryIndex + entries.append(entryData) + stateArrayWriter.writeUShort(entryIndex) + stateArrayData = pad(stateArrayWriter.getAllData(), 4) + entryTableData = pad(bytesjoin(entries), 4) + return stateArrayData, entryTableData + + def _compilePerGlyphLookups(self, table, font): + if self.perGlyphLookup is None: + return b"" + numLookups = self._countPerGlyphLookups(table) + assert len(table.PerGlyphLookups) == numLookups, ( + "len(AATStateTable.PerGlyphLookups) is %d, " + "but the actions inside the table refer to %d" + % (len(table.PerGlyphLookups), numLookups) + ) + writer = OTTableWriter() + for lookup in table.PerGlyphLookups: + lookupWriter = writer.getSubWriter() + self.perGlyphLookup.write(lookupWriter, font, {}, lookup, None) + writer.writeSubTable(lookupWriter, offsetSize=4) + return writer.getAllData() + + def _compileLigComponents(self, table, font): + if not hasattr(table, "LigComponents"): + return None + writer = OTTableWriter() + for component in table.LigComponents: + writer.writeUShort(component) + return writer.getAllData() + + def _compileLigatures(self, table, font): + if not hasattr(table, "Ligatures"): + return None + writer = OTTableWriter() + for glyphName in table.Ligatures: + writer.writeUShort(font.getGlyphID(glyphName)) + return writer.getAllData() + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + xmlWriter.comment("GlyphClassCount=%s" % value.GlyphClassCount) + xmlWriter.newline() + for g, klass in sorted(value.GlyphClasses.items()): + xmlWriter.simpletag("GlyphClass", glyph=g, value=klass) + xmlWriter.newline() + for stateIndex, state in enumerate(value.States): + xmlWriter.begintag("State", index=stateIndex) + xmlWriter.newline() + for glyphClass, trans in sorted(state.Transitions.items()): + trans.toXML( + xmlWriter, + font=font, + attrs={"onGlyphClass": glyphClass}, + name="Transition", + ) + xmlWriter.endtag("State") + xmlWriter.newline() + for i, lookup in enumerate(value.PerGlyphLookups): + xmlWriter.begintag("PerGlyphLookup", index=i) + xmlWriter.newline() + for glyph, val in sorted(lookup.items()): + xmlWriter.simpletag("Lookup", glyph=glyph, value=val) + xmlWriter.newline() + xmlWriter.endtag("PerGlyphLookup") + xmlWriter.newline() + if hasattr(value, "LigComponents"): + xmlWriter.begintag("LigComponents") + xmlWriter.newline() + for i, val in enumerate(getattr(value, "LigComponents")): + xmlWriter.simpletag("LigComponent", index=i, value=val) + xmlWriter.newline() + xmlWriter.endtag("LigComponents") + xmlWriter.newline() + self._xmlWriteLigatures(xmlWriter, font, value, name, attrs) + xmlWriter.endtag(name) + xmlWriter.newline() + + def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs): + if not hasattr(value, "Ligatures"): + return + xmlWriter.begintag("Ligatures") + xmlWriter.newline() + for i, g in enumerate(getattr(value, "Ligatures")): + xmlWriter.simpletag("Ligature", index=i, glyph=g) + xmlWriter.newline() + xmlWriter.endtag("Ligatures") + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + table = AATStateTable() + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "GlyphClass": + glyph = eltAttrs["glyph"] + value = eltAttrs["value"] + table.GlyphClasses[glyph] = safeEval(value) + elif eltName == "State": + state = self._xmlReadState(eltAttrs, eltContent, font) + table.States.append(state) + elif eltName == "PerGlyphLookup": + lookup = self.perGlyphLookup.xmlRead(eltAttrs, eltContent, font) + table.PerGlyphLookups.append(lookup) + elif eltName == "LigComponents": + table.LigComponents = self._xmlReadLigComponents( + eltAttrs, eltContent, font + ) + elif eltName == "Ligatures": + table.Ligatures = self._xmlReadLigatures(eltAttrs, eltContent, font) + table.GlyphClassCount = max(table.GlyphClasses.values()) + 1 + return table + + def _xmlReadState(self, attrs, content, font): + state = AATState() + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "Transition": + glyphClass = safeEval(eltAttrs["onGlyphClass"]) + transition = self.tableClass() + transition.fromXML(eltName, eltAttrs, eltContent, font) + state.Transitions[glyphClass] = transition + return state + + def _xmlReadLigComponents(self, attrs, content, font): + ligComponents = [] + for eltName, eltAttrs, _eltContent in filter(istuple, content): + if eltName == "LigComponent": + ligComponents.append(safeEval(eltAttrs["value"])) + return ligComponents + + def _xmlReadLigatures(self, attrs, content, font): + ligs = [] + for eltName, eltAttrs, _eltContent in filter(istuple, content): + if eltName == "Ligature": + ligs.append(eltAttrs["glyph"]) + return ligs class CIDGlyphMap(BaseConverter): - def read(self, reader, font, tableDict): - numCIDs = reader.readUShort() - result = {} - for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): - if glyphID != 0xFFFF: - result[cid] = font.getGlyphName(glyphID) - return result - - def write(self, writer, font, tableDict, value, repeatIndex=None): - items = {cid: font.getGlyphID(glyph) - for cid, glyph in value.items()} - count = max(items) + 1 if items else 0 - writer.writeUShort(count) - for cid in range(count): - writer.writeUShort(items.get(cid, 0xFFFF)) - - def xmlRead(self, attrs, content, font): - result = {} - for eName, eAttrs, _eContent in filter(istuple, content): - if eName == "CID": - result[safeEval(eAttrs["cid"])] = \ - eAttrs["glyph"].strip() - return result - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.begintag(name, attrs) - xmlWriter.newline() - for cid, glyph in sorted(value.items()): - if glyph is not None and glyph != 0xFFFF: - xmlWriter.simpletag( - "CID", cid=cid, glyph=glyph) - xmlWriter.newline() - xmlWriter.endtag(name) - xmlWriter.newline() + def read(self, reader, font, tableDict): + numCIDs = reader.readUShort() + result = {} + for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): + if glyphID != 0xFFFF: + result[cid] = font.getGlyphName(glyphID) + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + items = {cid: font.getGlyphID(glyph) for cid, glyph in value.items()} + count = max(items) + 1 if items else 0 + writer.writeUShort(count) + for cid in range(count): + writer.writeUShort(items.get(cid, 0xFFFF)) + + def xmlRead(self, attrs, content, font): + result = {} + for eName, eAttrs, _eContent in filter(istuple, content): + if eName == "CID": + result[safeEval(eAttrs["cid"])] = eAttrs["glyph"].strip() + return result + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for cid, glyph in sorted(value.items()): + if glyph is not None and glyph != 0xFFFF: + xmlWriter.simpletag("CID", cid=cid, glyph=glyph) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() class GlyphCIDMap(BaseConverter): - def read(self, reader, font, tableDict): - glyphOrder = font.getGlyphOrder() - count = reader.readUShort() - cids = reader.readUShortArray(count) - if count > len(glyphOrder): - log.warning("GlyphCIDMap has %d elements, " - "but the font has only %d glyphs; " - "ignoring the rest" % - (count, len(glyphOrder))) - result = {} - for glyphID in range(min(len(cids), len(glyphOrder))): - cid = cids[glyphID] - if cid != 0xFFFF: - result[glyphOrder[glyphID]] = cid - return result - - def write(self, writer, font, tableDict, value, repeatIndex=None): - items = {font.getGlyphID(g): cid - for g, cid in value.items() - if cid is not None and cid != 0xFFFF} - count = max(items) + 1 if items else 0 - writer.writeUShort(count) - for glyphID in range(count): - writer.writeUShort(items.get(glyphID, 0xFFFF)) - - def xmlRead(self, attrs, content, font): - result = {} - for eName, eAttrs, _eContent in filter(istuple, content): - if eName == "CID": - result[eAttrs["glyph"]] = \ - safeEval(eAttrs["value"]) - return result - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.begintag(name, attrs) - xmlWriter.newline() - for glyph, cid in sorted(value.items()): - if cid is not None and cid != 0xFFFF: - xmlWriter.simpletag( - "CID", glyph=glyph, value=cid) - xmlWriter.newline() - xmlWriter.endtag(name) - xmlWriter.newline() + def read(self, reader, font, tableDict): + glyphOrder = font.getGlyphOrder() + count = reader.readUShort() + cids = reader.readUShortArray(count) + if count > len(glyphOrder): + log.warning( + "GlyphCIDMap has %d elements, " + "but the font has only %d glyphs; " + "ignoring the rest" % (count, len(glyphOrder)) + ) + result = {} + for glyphID in range(min(len(cids), len(glyphOrder))): + cid = cids[glyphID] + if cid != 0xFFFF: + result[glyphOrder[glyphID]] = cid + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + items = { + font.getGlyphID(g): cid + for g, cid in value.items() + if cid is not None and cid != 0xFFFF + } + count = max(items) + 1 if items else 0 + writer.writeUShort(count) + for glyphID in range(count): + writer.writeUShort(items.get(glyphID, 0xFFFF)) + + def xmlRead(self, attrs, content, font): + result = {} + for eName, eAttrs, _eContent in filter(istuple, content): + if eName == "CID": + result[eAttrs["glyph"]] = safeEval(eAttrs["value"]) + return result + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for glyph, cid in sorted(value.items()): + if cid is not None and cid != 0xFFFF: + xmlWriter.simpletag("CID", glyph=glyph, value=cid) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() class DeltaValue(BaseConverter): - - def read(self, reader, font, tableDict): - StartSize = tableDict["StartSize"] - EndSize = tableDict["EndSize"] - DeltaFormat = tableDict["DeltaFormat"] - assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" - nItems = EndSize - StartSize + 1 - nBits = 1 << DeltaFormat - minusOffset = 1 << nBits - mask = (1 << nBits) - 1 - signMask = 1 << (nBits - 1) - - DeltaValue = [] - tmp, shift = 0, 0 - for i in range(nItems): - if shift == 0: - tmp, shift = reader.readUShort(), 16 - shift = shift - nBits - value = (tmp >> shift) & mask - if value & signMask: - value = value - minusOffset - DeltaValue.append(value) - return DeltaValue - - def write(self, writer, font, tableDict, value, repeatIndex=None): - StartSize = tableDict["StartSize"] - EndSize = tableDict["EndSize"] - DeltaFormat = tableDict["DeltaFormat"] - DeltaValue = value - assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" - nItems = EndSize - StartSize + 1 - nBits = 1 << DeltaFormat - assert len(DeltaValue) == nItems - mask = (1 << nBits) - 1 - - tmp, shift = 0, 16 - for value in DeltaValue: - shift = shift - nBits - tmp = tmp | ((value & mask) << shift) - if shift == 0: - writer.writeUShort(tmp) - tmp, shift = 0, 16 - if shift != 16: - writer.writeUShort(tmp) - - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", value)]) - xmlWriter.newline() - - def xmlRead(self, attrs, content, font): - return safeEval(attrs["value"]) + def read(self, reader, font, tableDict): + StartSize = tableDict["StartSize"] + EndSize = tableDict["EndSize"] + DeltaFormat = tableDict["DeltaFormat"] + assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" + nItems = EndSize - StartSize + 1 + nBits = 1 << DeltaFormat + minusOffset = 1 << nBits + mask = (1 << nBits) - 1 + signMask = 1 << (nBits - 1) + + DeltaValue = [] + tmp, shift = 0, 0 + for i in range(nItems): + if shift == 0: + tmp, shift = reader.readUShort(), 16 + shift = shift - nBits + value = (tmp >> shift) & mask + if value & signMask: + value = value - minusOffset + DeltaValue.append(value) + return DeltaValue + + def write(self, writer, font, tableDict, value, repeatIndex=None): + StartSize = tableDict["StartSize"] + EndSize = tableDict["EndSize"] + DeltaFormat = tableDict["DeltaFormat"] + DeltaValue = value + assert DeltaFormat in (1, 2, 3), "illegal DeltaFormat" + nItems = EndSize - StartSize + 1 + nBits = 1 << DeltaFormat + assert len(DeltaValue) == nItems + mask = (1 << nBits) - 1 + + tmp, shift = 0, 16 + for value in DeltaValue: + shift = shift - nBits + tmp = tmp | ((value & mask) << shift) + if shift == 0: + writer.writeUShort(tmp) + tmp, shift = 0, 16 + if shift != 16: + writer.writeUShort(tmp) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + return safeEval(attrs["value"]) class VarIdxMapValue(BaseConverter): - - def read(self, reader, font, tableDict): - fmt = tableDict['EntryFormat'] - nItems = tableDict['MappingCount'] - - innerBits = 1 + (fmt & 0x000F) - innerMask = (1<<innerBits) - 1 - outerMask = 0xFFFFFFFF - innerMask - outerShift = 16 - innerBits - - entrySize = 1 + ((fmt & 0x0030) >> 4) - readArray = { - 1: reader.readUInt8Array, - 2: reader.readUShortArray, - 3: reader.readUInt24Array, - 4: reader.readULongArray, - }[entrySize] - - return [(((raw & outerMask) << outerShift) | (raw & innerMask)) - for raw in readArray(nItems)] - - def write(self, writer, font, tableDict, value, repeatIndex=None): - fmt = tableDict['EntryFormat'] - mapping = value - writer['MappingCount'].setValue(len(mapping)) - - innerBits = 1 + (fmt & 0x000F) - innerMask = (1<<innerBits) - 1 - outerShift = 16 - innerBits - - entrySize = 1 + ((fmt & 0x0030) >> 4) - writeArray = { - 1: writer.writeUInt8Array, - 2: writer.writeUShortArray, - 3: writer.writeUInt24Array, - 4: writer.writeULongArray, - }[entrySize] - - writeArray([(((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)) - for idx in mapping]) + def read(self, reader, font, tableDict): + fmt = tableDict["EntryFormat"] + nItems = tableDict["MappingCount"] + + innerBits = 1 + (fmt & 0x000F) + innerMask = (1 << innerBits) - 1 + outerMask = 0xFFFFFFFF - innerMask + outerShift = 16 - innerBits + + entrySize = 1 + ((fmt & 0x0030) >> 4) + readArray = { + 1: reader.readUInt8Array, + 2: reader.readUShortArray, + 3: reader.readUInt24Array, + 4: reader.readULongArray, + }[entrySize] + + return [ + (((raw & outerMask) << outerShift) | (raw & innerMask)) + for raw in readArray(nItems) + ] + + def write(self, writer, font, tableDict, value, repeatIndex=None): + fmt = tableDict["EntryFormat"] + mapping = value + writer["MappingCount"].setValue(len(mapping)) + + innerBits = 1 + (fmt & 0x000F) + innerMask = (1 << innerBits) - 1 + outerShift = 16 - innerBits + + entrySize = 1 + ((fmt & 0x0030) >> 4) + writeArray = { + 1: writer.writeUInt8Array, + 2: writer.writeUShortArray, + 3: writer.writeUInt24Array, + 4: writer.writeULongArray, + }[entrySize] + + writeArray( + [ + (((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask)) + for idx in mapping + ] + ) class VarDataValue(BaseConverter): + def read(self, reader, font, tableDict): + values = [] - def read(self, reader, font, tableDict): - values = [] + regionCount = tableDict["VarRegionCount"] + wordCount = tableDict["NumShorts"] - regionCount = tableDict["VarRegionCount"] - wordCount = tableDict["NumShorts"] + # https://github.com/fonttools/fonttools/issues/2279 + longWords = bool(wordCount & 0x8000) + wordCount = wordCount & 0x7FFF - # https://github.com/fonttools/fonttools/issues/2279 - longWords = bool(wordCount & 0x8000) - wordCount = wordCount & 0x7FFF + if longWords: + readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray + else: + readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array - if longWords: - readBigArray, readSmallArray = reader.readLongArray, reader.readShortArray - else: - readBigArray, readSmallArray = reader.readShortArray, reader.readInt8Array + n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) + values.extend(readBigArray(n1)) + values.extend(readSmallArray(n2 - n1)) + if n2 > regionCount: # Padding + del values[regionCount:] - n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) - values.extend(readBigArray(n1)) - values.extend(readSmallArray(n2 - n1)) - if n2 > regionCount: # Padding - del values[regionCount:] + return values - return values + def write(self, writer, font, tableDict, values, repeatIndex=None): + regionCount = tableDict["VarRegionCount"] + wordCount = tableDict["NumShorts"] - def write(self, writer, font, tableDict, values, repeatIndex=None): - regionCount = tableDict["VarRegionCount"] - wordCount = tableDict["NumShorts"] + # https://github.com/fonttools/fonttools/issues/2279 + longWords = bool(wordCount & 0x8000) + wordCount = wordCount & 0x7FFF - # https://github.com/fonttools/fonttools/issues/2279 - longWords = bool(wordCount & 0x8000) - wordCount = wordCount & 0x7FFF + (writeBigArray, writeSmallArray) = { + False: (writer.writeShortArray, writer.writeInt8Array), + True: (writer.writeLongArray, writer.writeShortArray), + }[longWords] - (writeBigArray, writeSmallArray) = { - False: (writer.writeShortArray, writer.writeInt8Array), - True: (writer.writeLongArray, writer.writeShortArray), - }[longWords] + n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) + writeBigArray(values[:n1]) + writeSmallArray(values[n1:regionCount]) + if n2 > regionCount: # Padding + writer.writeSmallArray([0] * (n2 - regionCount)) - n1, n2 = min(regionCount, wordCount), max(regionCount, wordCount) - writeBigArray(values[:n1]) - writeSmallArray(values[n1:regionCount]) - if n2 > regionCount: # Padding - writer.writeSmallArray([0] * (n2 - regionCount)) + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", value)]) - xmlWriter.newline() + def xmlRead(self, attrs, content, font): + return safeEval(attrs["value"]) - def xmlRead(self, attrs, content, font): - return safeEval(attrs["value"]) class LookupFlag(UShort): - def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.simpletag(name, attrs + [("value", value)]) - flags = [] - if value & 0x01: flags.append("rightToLeft") - if value & 0x02: flags.append("ignoreBaseGlyphs") - if value & 0x04: flags.append("ignoreLigatures") - if value & 0x08: flags.append("ignoreMarks") - if value & 0x10: flags.append("useMarkFilteringSet") - if value & 0xff00: flags.append("markAttachmentType[%i]" % (value >> 8)) - if flags: - xmlWriter.comment(" ".join(flags)) - xmlWriter.newline() + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + flags = [] + if value & 0x01: + flags.append("rightToLeft") + if value & 0x02: + flags.append("ignoreBaseGlyphs") + if value & 0x04: + flags.append("ignoreLigatures") + if value & 0x08: + flags.append("ignoreMarks") + if value & 0x10: + flags.append("useMarkFilteringSet") + if value & 0xFF00: + flags.append("markAttachmentType[%i]" % (value >> 8)) + if flags: + xmlWriter.comment(" ".join(flags)) + xmlWriter.newline() class _UInt8Enum(UInt8): - enumClass = NotImplemented + enumClass = NotImplemented + + def read(self, reader, font, tableDict): + return self.enumClass(super().read(reader, font, tableDict)) + + @classmethod + def fromString(cls, value): + return getattr(cls.enumClass, value.upper()) - def read(self, reader, font, tableDict): - return self.enumClass(super().read(reader, font, tableDict)) - @classmethod - def fromString(cls, value): - return getattr(cls.enumClass, value.upper()) - @classmethod - def toString(cls, value): - return cls.enumClass(value).name.lower() + @classmethod + def toString(cls, value): + return cls.enumClass(value).name.lower() class ExtendMode(_UInt8Enum): - enumClass = _ExtendMode + enumClass = _ExtendMode class CompositeMode(_UInt8Enum): - enumClass = _CompositeMode + enumClass = _CompositeMode converterMapping = { - # type class - "int8": Int8, - "int16": Short, - "uint8": UInt8, - "uint16": UShort, - "uint24": UInt24, - "uint32": ULong, - "char64": Char64, - "Flags32": Flags32, - "VarIndex": VarIndex, - "Version": Version, - "Tag": Tag, - "GlyphID": GlyphID, - "GlyphID32": GlyphID32, - "NameID": NameID, - "DeciPoints": DeciPoints, - "Fixed": Fixed, - "F2Dot14": F2Dot14, - "Angle": Angle, - "BiasedAngle": BiasedAngle, - "struct": Struct, - "Offset": Table, - "LOffset": LTable, - "Offset24": Table24, - "ValueRecord": ValueRecord, - "DeltaValue": DeltaValue, - "VarIdxMapValue": VarIdxMapValue, - "VarDataValue": VarDataValue, - "LookupFlag": LookupFlag, - "ExtendMode": ExtendMode, - "CompositeMode": CompositeMode, - "STATFlags": STATFlags, - - # AAT - "CIDGlyphMap": CIDGlyphMap, - "GlyphCIDMap": GlyphCIDMap, - "MortChain": StructWithLength, - "MortSubtable": StructWithLength, - "MorxChain": StructWithLength, - "MorxSubtable": MorxSubtableConverter, - - # "Template" types - "AATLookup": lambda C: partial(AATLookup, tableClass=C), - "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C), - "STXHeader": lambda C: partial(STXHeader, tableClass=C), - "OffsetTo": lambda C: partial(Table, tableClass=C), - "LOffsetTo": lambda C: partial(LTable, tableClass=C), - "LOffset24To": lambda C: partial(Table24, tableClass=C), + # type class + "int8": Int8, + "int16": Short, + "uint8": UInt8, + "uint16": UShort, + "uint24": UInt24, + "uint32": ULong, + "char64": Char64, + "Flags32": Flags32, + "VarIndex": VarIndex, + "Version": Version, + "Tag": Tag, + "GlyphID": GlyphID, + "GlyphID32": GlyphID32, + "NameID": NameID, + "DeciPoints": DeciPoints, + "Fixed": Fixed, + "F2Dot14": F2Dot14, + "Angle": Angle, + "BiasedAngle": BiasedAngle, + "struct": Struct, + "Offset": Table, + "LOffset": LTable, + "Offset24": Table24, + "ValueRecord": ValueRecord, + "DeltaValue": DeltaValue, + "VarIdxMapValue": VarIdxMapValue, + "VarDataValue": VarDataValue, + "LookupFlag": LookupFlag, + "ExtendMode": ExtendMode, + "CompositeMode": CompositeMode, + "STATFlags": STATFlags, + # AAT + "CIDGlyphMap": CIDGlyphMap, + "GlyphCIDMap": GlyphCIDMap, + "MortChain": StructWithLength, + "MortSubtable": StructWithLength, + "MorxChain": StructWithLength, + "MorxSubtable": MorxSubtableConverter, + # "Template" types + "AATLookup": lambda C: partial(AATLookup, tableClass=C), + "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C), + "STXHeader": lambda C: partial(STXHeader, tableClass=C), + "OffsetTo": lambda C: partial(Table, tableClass=C), + "LOffsetTo": lambda C: partial(LTable, tableClass=C), + "LOffset24To": lambda C: partial(Table24, tableClass=C), } |