# 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. """ from enum import IntEnum import itertools from collections import namedtuple from fontTools.misc.py23 import bytesjoin from fontTools.misc.roundTools import otRound from fontTools.misc.textTools import pad, safeEval from .otBase import ( BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference, getFormatSwitchingBaseTableClass, ) from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY import logging import struct 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.getGlyphName(glyphID) for glyphID in 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: # 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. 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: 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)) 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} getGlyphID = font.getGlyphID if glyphs: # find out whether Format 2 is more compact or not glyphIDs = [getGlyphID(glyphName) for glyphName in 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"]) 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) 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<> 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.getGlyphID(name) for name in input ] outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] outNames = [ font.getGlyphName(glyphID) for glyphID in 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 = {} 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) 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): if cls: classDefs[glyphOrder[glyphID]] = 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) 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 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 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,lig in sorted(ligatures.items(), key=lambda item: (-len(item[0]), item[0])): ligature = Ligature() ligature.Component = comps[1:] ligature.CompCount = len(comps) ligature.LigGlyph = lig 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 conv = self.getConverterByName("LayerRecordCount") 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") } 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 BaseGlyphV1List(BaseTable): def preWrite(self, font): self.BaseGlyphV1Record = sorted( self.BaseGlyphV1Record, key=lambda rec: font.getGlyphID(rec.BaseGlyph) ) return self.__dict__.copy() class VariableValue(namedtuple("VariableValue", ["value", "varIdx"])): __slots__ = () _value_mapper = None 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 VariableFloat(VariableValue): __slots__ = () _value_mapper = float class VariableInt(VariableValue): __slots__ = () _value_mapper = otRound 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 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): 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 PaintRotate = 16 PaintVarRotate = 17 PaintSkew = 18 PaintVarSkew = 19 PaintComposite = 20 class Paint(getFormatSwitchingBaseTableClass("uint8")): def getFormatName(self): try: return PaintFormat(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 getChildren(self, colr): if self.Format == PaintFormat.PaintColrLayers: return colr.LayerV1List.Paint[ self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers ] if self.Format == PaintFormat.PaintColrGlyph: for record in colr.BaseGlyphV1List.BaseGlyphV1Record: if record.BaseGlyph == self.Glyph: return [record.Paint] else: raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List") children = [] for conv in self.getConverters(): if conv.tableClass is not None and issubclass(conv.tableClass, type(self)): children.append(getattr(self, conv.name)) return children 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") stack = [self] visited = set() while stack: current = stack.pop() if id(current) in visited: continue callback(current) visited.add(id(current)) stack.extend(reversed(current.getChildren(colr))) # 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] 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) 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 newSubTable.Format = oldSubTable.Format 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 newSubTable.Format = oldSubTable.Format 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 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