Buckets:
ktongue/docker_container / simsite /venv /lib /python3.14 /site-packages /fontTools /ttLib /tables /otConverters.py
| 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.ttLib.tables.TupleVariation import TupleVariation | |
| from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound | |
| from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval | |
| from fontTools.misc.lazyTools import LazyList | |
| from fontTools.ttLib import OPTIMIZE_FONT_SPEED, getSearchRange | |
| from .otBase import ( | |
| CountReference, | |
| FormatSwitchingBaseTable, | |
| OTTableReader, | |
| OTTableWriter, | |
| ValueRecordFactory, | |
| ) | |
| from .otTables import ( | |
| lookupTypes, | |
| VarCompositeGlyph, | |
| AATStateTable, | |
| AATState, | |
| AATAction, | |
| ContextualMorphAction, | |
| LigatureMorphAction, | |
| InsertionMorphAction, | |
| MorxSubtable, | |
| ExtendMode as _ExtendMode, | |
| CompositeMode as _CompositeMode, | |
| NO_VARIATION_INDEX, | |
| ) | |
| from itertools import zip_longest, accumulate | |
| from functools import partial | |
| from types import SimpleNamespace | |
| 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) knows 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 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 | |
| if self.aux and not self.repeat: | |
| self.aux = compile(self.aux, "<string>", "eval") | |
| 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", | |
| "AxisIndicesList", | |
| ] | |
| 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: | |
| def get_read_item(): | |
| reader_copy = reader.copy() | |
| pos = reader.pos | |
| def read_item(i): | |
| reader_copy.seek(pos + i * recordSize) | |
| return self.read(reader_copy, font, {}) | |
| return read_item | |
| read_item = get_read_item() | |
| l = LazyList(read_item 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): | |
| def toString(value): | |
| return value | |
| 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): | |
| 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): | |
| 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): | |
| 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)) | |
| def fromInt(cls, value): | |
| return fi2fl(value, cls.precisionBits) | |
| def toInt(cls, value): | |
| return fl2fi(value, cls.precisionBits) | |
| def fromString(cls, value): | |
| return str2fl(value, cls.precisionBits) | |
| 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 | |
| def fromInt(cls, value): | |
| return (super().fromInt(value) + cls.bias) * 180 | |
| def toInt(cls, value): | |
| return super().toInt((value / 180) - cls.bias) | |
| def fromString(cls, value): | |
| # quantize to nearest multiples of minimum fixed-precision angle | |
| return otRound(float(value) / cls.factor) * cls.factor | |
| 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) | |
| def fromString(value): | |
| return ve2fi(value) | |
| def toString(value): | |
| return "0x%08x" % value | |
| 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, | |
| ) | |
| 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 TupleValues: | |
| def read(self, data, font): | |
| return TupleVariation.decompileDeltas_(None, data)[0] | |
| def write(self, writer, font, tableDict, values, repeatIndex=None): | |
| optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED] | |
| return bytes( | |
| TupleVariation.compileDeltaValues_(values, optimizeSize=not optimizeSpeed) | |
| ) | |
| def xmlRead(self, attrs, content, font): | |
| return safeEval(attrs["value"]) | |
| def xmlWrite(self, xmlWriter, font, value, name, attrs): | |
| xmlWriter.simpletag(name, attrs + [("value", value)]) | |
| xmlWriter.newline() | |
| class CFF2Index(BaseConverter): | |
| def __init__( | |
| self, | |
| name, | |
| repeat, | |
| aux, | |
| tableClass=None, | |
| *, | |
| itemClass=None, | |
| itemConverterClass=None, | |
| description="", | |
| ): | |
| BaseConverter.__init__( | |
| self, name, repeat, aux, tableClass, description=description | |
| ) | |
| self._itemClass = itemClass | |
| self._converter = ( | |
| itemConverterClass() if itemConverterClass is not None else None | |
| ) | |
| def read(self, reader, font, tableDict): | |
| count = reader.readULong() | |
| if count == 0: | |
| return [] | |
| offSize = reader.readUInt8() | |
| def getReadArray(reader, offSize): | |
| return { | |
| 1: reader.readUInt8Array, | |
| 2: reader.readUShortArray, | |
| 3: reader.readUInt24Array, | |
| 4: reader.readULongArray, | |
| }[offSize] | |
| readArray = getReadArray(reader, offSize) | |
| lazy = font.lazy is not False and count > 8 | |
| if not lazy: | |
| offsets = readArray(count + 1) | |
| items = [] | |
| lastOffset = offsets.pop(0) | |
| reader.readData(lastOffset - 1) # In case first offset is not 1 | |
| for offset in offsets: | |
| assert lastOffset <= offset | |
| item = reader.readData(offset - lastOffset) | |
| if self._itemClass is not None: | |
| obj = self._itemClass() | |
| obj.decompile(item, font, reader.localState) | |
| item = obj | |
| elif self._converter is not None: | |
| item = self._converter.read(item, font) | |
| items.append(item) | |
| lastOffset = offset | |
| return items | |
| else: | |
| def get_read_item(): | |
| reader_copy = reader.copy() | |
| offset_pos = reader.pos | |
| data_pos = offset_pos + (count + 1) * offSize - 1 | |
| readArray = getReadArray(reader_copy, offSize) | |
| def read_item(i): | |
| reader_copy.seek(offset_pos + i * offSize) | |
| offsets = readArray(2) | |
| reader_copy.seek(data_pos + offsets[0]) | |
| item = reader_copy.readData(offsets[1] - offsets[0]) | |
| if self._itemClass is not None: | |
| obj = self._itemClass() | |
| obj.decompile(item, font, reader_copy.localState) | |
| item = obj | |
| elif self._converter is not None: | |
| item = self._converter.read(item, font) | |
| return item | |
| return read_item | |
| read_item = get_read_item() | |
| l = LazyList([read_item] * count) | |
| # TODO: Advance reader | |
| return l | |
| def write(self, writer, font, tableDict, values, repeatIndex=None): | |
| items = values | |
| writer.writeULong(len(items)) | |
| if not len(items): | |
| return | |
| if self._itemClass is not None: | |
| items = [item.compile(font) for item in items] | |
| elif self._converter is not None: | |
| items = [ | |
| self._converter.write(writer, font, tableDict, item, i) | |
| for i, item in enumerate(items) | |
| ] | |
| offsets = [len(item) for item in items] | |
| offsets = list(accumulate(offsets, initial=1)) | |
| lastOffset = offsets[-1] | |
| offSize = ( | |
| 1 | |
| if lastOffset < 0x100 | |
| else 2 if lastOffset < 0x10000 else 3 if lastOffset < 0x1000000 else 4 | |
| ) | |
| writer.writeUInt8(offSize) | |
| writeArray = { | |
| 1: writer.writeUInt8Array, | |
| 2: writer.writeUShortArray, | |
| 3: writer.writeUInt24Array, | |
| 4: writer.writeULongArray, | |
| }[offSize] | |
| writeArray(offsets) | |
| for item in items: | |
| writer.writeData(item) | |
| def xmlRead(self, attrs, content, font): | |
| if self._itemClass is not None: | |
| obj = self._itemClass() | |
| obj.fromXML(None, attrs, content, font) | |
| return obj | |
| elif self._converter is not None: | |
| return self._converter.xmlRead(attrs, content, font) | |
| else: | |
| raise NotImplementedError() | |
| def xmlWrite(self, xmlWriter, font, value, name, attrs): | |
| if self._itemClass is not None: | |
| for i, item in enumerate(value): | |
| item.toXML(xmlWriter, font, [("index", i)], name) | |
| elif self._converter is not None: | |
| for i, item in enumerate(value): | |
| self._converter.xmlWrite( | |
| xmlWriter, font, item, name, attrs + [("index", i)] | |
| ) | |
| else: | |
| raise NotImplementedError() | |
| 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)) | |
| def fromString(cls, value): | |
| return getattr(cls.enumClass, value.upper()) | |
| 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, | |
| "int32": Long, | |
| "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, | |
| "TupleList": partial(CFF2Index, itemConverterClass=TupleValues), | |
| "VarCompositeGlyphList": partial(CFF2Index, itemClass=VarCompositeGlyph), | |
| # 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), | |
| } | |
Xet Storage Details
- Size:
- 74.2 kB
- Xet hash:
- 094551aae1dcb09479595045edf8ee416f086dc80e491b4f014b746d61799cb8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.