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)
[docs]
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
[docs]
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
[docs]
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
[docs]
def getRecordSize(self, reader):
if hasattr(self, "staticSize"):
return self.staticSize
return NotImplemented
[docs]
def read(self, reader, font, tableDict):
"""Read a value from the reader."""
raise NotImplementedError(self)
[docs]
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
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
"""Write a value to the writer."""
raise NotImplementedError(self)
[docs]
def xmlRead(self, attrs, content, font):
"""Read a value from XML."""
raise NotImplementedError(self)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
"""Write a value to XML."""
raise NotImplementedError(self)
varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")
[docs]
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))
[docs]
class SimpleValue(BaseConverter):
[docs]
@staticmethod
def toString(value):
return value
[docs]
@staticmethod
def fromString(value):
return value
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", self.toString(value))])
xmlWriter.newline()
[docs]
def xmlRead(self, attrs, content, font):
return self.fromString(attrs["value"])
[docs]
class OptionalValue(SimpleValue):
DEFAULT = None
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
if value != self.DEFAULT:
attrs.append(("value", self.toString(value)))
xmlWriter.simpletag(name, attrs)
xmlWriter.newline()
[docs]
def xmlRead(self, attrs, content, font):
if "value" in attrs:
return self.fromString(attrs["value"])
return self.DEFAULT
[docs]
class IntValue(SimpleValue):
[docs]
@staticmethod
def fromString(value):
return int(value, 0)
[docs]
class Long(IntValue):
staticSize = 4
[docs]
def read(self, reader, font, tableDict):
return reader.readLong()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readLongArray(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeLong(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeLongArray(values)
[docs]
class ULong(IntValue):
staticSize = 4
[docs]
def read(self, reader, font, tableDict):
return reader.readULong()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readULongArray(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeULong(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeULongArray(values)
[docs]
class Flags32(ULong):
[docs]
@staticmethod
def toString(value):
return "0x%08X" % value
[docs]
class VarIndex(OptionalValue, ULong):
DEFAULT = NO_VARIATION_INDEX
[docs]
class Short(IntValue):
staticSize = 2
[docs]
def read(self, reader, font, tableDict):
return reader.readShort()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readShortArray(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeShort(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeShortArray(values)
[docs]
class UShort(IntValue):
staticSize = 2
[docs]
def read(self, reader, font, tableDict):
return reader.readUShort()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readUShortArray(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUShort(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeUShortArray(values)
[docs]
class Int8(IntValue):
staticSize = 1
[docs]
def read(self, reader, font, tableDict):
return reader.readInt8()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readInt8Array(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeInt8(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeInt8Array(values)
[docs]
class UInt8(IntValue):
staticSize = 1
[docs]
def read(self, reader, font, tableDict):
return reader.readUInt8()
[docs]
def readArray(self, reader, font, tableDict, count):
return reader.readUInt8Array(count)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUInt8(value)
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeUInt8Array(values)
[docs]
class UInt24(IntValue):
staticSize = 3
[docs]
def read(self, reader, font, tableDict):
return reader.readUInt24()
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUInt24(value)
[docs]
class ComputedInt(IntValue):
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
if value is not None:
xmlWriter.comment("%s=%s" % (name, value))
xmlWriter.newline()
[docs]
class ComputedUInt8(ComputedInt, UInt8):
pass
[docs]
class ComputedUShort(ComputedInt, UShort):
pass
[docs]
class ComputedULong(ComputedInt, ULong):
pass
[docs]
class Tag(SimpleValue):
staticSize = 4
[docs]
def read(self, reader, font, tableDict):
return reader.readTag()
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeTag(value)
[docs]
class GlyphID(SimpleValue):
staticSize = 2
typecode = "H"
[docs]
def readArray(self, reader, font, tableDict, count):
return font.getGlyphNameMany(
reader.readArray(self.typecode, self.staticSize, count)
)
[docs]
def read(self, reader, font, tableDict):
return font.getGlyphName(reader.readValue(self.typecode, self.staticSize))
[docs]
def writeArray(self, writer, font, tableDict, values):
writer.writeArray(self.typecode, font.getGlyphIDMany(values))
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeValue(self.typecode, font.getGlyphID(value))
[docs]
class GlyphID32(GlyphID):
staticSize = 4
typecode = "L"
[docs]
class NameID(UShort):
[docs]
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()
[docs]
class STATFlags(UShort):
[docs]
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()
[docs]
class FloatValue(SimpleValue):
[docs]
@staticmethod
def fromString(value):
return float(value)
[docs]
class DeciPoints(FloatValue):
staticSize = 2
[docs]
def read(self, reader, font, tableDict):
return reader.readUShort() / 10
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUShort(round(value * 10))
[docs]
class BaseFixedValue(FloatValue):
staticSize = NotImplemented
precisionBits = NotImplemented
readerMethod = NotImplemented
writerMethod = NotImplemented
[docs]
def read(self, reader, font, tableDict):
return self.fromInt(getattr(reader, self.readerMethod)())
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
getattr(writer, self.writerMethod)(self.toInt(value))
[docs]
@classmethod
def fromInt(cls, value):
return fi2fl(value, cls.precisionBits)
[docs]
@classmethod
def toInt(cls, value):
return fl2fi(value, cls.precisionBits)
[docs]
@classmethod
def fromString(cls, value):
return str2fl(value, cls.precisionBits)
[docs]
@classmethod
def toString(cls, value):
return fl2str(value, cls.precisionBits)
[docs]
class Fixed(BaseFixedValue):
staticSize = 4
precisionBits = 16
readerMethod = "readLong"
writerMethod = "writeLong"
[docs]
class F2Dot14(BaseFixedValue):
staticSize = 2
precisionBits = 14
readerMethod = "readShort"
writerMethod = "writeShort"
[docs]
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
[docs]
@classmethod
def fromInt(cls, value):
return (super().fromInt(value) + cls.bias) * 180
[docs]
@classmethod
def toInt(cls, value):
return super().toInt((value / 180) - cls.bias)
[docs]
@classmethod
def fromString(cls, value):
# quantize to nearest multiples of minimum fixed-precision angle
return otRound(float(value) / cls.factor) * cls.factor
[docs]
@classmethod
def toString(cls, value):
return nearestMultipleShortestRepr(value, cls.factor)
[docs]
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
[docs]
class Version(SimpleValue):
staticSize = 4
[docs]
def read(self, reader, font, tableDict):
value = reader.readLong()
return value
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
value = fi2ve(value)
writer.writeLong(value)
[docs]
@staticmethod
def fromString(value):
return ve2fi(value)
[docs]
@staticmethod
def toString(value):
return "0x%08x" % value
[docs]
@staticmethod
def fromFloat(v):
return fl2fi(v, 16)
[docs]
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
[docs]
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
[docs]
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)
[docs]
class Struct(BaseConverter):
[docs]
def getRecordSize(self, reader):
return self.tableClass and self.tableClass.getRecordSize(reader)
[docs]
def read(self, reader, font, tableDict):
table = self.tableClass()
table.decompile(reader, font)
return table
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
value.compile(writer, font)
[docs]
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)
[docs]
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)
[docs]
class StructWithLength(Struct):
[docs]
def read(self, reader, font, tableDict):
pos = reader.pos
table = self.tableClass()
table.decompile(reader, font)
reader.seek(pos + table.StructLength)
return table
[docs]
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()
[docs]
class Table(Struct):
staticSize = 2
[docs]
def readOffset(self, reader):
return reader.readUShort()
[docs]
def writeNullOffset(self, writer):
writer.writeUShort(0)
[docs]
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
[docs]
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)
[docs]
class LTable(Table):
staticSize = 4
[docs]
def readOffset(self, reader):
return reader.readULong()
[docs]
def writeNullOffset(self, writer):
writer.writeULong(0)
# Table pointed to by a 24-bit, 3-byte long offset
[docs]
class Table24(Table):
staticSize = 3
[docs]
def readOffset(self, reader):
return reader.readUInt24()
[docs]
def writeNullOffset(self, writer):
writer.writeUInt24(0)
# TODO Clean / merge the SubTable and SubStruct
[docs]
class SubStruct(Struct):
[docs]
def getConverter(self, tableType, lookupType):
tableClass = self.lookupTypes[tableType][lookupType]
return self.__class__(self.name, self.repeat, self.aux, tableClass)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs)
[docs]
class SubTable(Table):
[docs]
def getConverter(self, tableType, lookupType):
tableClass = self.lookupTypes[tableType][lookupType]
return self.__class__(self.name, self.repeat, self.aux, tableClass)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs)
[docs]
class ExtSubTable(LTable, SubTable):
[docs]
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)
[docs]
class FeatureParams(Table):
[docs]
def getConverter(self, featureTag):
tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams)
return self.__class__(self.name, self.repeat, self.aux, tableClass)
[docs]
class ValueRecord(ValueFormat):
[docs]
def getRecordSize(self, reader):
return 2 * len(reader[self.which])
[docs]
def read(self, reader, font, tableDict):
return reader[self.which].readValueRecord(reader, font)
[docs]
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer[self.which].writeValueRecord(writer, font, value)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
if value is None:
pass # NULL table, ignore
else:
value.toXML(xmlWriter, font, self.name, attrs)
[docs]
def xmlRead(self, attrs, content, font):
from .otBase import ValueRecord
value = ValueRecord()
value.fromXML(None, attrs, content, font)
return value
[docs]
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
)
[docs]
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
[docs]
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,
)
[docs]
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
[docs]
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.
[docs]
class AATLookupWithDataOffset(BaseConverter):
[docs]
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
[docs]
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)
[docs]
def xmlRead(self, attrs, content, font):
lookup = AATLookup("DataOffsets", None, None, self.tableClass)
return lookup.xmlRead(attrs, content, font)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
lookup = AATLookup("DataOffsets", None, None, self.tableClass)
lookup.xmlWrite(xmlWriter, font, value, name, attrs)
[docs]
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"
[docs]
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
[docs]
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()
[docs]
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
[docs]
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.
[docs]
class CIDGlyphMap(BaseConverter):
[docs]
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
[docs]
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))
[docs]
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
[docs]
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()
[docs]
class GlyphCIDMap(BaseConverter):
[docs]
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
[docs]
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))
[docs]
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
[docs]
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()
[docs]
class DeltaValue(BaseConverter):
[docs]
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
[docs]
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)
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
xmlWriter.newline()
[docs]
def xmlRead(self, attrs, content, font):
return safeEval(attrs["value"])
[docs]
class VarIdxMapValue(BaseConverter):
[docs]
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)
]
[docs]
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
]
)
[docs]
class VarDataValue(BaseConverter):
[docs]
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
[docs]
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))
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
xmlWriter.newline()
[docs]
def xmlRead(self, attrs, content, font):
return safeEval(attrs["value"])
[docs]
class TupleValues:
[docs]
def read(self, data, font):
return TupleVariation.decompileDeltas_(None, data)[0]
[docs]
def write(self, writer, font, tableDict, values, repeatIndex=None):
return bytes(TupleVariation.compileDeltaValues_(values))
[docs]
def xmlRead(self, attrs, content, font):
return safeEval(attrs["value"])
[docs]
def xmlWrite(self, xmlWriter, font, value, name, attrs):
xmlWriter.simpletag(name, attrs + [("value", value)])
xmlWriter.newline()
[docs]
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
)
[docs]
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
[docs]
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)
[docs]
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()
[docs]
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()
[docs]
class LookupFlag(UShort):
[docs]
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()
[docs]
class ExtendMode(_UInt8Enum):
enumClass = _ExtendMode
[docs]
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),
}