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 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, "", "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): @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 TupleValues: def read(self, data, font): return TupleVariation.decompileDeltas_(None, data)[0] def write(self, writer, font, tableDict, values, repeatIndex=None): return bytes(TupleVariation.compileDeltaValues_(values)) 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)) @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, "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), }