aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/ttLib/tables/otBase.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/ttLib/tables/otBase.py')
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py215
1 files changed, 154 insertions, 61 deletions
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 3c07f9e1..bc2c9fba 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -1,9 +1,10 @@
-from fontTools.misc.py23 import Tag, bytesjoin
+from fontTools.misc.textTools import Tag, bytesjoin
from .DefaultTable import DefaultTable
import sys
import array
import struct
import logging
+from typing import Iterator, NamedTuple, Optional
log = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ class BaseTTXConverter(DefaultTable):
"""
def decompile(self, data, font):
+ """Create an object from the binary data. Called automatically on access."""
from . import otTables
reader = OTTableReader(data, tableTag=self.tableTag)
tableClass = getattr(otTables, self.tableTag)
@@ -41,26 +43,28 @@ class BaseTTXConverter(DefaultTable):
self.table.decompile(reader, font)
def compile(self, font):
- """ Create a top-level OTTableWriter for the GPOS/GSUB table.
- Call the compile method for the the table
- for each 'converter' record in the table converter list
- call converter's write method for each item in the value.
- - For simple items, the write method adds a string to the
- writer's self.items list.
- - For Struct/Table/Subtable items, it add first adds new writer to the
- to the writer's self.items, then calls the item's compile method.
- This creates a tree of writers, rooted at the GUSB/GPOS writer, with
- each writer representing a table, and the writer.items list containing
- the child data strings and writers.
- call the getAllData method
- call _doneWriting, which removes duplicates
- call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
- Traverse the flat list of tables, calling getDataLength on each to update their position
- Traverse the flat list of tables again, calling getData each get the data in the table, now that
- pos's and offset are known.
-
- If a lookup subtable overflows an offset, we have to start all over.
- """
+ """Compiles the table into binary. Called automatically on save."""
+
+ # General outline:
+ # Create a top-level OTTableWriter for the GPOS/GSUB table.
+ # Call the compile method for the the table
+ # for each 'converter' record in the table converter list
+ # call converter's write method for each item in the value.
+ # - For simple items, the write method adds a string to the
+ # writer's self.items list.
+ # - For Struct/Table/Subtable items, it add first adds new writer to the
+ # to the writer's self.items, then calls the item's compile method.
+ # This creates a tree of writers, rooted at the GUSB/GPOS writer, with
+ # each writer representing a table, and the writer.items list containing
+ # the child data strings and writers.
+ # call the getAllData method
+ # call _doneWriting, which removes duplicates
+ # call _gatherTables. This traverses the tables, adding unique occurences to a flat list of tables
+ # Traverse the flat list of tables, calling getDataLength on each to update their position
+ # Traverse the flat list of tables again, calling getData each get the data in the table, now that
+ # pos's and offset are known.
+
+ # If a lookup subtable overflows an offset, we have to start all over.
overflowRecord = None
while True:
@@ -105,6 +109,13 @@ class BaseTTXConverter(DefaultTable):
self.table.fromXML(name, attrs, content, font)
self.table.populateDefaults()
+ def ensureDecompiled(self):
+ self.table.ensureDecompiled(recurse=True)
+
+
+# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
+assert len(struct.pack('i', 0)) == 4
+assert array.array('i').itemsize == 4, "Oops, file a bug against fonttools."
class OTTableReader(object):
@@ -140,32 +151,43 @@ class OTTableReader(object):
value, = struct.unpack(f">{typecode}", self.data[pos:newpos])
self.pos = newpos
return value
-
- def readUShort(self):
- return self.readValue("H", staticSize=2)
-
def readArray(self, typecode, staticSize, count):
pos = self.pos
newpos = pos + count * staticSize
value = array.array(typecode, self.data[pos:newpos])
if sys.byteorder != "big": value.byteswap()
self.pos = newpos
- return value
-
- def readUShortArray(self, count):
- return self.readArray("H", staticSize=2, count=count)
+ return value.tolist()
def readInt8(self):
return self.readValue("b", staticSize=1)
+ def readInt8Array(self, count):
+ return self.readArray("b", staticSize=1, count=count)
def readShort(self):
return self.readValue("h", staticSize=2)
+ def readShortArray(self, count):
+ return self.readArray("h", staticSize=2, count=count)
def readLong(self):
- return self.readValue("l", staticSize=4)
+ return self.readValue("i", staticSize=4)
+ def readLongArray(self, count):
+ return self.readArray("i", staticSize=4, count=count)
def readUInt8(self):
return self.readValue("B", staticSize=1)
+ def readUInt8Array(self, count):
+ return self.readArray("B", staticSize=1, count=count)
+
+ def readUShort(self):
+ return self.readValue("H", staticSize=2)
+ def readUShortArray(self, count):
+ return self.readArray("H", staticSize=2, count=count)
+
+ def readULong(self):
+ return self.readValue("I", staticSize=4)
+ def readULongArray(self, count):
+ return self.readArray("I", staticSize=4, count=count)
def readUInt24(self):
pos = self.pos
@@ -173,9 +195,8 @@ class OTTableReader(object):
value, = struct.unpack(">l", b'\0'+self.data[pos:newpos])
self.pos = newpos
return value
-
- def readULong(self):
- return self.readValue("L", staticSize=4)
+ def readUInt24Array(self, count):
+ return [self.readUInt24() for _ in range(count)]
def readTag(self):
pos = self.pos
@@ -316,6 +337,12 @@ class OTTableWriter(object):
items[i] = item.getCountData()
elif hasattr(item, "getData"):
item._doneWriting(internedTables)
+ # At this point, all subwriters are hashable based on their items.
+ # (See hash and comparison magic methods above.) So the ``setdefault``
+ # call here will return the first writer object we've seen with
+ # equal content, or store it in the dictionary if it's not been
+ # seen yet. We therefore replace the subwriter object with an equivalent
+ # object, which deduplicates the tree.
if not dontShare:
items[i] = item = internedTables.setdefault(item, item)
self.items = tuple(items)
@@ -344,13 +371,13 @@ class OTTableWriter(object):
tables, extTables, done = extTables, None, {}
# add Coverage table if it is sorted last.
- sortCoverageLast = 0
+ sortCoverageLast = False
if hasattr(self, "sortCoverageLast"):
# Find coverage table
for i in range(numItems):
item = self.items[i]
- if hasattr(item, "name") and (item.name == "Coverage"):
- sortCoverageLast = 1
+ if getattr(item, 'name', None) == "Coverage":
+ sortCoverageLast = True
break
if id(item) not in done:
item._gatherTables(tables, extTables, done)
@@ -363,7 +390,7 @@ class OTTableWriter(object):
if not hasattr(item, "getData"):
continue
- if sortCoverageLast and (i==1) and item.name == 'Coverage':
+ if sortCoverageLast and (i==1) and getattr(item, 'name', None) == 'Coverage':
# we've already 'gathered' it above
continue
@@ -419,33 +446,52 @@ class OTTableWriter(object):
def writeValue(self, typecode, value):
self.items.append(struct.pack(f">{typecode}", value))
+ def writeArray(self, typecode, values):
+ a = array.array(typecode, values)
+ if sys.byteorder != "big": a.byteswap()
+ self.items.append(a.tobytes())
- def writeUShort(self, value):
- assert 0 <= value < 0x10000, value
- self.items.append(struct.pack(">H", value))
+ def writeInt8(self, value):
+ assert -128 <= value < 128, value
+ self.items.append(struct.pack(">b", value))
+ def writeInt8Array(self, values):
+ self.writeArray('b', values)
def writeShort(self, value):
assert -32768 <= value < 32768, value
self.items.append(struct.pack(">h", value))
+ def writeShortArray(self, values):
+ self.writeArray('h', values)
+
+ def writeLong(self, value):
+ self.items.append(struct.pack(">i", value))
+ def writeLongArray(self, values):
+ self.writeArray('i', values)
def writeUInt8(self, value):
assert 0 <= value < 256, value
self.items.append(struct.pack(">B", value))
+ def writeUInt8Array(self, values):
+ self.writeArray('B', values)
- def writeInt8(self, value):
- assert -128 <= value < 128, value
- self.items.append(struct.pack(">b", value))
+ def writeUShort(self, value):
+ assert 0 <= value < 0x10000, value
+ self.items.append(struct.pack(">H", value))
+ def writeUShortArray(self, values):
+ self.writeArray('H', values)
+
+ def writeULong(self, value):
+ self.items.append(struct.pack(">I", value))
+ def writeULongArray(self, values):
+ self.writeArray('I', values)
def writeUInt24(self, value):
assert 0 <= value < 0x1000000, value
b = struct.pack(">L", value)
self.items.append(b[1:])
-
- def writeLong(self, value):
- self.items.append(struct.pack(">l", value))
-
- def writeULong(self, value):
- self.items.append(struct.pack(">L", value))
+ def writeUInt24Array(self, values):
+ for value in values:
+ self.writeUInt24(value)
def writeTag(self, tag):
tag = Tag(tag).tobytes()
@@ -532,11 +578,11 @@ def packUShort(value):
def packULong(value):
assert 0 <= value < 0x100000000, value
- return struct.pack(">L", value)
+ return struct.pack(">I", value)
def packUInt24(value):
assert 0 <= value < 0x1000000, value
- return struct.pack(">L", value)[1:]
+ return struct.pack(">I", value)[1:]
class BaseTable(object):
@@ -554,13 +600,16 @@ class BaseTable(object):
raise AttributeError(attr)
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=False):
reader = self.__dict__.get("reader")
if reader:
del self.reader
font = self.font
del self.font
self.decompile(reader, font)
+ if recurse:
+ for subtable in self.iterSubTables():
+ subtable.value.ensureDecompiled(recurse)
@classmethod
def getRecordSize(cls, reader):
@@ -571,7 +620,7 @@ class BaseTable(object):
countValue = 1
if conv.repeat:
if conv.repeat in reader:
- countValue = reader[conv.repeat]
+ countValue = reader[conv.repeat] + conv.aux
else:
return NotImplemented
totalSize += size * countValue
@@ -698,14 +747,11 @@ class BaseTable(object):
else:
# conv.repeat is a propagated count
writer[conv.repeat].setValue(countValue)
- values = value
- for i, value in enumerate(values):
- try:
- conv.write(writer, font, table, value, i)
- except Exception as e:
- name = value.__class__.__name__ if value is not None else conv.name
- e.args = e.args + (name+'['+str(i)+']',)
- raise
+ try:
+ conv.writeArray(writer, font, table, value)
+ except Exception as e:
+ e.args = e.args + (conv.name+'[]',)
+ raise
elif conv.isCount:
# Special-case Count values.
# Assumption: a Count field will *always* precede
@@ -812,6 +858,37 @@ class BaseTable(object):
return self.__dict__ == other.__dict__
+ class SubTableEntry(NamedTuple):
+ """See BaseTable.iterSubTables()"""
+ name: str
+ value: "BaseTable"
+ index: Optional[int] = None # index into given array, None for single values
+
+ def iterSubTables(self) -> Iterator[SubTableEntry]:
+ """Yield (name, value, index) namedtuples for all subtables of current table.
+
+ A sub-table is an instance of BaseTable (or subclass thereof) that is a child
+ of self, the current parent table.
+ The tuples also contain the attribute name (str) of the of parent table to get
+ a subtable, and optionally, for lists of subtables (i.e. attributes associated
+ with a converter that has a 'repeat'), an index into the list containing the
+ given subtable value.
+ This method can be useful to traverse trees of otTables.
+ """
+ for conv in self.getConverters():
+ name = conv.name
+ value = getattr(self, name, None)
+ if value is None:
+ continue
+ if isinstance(value, BaseTable):
+ yield self.SubTableEntry(name, value)
+ elif isinstance(value, list):
+ yield from (
+ self.SubTableEntry(name, v, index=i)
+ for i, v in enumerate(value)
+ if isinstance(v, BaseTable)
+ )
+
class FormatSwitchingBaseTable(BaseTable):
@@ -823,6 +900,15 @@ class FormatSwitchingBaseTable(BaseTable):
return NotImplemented
def getConverters(self):
+ try:
+ fmt = self.Format
+ except AttributeError:
+ # some FormatSwitchingBaseTables (e.g. Coverage) no longer have 'Format'
+ # attribute after fully decompiled, only gain one in preWrite before being
+ # recompiled. In the decompiled state, these hand-coded classes defined in
+ # otTables.py lose their format-specific nature and gain more high-level
+ # attributes that are not tied to converters.
+ return []
return self.converters.get(self.Format, [])
def getConverterByName(self, name):
@@ -970,6 +1056,13 @@ class ValueRecord(object):
format = format | valueRecordFormatDict[name][0]
return format
+ def getEffectiveFormat(self):
+ format = 0
+ for name,value in self.__dict__.items():
+ if value:
+ format = format | valueRecordFormatDict[name][0]
+ return format
+
def toXML(self, xmlWriter, font, valueName, attrs=None):
if attrs is None:
simpleItems = []