12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918 |
- from collections import namedtuple, OrderedDict
- import os
- from fontTools.misc.fixedTools import fixedToFloat
- from fontTools import ttLib
- from fontTools.ttLib.tables import otTables as ot
- from fontTools.ttLib.tables.otBase import (
- ValueRecord,
- valueRecordFormatDict,
- OTTableWriter,
- CountReference,
- )
- from fontTools.ttLib.tables import otBase
- from fontTools.feaLib.ast import STATNameStatement
- from fontTools.otlLib.optimize.gpos import (
- _compression_level_from_env,
- compact_lookup,
- )
- from fontTools.otlLib.error import OpenTypeLibError
- from functools import reduce
- import logging
- import copy
- log = logging.getLogger(__name__)
- def buildCoverage(glyphs, glyphMap):
- """Builds a coverage table.
- Coverage tables (as defined in the `OpenType spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#coverage-table>`__)
- are used in all OpenType Layout lookups apart from the Extension type, and
- define the glyphs involved in a layout subtable. This allows shaping engines
- to compare the glyph stream with the coverage table and quickly determine
- whether a subtable should be involved in a shaping operation.
- This function takes a list of glyphs and a glyphname-to-ID map, and
- returns a ``Coverage`` object representing the coverage table.
- Example::
- glyphMap = font.getReverseGlyphMap()
- glyphs = [ "A", "B", "C" ]
- coverage = buildCoverage(glyphs, glyphMap)
- Args:
- glyphs: a sequence of glyph names.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- An ``otTables.Coverage`` object or ``None`` if there are no glyphs
- supplied.
- """
- if not glyphs:
- return None
- self = ot.Coverage()
- try:
- self.glyphs = sorted(set(glyphs), key=glyphMap.__getitem__)
- except KeyError as e:
- raise ValueError(f"Could not find glyph {e} in font") from e
- return self
- LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001
- LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002
- LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004
- LOOKUP_FLAG_IGNORE_MARKS = 0x0008
- LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010
- def buildLookup(subtables, flags=0, markFilterSet=None):
- """Turns a collection of rules into a lookup.
- A Lookup (as defined in the `OpenType Spec <https://docs.microsoft.com/en-gb/typography/opentype/spec/chapter2#lookupTbl>`__)
- wraps the individual rules in a layout operation (substitution or
- positioning) in a data structure expressing their overall lookup type -
- for example, single substitution, mark-to-base attachment, and so on -
- as well as the lookup flags and any mark filtering sets. You may import
- the following constants to express lookup flags:
- - ``LOOKUP_FLAG_RIGHT_TO_LEFT``
- - ``LOOKUP_FLAG_IGNORE_BASE_GLYPHS``
- - ``LOOKUP_FLAG_IGNORE_LIGATURES``
- - ``LOOKUP_FLAG_IGNORE_MARKS``
- - ``LOOKUP_FLAG_USE_MARK_FILTERING_SET``
- Args:
- subtables: A list of layout subtable objects (e.g.
- ``MultipleSubst``, ``PairPos``, etc.) or ``None``.
- flags (int): This lookup's flags.
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- Returns:
- An ``otTables.Lookup`` object or ``None`` if there are no subtables
- supplied.
- """
- if subtables is None:
- return None
- subtables = [st for st in subtables if st is not None]
- if not subtables:
- return None
- assert all(
- t.LookupType == subtables[0].LookupType for t in subtables
- ), "all subtables must have the same LookupType; got %s" % repr(
- [t.LookupType for t in subtables]
- )
- self = ot.Lookup()
- self.LookupType = subtables[0].LookupType
- self.LookupFlag = flags
- self.SubTable = subtables
- self.SubTableCount = len(self.SubTable)
- if markFilterSet is not None:
- self.LookupFlag |= LOOKUP_FLAG_USE_MARK_FILTERING_SET
- assert isinstance(markFilterSet, int), markFilterSet
- self.MarkFilteringSet = markFilterSet
- else:
- assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, (
- "if markFilterSet is None, flags must not set "
- "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags
- )
- return self
- class LookupBuilder(object):
- SUBTABLE_BREAK_ = "SUBTABLE_BREAK"
- def __init__(self, font, location, table, lookup_type):
- self.font = font
- self.glyphMap = font.getReverseGlyphMap()
- self.location = location
- self.table, self.lookup_type = table, lookup_type
- self.lookupflag = 0
- self.markFilterSet = None
- self.lookup_index = None # assigned when making final tables
- assert table in ("GPOS", "GSUB")
- def equals(self, other):
- return (
- isinstance(other, self.__class__)
- and self.table == other.table
- and self.lookupflag == other.lookupflag
- and self.markFilterSet == other.markFilterSet
- )
- def inferGlyphClasses(self):
- """Infers glyph glasses for the GDEF table, such as {"cedilla":3}."""
- return {}
- def getAlternateGlyphs(self):
- """Helper for building 'aalt' features."""
- return {}
- def buildLookup_(self, subtables):
- return buildLookup(subtables, self.lookupflag, self.markFilterSet)
- def buildMarkClasses_(self, marks):
- """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1}
- Helper for MarkBasePostBuilder, MarkLigPosBuilder, and
- MarkMarkPosBuilder. Seems to return the same numeric IDs
- for mark classes as the AFDKO makeotf tool.
- """
- ids = {}
- for mark in sorted(marks.keys(), key=self.font.getGlyphID):
- markClassName, _markAnchor = marks[mark]
- if markClassName not in ids:
- ids[markClassName] = len(ids)
- return ids
- def setBacktrackCoverage_(self, prefix, subtable):
- subtable.BacktrackGlyphCount = len(prefix)
- subtable.BacktrackCoverage = []
- for p in reversed(prefix):
- coverage = buildCoverage(p, self.glyphMap)
- subtable.BacktrackCoverage.append(coverage)
- def setLookAheadCoverage_(self, suffix, subtable):
- subtable.LookAheadGlyphCount = len(suffix)
- subtable.LookAheadCoverage = []
- for s in suffix:
- coverage = buildCoverage(s, self.glyphMap)
- subtable.LookAheadCoverage.append(coverage)
- def setInputCoverage_(self, glyphs, subtable):
- subtable.InputGlyphCount = len(glyphs)
- subtable.InputCoverage = []
- for g in glyphs:
- coverage = buildCoverage(g, self.glyphMap)
- subtable.InputCoverage.append(coverage)
- def setCoverage_(self, glyphs, subtable):
- subtable.GlyphCount = len(glyphs)
- subtable.Coverage = []
- for g in glyphs:
- coverage = buildCoverage(g, self.glyphMap)
- subtable.Coverage.append(coverage)
- def build_subst_subtables(self, mapping, klass):
- substitutions = [{}]
- for key in mapping:
- if key[0] == self.SUBTABLE_BREAK_:
- substitutions.append({})
- else:
- substitutions[-1][key] = mapping[key]
- subtables = [klass(s) for s in substitutions]
- return subtables
- def add_subtable_break(self, location):
- """Add an explicit subtable break.
- Args:
- location: A string or tuple representing the location in the
- original source which produced this break, or ``None`` if
- no location is provided.
- """
- log.warning(
- OpenTypeLibError(
- 'unsupported "subtable" statement for lookup type', location
- )
- )
- class AlternateSubstBuilder(LookupBuilder):
- """Builds an Alternate Substitution (GSUB3) lookup.
- Users are expected to manually add alternate glyph substitutions to
- the ``alternates`` attribute after the object has been initialized,
- e.g.::
- builder.alternates["A"] = ["A.alt1", "A.alt2"]
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- alternates: An ordered dictionary of alternates, mapping glyph names
- to a list of names of alternates.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 3)
- self.alternates = OrderedDict()
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.alternates == other.alternates
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the alternate
- substitution lookup.
- """
- subtables = self.build_subst_subtables(
- self.alternates, buildAlternateSubstSubtable
- )
- return self.buildLookup_(subtables)
- def getAlternateGlyphs(self):
- return self.alternates
- def add_subtable_break(self, location):
- self.alternates[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
- class ChainContextualRule(
- namedtuple("ChainContextualRule", ["prefix", "glyphs", "suffix", "lookups"])
- ):
- @property
- def is_subtable_break(self):
- return self.prefix == LookupBuilder.SUBTABLE_BREAK_
- class ChainContextualRuleset:
- def __init__(self):
- self.rules = []
- def addRule(self, rule):
- self.rules.append(rule)
- @property
- def hasPrefixOrSuffix(self):
- # Do we have any prefixes/suffixes? If this is False for all
- # rulesets, we can express the whole lookup as GPOS5/GSUB7.
- for rule in self.rules:
- if len(rule.prefix) > 0 or len(rule.suffix) > 0:
- return True
- return False
- @property
- def hasAnyGlyphClasses(self):
- # Do we use glyph classes anywhere in the rules? If this is False
- # we can express this subtable as a Format 1.
- for rule in self.rules:
- for coverage in (rule.prefix, rule.glyphs, rule.suffix):
- if any(len(x) > 1 for x in coverage):
- return True
- return False
- def format2ClassDefs(self):
- PREFIX, GLYPHS, SUFFIX = 0, 1, 2
- classDefBuilders = []
- for ix in [PREFIX, GLYPHS, SUFFIX]:
- context = []
- for r in self.rules:
- context.append(r[ix])
- classes = self._classBuilderForContext(context)
- if not classes:
- return None
- classDefBuilders.append(classes)
- return classDefBuilders
- def _classBuilderForContext(self, context):
- classdefbuilder = ClassDefBuilder(useClass0=False)
- for position in context:
- for glyphset in position:
- glyphs = set(glyphset)
- if not classdefbuilder.canAdd(glyphs):
- return None
- classdefbuilder.add(glyphs)
- return classdefbuilder
- class ChainContextualBuilder(LookupBuilder):
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.rules == other.rules
- def rulesets(self):
- # Return a list of ChainContextRuleset objects, taking explicit
- # subtable breaks into account
- ruleset = [ChainContextualRuleset()]
- for rule in self.rules:
- if rule.is_subtable_break:
- ruleset.append(ChainContextualRuleset())
- continue
- ruleset[-1].addRule(rule)
- # Squish any empty subtables
- return [x for x in ruleset if len(x.rules) > 0]
- def getCompiledSize_(self, subtables):
- size = 0
- for st in subtables:
- w = OTTableWriter()
- w["LookupType"] = CountReference(
- {"LookupType": st.LookupType}, "LookupType"
- )
- # We need to make a copy here because compiling
- # modifies the subtable (finalizing formats etc.)
- copy.deepcopy(st).compile(w, self.font)
- size += len(w.getAllData())
- return size
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the chained
- contextual positioning lookup.
- """
- subtables = []
- rulesets = self.rulesets()
- chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
- # https://github.com/fonttools/fonttools/issues/2539
- #
- # Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not
- # correctly process GPOS7 lookups, so for now we force contextual
- # positioning lookups to be chaining (GPOS8).
- #
- # This seems to be fixed as of macOS 13.2, but we keep disabling this
- # for now until we are no longer concerned about old macOS versions.
- # But we allow people to opt-out of this with the config key below.
- write_gpos7 = self.font.cfg.get("fontTools.otlLib.builder:WRITE_GPOS7")
- # horrible separation of concerns breach
- if not write_gpos7 and self.subtable_type == "Pos":
- chaining = True
- for ruleset in rulesets:
- # Determine format strategy. We try to build formats 1, 2 and 3
- # subtables and then work out which is best. candidates list holds
- # the subtables in each format for this ruleset (including a dummy
- # "format 0" to make the addressing match the format numbers).
- # We can always build a format 3 lookup by accumulating each of
- # the rules into a list, so start with that.
- candidates = [None, None, None, []]
- for rule in ruleset.rules:
- candidates[3].append(self.buildFormat3Subtable(rule, chaining))
- # Can we express the whole ruleset as a format 2 subtable?
- classdefs = ruleset.format2ClassDefs()
- if classdefs:
- candidates[2] = [
- self.buildFormat2Subtable(ruleset, classdefs, chaining)
- ]
- if not ruleset.hasAnyGlyphClasses:
- candidates[1] = [self.buildFormat1Subtable(ruleset, chaining)]
- for i in [1, 2, 3]:
- if candidates[i]:
- try:
- self.getCompiledSize_(candidates[i])
- except Exception as e:
- log.warning(
- "Contextual format %i at %s overflowed (%s)"
- % (i, str(self.location), e)
- )
- candidates[i] = None
- candidates = [x for x in candidates if x is not None]
- if not candidates:
- raise OpenTypeLibError("All candidates overflowed", self.location)
- winner = min(candidates, key=self.getCompiledSize_)
- subtables.extend(winner)
- # If we are not chaining, lookup type will be automatically fixed by
- # buildLookup_
- return self.buildLookup_(subtables)
- def buildFormat1Subtable(self, ruleset, chaining=True):
- st = self.newSubtable_(chaining=chaining)
- st.Format = 1
- st.populateDefaults()
- coverage = set()
- rulesetsByFirstGlyph = {}
- ruleAttr = self.ruleAttr_(format=1, chaining=chaining)
- for rule in ruleset.rules:
- ruleAsSubtable = self.newRule_(format=1, chaining=chaining)
- if chaining:
- ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
- ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
- ruleAsSubtable.Backtrack = [list(x)[0] for x in reversed(rule.prefix)]
- ruleAsSubtable.LookAhead = [list(x)[0] for x in rule.suffix]
- ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
- else:
- ruleAsSubtable.GlyphCount = len(rule.glyphs)
- ruleAsSubtable.Input = [list(x)[0] for x in rule.glyphs[1:]]
- self.buildLookupList(rule, ruleAsSubtable)
- firstGlyph = list(rule.glyphs[0])[0]
- if firstGlyph not in rulesetsByFirstGlyph:
- coverage.add(firstGlyph)
- rulesetsByFirstGlyph[firstGlyph] = []
- rulesetsByFirstGlyph[firstGlyph].append(ruleAsSubtable)
- st.Coverage = buildCoverage(coverage, self.glyphMap)
- ruleSets = []
- for g in st.Coverage.glyphs:
- ruleSet = self.newRuleSet_(format=1, chaining=chaining)
- setattr(ruleSet, ruleAttr, rulesetsByFirstGlyph[g])
- setattr(ruleSet, f"{ruleAttr}Count", len(rulesetsByFirstGlyph[g]))
- ruleSets.append(ruleSet)
- setattr(st, self.ruleSetAttr_(format=1, chaining=chaining), ruleSets)
- setattr(
- st, self.ruleSetAttr_(format=1, chaining=chaining) + "Count", len(ruleSets)
- )
- return st
- def buildFormat2Subtable(self, ruleset, classdefs, chaining=True):
- st = self.newSubtable_(chaining=chaining)
- st.Format = 2
- st.populateDefaults()
- if chaining:
- (
- st.BacktrackClassDef,
- st.InputClassDef,
- st.LookAheadClassDef,
- ) = [c.build() for c in classdefs]
- else:
- st.ClassDef = classdefs[1].build()
- inClasses = classdefs[1].classes()
- classSets = []
- for _ in inClasses:
- classSet = self.newRuleSet_(format=2, chaining=chaining)
- classSets.append(classSet)
- coverage = set()
- classRuleAttr = self.ruleAttr_(format=2, chaining=chaining)
- for rule in ruleset.rules:
- ruleAsSubtable = self.newRule_(format=2, chaining=chaining)
- if chaining:
- ruleAsSubtable.BacktrackGlyphCount = len(rule.prefix)
- ruleAsSubtable.LookAheadGlyphCount = len(rule.suffix)
- # The glyphs in the rule may be list, tuple, odict_keys...
- # Order is not important anyway because they are guaranteed
- # to be members of the same class.
- ruleAsSubtable.Backtrack = [
- st.BacktrackClassDef.classDefs[list(x)[0]]
- for x in reversed(rule.prefix)
- ]
- ruleAsSubtable.LookAhead = [
- st.LookAheadClassDef.classDefs[list(x)[0]] for x in rule.suffix
- ]
- ruleAsSubtable.InputGlyphCount = len(rule.glyphs)
- ruleAsSubtable.Input = [
- st.InputClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
- ]
- setForThisRule = classSets[
- st.InputClassDef.classDefs[list(rule.glyphs[0])[0]]
- ]
- else:
- ruleAsSubtable.GlyphCount = len(rule.glyphs)
- ruleAsSubtable.Class = [ # The spec calls this InputSequence
- st.ClassDef.classDefs[list(x)[0]] for x in rule.glyphs[1:]
- ]
- setForThisRule = classSets[
- st.ClassDef.classDefs[list(rule.glyphs[0])[0]]
- ]
- self.buildLookupList(rule, ruleAsSubtable)
- coverage |= set(rule.glyphs[0])
- getattr(setForThisRule, classRuleAttr).append(ruleAsSubtable)
- setattr(
- setForThisRule,
- f"{classRuleAttr}Count",
- getattr(setForThisRule, f"{classRuleAttr}Count") + 1,
- )
- setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets)
- setattr(
- st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets)
- )
- st.Coverage = buildCoverage(coverage, self.glyphMap)
- return st
- def buildFormat3Subtable(self, rule, chaining=True):
- st = self.newSubtable_(chaining=chaining)
- st.Format = 3
- if chaining:
- self.setBacktrackCoverage_(rule.prefix, st)
- self.setLookAheadCoverage_(rule.suffix, st)
- self.setInputCoverage_(rule.glyphs, st)
- else:
- self.setCoverage_(rule.glyphs, st)
- self.buildLookupList(rule, st)
- return st
- def buildLookupList(self, rule, st):
- for sequenceIndex, lookupList in enumerate(rule.lookups):
- if lookupList is not None:
- if not isinstance(lookupList, list):
- # Can happen with synthesised lookups
- lookupList = [lookupList]
- for l in lookupList:
- if l.lookup_index is None:
- if isinstance(self, ChainContextPosBuilder):
- other = "substitution"
- else:
- other = "positioning"
- raise OpenTypeLibError(
- "Missing index of the specified "
- f"lookup, might be a {other} lookup",
- self.location,
- )
- rec = self.newLookupRecord_(st)
- rec.SequenceIndex = sequenceIndex
- rec.LookupListIndex = l.lookup_index
- def add_subtable_break(self, location):
- self.rules.append(
- ChainContextualRule(
- self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_,
- [self.SUBTABLE_BREAK_],
- )
- )
- def newSubtable_(self, chaining=True):
- subtablename = f"Context{self.subtable_type}"
- if chaining:
- subtablename = "Chain" + subtablename
- st = getattr(ot, subtablename)() # ot.ChainContextPos()/ot.ChainSubst()/etc.
- setattr(st, f"{self.subtable_type}Count", 0)
- setattr(st, f"{self.subtable_type}LookupRecord", [])
- return st
- # Format 1 and format 2 GSUB5/GSUB6/GPOS7/GPOS8 rulesets and rules form a family:
- #
- # format 1 ruleset format 1 rule format 2 ruleset format 2 rule
- # GSUB5 SubRuleSet SubRule SubClassSet SubClassRule
- # GSUB6 ChainSubRuleSet ChainSubRule ChainSubClassSet ChainSubClassRule
- # GPOS7 PosRuleSet PosRule PosClassSet PosClassRule
- # GPOS8 ChainPosRuleSet ChainPosRule ChainPosClassSet ChainPosClassRule
- #
- # The following functions generate the attribute names and subtables according
- # to this naming convention.
- def ruleSetAttr_(self, format=1, chaining=True):
- if format == 1:
- formatType = "Rule"
- elif format == 2:
- formatType = "Class"
- else:
- raise AssertionError(formatType)
- subtablename = f"{self.subtable_type[0:3]}{formatType}Set" # Sub, not Subst.
- if chaining:
- subtablename = "Chain" + subtablename
- return subtablename
- def ruleAttr_(self, format=1, chaining=True):
- if format == 1:
- formatType = ""
- elif format == 2:
- formatType = "Class"
- else:
- raise AssertionError(formatType)
- subtablename = f"{self.subtable_type[0:3]}{formatType}Rule" # Sub, not Subst.
- if chaining:
- subtablename = "Chain" + subtablename
- return subtablename
- def newRuleSet_(self, format=1, chaining=True):
- st = getattr(
- ot, self.ruleSetAttr_(format, chaining)
- )() # ot.ChainPosRuleSet()/ot.SubRuleSet()/etc.
- st.populateDefaults()
- return st
- def newRule_(self, format=1, chaining=True):
- st = getattr(
- ot, self.ruleAttr_(format, chaining)
- )() # ot.ChainPosClassRule()/ot.SubClassRule()/etc.
- st.populateDefaults()
- return st
- def attachSubtableWithCount_(
- self, st, subtable_name, count_name, existing=None, index=None, chaining=False
- ):
- if chaining:
- subtable_name = "Chain" + subtable_name
- count_name = "Chain" + count_name
- if not hasattr(st, count_name):
- setattr(st, count_name, 0)
- setattr(st, subtable_name, [])
- if existing:
- new_subtable = existing
- else:
- # Create a new, empty subtable from otTables
- new_subtable = getattr(ot, subtable_name)()
- setattr(st, count_name, getattr(st, count_name) + 1)
- if index:
- getattr(st, subtable_name).insert(index, new_subtable)
- else:
- getattr(st, subtable_name).append(new_subtable)
- return new_subtable
- def newLookupRecord_(self, st):
- return self.attachSubtableWithCount_(
- st,
- f"{self.subtable_type}LookupRecord",
- f"{self.subtable_type}Count",
- chaining=False,
- ) # Oddly, it isn't ChainSubstLookupRecord
- class ChainContextPosBuilder(ChainContextualBuilder):
- """Builds a Chained Contextual Positioning (GPOS8) lookup.
- Users are expected to manually add rules to the ``rules`` attribute after
- the object has been initialized, e.g.::
- # pos [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
- prefix = [ ["A", "B"], ["C", "D"] ]
- suffix = [ ["E"] ]
- glyphs = [ ["x"], ["y"], ["z"] ]
- lookups = [ [lu1], None, [lu2] ]
- builder.rules.append( (prefix, glyphs, suffix, lookups) )
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- rules: A list of tuples representing the rules in this lookup.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 8)
- self.rules = []
- self.subtable_type = "Pos"
- def find_chainable_single_pos(self, lookups, glyphs, value):
- """Helper for add_single_pos_chained_()"""
- res = None
- for lookup in lookups[::-1]:
- if lookup == self.SUBTABLE_BREAK_:
- return res
- if isinstance(lookup, SinglePosBuilder) and all(
- lookup.can_add(glyph, value) for glyph in glyphs
- ):
- res = lookup
- return res
- class ChainContextSubstBuilder(ChainContextualBuilder):
- """Builds a Chained Contextual Substitution (GSUB6) lookup.
- Users are expected to manually add rules to the ``rules`` attribute after
- the object has been initialized, e.g.::
- # sub [A B] [C D] x' lookup lu1 y' z' lookup lu2 E;
- prefix = [ ["A", "B"], ["C", "D"] ]
- suffix = [ ["E"] ]
- glyphs = [ ["x"], ["y"], ["z"] ]
- lookups = [ [lu1], None, [lu2] ]
- builder.rules.append( (prefix, glyphs, suffix, lookups) )
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- rules: A list of tuples representing the rules in this lookup.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 6)
- self.rules = [] # (prefix, input, suffix, lookups)
- self.subtable_type = "Subst"
- def getAlternateGlyphs(self):
- result = {}
- for rule in self.rules:
- if rule.is_subtable_break:
- continue
- for lookups in rule.lookups:
- if not isinstance(lookups, list):
- lookups = [lookups]
- for lookup in lookups:
- if lookup is not None:
- alts = lookup.getAlternateGlyphs()
- for glyph, replacements in alts.items():
- result.setdefault(glyph, set()).update(replacements)
- return result
- def find_chainable_single_subst(self, mapping):
- """Helper for add_single_subst_chained_()"""
- res = None
- for rule in self.rules[::-1]:
- if rule.is_subtable_break:
- return res
- for sub in rule.lookups:
- if isinstance(sub, SingleSubstBuilder) and not any(
- g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping
- ):
- res = sub
- return res
- class LigatureSubstBuilder(LookupBuilder):
- """Builds a Ligature Substitution (GSUB4) lookup.
- Users are expected to manually add ligatures to the ``ligatures``
- attribute after the object has been initialized, e.g.::
- # sub f i by f_i;
- builder.ligatures[("f","f","i")] = "f_f_i"
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- ligatures: An ordered dictionary mapping a tuple of glyph names to the
- ligature glyphname.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 4)
- self.ligatures = OrderedDict() # {('f','f','i'): 'f_f_i'}
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.ligatures == other.ligatures
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the ligature
- substitution lookup.
- """
- subtables = self.build_subst_subtables(
- self.ligatures, buildLigatureSubstSubtable
- )
- return self.buildLookup_(subtables)
- def add_subtable_break(self, location):
- self.ligatures[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
- class MultipleSubstBuilder(LookupBuilder):
- """Builds a Multiple Substitution (GSUB2) lookup.
- Users are expected to manually add substitutions to the ``mapping``
- attribute after the object has been initialized, e.g.::
- # sub uni06C0 by uni06D5.fina hamza.above;
- builder.mapping["uni06C0"] = [ "uni06D5.fina", "hamza.above"]
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- mapping: An ordered dictionary mapping a glyph name to a list of
- substituted glyph names.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 2)
- self.mapping = OrderedDict()
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.mapping == other.mapping
- def build(self):
- subtables = self.build_subst_subtables(self.mapping, buildMultipleSubstSubtable)
- return self.buildLookup_(subtables)
- def add_subtable_break(self, location):
- self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
- class CursivePosBuilder(LookupBuilder):
- """Builds a Cursive Positioning (GPOS3) lookup.
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- attachments: An ordered dictionary mapping a glyph name to a two-element
- tuple of ``otTables.Anchor`` objects.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 3)
- self.attachments = {}
- def equals(self, other):
- return (
- LookupBuilder.equals(self, other) and self.attachments == other.attachments
- )
- def add_attachment(self, location, glyphs, entryAnchor, exitAnchor):
- """Adds attachment information to the cursive positioning lookup.
- Args:
- location: A string or tuple representing the location in the
- original source which produced this lookup. (Unused.)
- glyphs: A list of glyph names sharing these entry and exit
- anchor locations.
- entryAnchor: A ``otTables.Anchor`` object representing the
- entry anchor, or ``None`` if no entry anchor is present.
- exitAnchor: A ``otTables.Anchor`` object representing the
- exit anchor, or ``None`` if no exit anchor is present.
- """
- for glyph in glyphs:
- self.attachments[glyph] = (entryAnchor, exitAnchor)
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the cursive
- positioning lookup.
- """
- st = buildCursivePosSubtable(self.attachments, self.glyphMap)
- return self.buildLookup_([st])
- class MarkBasePosBuilder(LookupBuilder):
- """Builds a Mark-To-Base Positioning (GPOS4) lookup.
- Users are expected to manually add marks and bases to the ``marks``
- and ``bases`` attributes after the object has been initialized, e.g.::
- builder.marks["acute"] = (0, a1)
- builder.marks["grave"] = (0, a1)
- builder.marks["cedilla"] = (1, a2)
- builder.bases["a"] = {0: a3, 1: a5}
- builder.bases["b"] = {0: a4, 1: a5}
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- marks: An dictionary mapping a glyph name to a two-element
- tuple containing a mark class ID and ``otTables.Anchor`` object.
- bases: An dictionary mapping a glyph name to a dictionary of
- mark class IDs and ``otTables.Anchor`` object.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 4)
- self.marks = {} # glyphName -> (markClassName, anchor)
- self.bases = {} # glyphName -> {markClassName: anchor}
- def equals(self, other):
- return (
- LookupBuilder.equals(self, other)
- and self.marks == other.marks
- and self.bases == other.bases
- )
- def inferGlyphClasses(self):
- result = {glyph: 1 for glyph in self.bases}
- result.update({glyph: 3 for glyph in self.marks})
- return result
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the mark-to-base
- positioning lookup.
- """
- markClasses = self.buildMarkClasses_(self.marks)
- marks = {}
- for mark, (mc, anchor) in self.marks.items():
- if mc not in markClasses:
- raise ValueError(
- "Mark class %s not found for mark glyph %s" % (mc, mark)
- )
- marks[mark] = (markClasses[mc], anchor)
- bases = {}
- for glyph, anchors in self.bases.items():
- bases[glyph] = {}
- for mc, anchor in anchors.items():
- if mc not in markClasses:
- raise ValueError(
- "Mark class %s not found for base glyph %s" % (mc, glyph)
- )
- bases[glyph][markClasses[mc]] = anchor
- subtables = buildMarkBasePos(marks, bases, self.glyphMap)
- return self.buildLookup_(subtables)
- class MarkLigPosBuilder(LookupBuilder):
- """Builds a Mark-To-Ligature Positioning (GPOS5) lookup.
- Users are expected to manually add marks and bases to the ``marks``
- and ``ligatures`` attributes after the object has been initialized, e.g.::
- builder.marks["acute"] = (0, a1)
- builder.marks["grave"] = (0, a1)
- builder.marks["cedilla"] = (1, a2)
- builder.ligatures["f_i"] = [
- { 0: a3, 1: a5 }, # f
- { 0: a4, 1: a5 } # i
- ]
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- marks: An dictionary mapping a glyph name to a two-element
- tuple containing a mark class ID and ``otTables.Anchor`` object.
- ligatures: An dictionary mapping a glyph name to an array with one
- element for each ligature component. Each array element should be
- a dictionary mapping mark class IDs to ``otTables.Anchor`` objects.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 5)
- self.marks = {} # glyphName -> (markClassName, anchor)
- self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...]
- def equals(self, other):
- return (
- LookupBuilder.equals(self, other)
- and self.marks == other.marks
- and self.ligatures == other.ligatures
- )
- def inferGlyphClasses(self):
- result = {glyph: 2 for glyph in self.ligatures}
- result.update({glyph: 3 for glyph in self.marks})
- return result
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the mark-to-ligature
- positioning lookup.
- """
- markClasses = self.buildMarkClasses_(self.marks)
- marks = {
- mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
- }
- ligs = {}
- for lig, components in self.ligatures.items():
- ligs[lig] = []
- for c in components:
- ligs[lig].append({markClasses[mc]: a for mc, a in c.items()})
- subtables = buildMarkLigPos(marks, ligs, self.glyphMap)
- return self.buildLookup_(subtables)
- class MarkMarkPosBuilder(LookupBuilder):
- """Builds a Mark-To-Mark Positioning (GPOS6) lookup.
- Users are expected to manually add marks and bases to the ``marks``
- and ``baseMarks`` attributes after the object has been initialized, e.g.::
- builder.marks["acute"] = (0, a1)
- builder.marks["grave"] = (0, a1)
- builder.marks["cedilla"] = (1, a2)
- builder.baseMarks["acute"] = {0: a3}
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- marks: An dictionary mapping a glyph name to a two-element
- tuple containing a mark class ID and ``otTables.Anchor`` object.
- baseMarks: An dictionary mapping a glyph name to a dictionary
- containing one item: a mark class ID and a ``otTables.Anchor`` object.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 6)
- self.marks = {} # glyphName -> (markClassName, anchor)
- self.baseMarks = {} # glyphName -> {markClassName: anchor}
- def equals(self, other):
- return (
- LookupBuilder.equals(self, other)
- and self.marks == other.marks
- and self.baseMarks == other.baseMarks
- )
- def inferGlyphClasses(self):
- result = {glyph: 3 for glyph in self.baseMarks}
- result.update({glyph: 3 for glyph in self.marks})
- return result
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the mark-to-mark
- positioning lookup.
- """
- markClasses = self.buildMarkClasses_(self.marks)
- markClassList = sorted(markClasses.keys(), key=markClasses.get)
- marks = {
- mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items()
- }
- st = ot.MarkMarkPos()
- st.Format = 1
- st.ClassCount = len(markClasses)
- st.Mark1Coverage = buildCoverage(marks, self.glyphMap)
- st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap)
- st.Mark1Array = buildMarkArray(marks, self.glyphMap)
- st.Mark2Array = ot.Mark2Array()
- st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs)
- st.Mark2Array.Mark2Record = []
- for base in st.Mark2Coverage.glyphs:
- anchors = [self.baseMarks[base].get(mc) for mc in markClassList]
- st.Mark2Array.Mark2Record.append(buildMark2Record(anchors))
- return self.buildLookup_([st])
- class ReverseChainSingleSubstBuilder(LookupBuilder):
- """Builds a Reverse Chaining Contextual Single Substitution (GSUB8) lookup.
- Users are expected to manually add substitutions to the ``substitutions``
- attribute after the object has been initialized, e.g.::
- # reversesub [a e n] d' by d.alt;
- prefix = [ ["a", "e", "n"] ]
- suffix = []
- mapping = { "d": "d.alt" }
- builder.substitutions.append( (prefix, suffix, mapping) )
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- substitutions: A three-element tuple consisting of a prefix sequence,
- a suffix sequence, and a dictionary of single substitutions.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 8)
- self.rules = [] # (prefix, suffix, mapping)
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.rules == other.rules
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the chained
- contextual substitution lookup.
- """
- subtables = []
- for prefix, suffix, mapping in self.rules:
- st = ot.ReverseChainSingleSubst()
- st.Format = 1
- self.setBacktrackCoverage_(prefix, st)
- self.setLookAheadCoverage_(suffix, st)
- st.Coverage = buildCoverage(mapping.keys(), self.glyphMap)
- st.GlyphCount = len(mapping)
- st.Substitute = [mapping[g] for g in st.Coverage.glyphs]
- subtables.append(st)
- return self.buildLookup_(subtables)
- def add_subtable_break(self, location):
- # Nothing to do here, each substitution is in its own subtable.
- pass
- class SingleSubstBuilder(LookupBuilder):
- """Builds a Single Substitution (GSUB1) lookup.
- Users are expected to manually add substitutions to the ``mapping``
- attribute after the object has been initialized, e.g.::
- # sub x by y;
- builder.mapping["x"] = "y"
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- mapping: A dictionary mapping a single glyph name to another glyph name.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GSUB", 1)
- self.mapping = OrderedDict()
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.mapping == other.mapping
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the multiple
- substitution lookup.
- """
- subtables = self.build_subst_subtables(self.mapping, buildSingleSubstSubtable)
- return self.buildLookup_(subtables)
- def getAlternateGlyphs(self):
- return {glyph: set([repl]) for glyph, repl in self.mapping.items()}
- def add_subtable_break(self, location):
- self.mapping[(self.SUBTABLE_BREAK_, location)] = self.SUBTABLE_BREAK_
- class ClassPairPosSubtableBuilder(object):
- """Builds class-based Pair Positioning (GPOS2 format 2) subtables.
- Note that this does *not* build a GPOS2 ``otTables.Lookup`` directly,
- but builds a list of ``otTables.PairPos`` subtables. It is used by the
- :class:`PairPosBuilder` below.
- Attributes:
- builder (PairPosBuilder): A pair positioning lookup builder.
- """
- def __init__(self, builder):
- self.builder_ = builder
- self.classDef1_, self.classDef2_ = None, None
- self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2)
- self.forceSubtableBreak_ = False
- self.subtables_ = []
- def addPair(self, gc1, value1, gc2, value2):
- """Add a pair positioning rule.
- Args:
- gc1: A set of glyph names for the "left" glyph
- value1: An ``otTables.ValueRecord`` object for the left glyph's
- positioning.
- gc2: A set of glyph names for the "right" glyph
- value2: An ``otTables.ValueRecord`` object for the right glyph's
- positioning.
- """
- mergeable = (
- not self.forceSubtableBreak_
- and self.classDef1_ is not None
- and self.classDef1_.canAdd(gc1)
- and self.classDef2_ is not None
- and self.classDef2_.canAdd(gc2)
- )
- if not mergeable:
- self.flush_()
- self.classDef1_ = ClassDefBuilder(useClass0=True)
- self.classDef2_ = ClassDefBuilder(useClass0=False)
- self.values_ = {}
- self.classDef1_.add(gc1)
- self.classDef2_.add(gc2)
- self.values_[(gc1, gc2)] = (value1, value2)
- def addSubtableBreak(self):
- """Add an explicit subtable break at this point."""
- self.forceSubtableBreak_ = True
- def subtables(self):
- """Return the list of ``otTables.PairPos`` subtables constructed."""
- self.flush_()
- return self.subtables_
- def flush_(self):
- if self.classDef1_ is None or self.classDef2_ is None:
- return
- st = buildPairPosClassesSubtable(self.values_, self.builder_.glyphMap)
- if st.Coverage is None:
- return
- self.subtables_.append(st)
- self.forceSubtableBreak_ = False
- class PairPosBuilder(LookupBuilder):
- """Builds a Pair Positioning (GPOS2) lookup.
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- pairs: An array of class-based pair positioning tuples. Usually
- manipulated with the :meth:`addClassPair` method below.
- glyphPairs: A dictionary mapping a tuple of glyph names to a tuple
- of ``otTables.ValueRecord`` objects. Usually manipulated with the
- :meth:`addGlyphPair` method below.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 2)
- self.pairs = [] # [(gc1, value1, gc2, value2)*]
- self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2)
- self.locations = {} # (gc1, gc2) --> (filepath, line, column)
- def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2):
- """Add a class pair positioning rule to the current lookup.
- Args:
- location: A string or tuple representing the location in the
- original source which produced this rule. Unused.
- glyphclass1: A set of glyph names for the "left" glyph in the pair.
- value1: A ``otTables.ValueRecord`` for positioning the left glyph.
- glyphclass2: A set of glyph names for the "right" glyph in the pair.
- value2: A ``otTables.ValueRecord`` for positioning the right glyph.
- """
- self.pairs.append((glyphclass1, value1, glyphclass2, value2))
- def addGlyphPair(self, location, glyph1, value1, glyph2, value2):
- """Add a glyph pair positioning rule to the current lookup.
- Args:
- location: A string or tuple representing the location in the
- original source which produced this rule.
- glyph1: A glyph name for the "left" glyph in the pair.
- value1: A ``otTables.ValueRecord`` for positioning the left glyph.
- glyph2: A glyph name for the "right" glyph in the pair.
- value2: A ``otTables.ValueRecord`` for positioning the right glyph.
- """
- key = (glyph1, glyph2)
- oldValue = self.glyphPairs.get(key, None)
- if oldValue is not None:
- # the Feature File spec explicitly allows specific pairs generated
- # by an 'enum' rule to be overridden by preceding single pairs
- otherLoc = self.locations[key]
- log.debug(
- "Already defined position for pair %s %s at %s; "
- "choosing the first value",
- glyph1,
- glyph2,
- otherLoc,
- )
- else:
- self.glyphPairs[key] = (value1, value2)
- self.locations[key] = location
- def add_subtable_break(self, location):
- self.pairs.append(
- (
- self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_,
- self.SUBTABLE_BREAK_,
- )
- )
- def equals(self, other):
- return (
- LookupBuilder.equals(self, other)
- and self.glyphPairs == other.glyphPairs
- and self.pairs == other.pairs
- )
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the pair positioning
- lookup.
- """
- builders = {}
- builder = ClassPairPosSubtableBuilder(self)
- for glyphclass1, value1, glyphclass2, value2 in self.pairs:
- if glyphclass1 is self.SUBTABLE_BREAK_:
- builder.addSubtableBreak()
- continue
- builder.addPair(glyphclass1, value1, glyphclass2, value2)
- subtables = []
- if self.glyphPairs:
- subtables.extend(buildPairPosGlyphs(self.glyphPairs, self.glyphMap))
- subtables.extend(builder.subtables())
- lookup = self.buildLookup_(subtables)
- # Compact the lookup
- # This is a good moment to do it because the compaction should create
- # smaller subtables, which may prevent overflows from happening.
- # Keep reading the value from the ENV until ufo2ft switches to the config system
- level = self.font.cfg.get(
- "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
- default=_compression_level_from_env(),
- )
- if level != 0:
- log.info("Compacting GPOS...")
- compact_lookup(self.font, level, lookup)
- return lookup
- class SinglePosBuilder(LookupBuilder):
- """Builds a Single Positioning (GPOS1) lookup.
- Attributes:
- font (``fontTools.TTLib.TTFont``): A font object.
- location: A string or tuple representing the location in the original
- source which produced this lookup.
- mapping: A dictionary mapping a glyph name to a ``otTables.ValueRecord``
- objects. Usually manipulated with the :meth:`add_pos` method below.
- lookupflag (int): The lookup's flag
- markFilterSet: Either ``None`` if no mark filtering set is used, or
- an integer representing the filtering set to be used for this
- lookup. If a mark filtering set is provided,
- `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's
- flags.
- """
- def __init__(self, font, location):
- LookupBuilder.__init__(self, font, location, "GPOS", 1)
- self.locations = {} # glyph -> (filename, line, column)
- self.mapping = {} # glyph -> ot.ValueRecord
- def add_pos(self, location, glyph, otValueRecord):
- """Add a single positioning rule.
- Args:
- location: A string or tuple representing the location in the
- original source which produced this lookup.
- glyph: A glyph name.
- otValueRection: A ``otTables.ValueRecord`` used to position the
- glyph.
- """
- if not self.can_add(glyph, otValueRecord):
- otherLoc = self.locations[glyph]
- raise OpenTypeLibError(
- 'Already defined different position for glyph "%s" at %s'
- % (glyph, otherLoc),
- location,
- )
- if otValueRecord:
- self.mapping[glyph] = otValueRecord
- self.locations[glyph] = location
- def can_add(self, glyph, value):
- assert isinstance(value, ValueRecord)
- curValue = self.mapping.get(glyph)
- return curValue is None or curValue == value
- def equals(self, other):
- return LookupBuilder.equals(self, other) and self.mapping == other.mapping
- def build(self):
- """Build the lookup.
- Returns:
- An ``otTables.Lookup`` object representing the single positioning
- lookup.
- """
- subtables = buildSinglePos(self.mapping, self.glyphMap)
- return self.buildLookup_(subtables)
- # GSUB
- def buildSingleSubstSubtable(mapping):
- """Builds a single substitution (GSUB1) subtable.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.SingleSubstBuilder` instead.
- Args:
- mapping: A dictionary mapping input glyph names to output glyph names.
- Returns:
- An ``otTables.SingleSubst`` object, or ``None`` if the mapping dictionary
- is empty.
- """
- if not mapping:
- return None
- self = ot.SingleSubst()
- self.mapping = dict(mapping)
- return self
- def buildMultipleSubstSubtable(mapping):
- """Builds a multiple substitution (GSUB2) subtable.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.MultipleSubstBuilder` instead.
- Example::
- # sub uni06C0 by uni06D5.fina hamza.above
- # sub uni06C2 by uni06C1.fina hamza.above;
- subtable = buildMultipleSubstSubtable({
- "uni06C0": [ "uni06D5.fina", "hamza.above"],
- "uni06C2": [ "uni06D1.fina", "hamza.above"]
- })
- Args:
- mapping: A dictionary mapping input glyph names to a list of output
- glyph names.
- Returns:
- An ``otTables.MultipleSubst`` object or ``None`` if the mapping dictionary
- is empty.
- """
- if not mapping:
- return None
- self = ot.MultipleSubst()
- self.mapping = dict(mapping)
- return self
- def buildAlternateSubstSubtable(mapping):
- """Builds an alternate substitution (GSUB3) subtable.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.AlternateSubstBuilder` instead.
- Args:
- mapping: A dictionary mapping input glyph names to a list of output
- glyph names.
- Returns:
- An ``otTables.AlternateSubst`` object or ``None`` if the mapping dictionary
- is empty.
- """
- if not mapping:
- return None
- self = ot.AlternateSubst()
- self.alternates = dict(mapping)
- return self
- def _getLigatureKey(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 primary key for sorting.
- # To make buildLigatureSubstSubtable() deterministic, we use the
- # component sequence as the secondary key.
- # For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l).
- return (-len(components), components)
- def buildLigatureSubstSubtable(mapping):
- """Builds a ligature substitution (GSUB4) subtable.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.LigatureSubstBuilder` instead.
- Example::
- # sub f f i by f_f_i;
- # sub f i by f_i;
- subtable = buildLigatureSubstSubtable({
- ("f", "f", "i"): "f_f_i",
- ("f", "i"): "f_i",
- })
- Args:
- mapping: A dictionary mapping tuples of glyph names to output
- glyph names.
- Returns:
- An ``otTables.LigatureSubst`` object or ``None`` if the mapping dictionary
- is empty.
- """
- if not mapping:
- return None
- self = ot.LigatureSubst()
- # The following single line can replace the rest of this function
- # with fontTools >= 3.1:
- # self.ligatures = dict(mapping)
- self.ligatures = {}
- for components in sorted(mapping.keys(), key=_getLigatureKey):
- ligature = ot.Ligature()
- ligature.Component = components[1:]
- ligature.CompCount = len(ligature.Component) + 1
- ligature.LigGlyph = mapping[components]
- firstGlyph = components[0]
- self.ligatures.setdefault(firstGlyph, []).append(ligature)
- return self
- # GPOS
- def buildAnchor(x, y, point=None, deviceX=None, deviceY=None):
- """Builds an Anchor table.
- This determines the appropriate anchor format based on the passed parameters.
- Args:
- x (int): X coordinate.
- y (int): Y coordinate.
- point (int): Index of glyph contour point, if provided.
- deviceX (``otTables.Device``): X coordinate device table, if provided.
- deviceY (``otTables.Device``): Y coordinate device table, if provided.
- Returns:
- An ``otTables.Anchor`` object.
- """
- self = ot.Anchor()
- self.XCoordinate, self.YCoordinate = x, y
- self.Format = 1
- if point is not None:
- self.AnchorPoint = point
- self.Format = 2
- if deviceX is not None or deviceY is not None:
- assert (
- self.Format == 1
- ), "Either point, or both of deviceX/deviceY, must be None."
- self.XDeviceTable = deviceX
- self.YDeviceTable = deviceY
- self.Format = 3
- return self
- def buildBaseArray(bases, numMarkClasses, glyphMap):
- """Builds a base array record.
- As part of building mark-to-base positioning rules, you will need to define
- a ``BaseArray`` record, which "defines for each base glyph an array of
- anchors, one for each mark class." This function builds the base array
- subtable.
- Example::
- bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
- basearray = buildBaseArray(bases, 2, font.getReverseGlyphMap())
- Args:
- bases (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being dictionaries mapping mark class ID
- to the appropriate ``otTables.Anchor`` object used for attaching marks
- of that class.
- numMarkClasses (int): The total number of mark classes for which anchors
- are defined.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- An ``otTables.BaseArray`` object.
- """
- self = ot.BaseArray()
- self.BaseRecord = []
- for base in sorted(bases, key=glyphMap.__getitem__):
- b = bases[base]
- anchors = [b.get(markClass) for markClass in range(numMarkClasses)]
- self.BaseRecord.append(buildBaseRecord(anchors))
- self.BaseCount = len(self.BaseRecord)
- return self
- def buildBaseRecord(anchors):
- # [otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord
- self = ot.BaseRecord()
- self.BaseAnchor = anchors
- return self
- def buildComponentRecord(anchors):
- """Builds a component record.
- As part of building mark-to-ligature positioning rules, you will need to
- define ``ComponentRecord`` objects, which contain "an array of offsets...
- to the Anchor tables that define all the attachment points used to attach
- marks to the component." This function builds the component record.
- Args:
- anchors: A list of ``otTables.Anchor`` objects or ``None``.
- Returns:
- A ``otTables.ComponentRecord`` object or ``None`` if no anchors are
- supplied.
- """
- if not anchors:
- return None
- self = ot.ComponentRecord()
- self.LigatureAnchor = anchors
- return self
- def buildCursivePosSubtable(attach, glyphMap):
- """Builds a cursive positioning (GPOS3) subtable.
- Cursive positioning lookups are made up of a coverage table of glyphs,
- and a set of ``EntryExitRecord`` records containing the anchors for
- each glyph. This function builds the cursive positioning subtable.
- Example::
- subtable = buildCursivePosSubtable({
- "AlifIni": (None, buildAnchor(0, 50)),
- "BehMed": (buildAnchor(500,250), buildAnchor(0,50)),
- # ...
- }, font.getReverseGlyphMap())
- Args:
- attach (dict): A mapping between glyph names and a tuple of two
- ``otTables.Anchor`` objects representing entry and exit anchors.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- An ``otTables.CursivePos`` object, or ``None`` if the attachment
- dictionary was empty.
- """
- if not attach:
- return None
- self = ot.CursivePos()
- self.Format = 1
- self.Coverage = buildCoverage(attach.keys(), glyphMap)
- self.EntryExitRecord = []
- for glyph in self.Coverage.glyphs:
- entryAnchor, exitAnchor = attach[glyph]
- rec = ot.EntryExitRecord()
- rec.EntryAnchor = entryAnchor
- rec.ExitAnchor = exitAnchor
- self.EntryExitRecord.append(rec)
- self.EntryExitCount = len(self.EntryExitRecord)
- return self
- def buildDevice(deltas):
- """Builds a Device record as part of a ValueRecord or Anchor.
- Device tables specify size-specific adjustments to value records
- and anchors to reflect changes based on the resolution of the output.
- For example, one could specify that an anchor's Y position should be
- increased by 1 pixel when displayed at 8 pixels per em. This routine
- builds device records.
- Args:
- deltas: A dictionary mapping pixels-per-em sizes to the delta
- adjustment in pixels when the font is displayed at that size.
- Returns:
- An ``otTables.Device`` object if any deltas were supplied, or
- ``None`` otherwise.
- """
- if not deltas:
- return None
- self = ot.Device()
- keys = deltas.keys()
- self.StartSize = startSize = min(keys)
- self.EndSize = endSize = max(keys)
- assert 0 <= startSize <= endSize
- self.DeltaValue = deltaValues = [
- deltas.get(size, 0) for size in range(startSize, endSize + 1)
- ]
- maxDelta = max(deltaValues)
- minDelta = min(deltaValues)
- assert minDelta > -129 and maxDelta < 128
- if minDelta > -3 and maxDelta < 2:
- self.DeltaFormat = 1
- elif minDelta > -9 and maxDelta < 8:
- self.DeltaFormat = 2
- else:
- self.DeltaFormat = 3
- return self
- def buildLigatureArray(ligs, numMarkClasses, glyphMap):
- """Builds a LigatureArray subtable.
- As part of building a mark-to-ligature lookup, you will need to define
- the set of anchors (for each mark class) on each component of the ligature
- where marks can be attached. For example, for an Arabic divine name ligature
- (lam lam heh), you may want to specify mark attachment positioning for
- superior marks (fatha, etc.) and inferior marks (kasra, etc.) on each glyph
- of the ligature. This routine builds the ligature array record.
- Example::
- buildLigatureArray({
- "lam-lam-heh": [
- { 0: superiorAnchor1, 1: inferiorAnchor1 }, # attach points for lam1
- { 0: superiorAnchor2, 1: inferiorAnchor2 }, # attach points for lam2
- { 0: superiorAnchor3, 1: inferiorAnchor3 }, # attach points for heh
- ]
- }, 2, font.getReverseGlyphMap())
- Args:
- ligs (dict): A mapping of ligature names to an array of dictionaries:
- for each component glyph in the ligature, an dictionary mapping
- mark class IDs to anchors.
- numMarkClasses (int): The number of mark classes.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- An ``otTables.LigatureArray`` object if deltas were supplied.
- """
- self = ot.LigatureArray()
- self.LigatureAttach = []
- for lig in sorted(ligs, key=glyphMap.__getitem__):
- anchors = []
- for component in ligs[lig]:
- anchors.append([component.get(mc) for mc in range(numMarkClasses)])
- self.LigatureAttach.append(buildLigatureAttach(anchors))
- self.LigatureCount = len(self.LigatureAttach)
- return self
- def buildLigatureAttach(components):
- # [[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach
- self = ot.LigatureAttach()
- self.ComponentRecord = [buildComponentRecord(c) for c in components]
- self.ComponentCount = len(self.ComponentRecord)
- return self
- def buildMarkArray(marks, glyphMap):
- """Builds a mark array subtable.
- As part of building mark-to-* positioning rules, you will need to define
- a MarkArray subtable, which "defines the class and the anchor point
- for a mark glyph." This function builds the mark array subtable.
- Example::
- mark = {
- "acute": (0, buildAnchor(300,712)),
- # ...
- }
- markarray = buildMarkArray(marks, font.getReverseGlyphMap())
- Args:
- marks (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being a tuple of mark class number and
- an ``otTables.Anchor`` object representing the mark's attachment
- point.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- An ``otTables.MarkArray`` object.
- """
- self = ot.MarkArray()
- self.MarkRecord = []
- for mark in sorted(marks.keys(), key=glyphMap.__getitem__):
- markClass, anchor = marks[mark]
- markrec = buildMarkRecord(markClass, anchor)
- self.MarkRecord.append(markrec)
- self.MarkCount = len(self.MarkRecord)
- return self
- def buildMarkBasePos(marks, bases, glyphMap):
- """Build a list of MarkBasePos (GPOS4) subtables.
- This routine turns a set of marks and bases into a list of mark-to-base
- positioning subtables. Currently the list will contain a single subtable
- containing all marks and bases, although at a later date it may return the
- optimal list of subtables subsetting the marks and bases into groups which
- save space. See :func:`buildMarkBasePosSubtable` below.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead.
- Example::
- # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
- marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)}
- bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}}
- markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap())
- Args:
- marks (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being a tuple of mark class number and
- an ``otTables.Anchor`` object representing the mark's attachment
- point. (See :func:`buildMarkArray`.)
- bases (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being dictionaries mapping mark class ID
- to the appropriate ``otTables.Anchor`` object used for attaching marks
- of that class. (See :func:`buildBaseArray`.)
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A list of ``otTables.MarkBasePos`` objects.
- """
- # TODO: Consider emitting multiple subtables to save space.
- # Partition the marks and bases into disjoint subsets, so that
- # MarkBasePos rules would only access glyphs from a single
- # subset. This would likely lead to smaller mark/base
- # matrices, so we might be able to omit many of the empty
- # anchor tables that we currently produce. Of course, this
- # would only work if the MarkBasePos rules of real-world fonts
- # allow partitioning into multiple subsets. We should find out
- # whether this is the case; if so, implement the optimization.
- # On the other hand, a very large number of subtables could
- # slow down layout engines; so this would need profiling.
- return [buildMarkBasePosSubtable(marks, bases, glyphMap)]
- def buildMarkBasePosSubtable(marks, bases, glyphMap):
- """Build a single MarkBasePos (GPOS4) subtable.
- This builds a mark-to-base lookup subtable containing all of the referenced
- marks and bases. See :func:`buildMarkBasePos`.
- Args:
- marks (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being a tuple of mark class number and
- an ``otTables.Anchor`` object representing the mark's attachment
- point. (See :func:`buildMarkArray`.)
- bases (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being dictionaries mapping mark class ID
- to the appropriate ``otTables.Anchor`` object used for attaching marks
- of that class. (See :func:`buildBaseArray`.)
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A ``otTables.MarkBasePos`` object.
- """
- self = ot.MarkBasePos()
- self.Format = 1
- self.MarkCoverage = buildCoverage(marks, glyphMap)
- self.MarkArray = buildMarkArray(marks, glyphMap)
- self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
- self.BaseCoverage = buildCoverage(bases, glyphMap)
- self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap)
- return self
- def buildMarkLigPos(marks, ligs, glyphMap):
- """Build a list of MarkLigPos (GPOS5) subtables.
- This routine turns a set of marks and ligatures into a list of mark-to-ligature
- positioning subtables. Currently the list will contain a single subtable
- containing all marks and ligatures, although at a later date it may return
- the optimal list of subtables subsetting the marks and ligatures into groups
- which save space. See :func:`buildMarkLigPosSubtable` below.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.MarkLigPosBuilder` instead.
- Example::
- # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ...
- marks = {
- "acute": (0, a1),
- "grave": (0, a1),
- "cedilla": (1, a2)
- }
- ligs = {
- "f_i": [
- { 0: a3, 1: a5 }, # f
- { 0: a4, 1: a5 } # i
- ],
- # "c_t": [{...}, {...}]
- }
- markligposes = buildMarkLigPos(marks, ligs,
- font.getReverseGlyphMap())
- Args:
- marks (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being a tuple of mark class number and
- an ``otTables.Anchor`` object representing the mark's attachment
- point. (See :func:`buildMarkArray`.)
- ligs (dict): A mapping of ligature names to an array of dictionaries:
- for each component glyph in the ligature, an dictionary mapping
- mark class IDs to anchors. (See :func:`buildLigatureArray`.)
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A list of ``otTables.MarkLigPos`` objects.
- """
- # TODO: Consider splitting into multiple subtables to save space,
- # as with MarkBasePos, this would be a trade-off that would need
- # profiling. And, depending on how typical fonts are structured,
- # it might not be worth doing at all.
- return [buildMarkLigPosSubtable(marks, ligs, glyphMap)]
- def buildMarkLigPosSubtable(marks, ligs, glyphMap):
- """Build a single MarkLigPos (GPOS5) subtable.
- This builds a mark-to-base lookup subtable containing all of the referenced
- marks and bases. See :func:`buildMarkLigPos`.
- Args:
- marks (dict): A dictionary mapping anchors to glyphs; the keys being
- glyph names, and the values being a tuple of mark class number and
- an ``otTables.Anchor`` object representing the mark's attachment
- point. (See :func:`buildMarkArray`.)
- ligs (dict): A mapping of ligature names to an array of dictionaries:
- for each component glyph in the ligature, an dictionary mapping
- mark class IDs to anchors. (See :func:`buildLigatureArray`.)
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A ``otTables.MarkLigPos`` object.
- """
- self = ot.MarkLigPos()
- self.Format = 1
- self.MarkCoverage = buildCoverage(marks, glyphMap)
- self.MarkArray = buildMarkArray(marks, glyphMap)
- self.ClassCount = max([mc for mc, _ in marks.values()]) + 1
- self.LigatureCoverage = buildCoverage(ligs, glyphMap)
- self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap)
- return self
- def buildMarkRecord(classID, anchor):
- assert isinstance(classID, int)
- assert isinstance(anchor, ot.Anchor)
- self = ot.MarkRecord()
- self.Class = classID
- self.MarkAnchor = anchor
- return self
- def buildMark2Record(anchors):
- # [otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record
- self = ot.Mark2Record()
- self.Mark2Anchor = anchors
- return self
- def _getValueFormat(f, values, i):
- # Helper for buildPairPos{Glyphs|Classes}Subtable.
- if f is not None:
- return f
- mask = 0
- for value in values:
- if value is not None and value[i] is not None:
- mask |= value[i].getFormat()
- return mask
- def buildPairPosClassesSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
- """Builds a class pair adjustment (GPOS2 format 2) subtable.
- Kerning tables are generally expressed as pair positioning tables using
- class-based pair adjustments. This routine builds format 2 PairPos
- subtables.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.ClassPairPosSubtableBuilder`
- instead, as this takes care of ensuring that the supplied pairs can be
- formed into non-overlapping classes and emitting individual subtables
- whenever the non-overlapping requirement means that a new subtable is
- required.
- Example::
- pairs = {}
- pairs[(
- [ "K", "X" ],
- [ "W", "V" ]
- )] = ( buildValue(xAdvance=+5), buildValue() )
- # pairs[(... , ...)] = (..., ...)
- pairpos = buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
- Args:
- pairs (dict): Pair positioning data; the keys being a two-element
- tuple of lists of glyphnames, and the values being a two-element
- tuple of ``otTables.ValueRecord`` objects.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- valueFormat1: Force the "left" value records to the given format.
- valueFormat2: Force the "right" value records to the given format.
- Returns:
- A ``otTables.PairPos`` object.
- """
- coverage = set()
- classDef1 = ClassDefBuilder(useClass0=True)
- classDef2 = ClassDefBuilder(useClass0=False)
- for gc1, gc2 in sorted(pairs):
- coverage.update(gc1)
- classDef1.add(gc1)
- classDef2.add(gc2)
- self = ot.PairPos()
- self.Format = 2
- valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
- valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
- self.Coverage = buildCoverage(coverage, glyphMap)
- self.ClassDef1 = classDef1.build()
- self.ClassDef2 = classDef2.build()
- classes1 = classDef1.classes()
- classes2 = classDef2.classes()
- self.Class1Record = []
- for c1 in classes1:
- rec1 = ot.Class1Record()
- rec1.Class2Record = []
- self.Class1Record.append(rec1)
- for c2 in classes2:
- rec2 = ot.Class2Record()
- val1, val2 = pairs.get((c1, c2), (None, None))
- rec2.Value1 = (
- ValueRecord(src=val1, valueFormat=valueFormat1)
- if valueFormat1
- else None
- )
- rec2.Value2 = (
- ValueRecord(src=val2, valueFormat=valueFormat2)
- if valueFormat2
- else None
- )
- rec1.Class2Record.append(rec2)
- self.Class1Count = len(self.Class1Record)
- self.Class2Count = len(classes2)
- return self
- def buildPairPosGlyphs(pairs, glyphMap):
- """Builds a list of glyph-based pair adjustment (GPOS2 format 1) subtables.
- This organises a list of pair positioning adjustments into subtables based
- on common value record formats.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder`
- instead.
- Example::
- pairs = {
- ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
- ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
- # ...
- }
- subtables = buildPairPosGlyphs(pairs, font.getReverseGlyphMap())
- Args:
- pairs (dict): Pair positioning data; the keys being a two-element
- tuple of glyphnames, and the values being a two-element
- tuple of ``otTables.ValueRecord`` objects.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A list of ``otTables.PairPos`` objects.
- """
- p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)}
- for (glyphA, glyphB), (valA, valB) in pairs.items():
- formatA = valA.getFormat() if valA is not None else 0
- formatB = valB.getFormat() if valB is not None else 0
- pos = p.setdefault((formatA, formatB), {})
- pos[(glyphA, glyphB)] = (valA, valB)
- return [
- buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB)
- for ((formatA, formatB), pos) in sorted(p.items())
- ]
- def buildPairPosGlyphsSubtable(pairs, glyphMap, valueFormat1=None, valueFormat2=None):
- """Builds a single glyph-based pair adjustment (GPOS2 format 1) subtable.
- This builds a PairPos subtable from a dictionary of glyph pairs and
- their positioning adjustments. See also :func:`buildPairPosGlyphs`.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.PairPosBuilder` instead.
- Example::
- pairs = {
- ("K", "W"): ( buildValue(xAdvance=+5), buildValue() ),
- ("K", "V"): ( buildValue(xAdvance=+5), buildValue() ),
- # ...
- }
- pairpos = buildPairPosGlyphsSubtable(pairs, font.getReverseGlyphMap())
- Args:
- pairs (dict): Pair positioning data; the keys being a two-element
- tuple of glyphnames, and the values being a two-element
- tuple of ``otTables.ValueRecord`` objects.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- valueFormat1: Force the "left" value records to the given format.
- valueFormat2: Force the "right" value records to the given format.
- Returns:
- A ``otTables.PairPos`` object.
- """
- self = ot.PairPos()
- self.Format = 1
- valueFormat1 = self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0)
- valueFormat2 = self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1)
- p = {}
- for (glyphA, glyphB), (valA, valB) in pairs.items():
- p.setdefault(glyphA, []).append((glyphB, valA, valB))
- self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap)
- self.PairSet = []
- for glyph in self.Coverage.glyphs:
- ps = ot.PairSet()
- ps.PairValueRecord = []
- self.PairSet.append(ps)
- for glyph2, val1, val2 in sorted(p[glyph], key=lambda x: glyphMap[x[0]]):
- pvr = ot.PairValueRecord()
- pvr.SecondGlyph = glyph2
- pvr.Value1 = (
- ValueRecord(src=val1, valueFormat=valueFormat1)
- if valueFormat1
- else None
- )
- pvr.Value2 = (
- ValueRecord(src=val2, valueFormat=valueFormat2)
- if valueFormat2
- else None
- )
- ps.PairValueRecord.append(pvr)
- ps.PairValueCount = len(ps.PairValueRecord)
- self.PairSetCount = len(self.PairSet)
- return self
- def buildSinglePos(mapping, glyphMap):
- """Builds a list of single adjustment (GPOS1) subtables.
- This builds a list of SinglePos subtables from a dictionary of glyph
- names and their positioning adjustments. The format of the subtables are
- determined to optimize the size of the resulting subtables.
- See also :func:`buildSinglePosSubtable`.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
- Example::
- mapping = {
- "V": buildValue({ "xAdvance" : +5 }),
- # ...
- }
- subtables = buildSinglePos(pairs, font.getReverseGlyphMap())
- Args:
- mapping (dict): A mapping between glyphnames and
- ``otTables.ValueRecord`` objects.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A list of ``otTables.SinglePos`` objects.
- """
- result, handled = [], set()
- # In SinglePos format 1, the covered glyphs all share the same ValueRecord.
- # In format 2, each glyph has its own ValueRecord, but these records
- # all have the same properties (eg., all have an X but no Y placement).
- coverages, masks, values = {}, {}, {}
- for glyph, value in mapping.items():
- key = _getSinglePosValueKey(value)
- coverages.setdefault(key, []).append(glyph)
- masks.setdefault(key[0], []).append(key)
- values[key] = value
- # If a ValueRecord is shared between multiple glyphs, we generate
- # a SinglePos format 1 subtable; that is the most compact form.
- for key, glyphs in coverages.items():
- # 5 ushorts is the length of introducing another sublookup
- if len(glyphs) * _getSinglePosValueSize(key) > 5:
- format1Mapping = {g: values[key] for g in glyphs}
- result.append(buildSinglePosSubtable(format1Mapping, glyphMap))
- handled.add(key)
- # In the remaining ValueRecords, look for those whose valueFormat
- # (the set of used properties) is shared between multiple records.
- # These will get encoded in format 2.
- for valueFormat, keys in masks.items():
- f2 = [k for k in keys if k not in handled]
- if len(f2) > 1:
- format2Mapping = {}
- for k in f2:
- format2Mapping.update((g, values[k]) for g in coverages[k])
- result.append(buildSinglePosSubtable(format2Mapping, glyphMap))
- handled.update(f2)
- # The remaining ValueRecords are only used by a few glyphs, normally
- # one. We encode these in format 1 again.
- for key, glyphs in coverages.items():
- if key not in handled:
- for g in glyphs:
- st = buildSinglePosSubtable({g: values[key]}, glyphMap)
- result.append(st)
- # When the OpenType layout engine traverses the subtables, it will
- # stop after the first matching subtable. Therefore, we sort the
- # resulting subtables by decreasing coverage size; this increases
- # the chance that the layout engine can do an early exit. (Of course,
- # this would only be true if all glyphs were equally frequent, which
- # is not really the case; but we do not know their distribution).
- # If two subtables cover the same number of glyphs, we sort them
- # by glyph ID so that our output is deterministic.
- result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap))
- return result
- def buildSinglePosSubtable(values, glyphMap):
- """Builds a single adjustment (GPOS1) subtable.
- This builds a list of SinglePos subtables from a dictionary of glyph
- names and their positioning adjustments. The format of the subtable is
- determined to optimize the size of the output.
- See also :func:`buildSinglePos`.
- Note that if you are implementing a layout compiler, you may find it more
- flexible to use
- :py:class:`fontTools.otlLib.lookupBuilders.SinglePosBuilder` instead.
- Example::
- mapping = {
- "V": buildValue({ "xAdvance" : +5 }),
- # ...
- }
- subtable = buildSinglePos(pairs, font.getReverseGlyphMap())
- Args:
- mapping (dict): A mapping between glyphnames and
- ``otTables.ValueRecord`` objects.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A ``otTables.SinglePos`` object.
- """
- self = ot.SinglePos()
- self.Coverage = buildCoverage(values.keys(), glyphMap)
- valueFormat = self.ValueFormat = reduce(
- int.__or__, [v.getFormat() for v in values.values()], 0
- )
- valueRecords = [
- ValueRecord(src=values[g], valueFormat=valueFormat)
- for g in self.Coverage.glyphs
- ]
- if all(v == valueRecords[0] for v in valueRecords):
- self.Format = 1
- if self.ValueFormat != 0:
- self.Value = valueRecords[0]
- else:
- self.Value = None
- else:
- self.Format = 2
- self.Value = valueRecords
- self.ValueCount = len(self.Value)
- return self
- def _getSinglePosTableKey(subtable, glyphMap):
- assert isinstance(subtable, ot.SinglePos), subtable
- glyphs = subtable.Coverage.glyphs
- return (-len(glyphs), glyphMap[glyphs[0]])
- def _getSinglePosValueKey(valueRecord):
- # otBase.ValueRecord --> (2, ("YPlacement": 12))
- assert isinstance(valueRecord, ValueRecord), valueRecord
- valueFormat, result = 0, []
- for name, value in valueRecord.__dict__.items():
- if isinstance(value, ot.Device):
- result.append((name, _makeDeviceTuple(value)))
- else:
- result.append((name, value))
- valueFormat |= valueRecordFormatDict[name][0]
- result.sort()
- result.insert(0, valueFormat)
- return tuple(result)
- _DeviceTuple = namedtuple("_DeviceTuple", "DeltaFormat StartSize EndSize DeltaValue")
- def _makeDeviceTuple(device):
- # otTables.Device --> tuple, for making device tables unique
- return _DeviceTuple(
- device.DeltaFormat,
- device.StartSize,
- device.EndSize,
- () if device.DeltaFormat & 0x8000 else tuple(device.DeltaValue),
- )
- def _getSinglePosValueSize(valueKey):
- # Returns how many ushorts this valueKey (short form of ValueRecord) takes up
- count = 0
- for _, v in valueKey[1:]:
- if isinstance(v, _DeviceTuple):
- count += len(v.DeltaValue) + 3
- else:
- count += 1
- return count
- def buildValue(value):
- """Builds a positioning value record.
- Value records are used to specify coordinates and adjustments for
- positioning and attaching glyphs. Many of the positioning functions
- in this library take ``otTables.ValueRecord`` objects as arguments.
- This function builds value records from dictionaries.
- Args:
- value (dict): A dictionary with zero or more of the following keys:
- - ``xPlacement``
- - ``yPlacement``
- - ``xAdvance``
- - ``yAdvance``
- - ``xPlaDevice``
- - ``yPlaDevice``
- - ``xAdvDevice``
- - ``yAdvDevice``
- Returns:
- An ``otTables.ValueRecord`` object.
- """
- self = ValueRecord()
- for k, v in value.items():
- setattr(self, k, v)
- return self
- # GDEF
- def buildAttachList(attachPoints, glyphMap):
- """Builds an AttachList subtable.
- A GDEF table may contain an Attachment Point List table (AttachList)
- which stores the contour indices of attachment points for glyphs with
- attachment points. This routine builds AttachList subtables.
- Args:
- attachPoints (dict): A mapping between glyph names and a list of
- contour indices.
- Returns:
- An ``otTables.AttachList`` object if attachment points are supplied,
- or ``None`` otherwise.
- """
- if not attachPoints:
- return None
- self = ot.AttachList()
- self.Coverage = buildCoverage(attachPoints.keys(), glyphMap)
- self.AttachPoint = [buildAttachPoint(attachPoints[g]) for g in self.Coverage.glyphs]
- self.GlyphCount = len(self.AttachPoint)
- return self
- def buildAttachPoint(points):
- # [4, 23, 41] --> otTables.AttachPoint
- # Only used by above.
- if not points:
- return None
- self = ot.AttachPoint()
- self.PointIndex = sorted(set(points))
- self.PointCount = len(self.PointIndex)
- return self
- def buildCaretValueForCoord(coord):
- # 500 --> otTables.CaretValue, format 1
- # (500, DeviceTable) --> otTables.CaretValue, format 3
- self = ot.CaretValue()
- if isinstance(coord, tuple):
- self.Format = 3
- self.Coordinate, self.DeviceTable = coord
- else:
- self.Format = 1
- self.Coordinate = coord
- return self
- def buildCaretValueForPoint(point):
- # 4 --> otTables.CaretValue, format 2
- self = ot.CaretValue()
- self.Format = 2
- self.CaretValuePoint = point
- return self
- def buildLigCaretList(coords, points, glyphMap):
- """Builds a ligature caret list table.
- Ligatures appear as a single glyph representing multiple characters; however
- when, for example, editing text containing a ``f_i`` ligature, the user may
- want to place the cursor between the ``f`` and the ``i``. The ligature caret
- list in the GDEF table specifies the position to display the "caret" (the
- character insertion indicator, typically a flashing vertical bar) "inside"
- the ligature to represent an insertion point. The insertion positions may
- be specified either by coordinate or by contour point.
- Example::
- coords = {
- "f_f_i": [300, 600] # f|fi cursor at 300 units, ff|i cursor at 600.
- }
- points = {
- "c_t": [28] # c|t cursor appears at coordinate of contour point 28.
- }
- ligcaretlist = buildLigCaretList(coords, points, font.getReverseGlyphMap())
- Args:
- coords: A mapping between glyph names and a list of coordinates for
- the insertion point of each ligature component after the first one.
- points: A mapping between glyph names and a list of contour points for
- the insertion point of each ligature component after the first one.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns:
- A ``otTables.LigCaretList`` object if any carets are present, or
- ``None`` otherwise."""
- glyphs = set(coords.keys()) if coords else set()
- if points:
- glyphs.update(points.keys())
- carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs}
- carets = {g: c for g, c in carets.items() if c is not None}
- if not carets:
- return None
- self = ot.LigCaretList()
- self.Coverage = buildCoverage(carets.keys(), glyphMap)
- self.LigGlyph = [carets[g] for g in self.Coverage.glyphs]
- self.LigGlyphCount = len(self.LigGlyph)
- return self
- def buildLigGlyph(coords, points):
- # ([500], [4]) --> otTables.LigGlyph; None for empty coords/points
- carets = []
- if coords:
- coords = sorted(coords, key=lambda c: c[0] if isinstance(c, tuple) else c)
- carets.extend([buildCaretValueForCoord(c) for c in coords])
- if points:
- carets.extend([buildCaretValueForPoint(p) for p in sorted(points)])
- if not carets:
- return None
- self = ot.LigGlyph()
- self.CaretValue = carets
- self.CaretCount = len(self.CaretValue)
- return self
- def buildMarkGlyphSetsDef(markSets, glyphMap):
- """Builds a mark glyph sets definition table.
- OpenType Layout lookups may choose to use mark filtering sets to consider
- or ignore particular combinations of marks. These sets are specified by
- setting a flag on the lookup, but the mark filtering sets are defined in
- the ``GDEF`` table. This routine builds the subtable containing the mark
- glyph set definitions.
- Example::
- set0 = set("acute", "grave")
- set1 = set("caron", "grave")
- markglyphsets = buildMarkGlyphSetsDef([set0, set1], font.getReverseGlyphMap())
- Args:
- markSets: A list of sets of glyphnames.
- glyphMap: a glyph name to ID map, typically returned from
- ``font.getReverseGlyphMap()``.
- Returns
- An ``otTables.MarkGlyphSetsDef`` object.
- """
- if not markSets:
- return None
- self = ot.MarkGlyphSetsDef()
- self.MarkSetTableFormat = 1
- self.Coverage = [buildCoverage(m, glyphMap) for m in markSets]
- self.MarkSetCount = len(self.Coverage)
- return self
- class ClassDefBuilder(object):
- """Helper for building ClassDef tables."""
- def __init__(self, useClass0):
- self.classes_ = set()
- self.glyphs_ = {}
- self.useClass0_ = useClass0
- def canAdd(self, glyphs):
- if isinstance(glyphs, (set, frozenset)):
- glyphs = sorted(glyphs)
- glyphs = tuple(glyphs)
- if glyphs in self.classes_:
- return True
- for glyph in glyphs:
- if glyph in self.glyphs_:
- return False
- return True
- def add(self, glyphs):
- if isinstance(glyphs, (set, frozenset)):
- glyphs = sorted(glyphs)
- glyphs = tuple(glyphs)
- if glyphs in self.classes_:
- return
- self.classes_.add(glyphs)
- for glyph in glyphs:
- if glyph in self.glyphs_:
- raise OpenTypeLibError(
- f"Glyph {glyph} is already present in class.", None
- )
- self.glyphs_[glyph] = glyphs
- def classes(self):
- # In ClassDef1 tables, class id #0 does not need to be encoded
- # because zero is the default. Therefore, we use id #0 for the
- # glyph class that has the largest number of members. However,
- # in other tables than ClassDef1, 0 means "every other glyph"
- # so we should not use that ID for any real glyph classes;
- # we implement this by inserting an empty set at position 0.
- #
- # TODO: Instead of counting the number of glyphs in each class,
- # we should determine the encoded size. If the glyphs in a large
- # class form a contiguous range, the encoding is actually quite
- # compact, whereas a non-contiguous set might need a lot of bytes
- # in the output file. We don't get this right with the key below.
- result = sorted(self.classes_, key=lambda s: (-len(s), s))
- if not self.useClass0_:
- result.insert(0, frozenset())
- return result
- def build(self):
- glyphClasses = {}
- for classID, glyphs in enumerate(self.classes()):
- if classID == 0:
- continue
- for glyph in glyphs:
- glyphClasses[glyph] = classID
- classDef = ot.ClassDef()
- classDef.classDefs = glyphClasses
- return classDef
- AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
- AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
- def buildStatTable(
- ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True
- ):
- """Add a 'STAT' table to 'ttFont'.
- 'axes' is a list of dictionaries describing axes and their
- values.
- Example::
- axes = [
- dict(
- tag="wght",
- name="Weight",
- ordering=0, # optional
- values=[
- dict(value=100, name='Thin'),
- dict(value=300, name='Light'),
- dict(value=400, name='Regular', flags=0x2),
- dict(value=900, name='Black'),
- ],
- )
- ]
- Each axis dict must have 'tag' and 'name' items. 'tag' maps
- to the 'AxisTag' field. 'name' can be a name ID (int), a string,
- or a dictionary containing multilingual names (see the
- addMultilingualName() name table method), and will translate to
- the AxisNameID field.
- An axis dict may contain an 'ordering' item that maps to the
- AxisOrdering field. If omitted, the order of the axes list is
- used to calculate AxisOrdering fields.
- The axis dict may contain a 'values' item, which is a list of
- dictionaries describing AxisValue records belonging to this axis.
- Each value dict must have a 'name' item, which can be a name ID
- (int), a string, or a dictionary containing multilingual names,
- like the axis name. It translates to the ValueNameID field.
- Optionally the value dict can contain a 'flags' item. It maps to
- the AxisValue Flags field, and will be 0 when omitted.
- The format of the AxisValue is determined by the remaining contents
- of the value dictionary:
- If the value dict contains a 'value' item, an AxisValue record
- Format 1 is created. If in addition to the 'value' item it contains
- a 'linkedValue' item, an AxisValue record Format 3 is built.
- If the value dict contains a 'nominalValue' item, an AxisValue
- record Format 2 is built. Optionally it may contain 'rangeMinValue'
- and 'rangeMaxValue' items. These map to -Infinity and +Infinity
- respectively if omitted.
- You cannot specify Format 4 AxisValue tables this way, as they are
- not tied to a single axis, and specify a name for a location that
- is defined by multiple axes values. Instead, you need to supply the
- 'locations' argument.
- The optional 'locations' argument specifies AxisValue Format 4
- tables. It should be a list of dicts, where each dict has a 'name'
- item, which works just like the value dicts above, an optional
- 'flags' item (defaulting to 0x0), and a 'location' dict. A
- location dict key is an axis tag, and the associated value is the
- location on the specified axis. They map to the AxisIndex and Value
- fields of the AxisValueRecord.
- Example::
- locations = [
- dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)),
- dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)),
- ]
- The optional 'elidedFallbackName' argument can be a name ID (int),
- a string, a dictionary containing multilingual names, or a list of
- STATNameStatements. It translates to the ElidedFallbackNameID field.
- The 'ttFont' argument must be a TTFont instance that already has a
- 'name' table. If a 'STAT' table already exists, it will be
- overwritten by the newly created one.
- """
- ttFont["STAT"] = ttLib.newTable("STAT")
- statTable = ttFont["STAT"].table = ot.STAT()
- statTable.ElidedFallbackNameID = _addName(
- ttFont, elidedFallbackName, windows=windowsNames, mac=macNames
- )
- # 'locations' contains data for AxisValue Format 4
- axisRecords, axisValues = _buildAxisRecords(
- axes, ttFont, windowsNames=windowsNames, macNames=macNames
- )
- if not locations:
- statTable.Version = 0x00010001
- else:
- # We'll be adding Format 4 AxisValue records, which
- # requires a higher table version
- statTable.Version = 0x00010002
- multiAxisValues = _buildAxisValuesFormat4(
- locations, axes, ttFont, windowsNames=windowsNames, macNames=macNames
- )
- axisValues = multiAxisValues + axisValues
- ttFont["name"].names.sort()
- # Store AxisRecords
- axisRecordArray = ot.AxisRecordArray()
- axisRecordArray.Axis = axisRecords
- # XXX these should not be hard-coded but computed automatically
- statTable.DesignAxisRecordSize = 8
- statTable.DesignAxisRecord = axisRecordArray
- statTable.DesignAxisCount = len(axisRecords)
- statTable.AxisValueCount = 0
- statTable.AxisValueArray = None
- if axisValues:
- # Store AxisValueRecords
- axisValueArray = ot.AxisValueArray()
- axisValueArray.AxisValue = axisValues
- statTable.AxisValueArray = axisValueArray
- statTable.AxisValueCount = len(axisValues)
- def _buildAxisRecords(axes, ttFont, windowsNames=True, macNames=True):
- axisRecords = []
- axisValues = []
- for axisRecordIndex, axisDict in enumerate(axes):
- axis = ot.AxisRecord()
- axis.AxisTag = axisDict["tag"]
- axis.AxisNameID = _addName(
- ttFont, axisDict["name"], 256, windows=windowsNames, mac=macNames
- )
- axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex)
- axisRecords.append(axis)
- for axisVal in axisDict.get("values", ()):
- axisValRec = ot.AxisValue()
- axisValRec.AxisIndex = axisRecordIndex
- axisValRec.Flags = axisVal.get("flags", 0)
- axisValRec.ValueNameID = _addName(
- ttFont, axisVal["name"], windows=windowsNames, mac=macNames
- )
- if "value" in axisVal:
- axisValRec.Value = axisVal["value"]
- if "linkedValue" in axisVal:
- axisValRec.Format = 3
- axisValRec.LinkedValue = axisVal["linkedValue"]
- else:
- axisValRec.Format = 1
- elif "nominalValue" in axisVal:
- axisValRec.Format = 2
- axisValRec.NominalValue = axisVal["nominalValue"]
- axisValRec.RangeMinValue = axisVal.get(
- "rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY
- )
- axisValRec.RangeMaxValue = axisVal.get(
- "rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY
- )
- else:
- raise ValueError("Can't determine format for AxisValue")
- axisValues.append(axisValRec)
- return axisRecords, axisValues
- def _buildAxisValuesFormat4(locations, axes, ttFont, windowsNames=True, macNames=True):
- axisTagToIndex = {}
- for axisRecordIndex, axisDict in enumerate(axes):
- axisTagToIndex[axisDict["tag"]] = axisRecordIndex
- axisValues = []
- for axisLocationDict in locations:
- axisValRec = ot.AxisValue()
- axisValRec.Format = 4
- axisValRec.ValueNameID = _addName(
- ttFont, axisLocationDict["name"], windows=windowsNames, mac=macNames
- )
- axisValRec.Flags = axisLocationDict.get("flags", 0)
- axisValueRecords = []
- for tag, value in axisLocationDict["location"].items():
- avr = ot.AxisValueRecord()
- avr.AxisIndex = axisTagToIndex[tag]
- avr.Value = value
- axisValueRecords.append(avr)
- axisValueRecords.sort(key=lambda avr: avr.AxisIndex)
- axisValRec.AxisCount = len(axisValueRecords)
- axisValRec.AxisValueRecord = axisValueRecords
- axisValues.append(axisValRec)
- return axisValues
- def _addName(ttFont, value, minNameID=0, windows=True, mac=True):
- nameTable = ttFont["name"]
- if isinstance(value, int):
- # Already a nameID
- return value
- if isinstance(value, str):
- names = dict(en=value)
- elif isinstance(value, dict):
- names = value
- elif isinstance(value, list):
- nameID = nameTable._findUnusedNameID()
- for nameRecord in value:
- if isinstance(nameRecord, STATNameStatement):
- nameTable.setName(
- nameRecord.string,
- nameID,
- nameRecord.platformID,
- nameRecord.platEncID,
- nameRecord.langID,
- )
- else:
- raise TypeError("value must be a list of STATNameStatements")
- return nameID
- else:
- raise TypeError("value must be int, str, dict or list")
- return nameTable.addMultilingualName(
- names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID
- )
|