diff options
Diffstat (limited to 'Lib/fontTools/ttLib/tables/otTables.py')
-rw-r--r-- | Lib/fontTools/ttLib/tables/otTables.py | 438 |
1 files changed, 291 insertions, 147 deletions
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 85befb3b..fbd9db7b 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 namedtuple -from fontTools.misc.py23 import bytesjoin +from collections import defaultdict, namedtuple from fontTools.misc.roundTools import otRound -from fontTools.misc.textTools import pad, safeEval +from fontTools.misc.textTools import bytesjoin, pad, safeEval from .otBase import ( BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference, getFormatSwitchingBaseTableClass, @@ -425,8 +425,7 @@ class InsertionMorphAction(AATAction): return [] reader = actionReader.getSubReader( actionReader.pos + index * 2) - return [font.getGlyphName(glyphID) - for glyphID in reader.readUShortArray(count)] + return font.getGlyphNameMany(reader.readUShortArray(count)) def toXML(self, xmlWriter, font, attrs, name): xmlWriter.begintag(name, **attrs) @@ -521,12 +520,10 @@ 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. @@ -536,25 +533,11 @@ 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 - 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)) + startID = font.getGlyphID(start) + endID = font.getGlyphID(end) + 1 + glyphs.extend(font.getGlyphNameMany(range(startID, endID))) else: self.glyphs = [] log.warning("Unknown Coverage format: %s", self.Format) @@ -566,10 +549,9 @@ 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 = [getGlyphID(glyphName) for glyphName in glyphs ] + glyphIDs = font.getGlyphIDMany(glyphs) brokenOrder = sorted(glyphIDs) != glyphIDs last = glyphIDs[0] @@ -618,32 +600,18 @@ class Coverage(FormatSwitchingBaseTable): glyphs.append(attrs["value"]) -class VarIdxMap(BaseTable): +class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): def populateDefaults(self, propagator=None): if not hasattr(self, 'mapping'): - 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] - - rawTable = { 'mapping': mapping } - rawTable['MappingCount'] = len(mapping) + self.mapping = rawTable['mapping'] + @staticmethod + def getEntryFormat(mapping): ored = 0 for idx in mapping: ored |= idx @@ -666,9 +634,65 @@ class VarIdxMap(BaseTable): else: entrySize = 4 - entryFormat = ((entrySize - 1) << 4) | (innerBits - 1) + 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] - rawTable['EntryFormat'] = entryFormat + rawTable = {'mapping': mapping} + rawTable['MappingCount'] = len(mapping) + rawTable['EntryFormat'] = DeltaSetIndexMap.getEntryFormat(mapping) return rawTable def toXML2(self, xmlWriter, font): @@ -726,9 +750,9 @@ class SingleSubst(FormatSwitchingBaseTable): input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) if self.Format == 1: delta = rawTable["DeltaGlyphID"] - inputGIDS = [ font.getGlyphID(name) for name in input ] + inputGIDS = font.getGlyphIDMany(input) outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] - outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ] + outNames = font.getGlyphNameMany(outGIDS) for inp, out in zip(input, outNames): mapping[inp] = out elif self.Format == 2: @@ -882,51 +906,30 @@ class ClassDef(FormatSwitchingBaseTable): def postRead(self, rawTable, font): classDefs = {} - glyphOrder = font.getGlyphOrder() if self.Format == 1: start = rawTable["StartGlyph"] classList = rawTable["ClassValueArray"] - 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) + startID = font.getGlyphID(start) endID = startID + len(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): + glyphNames = font.getGlyphNameMany(range(startID, endID)) + for glyphName, cls in zip(glyphNames, classList): if cls: - classDefs[glyphOrder[glyphID]] = cls + classDefs[glyphName] = cls elif self.Format == 2: records = rawTable["ClassRangeRecord"] for rec in records: - start = rec.Start - end = rec.End 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) + if not cls: 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 + 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 else: log.warning("Unknown ClassDef format: %s", self.Format) self.classDefs = classDefs @@ -1179,7 +1182,6 @@ 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: @@ -1245,51 +1247,176 @@ class BaseGlyphRecordArray(BaseTable): return self.__dict__.copy() -class BaseGlyphV1List(BaseTable): +class BaseGlyphList(BaseTable): def preWrite(self, font): - self.BaseGlyphV1Record = sorted( - self.BaseGlyphV1Record, + self.BaseGlyphPaintRecord = sorted( + self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph) ) return self.__dict__.copy() +class ClipBox(getFormatSwitchingBaseTableClass("uint8")): -class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])): - __slots__ = () + def as_tuple(self): + return tuple(getattr(self, conv.name) for conv in self.getConverters()) - _value_mapper = None + def __repr__(self): + return f"{self.__class__.__name__}{self.as_tuple()}" - def __new__(cls, value, varIdx=0): - return super().__new__( - cls, - cls._value_mapper(value) if cls._value_mapper else value, - varIdx - ) - @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) +class ClipList(getFormatSwitchingBaseTableClass("uint8")): + def populateDefaults(self, propagator=None): + if not hasattr(self, "clips"): + self.clips = {} -class VariableFloat(VariableValue): - __slots__ = () - _value_mapper = float + 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 + 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() -class VariableInt(VariableValue): - __slots__ = () - _value_mapper = otRound + 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 ExtendMode(IntEnum): @@ -1313,21 +1440,22 @@ class CompositeMode(IntEnum): SRC_ATOP = 9 DEST_ATOP = 10 XOR = 11 - 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 + 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 class PaintFormat(IntEnum): @@ -1346,11 +1474,23 @@ class PaintFormat(IntEnum): PaintVarTransform = 13 PaintTranslate = 14 PaintVarTranslate = 15 - PaintRotate = 16 - PaintVarRotate = 17 - PaintSkew = 18 - PaintVarSkew = 19 - PaintComposite = 20 + 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 class Paint(getFormatSwitchingBaseTableClass("uint8")): @@ -1375,16 +1515,20 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")): def getChildren(self, colr): if self.Format == PaintFormat.PaintColrLayers: - return colr.LayerV1List.Paint[ + # 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[ self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers ] if self.Format == PaintFormat.PaintColrGlyph: - for record in colr.BaseGlyphV1List.BaseGlyphV1Record: + for record in colr.BaseGlyphList.BaseGlyphPaintRecord: if record.BaseGlyph == self.Glyph: return [record.Paint] else: - raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List") + raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") children = [] for conv in self.getConverters(): @@ -1490,20 +1634,22 @@ def fixLookupOverFlows(ttf, overflowRecord): return ok lookup = lookups[lookupIndex] - 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 + 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 ok = 1 return ok def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 - newSubTable.Format = oldSubTable.Format oldMapping = sorted(oldSubTable.mapping.items()) oldLen = len(oldMapping) @@ -1529,7 +1675,6 @@ def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 - newSubTable.Format = oldSubTable.Format if hasattr(oldSubTable, 'sortCoverageLast'): newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast @@ -1559,7 +1704,6 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): ok = 1 - newSubTable.Format = oldSubTable.Format oldLigs = sorted(oldSubTable.ligatures.items()) oldLen = len(oldLigs) |