diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index 85ffcd7f9..05033fa7d 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -6,6 +6,10 @@ from types import TupleType class BaseTTXConverter(DefaultTable): + """Generic base class for TTX table converters. Functions as an adapter + between the TTX (ttLib actually) table model and the model we use for + OpenType tables, which is neccesarily subtly different.""" + def decompile(self, data, font): import otTables reader = OTTableReader(data, self.tableTag) @@ -31,6 +35,8 @@ class BaseTTXConverter(DefaultTable): class OTTableReader: + """Helper class to retrieve data from an OpenType table.""" + def __init__(self, data, tableType, offset=0, valueFormat=None, cachingStats=None): self.data = data self.offset = offset @@ -102,6 +108,8 @@ class OTTableReader: class OTTableWriter: + """Helper class to gather and assemble data for OpenType tables.""" + def __init__(self, tableType, valueFormat=None): self.items = [] self.tableType = tableType @@ -169,6 +177,7 @@ class OTTableWriter: class CountReference: + """A reference to a Count value, not a count of a reference.""" def __init__(self, table, name): self.table = table self.name = name @@ -176,14 +185,42 @@ class CountReference: return packUShort(self.table[self.name]) -def packUShort(offset): - assert 0 <= offset < 0x10000 - return struct.pack(">H", offset) +def packUShort(value): + assert 0 <= value < 0x10000 + return struct.pack(">H", value) +class TableStack: + """A stack of table dicts, working as a stack of namespaces so we can + retrieve values from (and store values to) tables higher up the stack.""" + def __init__(self): + self.stack = [] + def push(self, table): + self.stack.insert(0, table) + def pop(self): + self.stack.pop(0) + def getTop(self): + return self.stack[0] + def getValue(self, name): + return self.__findTable(name)[name] + def storeValue(self, name, value): + table = self.__findTable(name) + if table[name] is None: + table[name] = value + else: + assert table[name] == value, (table[name], value) + def __findTable(self, name): + for table in self.stack: + if table.has_key(name): + return table + raise KeyError, name + + class BaseTable: + """Generic base class for all OpenType (sub)tables.""" + def getConverters(self): return self.converters @@ -220,7 +257,7 @@ class BaseTable: value = table.get(conv.name) if conv.repeat: if value is None: - value = [] # XXXXXX + value = [] tableStack.storeValue(conv.repeat, len(value) - conv.repeatOffset) for item in value: conv.write(writer, font, tableStack, item) @@ -249,7 +286,7 @@ class BaseTable: if attrs is None: attrs = [] if hasattr(self, "Format"): - attrs = attrs + [("Format", str(self.Format))] + attrs = attrs + [("Format", self.Format)] xmlWriter.begintag(tableName, attrs) xmlWriter.newline() self.toXML2(xmlWriter, font) @@ -262,30 +299,30 @@ class BaseTable: # do it ourselves. I think I'm getting schizophrenic... for conv in self.getConverters(): value = getattr(self, conv.name) - if not conv.repeat: - conv.xmlWrite(xmlWriter, font, value, conv.name, []) - else: + if conv.repeat: for i in range(len(value)): item = value[i] - conv.xmlWrite(xmlWriter, font, item, conv.name, [("index", i)]) + conv.xmlWrite(xmlWriter, font, item, conv.name, + [("index", i)]) + else: + conv.xmlWrite(xmlWriter, font, value, conv.name, []) def fromXML(self, (name, attrs, content), font): try: conv = self.getConverterByName(name) except KeyError: - print self, name, attrs, content +## print self, name, attrs, content raise # XXX on KeyError, raise nice error value = conv.xmlRead(attrs, content, font) - name = conv.name if conv.repeat: try: - seq = getattr(self, name) + seq = getattr(self, conv.name) except AttributeError: seq = [] - setattr(self, name, seq) + setattr(self, conv.name, seq) seq.append(value) else: - setattr(self, name, value) + setattr(self, conv.name, value) def __cmp__(self, other): # this is only for debugging, so it's ok to barf @@ -300,6 +337,9 @@ class BaseTable: class FormatSwitchingBaseTable(BaseTable): + """Small specialization of BaseTable, for tables that have multiple + formats, eg. CoverageFormat1 vs. CoverageFormat2.""" + def getConverters(self): return self.converters[self.Format] @@ -316,6 +356,14 @@ class FormatSwitchingBaseTable(BaseTable): BaseTable.compile(self, writer, font, tableStack) +# +# Support for ValueRecords +# +# This data type is so different from all other OpenType data types that +# it requires quite a bit of code for itself. It even has special support +# in OTTableReader and OTTableWriter... +# + valueRecordFormat = [ # Mask Name isDevice signed (0x0001, "XPlacement", 0, 1), @@ -348,6 +396,8 @@ valueRecordFormatDict = _buildDict() class ValueRecordFactory: + """Given a format code, this object convert ValueRecords.""" + def setFormat(self, valueFormat): format = [] for mask, name, isDevice, signed in valueRecordFormat: @@ -453,27 +503,3 @@ class ValueRecord: else: return rv - -class TableStack: - def __init__(self): - self.stack = [] - def push(self, table): - self.stack.insert(0, table) - def pop(self): - self.stack.pop(0) - def getTop(self): - return self.stack[0] - def getValue(self, name): - return self.__findTable(name)[name] - def storeValue(self, name, value): - table = self.__findTable(name) - if table[name] is None: - table[name] = value - else: - assert table[name] == value, (table[name], value) - def __findTable(self, name): - for table in self.stack: - if table.has_key(name): - return table - raise KeyError, name - diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index ab18b1077..27dc29890 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -2,7 +2,10 @@ from types import TupleType from fontTools.misc.textTools import safeEval -def buildConverterList(tableSpec, tableNamespace): +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, repeatOffset, descr in tableSpec: @@ -35,6 +38,9 @@ def buildConverterList(tableSpec, tableNamespace): class BaseConverter: + """Base class for converter objects. Apart from the constructor, this + is an abstract class.""" + def __init__(self, name, repeat, repeatOffset, tableClass): self.name = name self.repeat = repeat @@ -43,15 +49,19 @@ class BaseConverter: self.isCount = name.endswith("Count") def read(self, reader, font, tableStack): + """Read a value from the reader.""" raise NotImplementedError, self def write(self, writer, font, tableStack, value): - raise NotImplementedError, self - - def xmlWrite(self, xmlWriter, font, value, name, attrs): + """Write a value to the writer.""" raise NotImplementedError, self def xmlRead(self, attrs, content, font): + """Read a value from XML.""" + raise NotImplementedError, self + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + """Write a value to XML.""" raise NotImplementedError, self @@ -240,6 +250,7 @@ class DeltaValue(BaseConverter): writer.writeUShort(tmp) def xmlWrite(self, xmlWriter, font, value, name, attrs): + # XXX this could do with a nicer format xmlWriter.simpletag(name, attrs + [("value", value)]) xmlWriter.newline() diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 24a5440e7..5438ae836 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -1,8 +1,8 @@ """fontTools.ttLib.tables.otTables -- A collection of classes representing the various OpenType subtables. -Most are constructed upon import from data in otData.py. Most smartness is contained -in otBase.BaseTable. +Most are constructed upon import from data in otData.py, all are populated with +converter objects from otConverters.py. """ from otBase import BaseTable, FormatSwitchingBaseTable @@ -15,28 +15,35 @@ class FeatureParams(BaseTable): """Dummy class; this table isn't defined, but is used, and is always NULL.""" -_equivalents = [ - ('MarkArray', ("Mark1Array",)), - ('LangSys', ('DefaultLangSys',)), - ('Coverage', ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage', +# +# For each subtable format there is a class. However, we don't really distinguish +# between "field name" and "format name": often these are the same. Yet there's +# a whole bunch of fields with different names. The following dict is a mapping +# from "format name" to "field name". _buildClasses() uses this to create a +# subclass for each alternate field name. +# +_equivalents = { + 'MarkArray': ("Mark1Array",), + 'LangSys': ('DefaultLangSys',), + 'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage', 'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage', - 'LookaheadCoverage')), - ('ClassDef', ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef', - 'LookaheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef')), - ('Anchor', ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor', - 'Mark2Anchor', 'MarkAnchor')), - ('Device', ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice', - 'XDeviceTable', 'YDeviceTable', 'DeviceTable')), - ('Axis', ('HorizAxis', 'VertAxis',)), - ('MinMax', ('DefaultMinMax',)), - ('BaseCoord', ('MinCoord', 'MaxCoord',)), - ('JstfLangSys', ('DefJstfLangSys',)), - ('JstfGSUBModList', ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB', - 'ExtensionDisableGSUB',)), - ('JstfGPOSModList', ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS', - 'ExtensionDisableGPOS',)), - ('JstfMax', ('ShrinkageJstfMax', 'ExtensionJstfMax',)), -] + 'LookaheadCoverage'), + 'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef', + 'LookaheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'), + 'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor', + 'Mark2Anchor', 'MarkAnchor'), + 'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice', + 'XDeviceTable', 'YDeviceTable', 'DeviceTable'), + 'Axis': ('HorizAxis', 'VertAxis',), + 'MinMax': ('DefaultMinMax',), + 'BaseCoord': ('MinCoord', 'MaxCoord',), + 'JstfLangSys': ('DefJstfLangSys',), + 'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB', + 'ExtensionDisableGSUB',), + 'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS', + 'ExtensionDisableGPOS',), + 'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',), +} def _buildClasses(): @@ -55,10 +62,11 @@ def _buildClasses(): name = m.group(1) baseClass = FormatSwitchingBaseTable if not namespace.has_key(name): + # the class doesn't exist yet, so the base implementation is used. cls = new.classobj(name, (baseClass,), {}) namespace[name] = cls - for base, alts in _equivalents: + for base, alts in _equivalents.items(): base = namespace[base] for alt in alts: namespace[alt] = new.classobj(alt, (base,), {}) @@ -87,9 +95,12 @@ def _buildClasses(): }, } lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS + for lookupEnum in lookupTypes.values(): + for enum, cls in lookupEnum.items(): + cls.LookupType = enum # add converters to classes - from otConverters import buildConverterList + from otConverters import buildConverters for name, table in otData: m = formatPat.match(name) if m: @@ -100,12 +111,12 @@ def _buildClasses(): if not hasattr(cls, "converters"): cls.converters = {} cls.convertersByName = {} - converters, convertersByName = buildConverterList(table[1:], namespace) + converters, convertersByName = buildConverters(table[1:], namespace) cls.converters[format] = converters cls.convertersByName[format] = convertersByName else: cls = namespace[name] - cls.converters, cls.convertersByName = buildConverterList(table, namespace) + cls.converters, cls.convertersByName = buildConverters(table, namespace) _buildClasses()