Misc patches from rroberts:
fontTools/ttx.py
# support virtual GIDs, support handling some GSUB offset overflows.
fontTools/ttlib/__init__.py
# 1) make getReverseGlyphMap a public function; I find a reverse map
to often be useful
# 2) support virtual glyphs, e.g. references to GID's that are not in the font.
# Added the TTFont argument allowVID (default 0) to turn this off and on;
# added the arg requireReal ( default 0) so as to get the obvious
default behaviour when
# allowVID is 0 or 1, but to allow requiring a true GID when allowVID is 1.
fontTools/ttlib/tables/otBase.py
fontTools/ttlib/tables/otConverters.py
fontTools/ttlib/tables/otData.py
fontTools/ttlib/tables/otTables.py
# 1) speed optimization
# - collapse for loops
# - do not decompile extension lookups until a record is requested
from within the lookup.
# 2) handling offset overflows
# 3) support of extension lookups
# 4) Fixed FetauresParam converter class def so as to survive a font
that has this offset non-NULL.
# This fixes a stack dump.
# The data will now just get ignored
git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@511 4cde692c-a291-49d1-8350-778aa11640f8
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index 70884c9..e420329 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -3,6 +3,24 @@
import struct
from types import TupleType
+class OverflowErrorRecord:
+ def __init__(self, overflowTuple):
+ self.tableType = overflowTuple[0]
+ self.LookupListIndex = overflowTuple[1]
+ self.SubTableIndex = overflowTuple[2]
+ self.itemName = overflowTuple[3]
+ self.itemIndex = overflowTuple[4]
+
+ def __repr__(self):
+ return str((self.tableType, "LookupIndex:", self.LookupListIndex, "SubTableIndex:", self.SubTableIndex, "ItemName:", self.itemName, "ItemIndex:", self.itemIndex))
+
+class OTLOffsetOverflowError(Exception):
+ def __init__(self, overflowErrorRecord):
+ self.value = overflowErrorRecord
+
+ def __str__(self):
+ return repr(self.value)
+
class BaseTTXConverter(DefaultTable):
@@ -30,10 +48,31 @@
print "---", len(stats)
def compile(self, font):
+ """ Create a top-level OTFWriter for the GPOS/GSUB table.
+ Call the compile method for the the table
+ for each 'convertor' record in the table convertor list
+ call convertor'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/Subtabl 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 _GetTables. 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
+ pas's and offset are known.
+
+ If a lookup subtable overflows an offset, we have to start all over.
+ """
writer = OTTableWriter(self.tableTag)
+ writer.parent = None
self.table.compile(writer, font)
return writer.getAllData()
-
+
def toXML(self, writer, font):
self.table.toXML2(writer, font)
@@ -92,6 +131,13 @@
self.pos = newpos
return value
+ def readULong(self):
+ pos = self.pos
+ newpos = pos + 4
+ value, = struct.unpack(">L", self.data[pos:newpos])
+ self.pos = newpos
+ return value
+
def readTag(self):
pos = self.pos
newpos = pos + 4
@@ -135,29 +181,45 @@
def getAllData(self):
"""Assemble all data, including all subtables."""
self._doneWriting()
- tables = self._gatherTables()
+ tables, extTables = self._gatherTables()
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 "".join(data)
def getDataLength(self):
"""Return the length of this table in bytes, without subtables."""
l = 0
+ if hasattr(self, "Extension"):
+ longOffset = 1
+ else:
+ longOffset = 0
for item in self.items:
if hasattr(item, "getData") or hasattr(item, "getCountData"):
- l = l + 2 # sizeof(UShort)
+ if longOffset:
+ l = l + 4 # sizeof(ULong)
+ else:
+ l = l + 2 # sizeof(UShort)
else:
l = l + len(item)
return l
@@ -165,10 +227,60 @@
def getData(self):
"""Assemble the data for this writer/table, without subtables."""
items = list(self.items) # make a shallow copy
- for i in range(len(items)):
+ if hasattr(self,"Extension"):
+ longOffset = 1
+ else:
+ longOffset = 0
+ pos = self.pos
+ numItems = len(items)
+ for i in range(numItems):
item = items[i]
+
if hasattr(item, "getData"):
- items[i] = packUShort(item.pos - self.pos)
+ if longOffset:
+ items[i] = packULong(item.pos - pos)
+ else:
+ try:
+ items[i] = packUShort(item.pos - pos)
+ except AssertionError:
+ # provide data to fix overflow problem.
+ # If the overflow is to a lookup, or from a lookup to a subtable,
+ # just report the current item.
+ if self.name in [ 'LookupList', 'Lookup']:
+ overflowErrorRecord = self.getOverflowErrorRecord(item)
+ else:
+ # overflow is within a subTable. Life is more complicated.
+ # If we split the sub-table just before the current item, we may still suffer overflow.
+ # This is because duplicate table merging is done only within an Extension subTable tree;
+ # when we split the subtable in two, some items may no longer be duplicates.
+ # Get worst case by adding up all the item lengths, depth first traversal.
+ # and then report the first item that overflows a short.
+ def getDeepItemLength(table):
+ if hasattr(table, "getDataLength"):
+ length = 0
+ for item in table.items:
+ length = length + getDeepItemLength(item)
+ else:
+ length = len(table)
+ return length
+
+ length = self.getDataLength()
+ if hasattr(self, "sortCoverageLast") and item.name == "Coverage":
+ # Coverage is first in the item list, but last in the table list,
+ # The original overflow is really in the item list. Skip the Coverage
+ # table in the following test.
+ items = items[i+1:]
+
+ for j in range(len(items)):
+ item = items[j]
+ length = length + getDeepItemLength(item)
+ if length > 65535:
+ break
+ overflowErrorRecord = self.getOverflowErrorRecord(item)
+
+
+ raise OTLOffsetOverflowError, overflowErrorRecord
+
return "".join(items)
def __hash__(self):
@@ -182,38 +294,109 @@
return cmp(id(self), id(other))
def _doneWriting(self, internedTables=None):
+ # Convert CountData references to data string items
+ # 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.
if internedTables is None:
internedTables = {}
items = self.items
- for i in range(len(items)):
+ iRange = range(len(items))
+
+ if hasattr(self, "Extension"):
+ newTree = 1
+ else:
+ newTree = 0
+ for i in iRange:
item = items[i]
if hasattr(item, "getCountData"):
items[i] = item.getCountData()
elif hasattr(item, "getData"):
- item._doneWriting(internedTables)
- if internedTables.has_key(item):
- items[i] = item = internedTables[item]
+ if newTree:
+ item._doneWriting()
else:
- internedTables[item] = item
+ item._doneWriting(internedTables)
+ if internedTables.has_key(item):
+ items[i] = item = internedTables[item]
+ else:
+ internedTables[item] = item
self.items = tuple(items)
- def _gatherTables(self, tables=None, done=None):
- if tables is None:
+ def _gatherTables(self, tables=None, extTables=None, done=None):
+ # Convert table references in self.items tree to a flat
+ # list of tables in depth-first traversal order.
+ # "tables" are OTTableWriter objects.
+ # We do the traversal in reverse order at each level, in order to
+ # resolve duplicate references to be the last reference in the list of tables.
+ # For extension lookups, duplicate references can be merged only within the
+ # writer tree under the extension lookup.
+ if tables is None: # init call for first time.
tables = []
+ extTables = []
done = {}
- for item in self.items:
+
+ done[self] = 1
+
+ numItems = len(self.items)
+ iRange = range(numItems)
+ iRange.reverse()
+
+ if hasattr(self, "Extension"):
+ appendExtensions = 1
+ else:
+ appendExtensions = 0
+
+ # add Coverage table if it is sorted last.
+ sortCoverageLast = 0
+ 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
+ break
+ if not done.has_key(item):
+ item._gatherTables(tables, extTables, done)
+ else:
+ index = max(item.parent.keys())
+ item.parent[index + 1] = self
+
+ saveItem = None
+ for i in iRange:
+ item = self.items[i]
if not hasattr(item, "getData"):
continue
- if not done.has_key(item):
- item._gatherTables(tables, done)
- done[self] = 1
+
+ if sortCoverageLast and (i==1) and item.name == 'Coverage':
+ # we've already 'gathered' it above
+ continue
+
+ if appendExtensions:
+ assert extTables != None, "Program or XML editing error. Extension subtables cannot contain extensions subtables"
+ newDone = {}
+ item._gatherTables(extTables, None, newDone)
+
+ elif not done.has_key(item):
+ item._gatherTables(tables, extTables, done)
+ else:
+ index = max(item.parent.keys())
+ item.parent[index + 1] = self
+
+
tables.append(self)
- return tables
+ return tables, extTables
# interface for gathering data, as used by table.compile()
def getSubWriter(self):
- return self.__class__(self.tableType, self.valueFormat)
+ subwriter = self.__class__(self.tableType, self.valueFormat)
+ subwriter.parent = {0: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.
+ return subwriter
def writeUShort(self, value):
assert 0 <= value < 0x10000
@@ -225,6 +408,9 @@
def writeLong(self, value):
self.items.append(struct.pack(">l", value))
+ def writeULong(self, value):
+ self.items.append(struct.pack(">L", value))
+
def writeTag(self, tag):
assert len(tag) == 4
self.items.append(tag)
@@ -239,12 +425,48 @@
data = apply(struct.pack, (format,) + values)
self.items.append(data)
+ def writeData(self, data):
+ self.items.append(data)
+
def setValueFormat(self, format, which):
self.valueFormat[which].setFormat(format)
def writeValueRecord(self, value, font, which):
return self.valueFormat[which].writeValueRecord(self, font, value)
+ def getOverflowErrorRecord(self, item):
+ LookupListIndex = SubTableIndex = itemName = itemIndex = None
+ if self.name == 'LookupList':
+ LookupListIndex = item.repeatIndex
+ elif self.name == 'Lookup':
+ LookupListIndex = self.repeatIndex
+ SubTableIndex = item.repeatIndex
+ else:
+ itemName = item.name
+ if hasattr(item, 'repeatIndex'):
+ itemIndex = item.repeatIndex
+ if self.name == 'SubTable':
+ LookupListIndex = self.parent[0].repeatIndex
+ SubTableIndex = self.repeatIndex
+ elif self.name == 'ExtSubTable':
+ LookupListIndex = self.parent[0].parent[0].repeatIndex
+ SubTableIndex = self.parent[0].repeatIndex
+ else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable.
+ itemName = ".".join(self.name, item.name)
+ p1 = self.parent[0]
+ while p1 and p1.name not in ['ExtSubTable', 'SubTable']:
+ itemName = ".".join(p1.name, item.name)
+ p1 = p1.parent[0]
+ if p1:
+ if p1.name == 'ExtSubTable':
+ LookupListIndex = self.parent[0].parent[0].repeatIndex
+ SubTableIndex = self.parent[0].repeatIndex
+ else:
+ LookupListIndex = self.parent[0].repeatIndex
+ SubTableIndex = self.repeatIndex
+
+ return OverflowErrorRecord( (self.tableType, LookupListIndex, SubTableIndex, itemName, itemIndex) )
+
class CountReference:
"""A reference to a Count value, not a count of references."""
@@ -260,6 +482,11 @@
return struct.pack(">H", value)
+def packULong(value):
+ assert 0 <= value < 0x1000000000, value
+ return struct.pack(">L", value)
+
+
class TableStack:
"""A stack of table dicts, working as a stack of namespaces so we can
@@ -288,7 +515,38 @@
class BaseTable:
+ def __init__(self):
+ self.compileStatus = 0 # 0 means table was created
+ # 1 means the table.read() function was called by a table which is subject
+ # to delayed compilation
+ # 2 means that it was subject to delayed compilation, and
+ # has been decompiled
+ # 3 means that the start and end fields have been filled out, and that we
+ # can use the data string rather than compiling from the table data.
+
+ self.recurse = 0
+ def __getattr__(self, attr):
+ # we get here only when the table does not have the attribute.
+ # This method ovveride exists so that we can try to de-compile
+ # a table which is subject to delayed decompilation, and then try
+ # to get the value again after decompilation.
+ self.recurse +=1
+ if self.recurse > 2:
+ # shouldn't ever get here - we should only get to two levels of recursion.
+ # this guards against self.decompile NOT setting compileStatus to other than 1.
+ raise AttributeError, attr
+ if self.compileStatus == 1:
+ # table.read() has been called, but table has not yet been decompiled
+ # This happens only for extension tables.
+ self.decompile(self.reader, self.font)
+ val = getattr(self, attr)
+ self.recurse -=1
+ return val
+
+ raise AttributeError, attr
+
+
"""Generic base class for all OpenType (sub)tables."""
def getConverters(self):
@@ -298,6 +556,7 @@
return self.convertersByName[name]
def decompile(self, reader, font, tableStack=None):
+ self.compileStatus = 2 # table has been decompiled.
if tableStack is None:
tableStack = TableStack()
self.readFormat(reader)
@@ -308,6 +567,9 @@
if conv.name == "SubTable":
conv = conv.getConverter(reader.tableType,
table["LookupType"])
+ if conv.name == "ExtSubTable":
+ conv = conv.getConverter(reader.tableType,
+ table["ExtensionLookupType"])
if conv.repeat:
l = []
for i in range(tableStack.getValue(conv.repeat) + conv.repeatOffset):
@@ -318,11 +580,18 @@
tableStack.pop()
self.postRead(table, font)
del self.__rawTable # succeeded, get rid of debugging info
-
+
+ def preCompile(self):
+ pass # used only by the LookupList class
+
def compile(self, writer, font, tableStack=None):
if tableStack is None:
tableStack = TableStack()
table = self.preWrite(font)
+
+ if hasattr(self, 'sortCoverageLast'):
+ writer.sortCoverageLast = 1
+
self.writeFormat(writer)
tableStack.push(table)
for conv in self.getConverters():
@@ -331,8 +600,8 @@
if value is None:
value = []
tableStack.storeValue(conv.repeat, len(value) - conv.repeatOffset)
- for item in value:
- conv.write(writer, font, tableStack, item)
+ for i in range(len(value)):
+ conv.write(writer, font, tableStack, value[i], i)
elif conv.isCount:
# Special-case Count values.
# Assumption: a Count field will *always* precede
@@ -389,7 +658,6 @@
try:
conv = self.getConverterByName(name)
except KeyError:
-## print self, name, attrs, content
raise # XXX on KeyError, raise nice error
value = conv.xmlRead(attrs, content, font)
if conv.repeat:
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 10cc02e..4d4739b 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -20,16 +20,18 @@
converterClass = Count
elif name == "SubTable":
converterClass = SubTable
+ elif name == "ExtSubTable":
+ converterClass = ExtSubTable
else:
converterClass = converterMapping[tp]
tableClass = tableNamespace.get(name)
conv = converterClass(name, repeat, repeatOffset, tableClass)
- if name == "SubTable":
+ if name in ["SubTable", "ExtSubTable"]:
conv.lookupTypes = tableNamespace['lookupTypes']
# also create reverse mapping
for t in conv.lookupTypes.values():
for cls in t.values():
- convertersByName[cls.__name__] = Table("SubTable", repeat, repeatOffset, cls)
+ convertersByName[cls.__name__] = Table(name, repeat, repeatOffset, cls)
converters.append(conv)
assert not convertersByName.has_key(name)
convertersByName[name] = conv
@@ -52,7 +54,7 @@
"""Read a value from the reader."""
raise NotImplementedError, self
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
"""Write a value to the writer."""
raise NotImplementedError, self
@@ -79,13 +81,13 @@
class Long(IntValue):
def read(self, reader, font, tableStack):
return reader.readLong()
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeLong(value)
class Fixed(IntValue):
def read(self, reader, font, tableStack):
return float(reader.readLong()) / 0x10000
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeLong(int(round(value * 0x10000)))
def xmlRead(self, attrs, content, font):
return float(attrs["value"])
@@ -93,13 +95,13 @@
class Short(IntValue):
def read(self, reader, font, tableStack):
return reader.readShort()
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeShort(value)
class UShort(IntValue):
def read(self, reader, font, tableStack):
return reader.readUShort()
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeUShort(value)
class Count(Short):
@@ -110,14 +112,18 @@
class Tag(SimpleValue):
def read(self, reader, font, tableStack):
return reader.readTag()
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeTag(value)
class GlyphID(SimpleValue):
def read(self, reader, font, tableStack):
- return font.getGlyphName(reader.readUShort())
- def write(self, writer, font, tableStack, value):
- writer.writeUShort(font.getGlyphID(value))
+ value = reader.readUShort()
+ value = font.getGlyphName(value)
+ return value
+
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
+ value = font.getGlyphID(value)
+ writer.writeUShort(value)
class Struct(BaseConverter):
@@ -127,7 +133,7 @@
table.decompile(reader, font, tableStack)
return table
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
value.compile(writer, font, tableStack)
def xmlWrite(self, xmlWriter, font, value, name, attrs):
@@ -166,20 +172,61 @@
table.decompile(subReader, font, tableStack)
return table
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
if value is None:
writer.writeUShort(0)
else:
subWriter = writer.getSubWriter()
+ subWriter.name = self.name
+ if repeatIndex is not None:
+ subWriter.repeatIndex = repeatIndex
+ value.preCompile()
writer.writeSubTable(subWriter)
value.compile(subWriter, font, tableStack)
-
class SubTable(Table):
def getConverter(self, tableType, lookupType):
lookupTypes = self.lookupTypes[tableType]
tableClass = lookupTypes[lookupType]
- return Table(self.name, self.repeat, self.repeatOffset, tableClass)
+ return SubTable(self.name, self.repeat, self.repeatOffset, tableClass)
+
+
+class ExtSubTable(Table):
+ def getConverter(self, tableType, lookupType):
+ lookupTypes = self.lookupTypes[tableType]
+ tableClass = lookupTypes[lookupType]
+ return ExtSubTable(self.name, self.repeat, self.repeatOffset, tableClass)
+
+ def read(self, reader, font, tableStack):
+ offset = reader.readULong()
+ if offset == 0:
+ return None
+ subReader = reader.getSubReader(offset)
+ table = self.tableClass()
+ table.reader = subReader
+ table.font = font
+ table.compileStatus = 1
+ table.start = table.reader.offset
+ return table
+
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
+ writer.Extension = 1 # actually, mere presence of the field flags it as an Ext Subtable writer.
+ if value is None:
+ writer.writeULong(0)
+ else:
+ # If the subtable has not yet been decompiled, we need to do so.
+ if value.compileStatus == 1:
+ value.decompile(value.reader, value.font, tableStack)
+ subWriter = writer.getSubWriter()
+ subWriter.name = self.name
+ writer.writeSubTable(subWriter)
+ # If the subtable has been sorted and we can just write the original
+ # data, then do so.
+ if value.compileStatus == 3:
+ data = value.reader.data[value.start:value.end]
+ subWriter.writeData(data)
+ else:
+ value.compile(subWriter, font, tableStack)
class ValueFormat(IntValue):
@@ -190,7 +237,7 @@
format = reader.readUShort()
reader.setValueFormat(format, self.which)
return format
- def write(self, writer, font, tableStack, format):
+ def write(self, writer, font, tableStack, format, repeatIndex=None):
writer.writeUShort(format)
writer.setValueFormat(format, self.which)
@@ -198,7 +245,7 @@
class ValueRecord(ValueFormat):
def read(self, reader, font, tableStack):
return reader.readValueRecord(font, self.which)
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
writer.writeValueRecord(value, font, self.which)
def xmlWrite(self, xmlWriter, font, value, name, attrs):
if value is None:
@@ -238,7 +285,7 @@
DeltaValue.append(value)
return DeltaValue
- def write(self, writer, font, tableStack, value):
+ def write(self, writer, font, tableStack, value, repeatIndex=None):
table = tableStack.getTop()
StartSize = table["StartSize"]
EndSize = table["EndSize"]
@@ -279,6 +326,7 @@
"GlyphID": GlyphID,
"struct": Struct,
"Offset": Table,
+ "LOffset": ExtSubTable,
"ValueRecord": ValueRecord,
}
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index 7accf91..89358e6 100644
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -357,9 +357,9 @@
]),
('ExtensionPosFormat1', [
- ('USHORT', 'PosFormat', None, None, 'Format identifier. Set to 1.'),
+ ('USHORT', 'ExtFormat', None, None, 'Format identifier. Set to 1.'),
('USHORT', 'ExtensionLookupType', None, None, 'Lookup type of subtable referenced by ExtensionOffset (i.e. the extension subtable).'),
- ('ULONG', 'ExtensionOffset', None, None, 'Offset to the extension subtable, of lookup type ExtensionLookupType, relative to the start of the ExtensionPosFormat1 subtable.'),
+ ('LOffset', 'ExtSubTable', None, None, 'Array of offsets to Lookup tables-from beginning of LookupList -zero based (first lookup is Lookup index = 0)'),
]),
('ValueRecord', [
@@ -585,9 +585,9 @@
]),
('ExtensionSubstFormat1', [
- ('USHORT', 'SubstFormat', None, None, 'Format identifier. Set to 1.'),
+ ('USHORT', 'ExtFormat', None, None, 'Format identifier. Set to 1.'),
('USHORT', 'ExtensionLookupType', None, None, 'Lookup type of subtable referenced by ExtensionOffset (i.e. the extension subtable).'),
- ('ULONG', 'ExtensionOffset', None, None, 'Offset to the extension subtable, of lookup type ExtensionLookupType, relative to the start of the ExtensionSubstFormat1 subtable.'),
+ ('LOffset', 'ExtSubTable', None, None, 'Array of offsets to Lookup tables-from beginning of LookupList -zero based (first lookup is Lookup index = 0)'),
]),
('ReverseChainSingleSubstFormat1', [
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index 7d35c93..253b211 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -4,7 +4,7 @@
Most are constructed upon import from data in otData.py, all are populated with
converter objects from otConverters.py.
"""
-
+import operator
from otBase import BaseTable, FormatSwitchingBaseTable
from types import TupleType
@@ -14,9 +14,14 @@
class FeatureParams(BaseTable):
- """Dummy class; this table isn't defined, but is used, and is always NULL."""
+ """This class has been used by Adobe, but but this one implementation was done wrong.
+ No other use has been made, becuase there is no way to know how to interpret
+ the data at the offset.. For now, if we see one, just skip the data on
+ decompiling and dumping to XML. """
# XXX The above is no longer true; the 'size' feature uses FeatureParams now.
-
+ def __init__(self):
+ BaseTable.__init__(self)
+ self.converters = []
class Coverage(FormatSwitchingBaseTable):
@@ -28,6 +33,7 @@
elif self.Format == 2:
glyphs = self.glyphs = []
ranges = rawTable["RangeRecord"]
+ getGlyphName = font.getGlyphName
for r in ranges:
assert r.StartCoverageIndex == len(glyphs), \
(r.StartCoverageIndex, len(glyphs))
@@ -36,8 +42,8 @@
startID = font.getGlyphID(start)
endID = font.getGlyphID(end)
glyphs.append(start)
- for glyphID in range(startID + 1, endID):
- glyphs.append(font.getGlyphName(glyphID))
+ rangeList = [getGlyphName(glyphID) for glyphID in range(startID + 1, endID) ]
+ glyphs += rangeList
if start != end:
glyphs.append(end)
else:
@@ -49,11 +55,10 @@
glyphs = self.glyphs = []
format = 1
rawTable = {"GlyphArray": glyphs}
+ getGlyphID = font.getGlyphID
if glyphs:
# find out whether Format 2 is more compact or not
- glyphIDs = []
- for glyphName in glyphs:
- glyphIDs.append(font.getGlyphID(glyphName))
+ glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ]
last = glyphIDs[0]
ranges = [[last]]
@@ -95,22 +100,79 @@
glyphs.append(attrs["value"])
+class LookupList(BaseTable):
+ def preCompile(self):
+ """ This function is used to optimize writing out extension subtables. This is useful
+ when a font has been read in, modified, and we are now writing out a new version. If the
+ the extension subtables have not been touched (proof being that they have not been decompiled)
+ then we can write them out using the original data, and do not have to recompile them. This can save
+ 20-30% of the compile time for fonts with large extension tables, such as Japanese Pro fonts."""
+
+ if hasattr(self, 'LookupCount'): #not defined if loading from xml
+ lookupCount = self.LookupCount
+ else:
+ return # The optimization of not recompiling extension lookup subtables is not possible
+ # when reading from XML.
+
+ liRange = range(lookupCount)
+ extTables = []
+ for li in liRange:
+ lookup = self.Lookup[li]
+ if hasattr(lookup, 'SubTableCount'): #not defined if loading from xml
+ subtableCount = lookup.SubTableCount
+ else:
+ subtableCount = len(lookup.SubTable)
+ siRange = range(subtableCount)
+ for si in siRange:
+ subtable = lookup.SubTable[si]
+ if hasattr(subtable, 'ExtSubTable'):
+ extTable = subtable.ExtSubTable
+ extTables.append([extTable.start, extTable] )
+
+ # Since offsets in one subtable can and do point forward into later
+ # subtables, we can afford to simply copy data only for the last subtables
+ # which were not decompiled. So we start figuring out the
+ # data segments starting with the last subtTable, and work our way towards
+ # the first subtable, and then quit as soon as we see a subtable that was decompiled.
+ if extTables:
+ extTables.sort()
+ extTables.reverse()
+ lastTable = extTables[0][1]
+ if lastTable.compileStatus == 1:
+ lastTable.end = len(lastTable.reader.data)
+ lastTable.compileStatus = 3
+ for i in range(1, len(extTables)):
+ extTable = extTables[i][1]
+ if extTable.compileStatus != 1:
+ break
+ extTable.end = lastTable.start
+ extTable.compileStatus = 3
+ lastTable = extTable
+
+def doModulo(value):
+ if value < 0:
+ return value + 65536
+ return value
+
class SingleSubst(FormatSwitchingBaseTable):
def postRead(self, rawTable, font):
mapping = {}
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
+ lenMapping = len(input)
if self.Format == 1:
delta = rawTable["DeltaGlyphID"]
- for inGlyph in input:
- glyphID = font.getGlyphID(inGlyph)
- mapping[inGlyph] = font.getGlyphName(glyphID + delta)
+ inputGIDS = [ font.getGlyphID(name) for name in input ]
+ inputGIDS = map(doModulo, inputGIDS)
+ outGIDS = [ glyphID + delta for glyphID in inputGIDS ]
+ outGIDS = map(doModulo, outGIDS)
+ outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ]
+ map(operator.setitem, [mapping]*lenMapping, input, outNames)
elif self.Format == 2:
assert len(input) == rawTable["GlyphCount"], \
"invalid SingleSubstFormat2 table"
subst = rawTable["Substitute"]
- for i in range(len(input)):
- mapping[input[i]] = subst[i]
+ map(operator.setitem, [mapping]*lenMapping, input, subst)
else:
assert 0, "unknown format: %s" % self.Format
self.mapping = mapping
@@ -120,15 +182,15 @@
if mapping is None:
mapping = self.mapping = {}
items = mapping.items()
- for i in range(len(items)):
- inGlyph, outGlyph = items[i]
- items[i] = font.getGlyphID(inGlyph), font.getGlyphID(outGlyph), \
- inGlyph, outGlyph
- items.sort()
-
+ getGlyphID = font.getGlyphID
+ gidItems = [(getGlyphID(item[0]), getGlyphID(item[1])) for item in items]
+ sortableItems = zip(gidItems, items)
+ sortableItems.sort()
+
+ # figure out format
format = 2
delta = None
- for inID, outID, inGlyph, outGlyph in items:
+ for inID, outID in gidItems:
if delta is None:
delta = outID - inID
else:
@@ -136,15 +198,13 @@
break
else:
format = 1
-
+
rawTable = {}
self.Format = format
cov = Coverage()
- cov.glyphs = input = []
- subst = []
- for inID, outID, inGlyph, outGlyph in items:
- input.append(inGlyph)
- subst.append(outGlyph)
+ input = [ item [1][0] for item in sortableItems]
+ subst = [ item [1][1] for item in sortableItems]
+ cov.glyphs = input
rawTable["Coverage"] = cov
if format == 1:
assert delta is not None
@@ -173,12 +233,18 @@
def postRead(self, rawTable, font):
classDefs = {}
+ getGlyphName = font.getGlyphName
+
if self.Format == 1:
start = rawTable["StartGlyph"]
+ classList = rawTable["ClassValueArray"]
+ lenList = len(classList)
glyphID = font.getGlyphID(start)
- for cls in rawTable["ClassValueArray"]:
- classDefs[font.getGlyphName(glyphID)] = cls
- glyphID = glyphID + 1
+ gidList = range(glyphID, glyphID + len(classList))
+ keyList = [getGlyphName(glyphID) for glyphID in gidList]
+
+ map(operator.setitem, [classDefs]*lenList, keyList, classList)
+
elif self.Format == 2:
records = rawTable["ClassRangeRecord"]
for rec in records:
@@ -186,9 +252,10 @@
end = rec.End
cls = rec.Class
classDefs[start] = cls
- for glyphID in range(font.getGlyphID(start) + 1,
- font.getGlyphID(end)):
- classDefs[font.getGlyphName(glyphID)] = cls
+ glyphIDs = range(font.getGlyphID(start) + 1, font.getGlyphID(end))
+ lenList = len(glyphIDs)
+ keyList = [getGlyphName(glyphID) for glyphID in glyphIDs]
+ map(operator.setitem, [classDefs]*lenList, keyList, [cls]*lenList)
classDefs[end] = cls
else:
assert 0, "unknown format: %s" % self.Format
@@ -199,9 +266,10 @@
if classDefs is None:
classDefs = self.classDefs = {}
items = classDefs.items()
+ getGlyphID = font.getGlyphID
for i in range(len(items)):
glyphName, cls = items[i]
- items[i] = font.getGlyphID(glyphName), glyphName, cls
+ items[i] = getGlyphID(glyphName), glyphName, cls
items.sort()
if items:
last, lastName, lastCls = items[0]
@@ -247,7 +315,8 @@
if self.Format == 1:
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
alts = rawTable["AlternateSet"]
- assert len(input) == len(alts)
+ if len(input) != len(alts):
+ assert len(input) == len(alts)
for i in range(len(input)):
alternates[input[i]] = alts[i].Alternate
else:
@@ -265,14 +334,19 @@
items[i] = font.getGlyphID(glyphName), glyphName, set
items.sort()
cov = Coverage()
- glyphs = []
+ cov.glyphs = [ item[1] for item in items]
alternates = []
- cov.glyphs = glyphs
- for glyphID, glyphName, set in items:
- glyphs.append(glyphName)
+ setList = [ item[-1] for item in items]
+ for set in setList:
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):
@@ -307,7 +381,7 @@
def postRead(self, rawTable, font):
ligatures = {}
if self.Format == 1:
- input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
+ input = rawTable["Coverage"].glyphs
ligSets = rawTable["LigatureSet"]
assert len(input) == len(ligSets)
for i in range(len(input)):
@@ -317,7 +391,6 @@
self.ligatures = ligatures
def preWrite(self, font):
- self.Format = 1
ligatures = getattr(self, "ligatures", None)
if ligatures is None:
ligatures = self.ligatures = {}
@@ -326,17 +399,21 @@
glyphName, set = items[i]
items[i] = font.getGlyphID(glyphName), glyphName, set
items.sort()
- glyphs = []
cov = Coverage()
- cov.glyphs = glyphs
+ cov.glyphs = [ item[1] for item in items]
+
ligSets = []
- for glyphID, glyphName, set in items:
- glyphs.append(glyphName)
+ setList = [ item[-1] for item in items ]
+ for set in setList:
ligSet = LigatureSet()
ligs = ligSet.Ligature = []
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):
@@ -401,6 +478,191 @@
}
+def fixLookupOverFlows(ttf, overflowRecord):
+ """ Either the offset from the LookupList to a lookup overflowed, or
+ an offset from a lookup to a subtable overflowed.
+ The table layout is:
+ GPSO/GUSB
+ Script List
+ Feature List
+ LookUpList
+ Lookup[0] and contents
+ SubTable offset list
+ SubTable[0] and contents
+ ...
+ SubTable[n] and contents
+ ...
+ Lookup[n] and contents
+ SubTable offset list
+ SubTable[0] and contents
+ ...
+ SubTable[n] and contents
+ If the offset to a lookup overflowed (SubTableIndex == None)
+ we must promote the *previous* lookup to an Extension type.
+ If the offset from a lookup to subtable overflowed, then we must promote it
+ to an Extension Lookup type.
+ """
+ ok = 0
+ lookupIndex = overflowRecord.LookupListIndex
+ if (overflowRecord.SubTableIndex == None):
+ lookupIndex = lookupIndex - 1
+ if lookupIndex < 0:
+ return ok
+ if overflowRecord.tableType == 'GSUB':
+ extType = 7
+ elif overflowRecord.tableType == 'GPOS':
+ extType = 9
+
+ lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
+ lookup = lookups[lookupIndex]
+ # If the previous lookup is an extType, look further back. Very unlikely, but possible.
+ while lookup.LookupType == extType:
+ lookupIndex = lookupIndex -1
+ if lookupIndex < 0:
+ return ok
+ lookup = lookups[lookupIndex]
+
+ for si in range(len(lookup.SubTable)):
+ subTable = lookup.SubTable[si]
+ extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
+ extSubTable = extSubTableClass()
+ extSubTable.Format = 1
+ extSubTable.ExtensionLookupType = lookup.LookupType
+ extSubTable.ExtSubTable = subTable
+ lookup.SubTable[si] = extSubTable
+ lookup.LookupType = extType
+ ok = 1
+ return ok
+
+def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
+ ok = 1
+ newSubTable.Format = oldSubTable.Format
+ if hasattr(oldSubTable, 'sortCoverageLast'):
+ newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
+
+ oldAlts = oldSubTable.alternates.items()
+ oldAlts.sort()
+ oldLen = len(oldAlts)
+
+ if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']:
+ # Coverage table is written last. overflow is to or within the
+ # the coverage table. We will just cut the subtable in half.
+ newLen = int(oldLen/2)
+
+ elif overflowRecord.itemName == 'AlternateSet':
+ # We just need to back up by two items
+ # from the overflowed AlternateSet index to make sure the offset
+ # to the Coverage table doesn't overflow.
+ newLen = overflowRecord.itemIndex - 1
+
+ newSubTable.alternates = {}
+ for i in range(newLen, oldLen):
+ item = oldAlts[i]
+ key = item[0]
+ newSubTable.alternates[key] = item[1]
+ del oldSubTable.alternates[key]
+
+
+ return ok
+
+
+def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
+ ok = 1
+ newSubTable.Format = oldSubTable.Format
+ oldLigs = oldSubTable.ligatures.items()
+ oldLigs.sort()
+ oldLen = len(oldLigs)
+
+ if overflowRecord.itemName in [ 'Coverage', 'RangeRecord']:
+ # Coverage table is written last. overflow is to or within the
+ # the coverage table. We will just cut the subtable in half.
+ newLen = int(oldLen/2)
+
+ elif overflowRecord.itemName == 'LigatureSet':
+ # We just need to back up by two items
+ # from the overflowed AlternateSet index to make sure the offset
+ # to the Coverage table doesn't overflow.
+ newLen = overflowRecord.itemIndex - 1
+
+ newSubTable.ligatures = {}
+ for i in range(newLen, oldLen):
+ item = oldLigs[i]
+ key = item[0]
+ newSubTable.ligatures[key] = item[1]
+ del oldSubTable.ligatures[key]
+
+ return ok
+
+
+splitTable = { 'GSUB': {
+# 1: splitSingleSubst,
+# 2: splitMultipleSubst,
+ 3: splitAlternateSubst,
+ 4: splitLigatureSubst,
+# 5: splitContextSubst,
+# 6: splitChainContextSubst,
+# 7: splitExtensionSubst,
+# 8: splitReverseChainSingleSubst,
+ },
+ 'GPOS': {
+# 1: splitSinglePos,
+# 2: splitPairPos,
+# 3: splitCursivePos,
+# 4: splitMarkBasePos,
+# 5: splitMarkLigPos,
+# 6: splitMarkMarkPos,
+# 7: splitContextPos,
+# 8: splitChainContextPos,
+# 9: splitExtensionPos,
+ }
+
+ }
+
+def fixSubTableOverFlows(ttf, overflowRecord):
+ """
+ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
+ """
+ ok = 0
+ table = ttf[overflowRecord.tableType].table
+ lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
+ subIndex = overflowRecord.SubTableIndex
+ subtable = lookup.SubTable[subIndex]
+
+ if hasattr(subtable, 'ExtSubTable'):
+ # We split the subtable of the Extension table, and add a new Extension table
+ # to contain the new subtable.
+
+ subTableType = subtable.ExtensionLookupType
+ extSubTable = subtable
+ subtable = extSubTable.ExtSubTable
+ newExtSubTableClass = lookupTypes[overflowRecord.tableType][lookup.LookupType]
+ newExtSubTable = newExtSubTableClass()
+ newExtSubTable.Format = extSubTable.Format
+ newExtSubTable.ExtensionLookupType = extSubTable.ExtensionLookupType
+ lookup.SubTable.insert(subIndex + 1, newExtSubTable)
+
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
+ newSubTable = newSubTableClass()
+ newExtSubTable.ExtSubTable = newSubTable
+ else:
+ subTableType = lookup.LookupType
+ newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
+ newSubTable = newSubTableClass()
+ lookup.SubTable.insert(subIndex + 1, newSubTable)
+
+ if hasattr(lookup, 'SubTableCount'): # may not be defined yet.
+ lookup.SubTableCount = lookup.SubTableCount + 1
+
+ try:
+ splitFunc = splitTable[overflowRecord.tableType][subTableType]
+ except KeyError:
+ return ok
+
+ ok = splitFunc(subtable, newSubTable, overflowRecord)
+ return ok
+
+
+
def _buildClasses():
import new, re
from otData import otData