12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273 |
- # 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
- 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
- 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
|