123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700 |
- # 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.TupleVariation import TupleVariation
- 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, DecomposedTransform
- from fontTools.misc.textTools import bytesjoin, pad, safeEval
- from fontTools.misc.vector import Vector
- from fontTools.pens.boundsPen import ControlBoundsPen
- from fontTools.pens.transformPen import TransformPen
- from .otBase import (
- BaseTable,
- FormatSwitchingBaseTable,
- ValueRecord,
- CountReference,
- getFormatSwitchingBaseTableClass,
- )
- from fontTools.misc.fixedTools import (
- fixedToFloat as fi2fl,
- floatToFixed as fl2fi,
- floatToFixedToStr as fl2str,
- strToFixedToFloat as str2fl,
- )
- from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
- import logging
- import struct
- import array
- import sys
- from enum import IntFlag
- from typing import TYPE_CHECKING, Iterator, List, Optional, Set
- if TYPE_CHECKING:
- from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
- log = logging.getLogger(__name__)
- class VarComponentFlags(IntFlag):
- RESET_UNSPECIFIED_AXES = 1 << 0
- HAVE_AXES = 1 << 1
- AXIS_VALUES_HAVE_VARIATION = 1 << 2
- TRANSFORM_HAS_VARIATION = 1 << 3
- HAVE_TRANSLATE_X = 1 << 4
- HAVE_TRANSLATE_Y = 1 << 5
- HAVE_ROTATION = 1 << 6
- HAVE_CONDITION = 1 << 7
- HAVE_SCALE_X = 1 << 8
- HAVE_SCALE_Y = 1 << 9
- HAVE_TCENTER_X = 1 << 10
- HAVE_TCENTER_Y = 1 << 11
- GID_IS_24BIT = 1 << 12
- HAVE_SKEW_X = 1 << 13
- HAVE_SKEW_Y = 1 << 14
- RESERVED_MASK = (1 << 32) - (1 << 15)
- VarTransformMappingValues = namedtuple(
- "VarTransformMappingValues",
- ["flag", "fractionalBits", "scale", "defaultValue"],
- )
- VAR_TRANSFORM_MAPPING = {
- "translateX": VarTransformMappingValues(
- VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
- ),
- "translateY": VarTransformMappingValues(
- VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
- ),
- "rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0),
- "scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1),
- "scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1),
- "skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0),
- "skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0),
- "tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0),
- "tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0),
- }
- # Probably should be somewhere in fontTools.misc
- _packer = {
- 1: lambda v: struct.pack(">B", v),
- 2: lambda v: struct.pack(">H", v),
- 3: lambda v: struct.pack(">L", v)[1:],
- 4: lambda v: struct.pack(">L", v),
- }
- _unpacker = {
- 1: lambda v: struct.unpack(">B", v)[0],
- 2: lambda v: struct.unpack(">H", v)[0],
- 3: lambda v: struct.unpack(">L", b"\0" + v)[0],
- 4: lambda v: struct.unpack(">L", v)[0],
- }
- def _read_uint32var(data, i):
- """Read a variable-length number from data starting at index i.
- Return the number and the next index.
- """
- b0 = data[i]
- if b0 < 0x80:
- return b0, i + 1
- elif b0 < 0xC0:
- return (b0 - 0x80) << 8 | data[i + 1], i + 2
- elif b0 < 0xE0:
- return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3
- elif b0 < 0xF0:
- return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[
- i + 3
- ], i + 4
- else:
- return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[
- i + 3
- ] << 8 | data[i + 4], i + 5
- def _write_uint32var(v):
- """Write a variable-length number.
- Return the data.
- """
- if v < 0x80:
- return struct.pack(">B", v)
- elif v < 0x4000:
- return struct.pack(">H", (v | 0x8000))
- elif v < 0x200000:
- return struct.pack(">L", (v | 0xC00000))[1:]
- elif v < 0x10000000:
- return struct.pack(">L", (v | 0xE0000000))
- else:
- return struct.pack(">B", 0xF0) + struct.pack(">L", v)
- class VarComponent:
- def __init__(self):
- self.populateDefaults()
- def populateDefaults(self, propagator=None):
- self.flags = 0
- self.glyphName = None
- self.conditionIndex = None
- self.axisIndicesIndex = None
- self.axisValues = ()
- self.axisValuesVarIndex = NO_VARIATION_INDEX
- self.transformVarIndex = NO_VARIATION_INDEX
- self.transform = DecomposedTransform()
- def decompile(self, data, font, localState):
- i = 0
- self.flags, i = _read_uint32var(data, i)
- flags = self.flags
- gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2
- glyphID = _unpacker[gidSize](data[i : i + gidSize])
- i += gidSize
- self.glyphName = font.glyphOrder[glyphID]
- if flags & VarComponentFlags.HAVE_CONDITION:
- self.conditionIndex, i = _read_uint32var(data, i)
- if flags & VarComponentFlags.HAVE_AXES:
- self.axisIndicesIndex, i = _read_uint32var(data, i)
- else:
- self.axisIndicesIndex = None
- if self.axisIndicesIndex is None:
- numAxes = 0
- else:
- axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex]
- numAxes = len(axisIndices)
- if flags & VarComponentFlags.HAVE_AXES:
- axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i)
- self.axisValues = tuple(fi2fl(v, 14) for v in axisValues)
- else:
- self.axisValues = ()
- assert len(self.axisValues) == numAxes
- if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION:
- self.axisValuesVarIndex, i = _read_uint32var(data, i)
- else:
- self.axisValuesVarIndex = NO_VARIATION_INDEX
- if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION:
- self.transformVarIndex, i = _read_uint32var(data, i)
- else:
- self.transformVarIndex = NO_VARIATION_INDEX
- self.transform = DecomposedTransform()
- def read_transform_component(values):
- nonlocal i
- if flags & values.flag:
- v = (
- fi2fl(
- struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits
- )
- * values.scale
- )
- i += 2
- return v
- else:
- return values.defaultValue
- for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
- value = read_transform_component(mapping_values)
- setattr(self.transform, attr_name, value)
- if not (flags & VarComponentFlags.HAVE_SCALE_Y):
- self.transform.scaleY = self.transform.scaleX
- n = flags & VarComponentFlags.RESERVED_MASK
- while n:
- _, i = _read_uint32var(data, i)
- n &= n - 1
- return data[i:]
- def compile(self, font):
- data = []
- flags = self.flags
- glyphID = font.getGlyphID(self.glyphName)
- if glyphID > 65535:
- flags |= VarComponentFlags.GID_IS_24BIT
- data.append(_packer[3](glyphID))
- else:
- flags &= ~VarComponentFlags.GID_IS_24BIT
- data.append(_packer[2](glyphID))
- if self.conditionIndex is not None:
- flags |= VarComponentFlags.HAVE_CONDITION
- data.append(_write_uint32var(self.conditionIndex))
- numAxes = len(self.axisValues)
- if numAxes:
- flags |= VarComponentFlags.HAVE_AXES
- data.append(_write_uint32var(self.axisIndicesIndex))
- data.append(
- TupleVariation.compileDeltaValues_(
- [fl2fi(v, 14) for v in self.axisValues]
- )
- )
- else:
- flags &= ~VarComponentFlags.HAVE_AXES
- if self.axisValuesVarIndex != NO_VARIATION_INDEX:
- flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
- data.append(_write_uint32var(self.axisValuesVarIndex))
- else:
- flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
- if self.transformVarIndex != NO_VARIATION_INDEX:
- flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION
- data.append(_write_uint32var(self.transformVarIndex))
- else:
- flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION
- def write_transform_component(value, values):
- if flags & values.flag:
- return struct.pack(
- ">h", fl2fi(value / values.scale, values.fractionalBits)
- )
- else:
- return b""
- for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
- value = getattr(self.transform, attr_name)
- data.append(write_transform_component(value, mapping_values))
- return _write_uint32var(flags) + bytesjoin(data)
- def toXML(self, writer, ttFont, attrs):
- writer.begintag("VarComponent", attrs)
- writer.newline()
- def write(name, value, attrs=()):
- if value is not None:
- writer.simpletag(name, (("value", value),) + attrs)
- writer.newline()
- write("glyphName", self.glyphName)
- if self.conditionIndex is not None:
- write("conditionIndex", self.conditionIndex)
- if self.axisIndicesIndex is not None:
- write("axisIndicesIndex", self.axisIndicesIndex)
- if (
- self.axisIndicesIndex is not None
- or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
- ):
- if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES:
- attrs = (("resetUnspecifiedAxes", 1),)
- else:
- attrs = ()
- write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs)
- if self.axisValuesVarIndex != NO_VARIATION_INDEX:
- write("axisValuesVarIndex", self.axisValuesVarIndex)
- if self.transformVarIndex != NO_VARIATION_INDEX:
- write("transformVarIndex", self.transformVarIndex)
- # Only write transform components that are specified in the
- # flags, even if they are the default value.
- for attr_name, mapping in VAR_TRANSFORM_MAPPING.items():
- if not (self.flags & mapping.flag):
- continue
- v = getattr(self.transform, attr_name)
- write(attr_name, fl2str(v, mapping.fractionalBits))
- writer.endtag("VarComponent")
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- content = [c for c in content if isinstance(c, tuple)]
- self.populateDefaults()
- for name, attrs, content in content:
- assert not content
- v = attrs["value"]
- if name == "glyphName":
- self.glyphName = v
- elif name == "conditionIndex":
- self.conditionIndex = safeEval(v)
- elif name == "axisIndicesIndex":
- self.axisIndicesIndex = safeEval(v)
- elif name == "axisValues":
- self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v))
- if safeEval(attrs.get("resetUnspecifiedAxes", "0")):
- self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES
- elif name == "axisValuesVarIndex":
- self.axisValuesVarIndex = safeEval(v)
- elif name == "transformVarIndex":
- self.transformVarIndex = safeEval(v)
- elif name in VAR_TRANSFORM_MAPPING:
- setattr(
- self.transform,
- name,
- safeEval(v),
- )
- self.flags |= VAR_TRANSFORM_MAPPING[name].flag
- else:
- assert False, name
- def applyTransformDeltas(self, deltas):
- i = 0
- def read_transform_component_delta(values):
- nonlocal i
- if self.flags & values.flag:
- v = fi2fl(deltas[i], values.fractionalBits) * values.scale
- i += 1
- return v
- else:
- return 0
- for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
- value = read_transform_component_delta(mapping_values)
- setattr(
- self.transform, attr_name, getattr(self.transform, attr_name) + value
- )
- if not (self.flags & VarComponentFlags.HAVE_SCALE_Y):
- self.transform.scaleY = self.transform.scaleX
- assert i == len(deltas), (i, len(deltas))
- def __eq__(self, other):
- if type(self) != type(other):
- return NotImplemented
- return self.__dict__ == other.__dict__
- def __ne__(self, other):
- result = self.__eq__(other)
- return result if result is NotImplemented else not result
- class VarCompositeGlyph:
- def __init__(self, components=None):
- self.components = components if components is not None else []
- def decompile(self, data, font, localState):
- self.components = []
- while data:
- component = VarComponent()
- data = component.decompile(data, font, localState)
- self.components.append(component)
- def compile(self, font):
- data = []
- for component in self.components:
- data.append(component.compile(font))
- return bytesjoin(data)
- def toXML(self, xmlWriter, font, attrs, name):
- xmlWriter.begintag("VarCompositeGlyph", attrs)
- xmlWriter.newline()
- for i, component in enumerate(self.components):
- component.toXML(xmlWriter, font, [("index", i)])
- xmlWriter.endtag("VarCompositeGlyph")
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- content = [c for c in content if isinstance(c, tuple)]
- for name, attrs, content in content:
- assert name == "VarComponent"
- component = VarComponent()
- component.fromXML(name, attrs, content, font)
- self.components.append(component)
- 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)
- def __getitem__(self, i):
- return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
- 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
- def __getitem__(self, glyphName):
- return self.mapping.get(glyphName, NO_VARIATION_INDEX)
- class VarRegionList(BaseTable):
- def preWrite(self, font):
- # The OT spec says VarStore.VarRegionList.RegionAxisCount should always
- # be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
- # even when the VarRegionList is empty. We can't treat RegionAxisCount
- # like a normal propagated count (== len(Region[i].VarRegionAxis)),
- # otherwise it would default to 0 if VarRegionList is empty.
- # Thus, we force it to always be equal to fvar.axisCount.
- # https://github.com/khaledhosny/ots/pull/192
- fvarTable = font.get("fvar")
- if fvarTable:
- self.RegionAxisCount = len(fvarTable.axes)
- return {
- **self.__dict__,
- "RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
- }
- class SingleSubst(FormatSwitchingBaseTable):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "mapping"):
- self.mapping = {}
- def postRead(self, rawTable, font):
- mapping = {}
- input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
- if self.Format == 1:
- delta = rawTable["DeltaGlyphID"]
- inputGIDS = font.getGlyphIDMany(input)
- outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
- outNames = font.getGlyphNameMany(outGIDS)
- for inp, out in zip(input, outNames):
- mapping[inp] = out
- elif self.Format == 2:
- assert (
- len(input) == rawTable["GlyphCount"]
- ), "invalid SingleSubstFormat2 table"
- subst = rawTable["Substitute"]
- for inp, sub in zip(input, subst):
- mapping[inp] = sub
- else:
- assert 0, "unknown format: %s" % self.Format
- self.mapping = mapping
- del self.Format # Don't need this anymore
- def preWrite(self, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = self.mapping = {}
- items = list(mapping.items())
- getGlyphID = font.getGlyphID
- gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
- sortableItems = sorted(zip(gidItems, items))
- # figure out format
- format = 2
- delta = None
- for inID, outID in gidItems:
- if delta is None:
- delta = (outID - inID) % 65536
- if (inID + delta) % 65536 != outID:
- break
- else:
- if delta is None:
- # the mapping is empty, better use format 2
- format = 2
- else:
- format = 1
- rawTable = {}
- self.Format = format
- cov = Coverage()
- input = [item[1][0] for item in sortableItems]
- subst = [item[1][1] for item in sortableItems]
- cov.glyphs = input
- rawTable["Coverage"] = cov
- if format == 1:
- assert delta is not None
- rawTable["DeltaGlyphID"] = delta
- else:
- rawTable["Substitute"] = subst
- return rawTable
- def toXML2(self, xmlWriter, font):
- items = sorted(self.mapping.items())
- for inGlyph, outGlyph in items:
- xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = {}
- self.mapping = mapping
- mapping[attrs["in"]] = attrs["out"]
- class MultipleSubst(FormatSwitchingBaseTable):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "mapping"):
- self.mapping = {}
- def postRead(self, rawTable, font):
- mapping = {}
- if self.Format == 1:
- glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
- subst = [s.Substitute for s in rawTable["Sequence"]]
- mapping = dict(zip(glyphs, subst))
- else:
- assert 0, "unknown format: %s" % self.Format
- self.mapping = mapping
- del self.Format # Don't need this anymore
- def preWrite(self, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = self.mapping = {}
- cov = Coverage()
- cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
- self.Format = 1
- rawTable = {
- "Coverage": cov,
- "Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
- }
- return rawTable
- def toXML2(self, xmlWriter, font):
- items = sorted(self.mapping.items())
- for inGlyph, outGlyphs in items:
- out = ",".join(outGlyphs)
- xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- mapping = getattr(self, "mapping", None)
- if mapping is None:
- mapping = {}
- self.mapping = mapping
- # TTX v3.0 and earlier.
- if name == "Coverage":
- self.old_coverage_ = []
- for element in content:
- if not isinstance(element, tuple):
- continue
- element_name, element_attrs, _ = element
- if element_name == "Glyph":
- self.old_coverage_.append(element_attrs["value"])
- return
- if name == "Sequence":
- index = int(attrs.get("index", len(mapping)))
- glyph = self.old_coverage_[index]
- glyph_mapping = mapping[glyph] = []
- for element in content:
- if not isinstance(element, tuple):
- continue
- element_name, element_attrs, _ = element
- if element_name == "Substitute":
- glyph_mapping.append(element_attrs["value"])
- return
- # TTX v3.1 and later.
- outGlyphs = attrs["out"].split(",") if attrs["out"] else []
- mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
- @staticmethod
- def makeSequence_(g):
- seq = Sequence()
- seq.Substitute = g
- return seq
- class ClassDef(FormatSwitchingBaseTable):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "classDefs"):
- self.classDefs = {}
- def postRead(self, rawTable, font):
- classDefs = {}
- if self.Format == 1:
- start = rawTable["StartGlyph"]
- classList = rawTable["ClassValueArray"]
- startID = font.getGlyphID(start)
- endID = startID + len(classList)
- glyphNames = font.getGlyphNameMany(range(startID, endID))
- for glyphName, cls in zip(glyphNames, classList):
- if cls:
- classDefs[glyphName] = cls
- elif self.Format == 2:
- records = rawTable["ClassRangeRecord"]
- for rec in records:
- cls = rec.Class
- if not cls:
- continue
- start = rec.Start
- end = rec.End
- startID = font.getGlyphID(start)
- endID = font.getGlyphID(end) + 1
- glyphNames = font.getGlyphNameMany(range(startID, endID))
- for glyphName in glyphNames:
- classDefs[glyphName] = cls
- else:
- log.warning("Unknown ClassDef format: %s", self.Format)
- self.classDefs = classDefs
- del self.Format # Don't need this anymore
- def _getClassRanges(self, font):
- classDefs = getattr(self, "classDefs", None)
- if classDefs is None:
- self.classDefs = {}
- return
- getGlyphID = font.getGlyphID
- items = []
- for glyphName, cls in classDefs.items():
- if not cls:
- continue
- items.append((getGlyphID(glyphName), glyphName, cls))
- if items:
- items.sort()
- last, lastName, lastCls = items[0]
- ranges = [[lastCls, last, lastName]]
- for glyphID, glyphName, cls in items[1:]:
- if glyphID != last + 1 or cls != lastCls:
- ranges[-1].extend([last, lastName])
- ranges.append([cls, glyphID, glyphName])
- last = glyphID
- lastName = glyphName
- lastCls = cls
- ranges[-1].extend([last, lastName])
- return ranges
- def preWrite(self, font):
- format = 2
- rawTable = {"ClassRangeRecord": []}
- ranges = self._getClassRanges(font)
- if ranges:
- startGlyph = ranges[0][1]
- endGlyph = ranges[-1][3]
- glyphCount = endGlyph - startGlyph + 1
- if len(ranges) * 3 < glyphCount + 1:
- # Format 2 is more compact
- for i in range(len(ranges)):
- cls, start, startName, end, endName = ranges[i]
- rec = ClassRangeRecord()
- rec.Start = startName
- rec.End = endName
- rec.Class = cls
- ranges[i] = rec
- format = 2
- rawTable = {"ClassRangeRecord": ranges}
- else:
- # Format 1 is more compact
- startGlyphName = ranges[0][2]
- classes = [0] * glyphCount
- for cls, start, startName, end, endName in ranges:
- for g in range(start - startGlyph, end - startGlyph + 1):
- classes[g] = cls
- format = 1
- rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes}
- self.Format = format
- return rawTable
- def toXML2(self, xmlWriter, font):
- items = sorted(self.classDefs.items())
- for glyphName, cls in items:
- xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- classDefs = getattr(self, "classDefs", None)
- if classDefs is None:
- classDefs = {}
- self.classDefs = classDefs
- classDefs[attrs["glyph"]] = int(attrs["class"])
- class AlternateSubst(FormatSwitchingBaseTable):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "alternates"):
- self.alternates = {}
- def postRead(self, rawTable, font):
- alternates = {}
- if self.Format == 1:
- input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
- alts = rawTable["AlternateSet"]
- assert len(input) == len(alts)
- for inp, alt in zip(input, alts):
- alternates[inp] = alt.Alternate
- else:
- assert 0, "unknown format: %s" % self.Format
- self.alternates = alternates
- del self.Format # Don't need this anymore
- def preWrite(self, font):
- self.Format = 1
- alternates = getattr(self, "alternates", None)
- if alternates is None:
- alternates = self.alternates = {}
- items = list(alternates.items())
- for i in range(len(items)):
- glyphName, set = items[i]
- items[i] = font.getGlyphID(glyphName), glyphName, set
- items.sort()
- cov = Coverage()
- cov.glyphs = [item[1] for item in items]
- alternates = []
- setList = [item[-1] for item in items]
- for set in setList:
- alts = AlternateSet()
- alts.Alternate = set
- alternates.append(alts)
- # a special case to deal with the fact that several hundred Adobe Japan1-5
- # CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
- # Also useful in that when splitting a sub-table because of an offset overflow
- # I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
- # Allows packing more rules in subtable.
- self.sortCoverageLast = 1
- return {"Coverage": cov, "AlternateSet": alternates}
- def toXML2(self, xmlWriter, font):
- items = sorted(self.alternates.items())
- for glyphName, alternates in items:
- xmlWriter.begintag("AlternateSet", glyph=glyphName)
- xmlWriter.newline()
- for alt in alternates:
- xmlWriter.simpletag("Alternate", glyph=alt)
- xmlWriter.newline()
- xmlWriter.endtag("AlternateSet")
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- alternates = getattr(self, "alternates", None)
- if alternates is None:
- alternates = {}
- self.alternates = alternates
- glyphName = attrs["glyph"]
- set = []
- alternates[glyphName] = set
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrs, content = element
- set.append(attrs["glyph"])
- class LigatureSubst(FormatSwitchingBaseTable):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "ligatures"):
- self.ligatures = {}
- def postRead(self, rawTable, font):
- ligatures = {}
- if self.Format == 1:
- input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
- ligSets = rawTable["LigatureSet"]
- assert len(input) == len(ligSets)
- for i in range(len(input)):
- ligatures[input[i]] = ligSets[i].Ligature
- else:
- assert 0, "unknown format: %s" % self.Format
- self.ligatures = ligatures
- del self.Format # Don't need this anymore
- @staticmethod
- def _getLigatureSortKey(components):
- # Computes a key for ordering ligatures in a GSUB Type-4 lookup.
- # When building the OpenType lookup, we need to make sure that
- # the longest sequence of components is listed first, so we
- # use the negative length as the key for sorting.
- # Note, we no longer need to worry about deterministic order because the
- # ligature mapping `dict` remembers the insertion order, and this in
- # turn depends on the order in which the ligatures are written in the FEA.
- # Since python sort algorithm is stable, the ligatures of equal length
- # will keep the relative order in which they appear in the feature file.
- # For example, given the following ligatures (all starting with 'f' and
- # thus belonging to the same LigatureSet):
- #
- # feature liga {
- # sub f i by f_i;
- # sub f f f by f_f_f;
- # sub f f by f_f;
- # sub f f i by f_f_i;
- # } liga;
- #
- # this should sort to: f_f_f, f_f_i, f_i, f_f
- # This is also what fea-rs does, see:
- # https://github.com/adobe-type-tools/afdko/issues/1727
- # https://github.com/fonttools/fonttools/issues/3428
- # https://github.com/googlefonts/fontc/pull/680
- return -len(components)
- def preWrite(self, font):
- self.Format = 1
- ligatures = getattr(self, "ligatures", None)
- if ligatures is None:
- ligatures = self.ligatures = {}
- if ligatures and isinstance(next(iter(ligatures)), tuple):
- # New high-level API in v3.1 and later. Note that we just support compiling this
- # for now. We don't load to this API, and don't do XML with it.
- # ligatures is map from components-sequence to lig-glyph
- newLigatures = dict()
- for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey):
- ligature = Ligature()
- ligature.Component = comps[1:]
- ligature.CompCount = len(comps)
- ligature.LigGlyph = ligatures[comps]
- newLigatures.setdefault(comps[0], []).append(ligature)
- ligatures = newLigatures
- items = list(ligatures.items())
- for i in range(len(items)):
- glyphName, set = items[i]
- items[i] = font.getGlyphID(glyphName), glyphName, set
- items.sort()
- cov = Coverage()
- cov.glyphs = [item[1] for item in items]
- ligSets = []
- setList = [item[-1] for item in items]
- for set in setList:
- ligSet = LigatureSet()
- ligs = ligSet.Ligature = []
- for lig in set:
- ligs.append(lig)
- ligSets.append(ligSet)
- # Useful in that when splitting a sub-table because of an offset overflow
- # I don't need to calculate the change in subtabl offset due to the coverage table size.
- # Allows packing more rules in subtable.
- self.sortCoverageLast = 1
- return {"Coverage": cov, "LigatureSet": ligSets}
- def toXML2(self, xmlWriter, font):
- items = sorted(self.ligatures.items())
- for glyphName, ligSets in items:
- xmlWriter.begintag("LigatureSet", glyph=glyphName)
- xmlWriter.newline()
- for lig in ligSets:
- xmlWriter.simpletag(
- "Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
- )
- xmlWriter.newline()
- xmlWriter.endtag("LigatureSet")
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- ligatures = getattr(self, "ligatures", None)
- if ligatures is None:
- ligatures = {}
- self.ligatures = ligatures
- glyphName = attrs["glyph"]
- ligs = []
- ligatures[glyphName] = ligs
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrs, content = element
- lig = Ligature()
- lig.LigGlyph = attrs["glyph"]
- components = attrs["components"]
- lig.Component = components.split(",") if components else []
- lig.CompCount = len(lig.Component)
- ligs.append(lig)
- class COLR(BaseTable):
- def decompile(self, reader, font):
- # COLRv0 is exceptional in that LayerRecordCount appears *after* the
- # LayerRecordArray it counts, but the parser logic expects Count fields
- # to always precede the arrays. Here we work around this by parsing the
- # LayerRecordCount before the rest of the table, and storing it in
- # the reader's local state.
- subReader = reader.getSubReader(offset=0)
- for conv in self.getConverters():
- if conv.name != "LayerRecordCount":
- subReader.advance(conv.staticSize)
- continue
- reader[conv.name] = conv.read(subReader, font, tableDict={})
- break
- else:
- raise AssertionError("LayerRecordCount converter not found")
- return BaseTable.decompile(self, reader, font)
- def preWrite(self, font):
- # The writer similarly assumes Count values precede the things counted,
- # thus here we pre-initialize a CountReference; the actual count value
- # will be set to the lenght of the array by the time this is assembled.
- self.LayerRecordCount = None
- return {
- **self.__dict__,
- "LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
- }
- def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
- if self.Version == 0:
- return
- clips = {}
- for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
- try:
- clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
- except Exception as e:
- from fontTools.ttLib import TTLibError
- raise TTLibError(
- f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
- ) from e
- if clipBox is not None:
- clips[rec.BaseGlyph] = clipBox
- hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
- if not clips:
- if hasClipList:
- self.ClipList = None
- else:
- if not hasClipList:
- self.ClipList = ClipList()
- self.ClipList.Format = 1
- self.ClipList.clips = clips
- class LookupList(BaseTable):
- @property
- def table(self):
- for l in self.Lookup:
- for st in l.SubTable:
- if type(st).__name__.endswith("Subst"):
- return "GSUB"
- if type(st).__name__.endswith("Pos"):
- return "GPOS"
- raise ValueError
- def toXML2(self, xmlWriter, font):
- if (
- not font
- or "Debg" not in font
- or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
- ):
- return super().toXML2(xmlWriter, font)
- debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
- for conv in self.getConverters():
- if conv.repeat:
- value = getattr(self, conv.name, [])
- for lookupIndex, item in enumerate(value):
- if str(lookupIndex) in debugData:
- info = LookupDebugInfo(*debugData[str(lookupIndex)])
- tag = info.location
- if info.name:
- tag = f"{info.name}: {tag}"
- if info.feature:
- script, language, feature = info.feature
- tag = f"{tag} in {feature} ({script}/{language})"
- xmlWriter.comment(tag)
- xmlWriter.newline()
- conv.xmlWrite(
- xmlWriter, font, item, conv.name, [("index", lookupIndex)]
- )
- else:
- if conv.aux and not eval(conv.aux, None, vars(self)):
- continue
- value = getattr(
- self, conv.name, None
- ) # TODO Handle defaults instead of defaulting to None!
- conv.xmlWrite(xmlWriter, font, value, conv.name, [])
- class BaseGlyphRecordArray(BaseTable):
- def preWrite(self, font):
- self.BaseGlyphRecord = sorted(
- self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
- )
- return self.__dict__.copy()
- class BaseGlyphList(BaseTable):
- def preWrite(self, font):
- self.BaseGlyphPaintRecord = sorted(
- self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
- )
- return self.__dict__.copy()
- class ClipBoxFormat(IntEnum):
- Static = 1
- Variable = 2
- def is_variable(self):
- return self is self.Variable
- def as_variable(self):
- return self.Variable
- class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
- formatEnum = ClipBoxFormat
- def as_tuple(self):
- return tuple(getattr(self, conv.name) for conv in self.getConverters())
- def __repr__(self):
- return f"{self.__class__.__name__}{self.as_tuple()}"
- class ClipList(getFormatSwitchingBaseTableClass("uint8")):
- def populateDefaults(self, propagator=None):
- if not hasattr(self, "clips"):
- self.clips = {}
- def postRead(self, rawTable, font):
- clips = {}
- glyphOrder = font.getGlyphOrder()
- for i, rec in enumerate(rawTable["ClipRecord"]):
- if rec.StartGlyphID > rec.EndGlyphID:
- log.warning(
- "invalid ClipRecord[%i].StartGlyphID (%i) > "
- "EndGlyphID (%i); skipped",
- i,
- rec.StartGlyphID,
- rec.EndGlyphID,
- )
- continue
- redefinedGlyphs = []
- missingGlyphs = []
- for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
- try:
- glyph = glyphOrder[glyphID]
- except IndexError:
- missingGlyphs.append(glyphID)
- continue
- if glyph not in clips:
- clips[glyph] = copy.copy(rec.ClipBox)
- else:
- redefinedGlyphs.append(glyphID)
- if redefinedGlyphs:
- log.warning(
- "ClipRecord[%i] overlaps previous records; "
- "ignoring redefined clip boxes for the "
- "following glyph ID range: [%i-%i]",
- i,
- min(redefinedGlyphs),
- max(redefinedGlyphs),
- )
- if missingGlyphs:
- log.warning(
- "ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
- i,
- min(missingGlyphs),
- max(missingGlyphs),
- )
- self.clips = clips
- def groups(self):
- glyphsByClip = defaultdict(list)
- uniqueClips = {}
- for glyphName, clipBox in self.clips.items():
- key = clipBox.as_tuple()
- glyphsByClip[key].append(glyphName)
- if key not in uniqueClips:
- uniqueClips[key] = clipBox
- return {
- frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
- }
- def preWrite(self, font):
- if not hasattr(self, "clips"):
- self.clips = {}
- clipBoxRanges = {}
- glyphMap = font.getReverseGlyphMap()
- for glyphs, clipBox in self.groups().items():
- glyphIDs = sorted(
- glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
- )
- if not glyphIDs:
- continue
- last = glyphIDs[0]
- ranges = [[last]]
- for glyphID in glyphIDs[1:]:
- if glyphID != last + 1:
- ranges[-1].append(last)
- ranges.append([glyphID])
- last = glyphID
- ranges[-1].append(last)
- for start, end in ranges:
- assert (start, end) not in clipBoxRanges
- clipBoxRanges[(start, end)] = clipBox
- clipRecords = []
- for (start, end), clipBox in sorted(clipBoxRanges.items()):
- record = ClipRecord()
- record.StartGlyphID = start
- record.EndGlyphID = end
- record.ClipBox = clipBox
- clipRecords.append(record)
- rawTable = {
- "ClipCount": len(clipRecords),
- "ClipRecord": clipRecords,
- }
- return rawTable
- def toXML(self, xmlWriter, font, attrs=None, name=None):
- tableName = name if name else self.__class__.__name__
- if attrs is None:
- attrs = []
- if hasattr(self, "Format"):
- attrs.append(("Format", self.Format))
- xmlWriter.begintag(tableName, attrs)
- xmlWriter.newline()
- # sort clips alphabetically to ensure deterministic XML dump
- for glyphs, clipBox in sorted(
- self.groups().items(), key=lambda item: min(item[0])
- ):
- xmlWriter.begintag("Clip")
- xmlWriter.newline()
- for glyphName in sorted(glyphs):
- xmlWriter.simpletag("Glyph", value=glyphName)
- xmlWriter.newline()
- xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
- xmlWriter.newline()
- clipBox.toXML2(xmlWriter, font)
- xmlWriter.endtag("ClipBox")
- xmlWriter.newline()
- xmlWriter.endtag("Clip")
- xmlWriter.newline()
- xmlWriter.endtag(tableName)
- xmlWriter.newline()
- def fromXML(self, name, attrs, content, font):
- clips = getattr(self, "clips", None)
- if clips is None:
- self.clips = clips = {}
- assert name == "Clip"
- glyphs = []
- clipBox = None
- for elem in content:
- if not isinstance(elem, tuple):
- continue
- name, attrs, content = elem
- if name == "Glyph":
- glyphs.append(attrs["value"])
- elif name == "ClipBox":
- clipBox = ClipBox()
- clipBox.Format = safeEval(attrs["Format"])
- for elem in content:
- if not isinstance(elem, tuple):
- continue
- name, attrs, content = elem
- clipBox.fromXML(name, attrs, content, font)
- if clipBox:
- for glyphName in glyphs:
- clips[glyphName] = clipBox
- class ExtendMode(IntEnum):
- PAD = 0
- REPEAT = 1
- REFLECT = 2
- # Porter-Duff modes for COLRv1 PaintComposite:
- # https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
- class CompositeMode(IntEnum):
- CLEAR = 0
- SRC = 1
- DEST = 2
- SRC_OVER = 3
- DEST_OVER = 4
- SRC_IN = 5
- DEST_IN = 6
- SRC_OUT = 7
- DEST_OUT = 8
- SRC_ATOP = 9
- DEST_ATOP = 10
- XOR = 11
- PLUS = 12
- SCREEN = 13
- OVERLAY = 14
- DARKEN = 15
- LIGHTEN = 16
- COLOR_DODGE = 17
- COLOR_BURN = 18
- HARD_LIGHT = 19
- SOFT_LIGHT = 20
- DIFFERENCE = 21
- EXCLUSION = 22
- MULTIPLY = 23
- HSL_HUE = 24
- HSL_SATURATION = 25
- HSL_COLOR = 26
- HSL_LUMINOSITY = 27
- class PaintFormat(IntEnum):
- PaintColrLayers = 1
- PaintSolid = 2
- PaintVarSolid = 3
- PaintLinearGradient = 4
- PaintVarLinearGradient = 5
- PaintRadialGradient = 6
- PaintVarRadialGradient = 7
- PaintSweepGradient = 8
- PaintVarSweepGradient = 9
- PaintGlyph = 10
- PaintColrGlyph = 11
- PaintTransform = 12
- PaintVarTransform = 13
- PaintTranslate = 14
- PaintVarTranslate = 15
- PaintScale = 16
- PaintVarScale = 17
- PaintScaleAroundCenter = 18
- PaintVarScaleAroundCenter = 19
- PaintScaleUniform = 20
- PaintVarScaleUniform = 21
- PaintScaleUniformAroundCenter = 22
- PaintVarScaleUniformAroundCenter = 23
- PaintRotate = 24
- PaintVarRotate = 25
- PaintRotateAroundCenter = 26
- PaintVarRotateAroundCenter = 27
- PaintSkew = 28
- PaintVarSkew = 29
- PaintSkewAroundCenter = 30
- PaintVarSkewAroundCenter = 31
- PaintComposite = 32
- def is_variable(self):
- return self.name.startswith("PaintVar")
- def as_variable(self):
- if self.is_variable():
- return self
- try:
- return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
- except KeyError:
- return None
- class Paint(getFormatSwitchingBaseTableClass("uint8")):
- formatEnum = PaintFormat
- def getFormatName(self):
- try:
- return self.formatEnum(self.Format).name
- except ValueError:
- raise NotImplementedError(f"Unknown Paint format: {self.Format}")
- def toXML(self, xmlWriter, font, attrs=None, name=None):
- tableName = name if name else self.__class__.__name__
- if attrs is None:
- attrs = []
- attrs.append(("Format", self.Format))
- xmlWriter.begintag(tableName, attrs)
- xmlWriter.comment(self.getFormatName())
- xmlWriter.newline()
- self.toXML2(xmlWriter, font)
- xmlWriter.endtag(tableName)
- xmlWriter.newline()
- def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
- if self.Format == PaintFormat.PaintColrLayers:
- # https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
- layers = []
- if colr.LayerList is not None:
- layers = colr.LayerList.Paint
- yield from (
- BaseTable.SubTableEntry(name="Layers", value=v, index=i)
- for i, v in enumerate(
- layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
- )
- )
- return
- if self.Format == PaintFormat.PaintColrGlyph:
- for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
- if record.BaseGlyph == self.Glyph:
- yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
- return
- else:
- raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
- for conv in self.getConverters():
- if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
- value = getattr(self, conv.name)
- yield BaseTable.SubTableEntry(name=conv.name, value=value)
- def getChildren(self, colr) -> List["Paint"]:
- # this is kept for backward compatibility (e.g. it's used by the subsetter)
- return [p.value for p in self.iterPaintSubTables(colr)]
- def traverse(self, colr: COLR, callback):
- """Depth-first traversal of graph rooted at self, callback on each node."""
- if not callable(callback):
- raise TypeError("callback must be callable")
- for path in dfs_base_table(
- self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
- ):
- paint = path[-1].value
- callback(paint)
- def getTransform(self) -> Transform:
- if self.Format == PaintFormat.PaintTransform:
- t = self.Transform
- return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
- elif self.Format == PaintFormat.PaintTranslate:
- return Identity.translate(self.dx, self.dy)
- elif self.Format == PaintFormat.PaintScale:
- return Identity.scale(self.scaleX, self.scaleY)
- elif self.Format == PaintFormat.PaintScaleAroundCenter:
- return (
- Identity.translate(self.centerX, self.centerY)
- .scale(self.scaleX, self.scaleY)
- .translate(-self.centerX, -self.centerY)
- )
- elif self.Format == PaintFormat.PaintScaleUniform:
- return Identity.scale(self.scale)
- elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
- return (
- Identity.translate(self.centerX, self.centerY)
- .scale(self.scale)
- .translate(-self.centerX, -self.centerY)
- )
- elif self.Format == PaintFormat.PaintRotate:
- return Identity.rotate(radians(self.angle))
- elif self.Format == PaintFormat.PaintRotateAroundCenter:
- return (
- Identity.translate(self.centerX, self.centerY)
- .rotate(radians(self.angle))
- .translate(-self.centerX, -self.centerY)
- )
- elif self.Format == PaintFormat.PaintSkew:
- return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
- elif self.Format == PaintFormat.PaintSkewAroundCenter:
- return (
- Identity.translate(self.centerX, self.centerY)
- .skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
- .translate(-self.centerX, -self.centerY)
- )
- if PaintFormat(self.Format).is_variable():
- raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
- return Identity
- def computeClipBox(
- self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
- ) -> Optional[ClipBox]:
- pen = ControlBoundsPen(glyphSet)
- for path in dfs_base_table(
- self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
- ):
- paint = path[-1].value
- if paint.Format == PaintFormat.PaintGlyph:
- transformation = reduce(
- Transform.transform,
- (st.value.getTransform() for st in path),
- Identity,
- )
- glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
- if pen.bounds is None:
- return None
- cb = ClipBox()
- cb.Format = int(ClipBoxFormat.Static)
- cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
- return cb
- # For each subtable format there is a class. However, we don't really distinguish
- # between "field name" and "format name": often these are the same. Yet there's
- # a whole bunch of fields with different names. The following dict is a mapping
- # from "format name" to "field name". _buildClasses() uses this to create a
- # subclass for each alternate field name.
- #
- _equivalents = {
- "MarkArray": ("Mark1Array",),
- "LangSys": ("DefaultLangSys",),
- "Coverage": (
- "MarkCoverage",
- "BaseCoverage",
- "LigatureCoverage",
- "Mark1Coverage",
- "Mark2Coverage",
- "BacktrackCoverage",
- "InputCoverage",
- "LookAheadCoverage",
- "VertGlyphCoverage",
- "HorizGlyphCoverage",
- "TopAccentCoverage",
- "ExtendedShapeCoverage",
- "MathKernCoverage",
- ),
- "ClassDef": (
- "ClassDef1",
- "ClassDef2",
- "BacktrackClassDef",
- "InputClassDef",
- "LookAheadClassDef",
- "GlyphClassDef",
- "MarkAttachClassDef",
- ),
- "Anchor": (
- "EntryAnchor",
- "ExitAnchor",
- "BaseAnchor",
- "LigatureAnchor",
- "Mark2Anchor",
- "MarkAnchor",
- ),
- "Device": (
- "XPlaDevice",
- "YPlaDevice",
- "XAdvDevice",
- "YAdvDevice",
- "XDeviceTable",
- "YDeviceTable",
- "DeviceTable",
- ),
- "Axis": (
- "HorizAxis",
- "VertAxis",
- ),
- "MinMax": ("DefaultMinMax",),
- "BaseCoord": (
- "MinCoord",
- "MaxCoord",
- ),
- "JstfLangSys": ("DefJstfLangSys",),
- "JstfGSUBModList": (
- "ShrinkageEnableGSUB",
- "ShrinkageDisableGSUB",
- "ExtensionEnableGSUB",
- "ExtensionDisableGSUB",
- ),
- "JstfGPOSModList": (
- "ShrinkageEnableGPOS",
- "ShrinkageDisableGPOS",
- "ExtensionEnableGPOS",
- "ExtensionDisableGPOS",
- ),
- "JstfMax": (
- "ShrinkageJstfMax",
- "ExtensionJstfMax",
- ),
- "MathKern": (
- "TopRightMathKern",
- "TopLeftMathKern",
- "BottomRightMathKern",
- "BottomLeftMathKern",
- ),
- "MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
- }
- #
- # OverFlow logic, to automatically create ExtensionLookups
- # XXX This should probably move to otBase.py
- #
- def fixLookupOverFlows(ttf, overflowRecord):
- """Either the offset from the LookupList to a lookup overflowed, or
- an offset from a lookup to a subtable overflowed.
- The table layout is:
- GPSO/GUSB
- Script List
- Feature List
- LookUpList
- Lookup[0] and contents
- SubTable offset list
- SubTable[0] and contents
- ...
- SubTable[n] and contents
- ...
- Lookup[n] and contents
- SubTable offset list
- SubTable[0] and contents
- ...
- SubTable[n] and contents
- If the offset to a lookup overflowed (SubTableIndex is None)
- we must promote the *previous* lookup to an Extension type.
- If the offset from a lookup to subtable overflowed, then we must promote it
- to an Extension Lookup type.
- """
- ok = 0
- lookupIndex = overflowRecord.LookupListIndex
- if overflowRecord.SubTableIndex is None:
- lookupIndex = lookupIndex - 1
- if lookupIndex < 0:
- return ok
- if overflowRecord.tableType == "GSUB":
- extType = 7
- elif overflowRecord.tableType == "GPOS":
- extType = 9
- lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
- lookup = lookups[lookupIndex]
- # If the previous lookup is an extType, look further back. Very unlikely, but possible.
- while lookup.SubTable[0].__class__.LookupType == extType:
- lookupIndex = lookupIndex - 1
- if lookupIndex < 0:
- return ok
- lookup = lookups[lookupIndex]
- for lookupIndex in range(lookupIndex, len(lookups)):
- lookup = lookups[lookupIndex]
- if lookup.LookupType != extType:
- lookup.LookupType = extType
- for si in range(len(lookup.SubTable)):
- subTable = lookup.SubTable[si]
- extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
- extSubTable = extSubTableClass()
- extSubTable.Format = 1
- extSubTable.ExtSubTable = subTable
- lookup.SubTable[si] = extSubTable
- ok = 1
- return ok
- def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
- ok = 1
- oldMapping = sorted(oldSubTable.mapping.items())
- oldLen = len(oldMapping)
- if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
- # Coverage table is written last. Overflow is to or within the
- # the coverage table. We will just cut the subtable in half.
- newLen = oldLen // 2
- elif overflowRecord.itemName == "Sequence":
- # We just need to back up by two items from the overflowed
- # Sequence index to make sure the offset to the Coverage table
- # doesn't overflow.
- newLen = overflowRecord.itemIndex - 1
- newSubTable.mapping = {}
- for i in range(newLen, oldLen):
- item = oldMapping[i]
- key = item[0]
- newSubTable.mapping[key] = item[1]
- del oldSubTable.mapping[key]
- return ok
- def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
- ok = 1
- if hasattr(oldSubTable, "sortCoverageLast"):
- newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
- oldAlts = sorted(oldSubTable.alternates.items())
- oldLen = len(oldAlts)
- if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
- # Coverage table is written last. overflow is to or within the
- # the coverage table. We will just cut the subtable in half.
- newLen = oldLen // 2
- elif overflowRecord.itemName == "AlternateSet":
- # We just need to back up by two items
- # from the overflowed AlternateSet index to make sure the offset
- # to the Coverage table doesn't overflow.
- newLen = overflowRecord.itemIndex - 1
- newSubTable.alternates = {}
- for i in range(newLen, oldLen):
- item = oldAlts[i]
- key = item[0]
- newSubTable.alternates[key] = item[1]
- del oldSubTable.alternates[key]
- return ok
- def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
- ok = 1
- oldLigs = sorted(oldSubTable.ligatures.items())
- oldLen = len(oldLigs)
- if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
- # Coverage table is written last. overflow is to or within the
- # the coverage table. We will just cut the subtable in half.
- newLen = oldLen // 2
- elif overflowRecord.itemName == "LigatureSet":
- # We just need to back up by two items
- # from the overflowed AlternateSet index to make sure the offset
- # to the Coverage table doesn't overflow.
- newLen = overflowRecord.itemIndex - 1
- newSubTable.ligatures = {}
- for i in range(newLen, oldLen):
- item = oldLigs[i]
- key = item[0]
- newSubTable.ligatures[key] = item[1]
- del oldSubTable.ligatures[key]
- return ok
- def splitPairPos(oldSubTable, newSubTable, overflowRecord):
- st = oldSubTable
- ok = False
- newSubTable.Format = oldSubTable.Format
- if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
- for name in "ValueFormat1", "ValueFormat2":
- setattr(newSubTable, name, getattr(oldSubTable, name))
- # Move top half of coverage to new subtable
- newSubTable.Coverage = oldSubTable.Coverage.__class__()
- coverage = oldSubTable.Coverage.glyphs
- records = oldSubTable.PairSet
- oldCount = len(oldSubTable.PairSet) // 2
- oldSubTable.Coverage.glyphs = coverage[:oldCount]
- oldSubTable.PairSet = records[:oldCount]
- newSubTable.Coverage.glyphs = coverage[oldCount:]
- newSubTable.PairSet = records[oldCount:]
- oldSubTable.PairSetCount = len(oldSubTable.PairSet)
- newSubTable.PairSetCount = len(newSubTable.PairSet)
- ok = True
- elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
- if not hasattr(oldSubTable, "Class2Count"):
- oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
- for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
- setattr(newSubTable, name, getattr(oldSubTable, name))
- # The two subtables will still have the same ClassDef2 and the table
- # sharing will still cause the sharing to overflow. As such, disable
- # sharing on the one that is serialized second (that's oldSubTable).
- oldSubTable.DontShare = True
- # Move top half of class numbers to new subtable
- newSubTable.Coverage = oldSubTable.Coverage.__class__()
- newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
- coverage = oldSubTable.Coverage.glyphs
- classDefs = oldSubTable.ClassDef1.classDefs
- records = oldSubTable.Class1Record
- oldCount = len(oldSubTable.Class1Record) // 2
- newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
- oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
- oldSubTable.ClassDef1.classDefs = {
- k: v for k, v in classDefs.items() if v < oldCount
- }
- oldSubTable.Class1Record = records[:oldCount]
- newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
- newSubTable.ClassDef1.classDefs = {
- k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
- }
- newSubTable.Class1Record = records[oldCount:]
- oldSubTable.Class1Count = len(oldSubTable.Class1Record)
- newSubTable.Class1Count = len(newSubTable.Class1Record)
- ok = True
- return ok
- def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
- # split half of the mark classes to the new subtable
- classCount = oldSubTable.ClassCount
- if classCount < 2:
- # oh well, not much left to split...
- return False
- oldClassCount = classCount // 2
- newClassCount = classCount - oldClassCount
- oldMarkCoverage, oldMarkRecords = [], []
- newMarkCoverage, newMarkRecords = [], []
- for glyphName, markRecord in zip(
- oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
- ):
- if markRecord.Class < oldClassCount:
- oldMarkCoverage.append(glyphName)
- oldMarkRecords.append(markRecord)
- else:
- markRecord.Class -= oldClassCount
- newMarkCoverage.append(glyphName)
- newMarkRecords.append(markRecord)
- oldBaseRecords, newBaseRecords = [], []
- for rec in oldSubTable.BaseArray.BaseRecord:
- oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
- oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
- newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
- oldBaseRecords.append(oldBaseRecord)
- newBaseRecords.append(newBaseRecord)
- newSubTable.Format = oldSubTable.Format
- oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
- newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
- newSubTable.MarkCoverage.glyphs = newMarkCoverage
- # share the same BaseCoverage in both halves
- newSubTable.BaseCoverage = oldSubTable.BaseCoverage
- oldSubTable.ClassCount = oldClassCount
- newSubTable.ClassCount = newClassCount
- oldSubTable.MarkArray.MarkRecord = oldMarkRecords
- newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
- newSubTable.MarkArray.MarkRecord = newMarkRecords
- oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
- newSubTable.MarkArray.MarkCount = len(newMarkRecords)
- oldSubTable.BaseArray.BaseRecord = oldBaseRecords
- newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
- newSubTable.BaseArray.BaseRecord = newBaseRecords
- oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
- newSubTable.BaseArray.BaseCount = len(newBaseRecords)
- return True
- splitTable = {
- "GSUB": {
- # 1: splitSingleSubst,
- 2: splitMultipleSubst,
- 3: splitAlternateSubst,
- 4: splitLigatureSubst,
- # 5: splitContextSubst,
- # 6: splitChainContextSubst,
- # 7: splitExtensionSubst,
- # 8: splitReverseChainSingleSubst,
- },
- "GPOS": {
- # 1: splitSinglePos,
- 2: splitPairPos,
- # 3: splitCursivePos,
- 4: splitMarkBasePos,
- # 5: splitMarkLigPos,
- # 6: splitMarkMarkPos,
- # 7: splitContextPos,
- # 8: splitChainContextPos,
- # 9: splitExtensionPos,
- },
- }
- def fixSubTableOverFlows(ttf, overflowRecord):
- """
- An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
- """
- table = ttf[overflowRecord.tableType].table
- lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
- subIndex = overflowRecord.SubTableIndex
- subtable = lookup.SubTable[subIndex]
- # First, try not sharing anything for this subtable...
- if not hasattr(subtable, "DontShare"):
- subtable.DontShare = True
- return True
- if hasattr(subtable, "ExtSubTable"):
- # We split the subtable of the Extension table, and add a new Extension table
- # to contain the new subtable.
- subTableType = subtable.ExtSubTable.__class__.LookupType
- extSubTable = subtable
- subtable = extSubTable.ExtSubTable
- newExtSubTableClass = lookupTypes[overflowRecord.tableType][
- extSubTable.__class__.LookupType
- ]
- newExtSubTable = newExtSubTableClass()
- newExtSubTable.Format = extSubTable.Format
- toInsert = newExtSubTable
- newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
- newSubTable = newSubTableClass()
- newExtSubTable.ExtSubTable = newSubTable
- else:
- subTableType = subtable.__class__.LookupType
- newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
- newSubTable = newSubTableClass()
- toInsert = newSubTable
- if hasattr(lookup, "SubTableCount"): # may not be defined yet.
- lookup.SubTableCount = lookup.SubTableCount + 1
- try:
- splitFunc = splitTable[overflowRecord.tableType][subTableType]
- except KeyError:
- log.error(
- "Don't know how to split %s lookup type %s",
- overflowRecord.tableType,
- subTableType,
- )
- return False
- ok = splitFunc(subtable, newSubTable, overflowRecord)
- if ok:
- lookup.SubTable.insert(subIndex + 1, toInsert)
- return ok
- # End of OverFlow logic
- def _buildClasses():
- import re
- from .otData import otData
- formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
- namespace = globals()
- # populate module with classes
- for name, table in otData:
- baseClass = BaseTable
- m = formatPat.match(name)
- if m:
- # XxxFormatN subtable, we only add the "base" table
- name = m.group(1)
- # the first row of a format-switching otData table describes the Format;
- # the first column defines the type of the Format field.
- # Currently this can be either 'uint16' or 'uint8'.
- formatType = table[0][0]
- baseClass = getFormatSwitchingBaseTableClass(formatType)
- if name not in namespace:
- # the class doesn't exist yet, so the base implementation is used.
- cls = type(name, (baseClass,), {})
- if name in ("GSUB", "GPOS"):
- cls.DontShare = True
- namespace[name] = cls
- # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
- for name, _ in otData:
- if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
- varType = namespace[name]
- noVarType = namespace[name[3:]]
- varType.NoVarType = noVarType
- noVarType.VarType = varType
- for base, alts in _equivalents.items():
- base = namespace[base]
- for alt in alts:
- namespace[alt] = base
- global lookupTypes
- lookupTypes = {
- "GSUB": {
- 1: SingleSubst,
- 2: MultipleSubst,
- 3: AlternateSubst,
- 4: LigatureSubst,
- 5: ContextSubst,
- 6: ChainContextSubst,
- 7: ExtensionSubst,
- 8: ReverseChainSingleSubst,
- },
- "GPOS": {
- 1: SinglePos,
- 2: PairPos,
- 3: CursivePos,
- 4: MarkBasePos,
- 5: MarkLigPos,
- 6: MarkMarkPos,
- 7: ContextPos,
- 8: ChainContextPos,
- 9: ExtensionPos,
- },
- "mort": {
- 4: NoncontextualMorph,
- },
- "morx": {
- 0: RearrangementMorph,
- 1: ContextualMorph,
- 2: LigatureMorph,
- # 3: Reserved,
- 4: NoncontextualMorph,
- 5: InsertionMorph,
- },
- }
- lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS
- for lookupEnum in lookupTypes.values():
- for enum, cls in lookupEnum.items():
- cls.LookupType = enum
- global featureParamTypes
- featureParamTypes = {
- "size": FeatureParamsSize,
- }
- for i in range(1, 20 + 1):
- featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
- for i in range(1, 99 + 1):
- featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
- # add converters to classes
- from .otConverters import buildConverters
- for name, table in otData:
- m = formatPat.match(name)
- if m:
- # XxxFormatN subtable, add converter to "base" table
- name, format = m.groups()
- format = int(format)
- cls = namespace[name]
- if not hasattr(cls, "converters"):
- cls.converters = {}
- cls.convertersByName = {}
- converters, convertersByName = buildConverters(table[1:], namespace)
- cls.converters[format] = converters
- cls.convertersByName[format] = convertersByName
- # XXX Add staticSize?
- else:
- cls = namespace[name]
- cls.converters, cls.convertersByName = buildConverters(table, namespace)
- # XXX Add staticSize?
- _buildClasses()
- def _getGlyphsFromCoverageTable(coverage):
- if coverage is None:
- # empty coverage table
- return []
- else:
- return coverage.glyphs
|