Merge branch 'main' into HEAD
diff --git a/Lib/fontTools/ttLib/tables/distillery/__init__.py b/Lib/fontTools/ttLib/tables/distillery/__init__.py
new file mode 100644
index 0000000..d15706d
--- /dev/null
+++ b/Lib/fontTools/ttLib/tables/distillery/__init__.py
@@ -0,0 +1,141 @@
+"""This module helps with distilling a set of objects (aka tables) linked together through
+a set of object references (represented eventually by offsets) into a bytestream in a compact
+and efficient manner. This is useful for compiling tables like GSUB/GPOS/GDEF and other
+tables represented in the otData.py module."""
+
+from __future__ import print_function, division, absolute_import
+from fontTools.misc.py23 import *
+
+from fibonacci_heap_mod import Fibonacci_heap
+
+
+def sortMeOut(table):
+ distillery = Distillery()
+ return distillery.distill(table)
+
+class ObjectSet(object):
+
+ def __init__(self):
+ self._s = set()
+ self._d = dict()
+
+ def add(self, obj):
+ self._s.add(id(obj))
+ self._d[id(obj)] = obj
+
+ def remove(self, obj):
+ self._s.remove(id(obj))
+ del self._d[id(obj)]
+
+ def issubset(self, other):
+ assert type(other) == ObjectSet
+ return self._s.issubset(other._s)
+
+ def __iter__(self):
+ return iter(self._d.values())
+
+ def __contains__(self, obj):
+ return id(obj) in self._s
+
+ def __bool__(self):
+ return bool(self._s)
+
+ __nonzero__ = __bool__
+
+ def __len__(self):
+ return len(self._s)
+
+ def __repr__(self):
+ return repr(self._s)
+
+class Distillery(object):
+
+ def __init__(self):
+ self.packedS = ObjectSet()
+ self.readyQ = Fibonacci_heap()
+ self.readyS = ObjectSet()
+ #self.awaitQ = Fibonacci_heap()
+ #self.awaitS = ObjectSet()
+
+ def _itemPriority(self, item):
+ leashLen = (65536)**2 if item.offsetSize == 4 else 65536
+ packedParentsPos = [p.pos for p in item.parents if p in self.packedS]
+ leashStart = min(packedParentsPos) if packedParentsPos else (65536)**2 * 2
+ itemLen = len(item)
+ return (leashStart + leashLen + itemLen)# / 4
+
+ def _itemIsReady(self, item):
+ return item.parents.issubset(self.packedS)
+
+ def _readyEnqueue(self, item):
+ item.node = self.readyQ.enqueue(item, self._itemPriority(item))
+ self.readyS.add(item)
+
+ #def _awaitEnqueue(self, item):
+ # item.node = self.awaitQ.enqueue(item, self._itemPriority(item))
+ # self.awaitS.add(item)
+
+ #def _awaitDelete(self, item):
+ # assert item in self.awaitS
+ # self.awaitS.remove(item)
+ # self.awaitQ.delete(item.node)
+ # del item.node
+
+ def _setParents(self, root, done):
+ done.add(root)
+ root.parents = ObjectSet()
+ for item in root.items:
+ if not hasattr(item, 'getData'):
+ continue
+ if item not in done:
+ self._setParents(item, done)
+ item.parents.add(root)
+
+ def distill(self, root):
+ self._setParents(root, ObjectSet())
+
+ out = []
+
+ self._readyEnqueue(root)
+
+ pos = 0
+ while self.readyQ:
+ entry = self.readyQ.dequeue_min()
+ obj = entry.get_value()
+ self.readyS.remove(obj)
+ prio = entry.get_priority()
+ del entry
+ del obj.node
+
+ out.append(obj)
+ obj.pos = pos
+ self.packedS.add(obj)
+
+ for item in obj.items:
+ if not hasattr(item, "getData"):
+ continue
+
+ assert item not in self.packedS
+ if item in self.readyS:
+ continue
+
+ if self._itemIsReady(item):
+ #if hasattr(item, 'node'):
+ # self._awaitDelete(item)
+
+ self._readyEnqueue(item)
+ else:
+ pass
+ #if hasattr(item, 'node'):
+ # self.awaitQ.decrease_key(item.node, self._itemPriority(item))
+ #else:
+ # self._awaitEnqueue(item)
+
+ pos += len(obj)
+
+ #assert not self.awaitS, self.awaitS
+ #assert not self.awaitQ, self.awaitQ
+ assert not self.readyS, self.readyS
+ assert not self.readyQ, self.readyQ
+
+ return out
diff --git a/Lib/fontTools/ttLib/tables/distillery/dot.py b/Lib/fontTools/ttLib/tables/distillery/dot.py
new file mode 100644
index 0000000..f89dbf3
--- /dev/null
+++ b/Lib/fontTools/ttLib/tables/distillery/dot.py
@@ -0,0 +1,93 @@
+"""Generates a Graphviz DOT file representing the (directed) graph of OTTableWriter objects."""
+
+from __future__ import print_function, division, absolute_import
+from fontTools.misc.py23 import *
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.tables.otBase import OTTableWriter
+import logging
+import sys
+import math
+from collections import deque
+
+log = logging.getLogger(__name__)
+
+def writerId(writer):
+ s = '"' + writer.name
+
+ if hasattr(writer, 'repeatIndex'):
+ s += ' ' + str(writer.repeatIndex)
+
+ s += '\n' + str(id(writer))
+ return s + '"'
+
+def writerLabel(writer):
+ size = writer.getDataLength()
+ return "%s\n%s bytes" % (writer.name, size)
+
+def graph(font, tableTags, stream=sys.stdout):
+ f = stream
+ for tableTag in tableTags:
+ log.debug("Processing %s", tableTag)
+ table = font[tableTag].table
+
+ writer = OTTableWriter(tableTag=tableTag)
+ writer.name = tableTag
+ table.compile(writer, font)
+
+ f.write("digraph %s {\n" % tableTag)
+ f.write(" rankdir=LR;\n")
+
+ names = {}
+ queue = deque()
+
+ queue.append (writer)
+ names[id(writer)] = tableTag
+ f.write('graph [root=%s];\n' % tableTag)
+ f.write('node [margin=0,width=0,height=0];\n')
+ while queue:
+ writer = queue.popleft()
+ lhs = names[id(writer)]
+ label = writerLabel(writer)
+ size = writer.getDataLength()
+ fontsize = 5+math.log(size) * 4
+ color = "green" if len(writer.parents) > 1 else "black"
+ f.write('%s [fontsize=%s,label="%s",color=%s]\n' % (lhs, fontsize, label, color))
+
+ for item in writer.items:
+ if not hasattr(item, "getData"):
+ continue
+ offsetSize = item.offsetSize
+ rhs = writerId(item)
+ f.write(' '+lhs+' -> '+rhs+';\n')
+
+ if id(item) not in names:
+ names[id(item)] = rhs
+ queue.append(item)
+
+ f.write("}\n")
+
+
+
+def main(args=None):
+ from fontTools import configLogger
+
+ if args is None:
+ args = sys.argv[1:]
+
+ # configure the library logger (for >= WARNING)
+ configLogger()
+ # comment this out to enable debug messages from logger
+ # log.setLevel(logging.DEBUG)
+
+ if len(args) < 1:
+ print("usage: fonttools ttLib.tables.distillery.dot font-file.ttf [table-tags...]", file=sys.stderr)
+ sys.exit(1)
+
+ font = TTFont(args[0])
+ tags = args[1:]
+
+ graph(font, tags)
+
+if __name__ == '__main__':
+ import sys
+ sys.exit(main())
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 0e74812..fe77196 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -4,6 +4,7 @@
import array
import struct
import logging
+from . import distillery
log = logging.getLogger(__name__)
@@ -66,6 +67,7 @@
while True:
try:
writer = OTTableWriter(tableTag=self.tableTag)
+ writer.name = self.tableTag
self.table.compile(writer, font)
return writer.getAllData()
@@ -222,24 +224,15 @@
"""Helper class to gather and assemble data for OpenType tables."""
- def __init__(self, localState=None, tableTag=None, offsetSize=2):
- self.items = []
- self.pos = None
+ def __init__(self, localState=None, tableTag=None, name='', offsetSize=2):
self.localState = localState
self.tableTag = tableTag
+ self.name = name
+ self.items = []
+ self.pos = None
self.offsetSize = offsetSize
self.parent = None
- # DEPRECATED: 'longOffset' is kept as a property for backward compat with old code.
- # You should use 'offsetSize' instead (2, 3 or 4 bytes).
- @property
- def longOffset(self):
- return self.offsetSize == 4
-
- @longOffset.setter
- def longOffset(self, value):
- self.offsetSize = 4 if value else 2
-
def __setitem__(self, name, value):
state = self.localState.copy() if self.localState else dict()
state[name] = value
@@ -265,13 +258,16 @@
l = l + len(item)
return l
+ def __len__(self):
+ if not hasattr(self, '_length'):
+ self._length = self.getDataLength()
+ return self._length
+
def getData(self):
"""Assemble the data for this writer/table, without subtables."""
items = list(self.items) # make a shallow copy
pos = self.pos
- numItems = len(items)
- for i in range(numItems):
- item = items[i]
+ for i,item in enumerate(items):
if hasattr(item, "getData"):
if item.offsetSize == 4:
@@ -292,8 +288,10 @@
return bytesjoin(items)
def __hash__(self):
- # only works after self._doneWriting() has been called
- return hash(self.items)
+ if not hasattr(self, '_hash'):
+ items = tuple(id(x) if hasattr(x, 'getData') else x for x in self.items)
+ self._hash = hash(items)
+ return self._hash
def __ne__(self, other):
result = self.__eq__(other)
@@ -302,36 +300,47 @@
def __eq__(self, other):
if type(self) != type(other):
return NotImplemented
- return self.offsetSize == other.offsetSize and self.items == other.items
+ if len(self.items) != len(other.items):
+ return False
+ if self.offsetSize != other.offsetSize:
+ return False
+ return all(x is y if hasattr(x, 'getData') else x == y for x,y in zip(self.items, other.items))
+
+ def _writeCountData(self):
+ # Convert CountData references to data string items.
+ items = self.items
+ for i in range(len(items)):
+ item = items[i]
+ if hasattr(item, "getCountData"):
+ items[i] = item.getCountData()
+ elif hasattr(item, "getData"):
+ item._writeCountData()
def _doneWriting(self, internedTables):
- # Convert CountData references to data string items
- # collapse duplicate table references to a unique entry
+ # Collapse duplicate table references to a unique entry
# "tables" are OTTableWriter objects.
- # For Extension Lookup types, we can
- # eliminate duplicates only within the tree under the Extension Lookup,
- # as offsets may exceed 64K even between Extension LookupTable subtables.
- isExtension = hasattr(self, "Extension")
-
# Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level
# arrays (ScriptList, FeatureList, LookupList) point to the same, possibly
# empty, array. So, we don't share those.
# See: https://github.com/fonttools/fonttools/issues/518
dontShare = hasattr(self, 'DontShare')
- if isExtension:
- internedTables = {}
-
items = self.items
for i in range(len(items)):
item = items[i]
if hasattr(item, "getCountData"):
items[i] = item.getCountData()
- elif hasattr(item, "getData"):
- item._doneWriting(internedTables)
- if not dontShare:
- items[i] = item = internedTables.setdefault(item, item)
+ if not hasattr(item, "getData"):
+ continue
+
+ item._doneWriting(internedTables)
+ if not dontShare:
+ internedItem = internedTables.get(item)
+ if internedItem:
+ item = items[i] = internedItem
+ else:
+ internedTables[item] = item
self.items = tuple(items)
def _gatherTables(self, tables, extTables, done):
@@ -387,44 +396,21 @@
# Item is already written out by other parent
pass
- selfTables.append(self)
+ self.items = tuple(items)
def getAllData(self):
"""Assemble all data, including all subtables."""
+ self._writeCountData()
internedTables = {}
self._doneWriting(internedTables)
- tables = []
- extTables = []
- done = {}
- self._gatherTables(tables, extTables, done)
- tables.reverse()
- extTables.reverse()
- # Gather all data in two passes: the absolute positions of all
- # subtable are needed before the actual data can be assembled.
- pos = 0
- for table in tables:
- table.pos = pos
- pos = pos + table.getDataLength()
-
- for table in extTables:
- table.pos = pos
- pos = pos + table.getDataLength()
-
- data = []
- for table in tables:
- tableData = table.getData()
- data.append(tableData)
-
- for table in extTables:
- tableData = table.getData()
- data.append(tableData)
-
- return bytesjoin(data)
+ tables = distillery.sortMeOut(self)
+ return bytesjoin(table.getData() for table in tables)
# interface for gathering data, as used by table.compile()
- def getSubWriter(self, offsetSize=2):
- subwriter = self.__class__(self.localState, self.tableTag, offsetSize=offsetSize)
+ def getSubWriter(self, offsetSize=2, name=''):
+ subwriter = self.__class__(self.localState, self.tableTag, name, offsetSize=offsetSize)
+ subwriter.offsetSize = offsetSize
subwriter.parent = self # because some subtables have idential values, we discard
# the duplicates under the getAllData method. Hence some
# subtable writers can have more than one parent writer.
@@ -512,23 +498,23 @@
if hasattr(item, 'repeatIndex'):
itemIndex = item.repeatIndex
if self.name == 'SubTable':
- LookupListIndex = self.parent.repeatIndex
+ LookupListIndex = self.parents[0].repeatIndex
SubTableIndex = self.repeatIndex
elif self.name == 'ExtSubTable':
- LookupListIndex = self.parent.parent.repeatIndex
- SubTableIndex = self.parent.repeatIndex
+ LookupListIndex = self.parents[0].parents[0].repeatIndex
+ SubTableIndex = self.parents[0].repeatIndex
else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable.
itemName = ".".join([self.name, itemName])
- p1 = self.parent
+ p1 = self.parents[0]
while p1 and p1.name not in ['ExtSubTable', 'SubTable']:
itemName = ".".join([p1.name, itemName])
- p1 = p1.parent
+ p1 = p1.parents[0]
if p1:
if p1.name == 'ExtSubTable':
- LookupListIndex = p1.parent.parent.repeatIndex
- SubTableIndex = p1.parent.repeatIndex
+ LookupListIndex = p1.parents[0].parents[0].repeatIndex
+ SubTableIndex = p1.parents[0].repeatIndex
else:
- LookupListIndex = p1.parent.repeatIndex
+ LookupListIndex = p1.parents[0].repeatIndex
SubTableIndex = p1.repeatIndex
return OverflowErrorRecord( (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) )
@@ -966,7 +952,7 @@
value = getattr(valueRecord, name, 0)
if isDevice:
if value:
- subWriter = writer.getSubWriter()
+ subWriter = writer.getSubWriter(name=name)
writer.writeSubTable(subWriter)
value.compile(subWriter, font)
else:
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 058813a..9cd9bfc 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -637,8 +637,7 @@
if value is None:
self.writeNullOffset(writer)
else:
- subWriter = writer.getSubWriter(offsetSize=self.staticSize)
- subWriter.name = self.name
+ subWriter = writer.getSubWriter(offsetSize=self.staticSize, name = self.name)
if repeatIndex is not None:
subWriter.repeatIndex = repeatIndex
writer.writeSubTable(subWriter)
@@ -1006,6 +1005,7 @@
lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None)
dataWriter = writer.getSubWriter(offsetSize=4)
+
writer.writeSubTable(lookupWriter)
writer.writeSubTable(dataWriter)
for d in compiledData:
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 679df75..c2d53d1 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -1082,12 +1082,6 @@
alts = AlternateSet()
alts.Alternate = set
alternates.append(alts)
- # a special case to deal with the fact that several hundred Adobe Japan1-5
- # CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
- # Also useful in that when splitting a sub-table because of an offset overflow
- # I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
- # Allows packing more rules in subtable.
- self.sortCoverageLast = 1
return {"Coverage": cov, "AlternateSet": alternates}
def toXML2(self, xmlWriter, font):
@@ -1171,10 +1165,6 @@
for lig in set:
ligs.append(lig)
ligSets.append(ligSet)
- # Useful in that when splitting a sub-table because of an offset overflow
- # I don't need to calculate the change in subtabl offset due to the coverage table size.
- # Allows packing more rules in subtable.
- self.sortCoverageLast = 1
return {"Coverage": cov, "LigatureSet": ligSets}
def toXML2(self, xmlWriter, font):
@@ -1709,6 +1699,7 @@
def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
ok = 1
+ newSubTable.Format = oldSubTable.Format
if hasattr(oldSubTable, 'sortCoverageLast'):
newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast