diff options
Diffstat (limited to 'Lib/fontTools/ttLib/tables/otTables.py')
-rw-r--r-- | Lib/fontTools/ttLib/tables/otTables.py | 438 |
1 files changed, 147 insertions, 291 deletions
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index fbd9db7b..85befb3b 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -5,12 +5,12 @@ OpenType subtables. Most are constructed upon import from data in otData.py, all are populated with converter objects from otConverters.py. """ -import copy from enum import IntEnum import itertools -from collections import defaultdict, namedtuple +from collections import namedtuple +from fontTools.misc.py23 import bytesjoin from fontTools.misc.roundTools import otRound -from fontTools.misc.textTools import bytesjoin, pad, safeEval +from fontTools.misc.textTools import pad, safeEval from .otBase import ( BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference, getFormatSwitchingBaseTableClass, @@ -425,7 +425,8 @@ class InsertionMorphAction(AATAction): return [] reader = actionReader.getSubReader( actionReader.pos + index * 2) - return font.getGlyphNameMany(reader.readUShortArray(count)) + return [font.getGlyphName(glyphID) + for glyphID in reader.readUShortArray(count)] def toXML(self, xmlWriter, font, attrs, name): xmlWriter.begintag(name, **attrs) @@ -520,10 +521,12 @@ class Coverage(FormatSwitchingBaseTable): def postRead(self, rawTable, font): if self.Format == 1: + # TODO only allow glyphs that are valid? self.glyphs = rawTable["GlyphArray"] elif self.Format == 2: glyphs = self.glyphs = [] ranges = rawTable["RangeRecord"] + glyphOrder = font.getGlyphOrder() # Some SIL fonts have coverage entries that don't have sorted # StartCoverageIndex. If it is so, fixup and warn. We undo # this when writing font out. @@ -533,11 +536,25 @@ class Coverage(FormatSwitchingBaseTable): ranges = sorted_ranges del sorted_ranges for r in ranges: + assert r.StartCoverageIndex == len(glyphs), \ + (r.StartCoverageIndex, len(glyphs)) start = r.Start end = r.End - startID = font.getGlyphID(start) - endID = font.getGlyphID(end) + 1 - glyphs.extend(font.getGlyphNameMany(range(startID, endID))) + try: + startID = font.getGlyphID(start, requireReal=True) + except KeyError: + log.warning("Coverage table has start glyph ID out of range: %s.", start) + continue + try: + endID = font.getGlyphID(end, requireReal=True) + 1 + except KeyError: + # Apparently some tools use 65535 to "match all" the range + if end != 'glyph65535': + log.warning("Coverage table has end glyph ID out of range: %s.", end) + # NOTE: We clobber out-of-range things here. There are legit uses for those, + # but none that we have seen in the wild. + endID = len(glyphOrder) + glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID)) else: self.glyphs = [] log.warning("Unknown Coverage format: %s", self.Format) @@ -549,9 +566,10 @@ class Coverage(FormatSwitchingBaseTable): glyphs = self.glyphs = [] format = 1 rawTable = {"GlyphArray": glyphs} + getGlyphID = font.getGlyphID if glyphs: # find out whether Format 2 is more compact or not - glyphIDs = font.getGlyphIDMany(glyphs) + glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ] brokenOrder = sorted(glyphIDs) != glyphIDs last = glyphIDs[0] @@ -600,18 +618,32 @@ class Coverage(FormatSwitchingBaseTable): glyphs.append(attrs["value"]) -class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): +class VarIdxMap(BaseTable): def populateDefaults(self, propagator=None): if not hasattr(self, 'mapping'): - self.mapping = [] + self.mapping = {} def postRead(self, rawTable, font): assert (rawTable['EntryFormat'] & 0xFFC0) == 0 - self.mapping = rawTable['mapping'] + glyphOrder = font.getGlyphOrder() + mapList = rawTable['mapping'] + mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) + self.mapping = dict(zip(glyphOrder, mapList)) + + def preWrite(self, font): + mapping = getattr(self, "mapping", None) + if mapping is None: + mapping = self.mapping = {} + + glyphOrder = font.getGlyphOrder() + mapping = [mapping[g] for g in glyphOrder] + while len(mapping) > 1 and mapping[-2] == mapping[-1]: + del mapping[-1] + + rawTable = { 'mapping': mapping } + rawTable['MappingCount'] = len(mapping) - @staticmethod - def getEntryFormat(mapping): ored = 0 for idx in mapping: ored |= idx @@ -634,65 +666,9 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): else: entrySize = 4 - return ((entrySize - 1) << 4) | (innerBits - 1) - - def preWrite(self, font): - mapping = getattr(self, "mapping", None) - if mapping is None: - mapping = self.mapping = [] - self.Format = 1 if len(mapping) > 0xFFFF else 0 - rawTable = self.__dict__.copy() - rawTable['MappingCount'] = len(mapping) - rawTable['EntryFormat'] = self.getEntryFormat(mapping) - return rawTable - - def toXML2(self, xmlWriter, font): - for i, value in enumerate(getattr(self, "mapping", [])): - attrs = ( - ('index', i), - ('outer', value >> 16), - ('inner', value & 0xFFFF), - ) - xmlWriter.simpletag("Map", attrs) - xmlWriter.newline() - - def fromXML(self, name, attrs, content, font): - mapping = getattr(self, "mapping", None) - if mapping is None: - self.mapping = mapping = [] - index = safeEval(attrs['index']) - outer = safeEval(attrs['outer']) - inner = safeEval(attrs['inner']) - assert inner <= 0xFFFF - mapping.insert(index, (outer << 16) | inner) - - -class VarIdxMap(BaseTable): - - def populateDefaults(self, propagator=None): - if not hasattr(self, 'mapping'): - self.mapping = {} - - def postRead(self, rawTable, font): - assert (rawTable['EntryFormat'] & 0xFFC0) == 0 - glyphOrder = font.getGlyphOrder() - mapList = rawTable['mapping'] - mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) - self.mapping = dict(zip(glyphOrder, mapList)) - - def preWrite(self, font): - mapping = getattr(self, "mapping", None) - if mapping is None: - mapping = self.mapping = {} - - glyphOrder = font.getGlyphOrder() - mapping = [mapping[g] for g in glyphOrder] - while len(mapping) > 1 and mapping[-2] == mapping[-1]: - del mapping[-1] + entryFormat = ((entrySize - 1) << 4) | (innerBits - 1) - rawTable = {'mapping': mapping} - rawTable['MappingCount'] = len(mapping) - rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping) + rawTable['EntryFormat'] = entryFormat return rawTable def toXML2(self, xmlWriter, font): @@ -750,9 +726,9 @@ class SingleSubst(FormatSwitchingBaseTable): input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) if self.Format == 1: delta = rawTable["DeltaGlyphID"] - inputGIDS = font.getGlyphIDMany(input) + inputGIDS = [ font.getGlyphID(name) for name in input ] outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] - outNames = font.getGlyphNameMany(outGIDS) + outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ] for inp, out in zip(input, outNames): mapping[inp] = out elif self.Format == 2: @@ -906,30 +882,51 @@ class ClassDef(FormatSwitchingBaseTable): def postRead(self, rawTable, font): classDefs = {} + glyphOrder = font.getGlyphOrder() if self.Format == 1: start = rawTable["StartGlyph"] classList = rawTable["ClassValueArray"] - startID = font.getGlyphID(start) + try: + startID = font.getGlyphID(start, requireReal=True) + except KeyError: + log.warning("ClassDef table has start glyph ID out of range: %s.", start) + startID = len(glyphOrder) endID = startID + len(classList) - glyphNames = font.getGlyphNameMany(range(startID, endID)) - for glyphName, cls in zip(glyphNames, classList): + if endID > len(glyphOrder): + log.warning("ClassDef table has entries for out of range glyph IDs: %s,%s.", + start, len(classList)) + # NOTE: We clobber out-of-range things here. There are legit uses for those, + # but none that we have seen in the wild. + endID = len(glyphOrder) + + for glyphID, cls in zip(range(startID, endID), classList): if cls: - classDefs[glyphName] = cls + classDefs[glyphOrder[glyphID]] = cls elif self.Format == 2: records = rawTable["ClassRangeRecord"] for rec in records: - cls = rec.Class - if not cls: - continue start = rec.Start end = rec.End - startID = font.getGlyphID(start) - endID = font.getGlyphID(end) + 1 - glyphNames = font.getGlyphNameMany(range(startID, endID)) - for glyphName in glyphNames: - classDefs[glyphName] = cls + cls = rec.Class + try: + startID = font.getGlyphID(start, requireReal=True) + except KeyError: + log.warning("ClassDef table has start glyph ID out of range: %s.", start) + continue + try: + endID = font.getGlyphID(end, requireReal=True) + 1 + except KeyError: + # Apparently some tools use 65535 to "match all" the range + if end != 'glyph65535': + log.warning("ClassDef table has end glyph ID out of range: %s.", end) + # NOTE: We clobber out-of-range things here. There are legit uses for those, + # but none that we have seen in the wild. + endID = len(glyphOrder) + for glyphID in range(startID, endID): + if cls: + classDefs[glyphOrder[glyphID]] = cls else: log.warning("Unknown ClassDef format: %s", self.Format) self.classDefs = classDefs @@ -1182,6 +1179,7 @@ class COLR(BaseTable): if conv.name != "LayerRecordCount": subReader.advance(conv.staticSize) continue + conv = self.getConverterByName("LayerRecordCount") reader[conv.name] = conv.read(subReader, font, tableDict={}) break else: @@ -1247,176 +1245,51 @@ class BaseGlyphRecordArray(BaseTable): return self.__dict__.copy() -class BaseGlyphList(BaseTable): +class BaseGlyphV1List(BaseTable): def preWrite(self, font): - self.BaseGlyphPaintRecord = sorted( - self.BaseGlyphPaintRecord, + self.BaseGlyphV1Record = sorted( + self.BaseGlyphV1Record, key=lambda rec: font.getGlyphID(rec.BaseGlyph) ) return self.__dict__.copy() -class ClipBox(getFormatSwitchingBaseTableClass("uint8")): - def as_tuple(self): - return tuple(getattr(self, conv.name) for conv in self.getConverters()) +class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])): + __slots__ = () - def __repr__(self): - return f"{self.__class__.__name__}{self.as_tuple()}" + _value_mapper = None + def __new__(cls, value, varIdx=0): + return super().__new__( + cls, + cls._value_mapper(value) if cls._value_mapper else value, + varIdx + ) -class ClipList(getFormatSwitchingBaseTableClass("uint8")): + @classmethod + def _make(cls, iterable): + if cls._value_mapper: + it = iter(iterable) + try: + value = next(it) + except StopIteration: + pass + else: + value = cls._value_mapper(value) + iterable = itertools.chain((value,), it) + return super()._make(iterable) - def populateDefaults(self, propagator=None): - if not hasattr(self, "clips"): - self.clips = {} - def postRead(self, rawTable, font): - clips = {} - glyphOrder = font.getGlyphOrder() - for i, rec in enumerate(rawTable["ClipRecord"]): - if rec.StartGlyphID > rec.EndGlyphID: - log.warning( - "invalid ClipRecord[%i].StartGlyphID (%i) > " - "EndGlyphID (%i); skipped", - i, - rec.StartGlyphID, - rec.EndGlyphID, - ) - continue - redefinedGlyphs = [] - missingGlyphs = [] - for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1): - try: - glyph = glyphOrder[glyphID] - except IndexError: - missingGlyphs.append(glyphID) - continue - if glyph not in clips: - clips[glyph] = copy.copy(rec.ClipBox) - else: - redefinedGlyphs.append(glyphID) - if redefinedGlyphs: - log.warning( - "ClipRecord[%i] overlaps previous records; " - "ignoring redefined clip boxes for the " - "following glyph ID range: [%i-%i]", - i, - min(redefinedGlyphs), - max(redefinedGlyphs), - ) - if missingGlyphs: - log.warning( - "ClipRecord[%i] range references missing " - "glyph IDs: [%i-%i]", - i, - min(missingGlyphs), - max(missingGlyphs), - ) - self.clips = clips - - def groups(self): - glyphsByClip = defaultdict(list) - uniqueClips = {} - for glyphName, clipBox in self.clips.items(): - key = clipBox.as_tuple() - glyphsByClip[key].append(glyphName) - if key not in uniqueClips: - uniqueClips[key] = clipBox - return { - frozenset(glyphs): uniqueClips[key] - for key, glyphs in glyphsByClip.items() - } - - def preWrite(self, font): - if not hasattr(self, "clips"): - self.clips = {} - clipBoxRanges = {} - glyphMap = font.getReverseGlyphMap() - for glyphs, clipBox in self.groups().items(): - glyphIDs = sorted( - glyphMap[glyphName] for glyphName in glyphs - if glyphName in glyphMap - ) - if not glyphIDs: - continue - last = glyphIDs[0] - ranges = [[last]] - for glyphID in glyphIDs[1:]: - if glyphID != last + 1: - ranges[-1].append(last) - ranges.append([glyphID]) - last = glyphID - ranges[-1].append(last) - for start, end in ranges: - assert (start, end) not in clipBoxRanges - clipBoxRanges[(start, end)] = clipBox - - clipRecords = [] - for (start, end), clipBox in sorted(clipBoxRanges.items()): - record = ClipRecord() - record.StartGlyphID = start - record.EndGlyphID = end - record.ClipBox = clipBox - clipRecords.append(record) - rawTable = { - "ClipCount": len(clipRecords), - "ClipRecord": clipRecords, - } - return rawTable +class VariableFloat(VariableValue): + __slots__ = () + _value_mapper = float - def toXML(self, xmlWriter, font, attrs=None, name=None): - tableName = name if name else self.__class__.__name__ - if attrs is None: - attrs = [] - if hasattr(self, "Format"): - attrs.append(("Format", self.Format)) - xmlWriter.begintag(tableName, attrs) - xmlWriter.newline() - # sort clips alphabetically to ensure deterministic XML dump - for glyphs, clipBox in sorted( - self.groups().items(), key=lambda item: min(item[0]) - ): - xmlWriter.begintag("Clip") - xmlWriter.newline() - for glyphName in sorted(glyphs): - xmlWriter.simpletag("Glyph", value=glyphName) - xmlWriter.newline() - xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)]) - xmlWriter.newline() - clipBox.toXML2(xmlWriter, font) - xmlWriter.endtag("ClipBox") - xmlWriter.newline() - xmlWriter.endtag("Clip") - xmlWriter.newline() - xmlWriter.endtag(tableName) - xmlWriter.newline() - def fromXML(self, name, attrs, content, font): - clips = getattr(self, "clips", None) - if clips is None: - self.clips = clips = {} - assert name == "Clip" - glyphs = [] - clipBox = None - for elem in content: - if not isinstance(elem, tuple): - continue - name, attrs, content = elem - if name == "Glyph": - glyphs.append(attrs["value"]) - elif name == "ClipBox": - clipBox = ClipBox() - clipBox.Format = safeEval(attrs["Format"]) - for elem in content: - if not isinstance(elem, tuple): - continue - name, attrs, content = elem - clipBox.fromXML(name, attrs, content, font) - if clipBox: - for glyphName in glyphs: - clips[glyphName] = clipBox +class VariableInt(VariableValue): + __slots__ = () + _value_mapper = otRound class ExtendMode(IntEnum): @@ -1440,22 +1313,21 @@ class CompositeMode(IntEnum): SRC_ATOP = 9 DEST_ATOP = 10 XOR = 11 - PLUS = 12 - SCREEN = 13 - OVERLAY = 14 - DARKEN = 15 - LIGHTEN = 16 - COLOR_DODGE = 17 - COLOR_BURN = 18 - HARD_LIGHT = 19 - SOFT_LIGHT = 20 - DIFFERENCE = 21 - EXCLUSION = 22 - MULTIPLY = 23 - HSL_HUE = 24 - HSL_SATURATION = 25 - HSL_COLOR = 26 - HSL_LUMINOSITY = 27 + SCREEN = 12 + OVERLAY = 13 + DARKEN = 14 + LIGHTEN = 15 + COLOR_DODGE = 16 + COLOR_BURN = 17 + HARD_LIGHT = 18 + SOFT_LIGHT = 19 + DIFFERENCE = 20 + EXCLUSION = 21 + MULTIPLY = 22 + HSL_HUE = 23 + HSL_SATURATION = 24 + HSL_COLOR = 25 + HSL_LUMINOSITY = 26 class PaintFormat(IntEnum): @@ -1474,23 +1346,11 @@ class PaintFormat(IntEnum): PaintVarTransform = 13 PaintTranslate = 14 PaintVarTranslate = 15 - PaintScale = 16 - PaintVarScale = 17 - PaintScaleAroundCenter = 18 - PaintVarScaleAroundCenter = 19 - PaintScaleUniform = 20 - PaintVarScaleUniform = 21 - PaintScaleUniformAroundCenter = 22 - PaintVarScaleUniformAroundCenter = 23 - PaintRotate = 24 - PaintVarRotate = 25 - PaintRotateAroundCenter = 26 - PaintVarRotateAroundCenter = 27 - PaintSkew = 28 - PaintVarSkew = 29 - PaintSkewAroundCenter = 30 - PaintVarSkewAroundCenter = 31 - PaintComposite = 32 + PaintRotate = 16 + PaintVarRotate = 17 + PaintSkew = 18 + PaintVarSkew = 19 + PaintComposite = 20 class Paint(getFormatSwitchingBaseTableClass("uint8")): @@ -1515,20 +1375,16 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")): def getChildren(self, colr): if self.Format == PaintFormat.PaintColrLayers: - # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists - layers = [] - if colr.LayerList is not None: - layers = colr.LayerList.Paint - return layers[ + return colr.LayerV1List.Paint[ self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers ] if self.Format == PaintFormat.PaintColrGlyph: - for record in colr.BaseGlyphList.BaseGlyphPaintRecord: + for record in colr.BaseGlyphV1List.BaseGlyphV1Record: if record.BaseGlyph == self.Glyph: return [record.Paint] else: - raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") + raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List") children = [] for conv in self.getConverters(): @@ -1634,22 +1490,20 @@ def fixLookupOverFlows(ttf, overflowRecord): return ok lookup = lookups[lookupIndex] - for lookupIndex in range(lookupIndex, len(lookups)): - lookup = lookups[lookupIndex] - if lookup.LookupType != extType: - lookup.LookupType = extType - for si in range(len(lookup.SubTable)): - subTable = lookup.SubTable[si] - extSubTableClass = lookupTypes[overflowRecord.tableType][extType] - extSubTable = extSubTableClass() - extSubTable.Format = 1 - extSubTable.ExtSubTable = subTable - lookup.SubTable[si] = extSubTable + lookup.LookupType = extType + for si in range(len(lookup.SubTable)): + subTable = lookup.SubTable[si] + extSubTableClass = lookupTypes[overflowRecord.tableType][extType] + extSubTable = extSubTableClass() + extSubTable.Format = 1 + extSubTable.ExtSubTable = subTable + lookup.SubTable[si] = extSubTable ok = 1 return ok def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 + newSubTable.Format = oldSubTable.Format oldMapping = sorted(oldSubTable.mapping.items()) oldLen = len(oldMapping) @@ -1675,6 +1529,7 @@ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 + newSubTable.Format = oldSubTable.Format if hasattr(oldSubTable, 'sortCoverageLast'): newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast @@ -1704,6 +1559,7 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 + newSubTable.Format = oldSubTable.Format oldLigs = sorted(oldSubTable.ligatures.items()) oldLen = len(oldLigs) |