| from fontTools.misc.fixedTools import ( |
| 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 itertools import zip_longest |
| from functools import partial |
| import re |
| import struct |
| from typing import Optional |
| import logging |
| |
| |
| log = logging.getLogger(__name__) |
| 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 |
| |
| |
| class _MissingItem(tuple): |
| __slots__ = () |
| |
| |
| try: |
| from collections import UserList |
| except ImportError: |
| 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) |
| |
| |
| 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)) |
| |
| |
| 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"]) |
| |
| |
| 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 |
| |
| |
| class IntValue(SimpleValue): |
| @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) |
| |
| |
| 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) |
| |
| |
| class Flags32(ULong): |
| @staticmethod |
| def toString(value): |
| return "0x%08X" % value |
| |
| |
| class VarIndex(OptionalValue, ULong): |
| 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) |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| class ComputedInt(IntValue): |
| 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 |
| |
| |
| class ComputedUShort(ComputedInt, UShort): |
| pass |
| |
| |
| class ComputedULong(ComputedInt, ULong): |
| 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) |
| |
| |
| 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)) |
| |
| |
| class GlyphID32(GlyphID): |
| 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() |
| |
| |
| 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() |
| |
| |
| class FloatValue(SimpleValue): |
| @staticmethod |
| def fromString(value): |
| return float(value) |
| |
| |
| class DeciPoints(FloatValue): |
| 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)) |
| |
| |
| 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) |
| |
| |
| class Fixed(BaseFixedValue): |
| staticSize = 4 |
| precisionBits = 16 |
| readerMethod = "readLong" |
| writerMethod = "writeLong" |
| |
| |
| class F2Dot14(BaseFixedValue): |
| 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) |
| |
| |
| 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 |
| |
| |
| class Version(SimpleValue): |
| 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) |
| |
| |
| 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) |
| |
| |
| 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() |
| |
| |
| 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) |
| |
| |
| class LTable(Table): |
| staticSize = 4 |
| |
| def readOffset(self, reader): |
| return reader.readULong() |
| |
| def writeNullOffset(self, writer): |
| writer.writeULong(0) |
| |
| |
| # Table pointed to by a 24-bit, 3-byte long offset |
| class Table24(Table): |
| staticSize = 3 |
| |
| def readOffset(self, reader): |
| return reader.readUInt24() |
| |
| 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 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 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 FeatureParams(Table): |
| 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) |
| |
| |
| 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 |
| |
| |
| 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() |
| |
| |
| # The AAT 'ankr' table has an unusual structure: An offset to an AATLookup |
| # followed by an offset to a glyph data table. Other than usual, the |
| # offsets in the AATLookup are not relative to the beginning of |
| # the beginning of the 'ankr' table, but relative to the glyph data table. |
| # So, to find the anchor data for a glyph, one needs to add the offset |
| # 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() |
| 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) |
| |
| |
| # 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() |
| 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() |
| |
| |
| 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() |
| |
| |
| 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"]) |
| |
| |
| 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 |
| ] |
| ) |
| |
| |
| class VarDataValue(BaseConverter): |
| def read(self, reader, font, tableDict): |
| values = [] |
| |
| regionCount = tableDict["VarRegionCount"] |
| wordCount = tableDict["NumShorts"] |
| |
| # 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 |
| |
| 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 |
| |
| 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 |
| |
| (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)) |
| |
| 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 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() |
| |
| |
| class _UInt8Enum(UInt8): |
| 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()) |
| |
| @classmethod |
| def toString(cls, value): |
| return cls.enumClass(value).name.lower() |
| |
| |
| class ExtendMode(_UInt8Enum): |
| enumClass = _ExtendMode |
| |
| |
| class CompositeMode(_UInt8Enum): |
| 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), |
| } |