| # coding: utf-8 |
| """fontTools.ttLib.tables.otTables -- A collection of classes representing the various |
| 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 |
| from functools import reduce |
| from math import radians |
| import itertools |
| from collections import defaultdict, namedtuple |
| from fontTools.ttLib.tables.otTraverse import dfs_base_table |
| from fontTools.misc.arrayTools import quantizeRect |
| from fontTools.misc.roundTools import otRound |
| from fontTools.misc.transform import Transform, Identity |
| from fontTools.misc.textTools import bytesjoin, pad, safeEval |
| from fontTools.pens.boundsPen import ControlBoundsPen |
| from fontTools.pens.transformPen import TransformPen |
| from .otBase import ( |
| BaseTable, |
| FormatSwitchingBaseTable, |
| ValueRecord, |
| CountReference, |
| getFormatSwitchingBaseTableClass, |
| ) |
| from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY |
| import logging |
| import struct |
| from typing import TYPE_CHECKING, Iterator, List, Optional, Set |
| |
| if TYPE_CHECKING: |
| from fontTools.ttLib.ttGlyphSet import _TTGlyphSet |
| |
| |
| log = logging.getLogger(__name__) |
| |
| |
| class AATStateTable(object): |
| def __init__(self): |
| self.GlyphClasses = {} # GlyphID --> GlyphClass |
| self.States = [] # List of AATState, indexed by state number |
| self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...] |
| |
| |
| class AATState(object): |
| def __init__(self): |
| self.Transitions = {} # GlyphClass --> AATAction |
| |
| |
| class AATAction(object): |
| _FLAGS = None |
| |
| @staticmethod |
| def compileActions(font, states): |
| return (None, None) |
| |
| def _writeFlagsToXML(self, xmlWriter): |
| flags = [f for f in self._FLAGS if self.__dict__[f]] |
| if flags: |
| xmlWriter.simpletag("Flags", value=",".join(flags)) |
| xmlWriter.newline() |
| if self.ReservedFlags != 0: |
| xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags) |
| xmlWriter.newline() |
| |
| def _setFlag(self, flag): |
| assert flag in self._FLAGS, "unsupported flag %s" % flag |
| self.__dict__[flag] = True |
| |
| |
| class RearrangementMorphAction(AATAction): |
| staticSize = 4 |
| actionHeaderSize = 0 |
| _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"] |
| |
| _VERBS = { |
| 0: "no change", |
| 1: "Ax ⇒ xA", |
| 2: "xD ⇒ Dx", |
| 3: "AxD ⇒ DxA", |
| 4: "ABx ⇒ xAB", |
| 5: "ABx ⇒ xBA", |
| 6: "xCD ⇒ CDx", |
| 7: "xCD ⇒ DCx", |
| 8: "AxCD ⇒ CDxA", |
| 9: "AxCD ⇒ DCxA", |
| 10: "ABxD ⇒ DxAB", |
| 11: "ABxD ⇒ DxBA", |
| 12: "ABxCD ⇒ CDxAB", |
| 13: "ABxCD ⇒ CDxBA", |
| 14: "ABxCD ⇒ DCxAB", |
| 15: "ABxCD ⇒ DCxBA", |
| } |
| |
| def __init__(self): |
| self.NewState = 0 |
| self.Verb = 0 |
| self.MarkFirst = False |
| self.DontAdvance = False |
| self.MarkLast = False |
| self.ReservedFlags = 0 |
| |
| def compile(self, writer, font, actionIndex): |
| assert actionIndex is None |
| writer.writeUShort(self.NewState) |
| assert self.Verb >= 0 and self.Verb <= 15, self.Verb |
| flags = self.Verb | self.ReservedFlags |
| if self.MarkFirst: |
| flags |= 0x8000 |
| if self.DontAdvance: |
| flags |= 0x4000 |
| if self.MarkLast: |
| flags |= 0x2000 |
| writer.writeUShort(flags) |
| |
| def decompile(self, reader, font, actionReader): |
| assert actionReader is None |
| self.NewState = reader.readUShort() |
| flags = reader.readUShort() |
| self.Verb = flags & 0xF |
| self.MarkFirst = bool(flags & 0x8000) |
| self.DontAdvance = bool(flags & 0x4000) |
| self.MarkLast = bool(flags & 0x2000) |
| self.ReservedFlags = flags & 0x1FF0 |
| |
| def toXML(self, xmlWriter, font, attrs, name): |
| xmlWriter.begintag(name, **attrs) |
| xmlWriter.newline() |
| xmlWriter.simpletag("NewState", value=self.NewState) |
| xmlWriter.newline() |
| self._writeFlagsToXML(xmlWriter) |
| xmlWriter.simpletag("Verb", value=self.Verb) |
| verbComment = self._VERBS.get(self.Verb) |
| if verbComment is not None: |
| xmlWriter.comment(verbComment) |
| xmlWriter.newline() |
| xmlWriter.endtag(name) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| self.NewState = self.Verb = self.ReservedFlags = 0 |
| self.MarkFirst = self.DontAdvance = self.MarkLast = False |
| content = [t for t in content if isinstance(t, tuple)] |
| for eltName, eltAttrs, eltContent in content: |
| if eltName == "NewState": |
| self.NewState = safeEval(eltAttrs["value"]) |
| elif eltName == "Verb": |
| self.Verb = safeEval(eltAttrs["value"]) |
| elif eltName == "ReservedFlags": |
| self.ReservedFlags = safeEval(eltAttrs["value"]) |
| elif eltName == "Flags": |
| for flag in eltAttrs["value"].split(","): |
| self._setFlag(flag.strip()) |
| |
| |
| class ContextualMorphAction(AATAction): |
| staticSize = 8 |
| actionHeaderSize = 0 |
| _FLAGS = ["SetMark", "DontAdvance"] |
| |
| def __init__(self): |
| self.NewState = 0 |
| self.SetMark, self.DontAdvance = False, False |
| self.ReservedFlags = 0 |
| self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF |
| |
| def compile(self, writer, font, actionIndex): |
| assert actionIndex is None |
| writer.writeUShort(self.NewState) |
| flags = self.ReservedFlags |
| if self.SetMark: |
| flags |= 0x8000 |
| if self.DontAdvance: |
| flags |= 0x4000 |
| writer.writeUShort(flags) |
| writer.writeUShort(self.MarkIndex) |
| writer.writeUShort(self.CurrentIndex) |
| |
| def decompile(self, reader, font, actionReader): |
| assert actionReader is None |
| self.NewState = reader.readUShort() |
| flags = reader.readUShort() |
| self.SetMark = bool(flags & 0x8000) |
| self.DontAdvance = bool(flags & 0x4000) |
| self.ReservedFlags = flags & 0x3FFF |
| self.MarkIndex = reader.readUShort() |
| self.CurrentIndex = reader.readUShort() |
| |
| def toXML(self, xmlWriter, font, attrs, name): |
| xmlWriter.begintag(name, **attrs) |
| xmlWriter.newline() |
| xmlWriter.simpletag("NewState", value=self.NewState) |
| xmlWriter.newline() |
| self._writeFlagsToXML(xmlWriter) |
| xmlWriter.simpletag("MarkIndex", value=self.MarkIndex) |
| xmlWriter.newline() |
| xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex) |
| xmlWriter.newline() |
| xmlWriter.endtag(name) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| self.NewState = self.ReservedFlags = 0 |
| self.SetMark = self.DontAdvance = False |
| self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF |
| content = [t for t in content if isinstance(t, tuple)] |
| for eltName, eltAttrs, eltContent in content: |
| if eltName == "NewState": |
| self.NewState = safeEval(eltAttrs["value"]) |
| elif eltName == "Flags": |
| for flag in eltAttrs["value"].split(","): |
| self._setFlag(flag.strip()) |
| elif eltName == "ReservedFlags": |
| self.ReservedFlags = safeEval(eltAttrs["value"]) |
| elif eltName == "MarkIndex": |
| self.MarkIndex = safeEval(eltAttrs["value"]) |
| elif eltName == "CurrentIndex": |
| self.CurrentIndex = safeEval(eltAttrs["value"]) |
| |
| |
| class LigAction(object): |
| def __init__(self): |
| self.Store = False |
| # GlyphIndexDelta is a (possibly negative) delta that gets |
| # added to the glyph ID at the top of the AAT runtime |
| # execution stack. It is *not* a byte offset into the |
| # morx table. The result of the addition, which is performed |
| # at run time by the shaping engine, is an index into |
| # the ligature components table. See 'morx' specification. |
| # In the AAT specification, this field is called Offset; |
| # but its meaning is quite different from other offsets |
| # in either AAT or OpenType, so we use a different name. |
| self.GlyphIndexDelta = 0 |
| |
| |
| class LigatureMorphAction(AATAction): |
| staticSize = 6 |
| |
| # 4 bytes for each of {action,ligComponents,ligatures}Offset |
| actionHeaderSize = 12 |
| |
| _FLAGS = ["SetComponent", "DontAdvance"] |
| |
| def __init__(self): |
| self.NewState = 0 |
| self.SetComponent, self.DontAdvance = False, False |
| self.ReservedFlags = 0 |
| self.Actions = [] |
| |
| def compile(self, writer, font, actionIndex): |
| assert actionIndex is not None |
| writer.writeUShort(self.NewState) |
| flags = self.ReservedFlags |
| if self.SetComponent: |
| flags |= 0x8000 |
| if self.DontAdvance: |
| flags |= 0x4000 |
| if len(self.Actions) > 0: |
| flags |= 0x2000 |
| writer.writeUShort(flags) |
| if len(self.Actions) > 0: |
| actions = self.compileLigActions() |
| writer.writeUShort(actionIndex[actions]) |
| else: |
| writer.writeUShort(0) |
| |
| def decompile(self, reader, font, actionReader): |
| assert actionReader is not None |
| self.NewState = reader.readUShort() |
| flags = reader.readUShort() |
| self.SetComponent = bool(flags & 0x8000) |
| self.DontAdvance = bool(flags & 0x4000) |
| performAction = bool(flags & 0x2000) |
| # As of 2017-09-12, the 'morx' specification says that |
| # the reserved bitmask in ligature subtables is 0x3FFF. |
| # However, the specification also defines a flag 0x2000, |
| # so the reserved value should actually be 0x1FFF. |
| # TODO: Report this specification bug to Apple. |
| self.ReservedFlags = flags & 0x1FFF |
| actionIndex = reader.readUShort() |
| if performAction: |
| self.Actions = self._decompileLigActions(actionReader, actionIndex) |
| else: |
| self.Actions = [] |
| |
| @staticmethod |
| def compileActions(font, states): |
| result, actions, actionIndex = b"", set(), {} |
| for state in states: |
| for _glyphClass, trans in state.Transitions.items(): |
| actions.add(trans.compileLigActions()) |
| # Sort the compiled actions in decreasing order of |
| # length, so that the longer sequence come before the |
| # shorter ones. For each compiled action ABCD, its |
| # suffixes BCD, CD, and D do not be encoded separately |
| # (in case they occur); instead, we can just store an |
| # index that points into the middle of the longer |
| # sequence. Every compiled AAT ligature sequence is |
| # terminated with an end-of-sequence flag, which can |
| # only be set on the last element of the sequence. |
| # Therefore, it is sufficient to consider just the |
| # suffixes. |
| for a in sorted(actions, key=lambda x: (-len(x), x)): |
| if a not in actionIndex: |
| for i in range(0, len(a), 4): |
| suffix = a[i:] |
| suffixIndex = (len(result) + i) // 4 |
| actionIndex.setdefault(suffix, suffixIndex) |
| result += a |
| result = pad(result, 4) |
| return (result, actionIndex) |
| |
| def compileLigActions(self): |
| result = [] |
| for i, action in enumerate(self.Actions): |
| last = i == len(self.Actions) - 1 |
| value = action.GlyphIndexDelta & 0x3FFFFFFF |
| value |= 0x80000000 if last else 0 |
| value |= 0x40000000 if action.Store else 0 |
| result.append(struct.pack(">L", value)) |
| return bytesjoin(result) |
| |
| def _decompileLigActions(self, actionReader, actionIndex): |
| actions = [] |
| last = False |
| reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4) |
| while not last: |
| value = reader.readULong() |
| last = bool(value & 0x80000000) |
| action = LigAction() |
| actions.append(action) |
| action.Store = bool(value & 0x40000000) |
| delta = value & 0x3FFFFFFF |
| if delta >= 0x20000000: # sign-extend 30-bit value |
| delta = -0x40000000 + delta |
| action.GlyphIndexDelta = delta |
| return actions |
| |
| def fromXML(self, name, attrs, content, font): |
| self.NewState = self.ReservedFlags = 0 |
| self.SetComponent = self.DontAdvance = False |
| self.ReservedFlags = 0 |
| self.Actions = [] |
| content = [t for t in content if isinstance(t, tuple)] |
| for eltName, eltAttrs, eltContent in content: |
| if eltName == "NewState": |
| self.NewState = safeEval(eltAttrs["value"]) |
| elif eltName == "Flags": |
| for flag in eltAttrs["value"].split(","): |
| self._setFlag(flag.strip()) |
| elif eltName == "ReservedFlags": |
| self.ReservedFlags = safeEval(eltAttrs["value"]) |
| elif eltName == "Action": |
| action = LigAction() |
| flags = eltAttrs.get("Flags", "").split(",") |
| flags = [f.strip() for f in flags] |
| action.Store = "Store" in flags |
| action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"]) |
| self.Actions.append(action) |
| |
| def toXML(self, xmlWriter, font, attrs, name): |
| xmlWriter.begintag(name, **attrs) |
| xmlWriter.newline() |
| xmlWriter.simpletag("NewState", value=self.NewState) |
| xmlWriter.newline() |
| self._writeFlagsToXML(xmlWriter) |
| for action in self.Actions: |
| attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)] |
| if action.Store: |
| attribs.append(("Flags", "Store")) |
| xmlWriter.simpletag("Action", attribs) |
| xmlWriter.newline() |
| xmlWriter.endtag(name) |
| xmlWriter.newline() |
| |
| |
| class InsertionMorphAction(AATAction): |
| staticSize = 8 |
| actionHeaderSize = 4 # 4 bytes for actionOffset |
| _FLAGS = [ |
| "SetMark", |
| "DontAdvance", |
| "CurrentIsKashidaLike", |
| "MarkedIsKashidaLike", |
| "CurrentInsertBefore", |
| "MarkedInsertBefore", |
| ] |
| |
| def __init__(self): |
| self.NewState = 0 |
| for flag in self._FLAGS: |
| setattr(self, flag, False) |
| self.ReservedFlags = 0 |
| self.CurrentInsertionAction, self.MarkedInsertionAction = [], [] |
| |
| def compile(self, writer, font, actionIndex): |
| assert actionIndex is not None |
| writer.writeUShort(self.NewState) |
| flags = self.ReservedFlags |
| if self.SetMark: |
| flags |= 0x8000 |
| if self.DontAdvance: |
| flags |= 0x4000 |
| if self.CurrentIsKashidaLike: |
| flags |= 0x2000 |
| if self.MarkedIsKashidaLike: |
| flags |= 0x1000 |
| if self.CurrentInsertBefore: |
| flags |= 0x0800 |
| if self.MarkedInsertBefore: |
| flags |= 0x0400 |
| flags |= len(self.CurrentInsertionAction) << 5 |
| flags |= len(self.MarkedInsertionAction) |
| writer.writeUShort(flags) |
| if len(self.CurrentInsertionAction) > 0: |
| currentIndex = actionIndex[tuple(self.CurrentInsertionAction)] |
| else: |
| currentIndex = 0xFFFF |
| writer.writeUShort(currentIndex) |
| if len(self.MarkedInsertionAction) > 0: |
| markedIndex = actionIndex[tuple(self.MarkedInsertionAction)] |
| else: |
| markedIndex = 0xFFFF |
| writer.writeUShort(markedIndex) |
| |
| def decompile(self, reader, font, actionReader): |
| assert actionReader is not None |
| self.NewState = reader.readUShort() |
| flags = reader.readUShort() |
| self.SetMark = bool(flags & 0x8000) |
| self.DontAdvance = bool(flags & 0x4000) |
| self.CurrentIsKashidaLike = bool(flags & 0x2000) |
| self.MarkedIsKashidaLike = bool(flags & 0x1000) |
| self.CurrentInsertBefore = bool(flags & 0x0800) |
| self.MarkedInsertBefore = bool(flags & 0x0400) |
| self.CurrentInsertionAction = self._decompileInsertionAction( |
| actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5) |
| ) |
| self.MarkedInsertionAction = self._decompileInsertionAction( |
| actionReader, font, index=reader.readUShort(), count=(flags & 0x001F) |
| ) |
| |
| def _decompileInsertionAction(self, actionReader, font, index, count): |
| if index == 0xFFFF or count == 0: |
| return [] |
| reader = actionReader.getSubReader(actionReader.pos + index * 2) |
| return font.getGlyphNameMany(reader.readUShortArray(count)) |
| |
| def toXML(self, xmlWriter, font, attrs, name): |
| xmlWriter.begintag(name, **attrs) |
| xmlWriter.newline() |
| xmlWriter.simpletag("NewState", value=self.NewState) |
| xmlWriter.newline() |
| self._writeFlagsToXML(xmlWriter) |
| for g in self.CurrentInsertionAction: |
| xmlWriter.simpletag("CurrentInsertionAction", glyph=g) |
| xmlWriter.newline() |
| for g in self.MarkedInsertionAction: |
| xmlWriter.simpletag("MarkedInsertionAction", glyph=g) |
| xmlWriter.newline() |
| xmlWriter.endtag(name) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| self.__init__() |
| content = [t for t in content if isinstance(t, tuple)] |
| for eltName, eltAttrs, eltContent in content: |
| if eltName == "NewState": |
| self.NewState = safeEval(eltAttrs["value"]) |
| elif eltName == "Flags": |
| for flag in eltAttrs["value"].split(","): |
| self._setFlag(flag.strip()) |
| elif eltName == "CurrentInsertionAction": |
| self.CurrentInsertionAction.append(eltAttrs["glyph"]) |
| elif eltName == "MarkedInsertionAction": |
| self.MarkedInsertionAction.append(eltAttrs["glyph"]) |
| else: |
| assert False, eltName |
| |
| @staticmethod |
| def compileActions(font, states): |
| actions, actionIndex, result = set(), {}, b"" |
| for state in states: |
| for _glyphClass, trans in state.Transitions.items(): |
| if trans.CurrentInsertionAction is not None: |
| actions.add(tuple(trans.CurrentInsertionAction)) |
| if trans.MarkedInsertionAction is not None: |
| actions.add(tuple(trans.MarkedInsertionAction)) |
| # Sort the compiled actions in decreasing order of |
| # length, so that the longer sequence come before the |
| # shorter ones. |
| for action in sorted(actions, key=lambda x: (-len(x), x)): |
| # We insert all sub-sequences of the action glyph sequence |
| # into actionIndex. For example, if one action triggers on |
| # glyph sequence [A, B, C, D, E] and another action triggers |
| # on [C, D], we return result=[A, B, C, D, E] (as list of |
| # encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0, |
| # ('C','D'): 2}. |
| if action in actionIndex: |
| continue |
| for start in range(0, len(action)): |
| startIndex = (len(result) // 2) + start |
| for limit in range(start, len(action)): |
| glyphs = action[start : limit + 1] |
| actionIndex.setdefault(glyphs, startIndex) |
| for glyph in action: |
| glyphID = font.getGlyphID(glyph) |
| result += struct.pack(">H", glyphID) |
| return result, actionIndex |
| |
| |
| class FeatureParams(BaseTable): |
| def compile(self, writer, font): |
| assert ( |
| featureParamTypes.get(writer["FeatureTag"]) == self.__class__ |
| ), "Wrong FeatureParams type for feature '%s': %s" % ( |
| writer["FeatureTag"], |
| self.__class__.__name__, |
| ) |
| BaseTable.compile(self, writer, font) |
| |
| def toXML(self, xmlWriter, font, attrs=None, name=None): |
| BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__) |
| |
| |
| class FeatureParamsSize(FeatureParams): |
| pass |
| |
| |
| class FeatureParamsStylisticSet(FeatureParams): |
| pass |
| |
| |
| class FeatureParamsCharacterVariants(FeatureParams): |
| pass |
| |
| |
| class Coverage(FormatSwitchingBaseTable): |
| # manual implementation to get rid of glyphID dependencies |
| |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "glyphs"): |
| self.glyphs = [] |
| |
| def postRead(self, rawTable, font): |
| if self.Format == 1: |
| self.glyphs = rawTable["GlyphArray"] |
| elif self.Format == 2: |
| glyphs = self.glyphs = [] |
| ranges = rawTable["RangeRecord"] |
| # 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. |
| sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex) |
| if ranges != sorted_ranges: |
| log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") |
| ranges = sorted_ranges |
| del sorted_ranges |
| for r in ranges: |
| start = r.Start |
| end = r.End |
| 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) |
| del self.Format # Don't need this anymore |
| |
| def preWrite(self, font): |
| glyphs = getattr(self, "glyphs", None) |
| if glyphs is None: |
| glyphs = self.glyphs = [] |
| format = 1 |
| rawTable = {"GlyphArray": glyphs} |
| if glyphs: |
| # find out whether Format 2 is more compact or not |
| glyphIDs = font.getGlyphIDMany(glyphs) |
| brokenOrder = sorted(glyphIDs) != glyphIDs |
| |
| 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) |
| |
| if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word |
| # Format 2 is more compact |
| index = 0 |
| for i in range(len(ranges)): |
| start, end = ranges[i] |
| r = RangeRecord() |
| r.StartID = start |
| r.Start = font.getGlyphName(start) |
| r.End = font.getGlyphName(end) |
| r.StartCoverageIndex = index |
| ranges[i] = r |
| index = index + end - start + 1 |
| if brokenOrder: |
| log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") |
| ranges.sort(key=lambda a: a.StartID) |
| for r in ranges: |
| del r.StartID |
| format = 2 |
| rawTable = {"RangeRecord": ranges} |
| # else: |
| # fallthrough; Format 1 is more compact |
| self.Format = format |
| return rawTable |
| |
| def toXML2(self, xmlWriter, font): |
| for glyphName in getattr(self, "glyphs", []): |
| xmlWriter.simpletag("Glyph", value=glyphName) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| glyphs = getattr(self, "glyphs", None) |
| if glyphs is None: |
| glyphs = [] |
| self.glyphs = glyphs |
| glyphs.append(attrs["value"]) |
| |
| |
| # The special 0xFFFFFFFF delta-set index is used to indicate that there |
| # is no variation data in the ItemVariationStore for a given variable field |
| NO_VARIATION_INDEX = 0xFFFFFFFF |
| |
| |
| class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "mapping"): |
| self.mapping = [] |
| |
| def postRead(self, rawTable, font): |
| assert (rawTable["EntryFormat"] & 0xFFC0) == 0 |
| self.mapping = rawTable["mapping"] |
| |
| @staticmethod |
| def getEntryFormat(mapping): |
| ored = 0 |
| for idx in mapping: |
| ored |= idx |
| |
| inner = ored & 0xFFFF |
| innerBits = 0 |
| while inner: |
| innerBits += 1 |
| inner >>= 1 |
| innerBits = max(innerBits, 1) |
| assert innerBits <= 16 |
| |
| ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1)) |
| if ored <= 0x000000FF: |
| entrySize = 1 |
| elif ored <= 0x0000FFFF: |
| entrySize = 2 |
| elif ored <= 0x00FFFFFF: |
| entrySize = 3 |
| 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): |
| # Make xml dump less verbose, by omitting no-op entries like: |
| # <Map index="..." outer="65535" inner="65535"/> |
| xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)") |
| xmlWriter.newline() |
| for i, value in enumerate(getattr(self, "mapping", [])): |
| attrs = [("index", i)] |
| if value != NO_VARIATION_INDEX: |
| attrs.extend( |
| [ |
| ("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.get("outer", "0xFFFF")) |
| inner = safeEval(attrs.get("inner", "0xFFFF")) |
| 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 = {"mapping": mapping} |
| rawTable["MappingCount"] = len(mapping) |
| rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping) |
| return rawTable |
| |
| def toXML2(self, xmlWriter, font): |
| for glyph, value in sorted(getattr(self, "mapping", {}).items()): |
| attrs = ( |
| ("glyph", glyph), |
| ("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: |
| mapping = {} |
| self.mapping = mapping |
| try: |
| glyph = attrs["glyph"] |
| except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 |
| glyph = font.getGlyphOrder()[attrs["index"]] |
| outer = safeEval(attrs["outer"]) |
| inner = safeEval(attrs["inner"]) |
| assert inner <= 0xFFFF |
| mapping[glyph] = (outer << 16) | inner |
| |
| |
| class VarRegionList(BaseTable): |
| def preWrite(self, font): |
| # The OT spec says VarStore.VarRegionList.RegionAxisCount should always |
| # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule |
| # even when the VarRegionList is empty. We can't treat RegionAxisCount |
| # like a normal propagated count (== len(Region[i].VarRegionAxis)), |
| # otherwise it would default to 0 if VarRegionList is empty. |
| # Thus, we force it to always be equal to fvar.axisCount. |
| # https://github.com/khaledhosny/ots/pull/192 |
| fvarTable = font.get("fvar") |
| if fvarTable: |
| self.RegionAxisCount = len(fvarTable.axes) |
| return { |
| **self.__dict__, |
| "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"), |
| } |
| |
| |
| class SingleSubst(FormatSwitchingBaseTable): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "mapping"): |
| self.mapping = {} |
| |
| def postRead(self, rawTable, font): |
| mapping = {} |
| input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) |
| if self.Format == 1: |
| delta = rawTable["DeltaGlyphID"] |
| inputGIDS = font.getGlyphIDMany(input) |
| outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS] |
| outNames = font.getGlyphNameMany(outGIDS) |
| for inp, out in zip(input, outNames): |
| mapping[inp] = out |
| elif self.Format == 2: |
| assert ( |
| len(input) == rawTable["GlyphCount"] |
| ), "invalid SingleSubstFormat2 table" |
| subst = rawTable["Substitute"] |
| for inp, sub in zip(input, subst): |
| mapping[inp] = sub |
| else: |
| assert 0, "unknown format: %s" % self.Format |
| self.mapping = mapping |
| del self.Format # Don't need this anymore |
| |
| def preWrite(self, font): |
| mapping = getattr(self, "mapping", None) |
| if mapping is None: |
| mapping = self.mapping = {} |
| items = list(mapping.items()) |
| getGlyphID = font.getGlyphID |
| gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items] |
| sortableItems = sorted(zip(gidItems, items)) |
| |
| # figure out format |
| format = 2 |
| delta = None |
| for inID, outID in gidItems: |
| if delta is None: |
| delta = (outID - inID) % 65536 |
| |
| if (inID + delta) % 65536 != outID: |
| break |
| else: |
| if delta is None: |
| # the mapping is empty, better use format 2 |
| format = 2 |
| else: |
| format = 1 |
| |
| rawTable = {} |
| self.Format = format |
| cov = Coverage() |
| 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 |
| rawTable["DeltaGlyphID"] = delta |
| else: |
| rawTable["Substitute"] = subst |
| return rawTable |
| |
| def toXML2(self, xmlWriter, font): |
| items = sorted(self.mapping.items()) |
| for inGlyph, outGlyph in items: |
| xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)]) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| mapping = getattr(self, "mapping", None) |
| if mapping is None: |
| mapping = {} |
| self.mapping = mapping |
| mapping[attrs["in"]] = attrs["out"] |
| |
| |
| class MultipleSubst(FormatSwitchingBaseTable): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "mapping"): |
| self.mapping = {} |
| |
| def postRead(self, rawTable, font): |
| mapping = {} |
| if self.Format == 1: |
| glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"]) |
| subst = [s.Substitute for s in rawTable["Sequence"]] |
| mapping = dict(zip(glyphs, subst)) |
| else: |
| assert 0, "unknown format: %s" % self.Format |
| self.mapping = mapping |
| del self.Format # Don't need this anymore |
| |
| def preWrite(self, font): |
| mapping = getattr(self, "mapping", None) |
| if mapping is None: |
| mapping = self.mapping = {} |
| cov = Coverage() |
| cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID) |
| self.Format = 1 |
| rawTable = { |
| "Coverage": cov, |
| "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs], |
| } |
| return rawTable |
| |
| def toXML2(self, xmlWriter, font): |
| items = sorted(self.mapping.items()) |
| for inGlyph, outGlyphs in items: |
| out = ",".join(outGlyphs) |
| xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)]) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| mapping = getattr(self, "mapping", None) |
| if mapping is None: |
| mapping = {} |
| self.mapping = mapping |
| |
| # TTX v3.0 and earlier. |
| if name == "Coverage": |
| self.old_coverage_ = [] |
| for element in content: |
| if not isinstance(element, tuple): |
| continue |
| element_name, element_attrs, _ = element |
| if element_name == "Glyph": |
| self.old_coverage_.append(element_attrs["value"]) |
| return |
| if name == "Sequence": |
| index = int(attrs.get("index", len(mapping))) |
| glyph = self.old_coverage_[index] |
| glyph_mapping = mapping[glyph] = [] |
| for element in content: |
| if not isinstance(element, tuple): |
| continue |
| element_name, element_attrs, _ = element |
| if element_name == "Substitute": |
| glyph_mapping.append(element_attrs["value"]) |
| return |
| |
| # TTX v3.1 and later. |
| outGlyphs = attrs["out"].split(",") if attrs["out"] else [] |
| mapping[attrs["in"]] = [g.strip() for g in outGlyphs] |
| |
| @staticmethod |
| def makeSequence_(g): |
| seq = Sequence() |
| seq.Substitute = g |
| return seq |
| |
| |
| class ClassDef(FormatSwitchingBaseTable): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "classDefs"): |
| self.classDefs = {} |
| |
| def postRead(self, rawTable, font): |
| classDefs = {} |
| |
| if self.Format == 1: |
| start = rawTable["StartGlyph"] |
| classList = rawTable["ClassValueArray"] |
| startID = font.getGlyphID(start) |
| endID = startID + len(classList) |
| glyphNames = font.getGlyphNameMany(range(startID, endID)) |
| for glyphName, cls in zip(glyphNames, classList): |
| if cls: |
| classDefs[glyphName] = 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 |
| else: |
| log.warning("Unknown ClassDef format: %s", self.Format) |
| self.classDefs = classDefs |
| del self.Format # Don't need this anymore |
| |
| def _getClassRanges(self, font): |
| classDefs = getattr(self, "classDefs", None) |
| if classDefs is None: |
| self.classDefs = {} |
| return |
| getGlyphID = font.getGlyphID |
| items = [] |
| for glyphName, cls in classDefs.items(): |
| if not cls: |
| continue |
| items.append((getGlyphID(glyphName), glyphName, cls)) |
| if items: |
| items.sort() |
| last, lastName, lastCls = items[0] |
| ranges = [[lastCls, last, lastName]] |
| for glyphID, glyphName, cls in items[1:]: |
| if glyphID != last + 1 or cls != lastCls: |
| ranges[-1].extend([last, lastName]) |
| ranges.append([cls, glyphID, glyphName]) |
| last = glyphID |
| lastName = glyphName |
| lastCls = cls |
| ranges[-1].extend([last, lastName]) |
| return ranges |
| |
| def preWrite(self, font): |
| format = 2 |
| rawTable = {"ClassRangeRecord": []} |
| ranges = self._getClassRanges(font) |
| if ranges: |
| startGlyph = ranges[0][1] |
| endGlyph = ranges[-1][3] |
| glyphCount = endGlyph - startGlyph + 1 |
| if len(ranges) * 3 < glyphCount + 1: |
| # Format 2 is more compact |
| for i in range(len(ranges)): |
| cls, start, startName, end, endName = ranges[i] |
| rec = ClassRangeRecord() |
| rec.Start = startName |
| rec.End = endName |
| rec.Class = cls |
| ranges[i] = rec |
| format = 2 |
| rawTable = {"ClassRangeRecord": ranges} |
| else: |
| # Format 1 is more compact |
| startGlyphName = ranges[0][2] |
| classes = [0] * glyphCount |
| for cls, start, startName, end, endName in ranges: |
| for g in range(start - startGlyph, end - startGlyph + 1): |
| classes[g] = cls |
| format = 1 |
| rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes} |
| self.Format = format |
| return rawTable |
| |
| def toXML2(self, xmlWriter, font): |
| items = sorted(self.classDefs.items()) |
| for glyphName, cls in items: |
| xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| classDefs = getattr(self, "classDefs", None) |
| if classDefs is None: |
| classDefs = {} |
| self.classDefs = classDefs |
| classDefs[attrs["glyph"]] = int(attrs["class"]) |
| |
| |
| class AlternateSubst(FormatSwitchingBaseTable): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "alternates"): |
| self.alternates = {} |
| |
| def postRead(self, rawTable, font): |
| alternates = {} |
| if self.Format == 1: |
| input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) |
| alts = rawTable["AlternateSet"] |
| assert len(input) == len(alts) |
| for inp, alt in zip(input, alts): |
| alternates[inp] = alt.Alternate |
| else: |
| assert 0, "unknown format: %s" % self.Format |
| self.alternates = alternates |
| del self.Format # Don't need this anymore |
| |
| def preWrite(self, font): |
| self.Format = 1 |
| alternates = getattr(self, "alternates", None) |
| if alternates is None: |
| alternates = self.alternates = {} |
| items = list(alternates.items()) |
| for i in range(len(items)): |
| glyphName, set = items[i] |
| items[i] = font.getGlyphID(glyphName), glyphName, set |
| items.sort() |
| cov = Coverage() |
| cov.glyphs = [item[1] for item in items] |
| alternates = [] |
| 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): |
| items = sorted(self.alternates.items()) |
| for glyphName, alternates in items: |
| xmlWriter.begintag("AlternateSet", glyph=glyphName) |
| xmlWriter.newline() |
| for alt in alternates: |
| xmlWriter.simpletag("Alternate", glyph=alt) |
| xmlWriter.newline() |
| xmlWriter.endtag("AlternateSet") |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| alternates = getattr(self, "alternates", None) |
| if alternates is None: |
| alternates = {} |
| self.alternates = alternates |
| glyphName = attrs["glyph"] |
| set = [] |
| alternates[glyphName] = set |
| for element in content: |
| if not isinstance(element, tuple): |
| continue |
| name, attrs, content = element |
| set.append(attrs["glyph"]) |
| |
| |
| class LigatureSubst(FormatSwitchingBaseTable): |
| def populateDefaults(self, propagator=None): |
| if not hasattr(self, "ligatures"): |
| self.ligatures = {} |
| |
| def postRead(self, rawTable, font): |
| ligatures = {} |
| if self.Format == 1: |
| input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) |
| ligSets = rawTable["LigatureSet"] |
| assert len(input) == len(ligSets) |
| for i in range(len(input)): |
| ligatures[input[i]] = ligSets[i].Ligature |
| else: |
| assert 0, "unknown format: %s" % self.Format |
| self.ligatures = ligatures |
| del self.Format # Don't need this anymore |
| |
| @staticmethod |
| def _getLigatureSortKey(components): |
| # Computes a key for ordering ligatures in a GSUB Type-4 lookup. |
| |
| # When building the OpenType lookup, we need to make sure that |
| # the longest sequence of components is listed first, so we |
| # use the negative length as the key for sorting. |
| # Note, we no longer need to worry about deterministic order because the |
| # ligature mapping `dict` remembers the insertion order, and this in |
| # turn depends on the order in which the ligatures are written in the FEA. |
| # Since python sort algorithm is stable, the ligatures of equal length |
| # will keep the relative order in which they appear in the feature file. |
| # For example, given the following ligatures (all starting with 'f' and |
| # thus belonging to the same LigatureSet): |
| # |
| # feature liga { |
| # sub f i by f_i; |
| # sub f f f by f_f_f; |
| # sub f f by f_f; |
| # sub f f i by f_f_i; |
| # } liga; |
| # |
| # this should sort to: f_f_f, f_f_i, f_i, f_f |
| # This is also what fea-rs does, see: |
| # https://github.com/adobe-type-tools/afdko/issues/1727 |
| # https://github.com/fonttools/fonttools/issues/3428 |
| # https://github.com/googlefonts/fontc/pull/680 |
| return -len(components) |
| |
| def preWrite(self, font): |
| self.Format = 1 |
| ligatures = getattr(self, "ligatures", None) |
| if ligatures is None: |
| ligatures = self.ligatures = {} |
| |
| if ligatures and isinstance(next(iter(ligatures)), tuple): |
| # New high-level API in v3.1 and later. Note that we just support compiling this |
| # for now. We don't load to this API, and don't do XML with it. |
| |
| # ligatures is map from components-sequence to lig-glyph |
| newLigatures = dict() |
| for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey): |
| ligature = Ligature() |
| ligature.Component = comps[1:] |
| ligature.CompCount = len(comps) |
| ligature.LigGlyph = ligatures[comps] |
| newLigatures.setdefault(comps[0], []).append(ligature) |
| ligatures = newLigatures |
| |
| items = list(ligatures.items()) |
| for i in range(len(items)): |
| glyphName, set = items[i] |
| items[i] = font.getGlyphID(glyphName), glyphName, set |
| items.sort() |
| cov = Coverage() |
| cov.glyphs = [item[1] for item in items] |
| |
| ligSets = [] |
| 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): |
| items = sorted(self.ligatures.items()) |
| for glyphName, ligSets in items: |
| xmlWriter.begintag("LigatureSet", glyph=glyphName) |
| xmlWriter.newline() |
| for lig in ligSets: |
| xmlWriter.simpletag( |
| "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component) |
| ) |
| xmlWriter.newline() |
| xmlWriter.endtag("LigatureSet") |
| xmlWriter.newline() |
| |
| def fromXML(self, name, attrs, content, font): |
| ligatures = getattr(self, "ligatures", None) |
| if ligatures is None: |
| ligatures = {} |
| self.ligatures = ligatures |
| glyphName = attrs["glyph"] |
| ligs = [] |
| ligatures[glyphName] = ligs |
| for element in content: |
| if not isinstance(element, tuple): |
| continue |
| name, attrs, content = element |
| lig = Ligature() |
| lig.LigGlyph = attrs["glyph"] |
| components = attrs["components"] |
| lig.Component = components.split(",") if components else [] |
| lig.CompCount = len(lig.Component) |
| ligs.append(lig) |
| |
| |
| class COLR(BaseTable): |
| def decompile(self, reader, font): |
| # COLRv0 is exceptional in that LayerRecordCount appears *after* the |
| # LayerRecordArray it counts, but the parser logic expects Count fields |
| # to always precede the arrays. Here we work around this by parsing the |
| # LayerRecordCount before the rest of the table, and storing it in |
| # the reader's local state. |
| subReader = reader.getSubReader(offset=0) |
| for conv in self.getConverters(): |
| if conv.name != "LayerRecordCount": |
| subReader.advance(conv.staticSize) |
| continue |
| reader[conv.name] = conv.read(subReader, font, tableDict={}) |
| break |
| else: |
| raise AssertionError("LayerRecordCount converter not found") |
| return BaseTable.decompile(self, reader, font) |
| |
| def preWrite(self, font): |
| # The writer similarly assumes Count values precede the things counted, |
| # thus here we pre-initialize a CountReference; the actual count value |
| # will be set to the lenght of the array by the time this is assembled. |
| self.LayerRecordCount = None |
| return { |
| **self.__dict__, |
| "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"), |
| } |
| |
| def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1): |
| if self.Version == 0: |
| return |
| |
| clips = {} |
| for rec in self.BaseGlyphList.BaseGlyphPaintRecord: |
| try: |
| clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization) |
| except Exception as e: |
| from fontTools.ttLib import TTLibError |
| |
| raise TTLibError( |
| f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}" |
| ) from e |
| |
| if clipBox is not None: |
| clips[rec.BaseGlyph] = clipBox |
| |
| hasClipList = hasattr(self, "ClipList") and self.ClipList is not None |
| if not clips: |
| if hasClipList: |
| self.ClipList = None |
| else: |
| if not hasClipList: |
| self.ClipList = ClipList() |
| self.ClipList.Format = 1 |
| self.ClipList.clips = clips |
| |
| |
| class LookupList(BaseTable): |
| @property |
| def table(self): |
| for l in self.Lookup: |
| for st in l.SubTable: |
| if type(st).__name__.endswith("Subst"): |
| return "GSUB" |
| if type(st).__name__.endswith("Pos"): |
| return "GPOS" |
| raise ValueError |
| |
| def toXML2(self, xmlWriter, font): |
| if ( |
| not font |
| or "Debg" not in font |
| or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data |
| ): |
| return super().toXML2(xmlWriter, font) |
| debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table] |
| for conv in self.getConverters(): |
| if conv.repeat: |
| value = getattr(self, conv.name, []) |
| for lookupIndex, item in enumerate(value): |
| if str(lookupIndex) in debugData: |
| info = LookupDebugInfo(*debugData[str(lookupIndex)]) |
| tag = info.location |
| if info.name: |
| tag = f"{info.name}: {tag}" |
| if info.feature: |
| script, language, feature = info.feature |
| tag = f"{tag} in {feature} ({script}/{language})" |
| xmlWriter.comment(tag) |
| xmlWriter.newline() |
| |
| conv.xmlWrite( |
| xmlWriter, font, item, conv.name, [("index", lookupIndex)] |
| ) |
| else: |
| if conv.aux and not eval(conv.aux, None, vars(self)): |
| continue |
| value = getattr( |
| self, conv.name, None |
| ) # TODO Handle defaults instead of defaulting to None! |
| conv.xmlWrite(xmlWriter, font, value, conv.name, []) |
| |
| |
| class BaseGlyphRecordArray(BaseTable): |
| def preWrite(self, font): |
| self.BaseGlyphRecord = sorted( |
| self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph) |
| ) |
| return self.__dict__.copy() |
| |
| |
| class BaseGlyphList(BaseTable): |
| def preWrite(self, font): |
| self.BaseGlyphPaintRecord = sorted( |
| self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph) |
| ) |
| return self.__dict__.copy() |
| |
| |
| class ClipBoxFormat(IntEnum): |
| Static = 1 |
| Variable = 2 |
| |
| def is_variable(self): |
| return self is self.Variable |
| |
| def as_variable(self): |
| return self.Variable |
| |
| |
| class ClipBox(getFormatSwitchingBaseTableClass("uint8")): |
| formatEnum = ClipBoxFormat |
| |
| def as_tuple(self): |
| return tuple(getattr(self, conv.name) for conv in self.getConverters()) |
| |
| def __repr__(self): |
| return f"{self.__class__.__name__}{self.as_tuple()}" |
| |
| |
| class ClipList(getFormatSwitchingBaseTableClass("uint8")): |
| 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 |
| |
| 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 ExtendMode(IntEnum): |
| PAD = 0 |
| REPEAT = 1 |
| REFLECT = 2 |
| |
| |
| # Porter-Duff modes for COLRv1 PaintComposite: |
| # https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration |
| class CompositeMode(IntEnum): |
| CLEAR = 0 |
| SRC = 1 |
| DEST = 2 |
| SRC_OVER = 3 |
| DEST_OVER = 4 |
| SRC_IN = 5 |
| DEST_IN = 6 |
| SRC_OUT = 7 |
| DEST_OUT = 8 |
| 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 |
| |
| |
| class PaintFormat(IntEnum): |
| PaintColrLayers = 1 |
| PaintSolid = 2 |
| PaintVarSolid = 3 |
| PaintLinearGradient = 4 |
| PaintVarLinearGradient = 5 |
| PaintRadialGradient = 6 |
| PaintVarRadialGradient = 7 |
| PaintSweepGradient = 8 |
| PaintVarSweepGradient = 9 |
| PaintGlyph = 10 |
| PaintColrGlyph = 11 |
| PaintTransform = 12 |
| 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 |
| |
| def is_variable(self): |
| return self.name.startswith("PaintVar") |
| |
| def as_variable(self): |
| if self.is_variable(): |
| return self |
| try: |
| return PaintFormat.__members__[f"PaintVar{self.name[5:]}"] |
| except KeyError: |
| return None |
| |
| |
| class Paint(getFormatSwitchingBaseTableClass("uint8")): |
| formatEnum = PaintFormat |
| |
| def getFormatName(self): |
| try: |
| return self.formatEnum(self.Format).name |
| except ValueError: |
| raise NotImplementedError(f"Unknown Paint format: {self.Format}") |
| |
| def toXML(self, xmlWriter, font, attrs=None, name=None): |
| tableName = name if name else self.__class__.__name__ |
| if attrs is None: |
| attrs = [] |
| attrs.append(("Format", self.Format)) |
| xmlWriter.begintag(tableName, attrs) |
| xmlWriter.comment(self.getFormatName()) |
| xmlWriter.newline() |
| self.toXML2(xmlWriter, font) |
| xmlWriter.endtag(tableName) |
| xmlWriter.newline() |
| |
| def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]: |
| 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 |
| yield from ( |
| BaseTable.SubTableEntry(name="Layers", value=v, index=i) |
| for i, v in enumerate( |
| layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers] |
| ) |
| ) |
| return |
| |
| if self.Format == PaintFormat.PaintColrGlyph: |
| for record in colr.BaseGlyphList.BaseGlyphPaintRecord: |
| if record.BaseGlyph == self.Glyph: |
| yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint) |
| return |
| else: |
| raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList") |
| |
| for conv in self.getConverters(): |
| if conv.tableClass is not None and issubclass(conv.tableClass, type(self)): |
| value = getattr(self, conv.name) |
| yield BaseTable.SubTableEntry(name=conv.name, value=value) |
| |
| def getChildren(self, colr) -> List["Paint"]: |
| # this is kept for backward compatibility (e.g. it's used by the subsetter) |
| return [p.value for p in self.iterPaintSubTables(colr)] |
| |
| def traverse(self, colr: COLR, callback): |
| """Depth-first traversal of graph rooted at self, callback on each node.""" |
| if not callable(callback): |
| raise TypeError("callback must be callable") |
| |
| for path in dfs_base_table( |
| self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr) |
| ): |
| paint = path[-1].value |
| callback(paint) |
| |
| def getTransform(self) -> Transform: |
| if self.Format == PaintFormat.PaintTransform: |
| t = self.Transform |
| return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy) |
| elif self.Format == PaintFormat.PaintTranslate: |
| return Identity.translate(self.dx, self.dy) |
| elif self.Format == PaintFormat.PaintScale: |
| return Identity.scale(self.scaleX, self.scaleY) |
| elif self.Format == PaintFormat.PaintScaleAroundCenter: |
| return ( |
| Identity.translate(self.centerX, self.centerY) |
| .scale(self.scaleX, self.scaleY) |
| .translate(-self.centerX, -self.centerY) |
| ) |
| elif self.Format == PaintFormat.PaintScaleUniform: |
| return Identity.scale(self.scale) |
| elif self.Format == PaintFormat.PaintScaleUniformAroundCenter: |
| return ( |
| Identity.translate(self.centerX, self.centerY) |
| .scale(self.scale) |
| .translate(-self.centerX, -self.centerY) |
| ) |
| elif self.Format == PaintFormat.PaintRotate: |
| return Identity.rotate(radians(self.angle)) |
| elif self.Format == PaintFormat.PaintRotateAroundCenter: |
| return ( |
| Identity.translate(self.centerX, self.centerY) |
| .rotate(radians(self.angle)) |
| .translate(-self.centerX, -self.centerY) |
| ) |
| elif self.Format == PaintFormat.PaintSkew: |
| return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle)) |
| elif self.Format == PaintFormat.PaintSkewAroundCenter: |
| return ( |
| Identity.translate(self.centerX, self.centerY) |
| .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle)) |
| .translate(-self.centerX, -self.centerY) |
| ) |
| if PaintFormat(self.Format).is_variable(): |
| raise NotImplementedError(f"Variable Paints not supported: {self.Format}") |
| |
| return Identity |
| |
| def computeClipBox( |
| self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1 |
| ) -> Optional[ClipBox]: |
| pen = ControlBoundsPen(glyphSet) |
| for path in dfs_base_table( |
| self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr) |
| ): |
| paint = path[-1].value |
| if paint.Format == PaintFormat.PaintGlyph: |
| transformation = reduce( |
| Transform.transform, |
| (st.value.getTransform() for st in path), |
| Identity, |
| ) |
| glyphSet[paint.Glyph].draw(TransformPen(pen, transformation)) |
| |
| if pen.bounds is None: |
| return None |
| |
| cb = ClipBox() |
| cb.Format = int(ClipBoxFormat.Static) |
| cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization) |
| return cb |
| |
| |
| # 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", |
| "VertGlyphCoverage", |
| "HorizGlyphCoverage", |
| "TopAccentCoverage", |
| "ExtendedShapeCoverage", |
| "MathKernCoverage", |
| ), |
| "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", |
| ), |
| "MathKern": ( |
| "TopRightMathKern", |
| "TopLeftMathKern", |
| "BottomRightMathKern", |
| "BottomLeftMathKern", |
| ), |
| "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"), |
| } |
| |
| # |
| # OverFlow logic, to automatically create ExtensionLookups |
| # XXX This should probably move to otBase.py |
| # |
| |
| |
| 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 is 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 is 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.SubTable[0].__class__.LookupType == extType: |
| lookupIndex = lookupIndex - 1 |
| if lookupIndex < 0: |
| 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 |
| ok = 1 |
| return ok |
| |
| |
| def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord): |
| ok = 1 |
| oldMapping = sorted(oldSubTable.mapping.items()) |
| oldLen = len(oldMapping) |
| |
| 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 = oldLen // 2 |
| |
| elif overflowRecord.itemName == "Sequence": |
| # We just need to back up by two items from the overflowed |
| # Sequence index to make sure the offset to the Coverage table |
| # doesn't overflow. |
| newLen = overflowRecord.itemIndex - 1 |
| |
| newSubTable.mapping = {} |
| for i in range(newLen, oldLen): |
| item = oldMapping[i] |
| key = item[0] |
| newSubTable.mapping[key] = item[1] |
| del oldSubTable.mapping[key] |
| |
| return ok |
| |
| |
| def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): |
| ok = 1 |
| if hasattr(oldSubTable, "sortCoverageLast"): |
| newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast |
| |
| oldAlts = sorted(oldSubTable.alternates.items()) |
| 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 = 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 |
| oldLigs = sorted(oldSubTable.ligatures.items()) |
| 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 = 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 |
| |
| |
| def splitPairPos(oldSubTable, newSubTable, overflowRecord): |
| st = oldSubTable |
| ok = False |
| newSubTable.Format = oldSubTable.Format |
| if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1: |
| for name in "ValueFormat1", "ValueFormat2": |
| setattr(newSubTable, name, getattr(oldSubTable, name)) |
| |
| # Move top half of coverage to new subtable |
| |
| newSubTable.Coverage = oldSubTable.Coverage.__class__() |
| |
| coverage = oldSubTable.Coverage.glyphs |
| records = oldSubTable.PairSet |
| |
| oldCount = len(oldSubTable.PairSet) // 2 |
| |
| oldSubTable.Coverage.glyphs = coverage[:oldCount] |
| oldSubTable.PairSet = records[:oldCount] |
| |
| newSubTable.Coverage.glyphs = coverage[oldCount:] |
| newSubTable.PairSet = records[oldCount:] |
| |
| oldSubTable.PairSetCount = len(oldSubTable.PairSet) |
| newSubTable.PairSetCount = len(newSubTable.PairSet) |
| |
| ok = True |
| |
| elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1: |
| if not hasattr(oldSubTable, "Class2Count"): |
| oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record) |
| for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2": |
| setattr(newSubTable, name, getattr(oldSubTable, name)) |
| |
| # The two subtables will still have the same ClassDef2 and the table |
| # sharing will still cause the sharing to overflow. As such, disable |
| # sharing on the one that is serialized second (that's oldSubTable). |
| oldSubTable.DontShare = True |
| |
| # Move top half of class numbers to new subtable |
| |
| newSubTable.Coverage = oldSubTable.Coverage.__class__() |
| newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__() |
| |
| coverage = oldSubTable.Coverage.glyphs |
| classDefs = oldSubTable.ClassDef1.classDefs |
| records = oldSubTable.Class1Record |
| |
| oldCount = len(oldSubTable.Class1Record) // 2 |
| newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount) |
| |
| oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs] |
| oldSubTable.ClassDef1.classDefs = { |
| k: v for k, v in classDefs.items() if v < oldCount |
| } |
| oldSubTable.Class1Record = records[:oldCount] |
| |
| newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs] |
| newSubTable.ClassDef1.classDefs = { |
| k: (v - oldCount) for k, v in classDefs.items() if v > oldCount |
| } |
| newSubTable.Class1Record = records[oldCount:] |
| |
| oldSubTable.Class1Count = len(oldSubTable.Class1Record) |
| newSubTable.Class1Count = len(newSubTable.Class1Record) |
| |
| ok = True |
| |
| return ok |
| |
| |
| def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord): |
| # split half of the mark classes to the new subtable |
| classCount = oldSubTable.ClassCount |
| if classCount < 2: |
| # oh well, not much left to split... |
| return False |
| |
| oldClassCount = classCount // 2 |
| newClassCount = classCount - oldClassCount |
| |
| oldMarkCoverage, oldMarkRecords = [], [] |
| newMarkCoverage, newMarkRecords = [], [] |
| for glyphName, markRecord in zip( |
| oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord |
| ): |
| if markRecord.Class < oldClassCount: |
| oldMarkCoverage.append(glyphName) |
| oldMarkRecords.append(markRecord) |
| else: |
| markRecord.Class -= oldClassCount |
| newMarkCoverage.append(glyphName) |
| newMarkRecords.append(markRecord) |
| |
| oldBaseRecords, newBaseRecords = [], [] |
| for rec in oldSubTable.BaseArray.BaseRecord: |
| oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__() |
| oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount] |
| newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:] |
| oldBaseRecords.append(oldBaseRecord) |
| newBaseRecords.append(newBaseRecord) |
| |
| newSubTable.Format = oldSubTable.Format |
| |
| oldSubTable.MarkCoverage.glyphs = oldMarkCoverage |
| newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__() |
| newSubTable.MarkCoverage.glyphs = newMarkCoverage |
| |
| # share the same BaseCoverage in both halves |
| newSubTable.BaseCoverage = oldSubTable.BaseCoverage |
| |
| oldSubTable.ClassCount = oldClassCount |
| newSubTable.ClassCount = newClassCount |
| |
| oldSubTable.MarkArray.MarkRecord = oldMarkRecords |
| newSubTable.MarkArray = oldSubTable.MarkArray.__class__() |
| newSubTable.MarkArray.MarkRecord = newMarkRecords |
| |
| oldSubTable.MarkArray.MarkCount = len(oldMarkRecords) |
| newSubTable.MarkArray.MarkCount = len(newMarkRecords) |
| |
| oldSubTable.BaseArray.BaseRecord = oldBaseRecords |
| newSubTable.BaseArray = oldSubTable.BaseArray.__class__() |
| newSubTable.BaseArray.BaseRecord = newBaseRecords |
| |
| oldSubTable.BaseArray.BaseCount = len(oldBaseRecords) |
| newSubTable.BaseArray.BaseCount = len(newBaseRecords) |
| |
| return True |
| |
| |
| 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. |
| """ |
| table = ttf[overflowRecord.tableType].table |
| lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex] |
| subIndex = overflowRecord.SubTableIndex |
| subtable = lookup.SubTable[subIndex] |
| |
| # First, try not sharing anything for this subtable... |
| if not hasattr(subtable, "DontShare"): |
| subtable.DontShare = True |
| return True |
| |
| 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.ExtSubTable.__class__.LookupType |
| extSubTable = subtable |
| subtable = extSubTable.ExtSubTable |
| newExtSubTableClass = lookupTypes[overflowRecord.tableType][ |
| extSubTable.__class__.LookupType |
| ] |
| newExtSubTable = newExtSubTableClass() |
| newExtSubTable.Format = extSubTable.Format |
| toInsert = newExtSubTable |
| |
| newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] |
| newSubTable = newSubTableClass() |
| newExtSubTable.ExtSubTable = newSubTable |
| else: |
| subTableType = subtable.__class__.LookupType |
| newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType] |
| newSubTable = newSubTableClass() |
| toInsert = newSubTable |
| |
| if hasattr(lookup, "SubTableCount"): # may not be defined yet. |
| lookup.SubTableCount = lookup.SubTableCount + 1 |
| |
| try: |
| splitFunc = splitTable[overflowRecord.tableType][subTableType] |
| except KeyError: |
| log.error( |
| "Don't know how to split %s lookup type %s", |
| overflowRecord.tableType, |
| subTableType, |
| ) |
| return False |
| |
| ok = splitFunc(subtable, newSubTable, overflowRecord) |
| if ok: |
| lookup.SubTable.insert(subIndex + 1, toInsert) |
| return ok |
| |
| |
| # End of OverFlow logic |
| |
| |
| def _buildClasses(): |
| import re |
| from .otData import otData |
| |
| formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$") |
| namespace = globals() |
| |
| # populate module with classes |
| for name, table in otData: |
| baseClass = BaseTable |
| m = formatPat.match(name) |
| if m: |
| # XxxFormatN subtable, we only add the "base" table |
| name = m.group(1) |
| # the first row of a format-switching otData table describes the Format; |
| # the first column defines the type of the Format field. |
| # Currently this can be either 'uint16' or 'uint8'. |
| formatType = table[0][0] |
| baseClass = getFormatSwitchingBaseTableClass(formatType) |
| if name not in namespace: |
| # the class doesn't exist yet, so the base implementation is used. |
| cls = type(name, (baseClass,), {}) |
| if name in ("GSUB", "GPOS"): |
| cls.DontShare = True |
| namespace[name] = cls |
| |
| # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.) |
| for name, _ in otData: |
| if name.startswith("Var") and len(name) > 3 and name[3:] in namespace: |
| varType = namespace[name] |
| noVarType = namespace[name[3:]] |
| varType.NoVarType = noVarType |
| noVarType.VarType = varType |
| |
| for base, alts in _equivalents.items(): |
| base = namespace[base] |
| for alt in alts: |
| namespace[alt] = base |
| |
| global lookupTypes |
| lookupTypes = { |
| "GSUB": { |
| 1: SingleSubst, |
| 2: MultipleSubst, |
| 3: AlternateSubst, |
| 4: LigatureSubst, |
| 5: ContextSubst, |
| 6: ChainContextSubst, |
| 7: ExtensionSubst, |
| 8: ReverseChainSingleSubst, |
| }, |
| "GPOS": { |
| 1: SinglePos, |
| 2: PairPos, |
| 3: CursivePos, |
| 4: MarkBasePos, |
| 5: MarkLigPos, |
| 6: MarkMarkPos, |
| 7: ContextPos, |
| 8: ChainContextPos, |
| 9: ExtensionPos, |
| }, |
| "mort": { |
| 4: NoncontextualMorph, |
| }, |
| "morx": { |
| 0: RearrangementMorph, |
| 1: ContextualMorph, |
| 2: LigatureMorph, |
| # 3: Reserved, |
| 4: NoncontextualMorph, |
| 5: InsertionMorph, |
| }, |
| } |
| lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS |
| for lookupEnum in lookupTypes.values(): |
| for enum, cls in lookupEnum.items(): |
| cls.LookupType = enum |
| |
| global featureParamTypes |
| featureParamTypes = { |
| "size": FeatureParamsSize, |
| } |
| for i in range(1, 20 + 1): |
| featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet |
| for i in range(1, 99 + 1): |
| featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants |
| |
| # add converters to classes |
| from .otConverters import buildConverters |
| |
| for name, table in otData: |
| m = formatPat.match(name) |
| if m: |
| # XxxFormatN subtable, add converter to "base" table |
| name, format = m.groups() |
| format = int(format) |
| cls = namespace[name] |
| if not hasattr(cls, "converters"): |
| cls.converters = {} |
| cls.convertersByName = {} |
| converters, convertersByName = buildConverters(table[1:], namespace) |
| cls.converters[format] = converters |
| cls.convertersByName[format] = convertersByName |
| # XXX Add staticSize? |
| else: |
| cls = namespace[name] |
| cls.converters, cls.convertersByName = buildConverters(table, namespace) |
| # XXX Add staticSize? |
| |
| |
| _buildClasses() |
| |
| |
| def _getGlyphsFromCoverageTable(coverage): |
| if coverage is None: |
| # empty coverage table |
| return [] |
| else: |
| return coverage.glyphs |