12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712 |
- from fontTools.misc import sstruct
- from fontTools.misc.textTools import Tag, tostr, binary2num, safeEval
- from fontTools.feaLib.error import FeatureLibError
- from fontTools.feaLib.lookupDebugInfo import (
- LookupDebugInfo,
- LOOKUP_DEBUG_INFO_KEY,
- LOOKUP_DEBUG_ENV_VAR,
- )
- from fontTools.feaLib.parser import Parser
- from fontTools.feaLib.ast import FeatureFile
- from fontTools.feaLib.variableScalar import VariableScalar
- from fontTools.otlLib import builder as otl
- from fontTools.otlLib.maxContextCalc import maxCtxFont
- from fontTools.ttLib import newTable, getTableModule
- from fontTools.ttLib.tables import otBase, otTables
- from fontTools.otlLib.builder import (
- AlternateSubstBuilder,
- ChainContextPosBuilder,
- ChainContextSubstBuilder,
- LigatureSubstBuilder,
- MultipleSubstBuilder,
- CursivePosBuilder,
- MarkBasePosBuilder,
- MarkLigPosBuilder,
- MarkMarkPosBuilder,
- ReverseChainSingleSubstBuilder,
- SingleSubstBuilder,
- ClassPairPosSubtableBuilder,
- PairPosBuilder,
- SinglePosBuilder,
- ChainContextualRule,
- )
- from fontTools.otlLib.error import OpenTypeLibError
- from fontTools.varLib.varStore import OnlineVarStoreBuilder
- from fontTools.varLib.builder import buildVarDevTable
- from fontTools.varLib.featureVars import addFeatureVariationsRaw
- from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
- from collections import defaultdict
- import copy
- import itertools
- from io import StringIO
- import logging
- import warnings
- import os
- log = logging.getLogger(__name__)
- def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
- """Add features from a file to a font. Note that this replaces any features
- currently present.
- Args:
- font (feaLib.ttLib.TTFont): The font object.
- featurefile: Either a path or file object (in which case we
- parse it into an AST), or a pre-parsed AST instance.
- tables: If passed, restrict the set of affected tables to those in the
- list.
- debug: Whether to add source debugging information to the font in the
- ``Debg`` table
- """
- builder = Builder(font, featurefile)
- builder.build(tables=tables, debug=debug)
- def addOpenTypeFeaturesFromString(
- font, features, filename=None, tables=None, debug=False
- ):
- """Add features from a string to a font. Note that this replaces any
- features currently present.
- Args:
- font (feaLib.ttLib.TTFont): The font object.
- features: A string containing feature code.
- filename: The directory containing ``filename`` is used as the root of
- relative ``include()`` paths; if ``None`` is provided, the current
- directory is assumed.
- tables: If passed, restrict the set of affected tables to those in the
- list.
- debug: Whether to add source debugging information to the font in the
- ``Debg`` table
- """
- featurefile = StringIO(tostr(features))
- if filename:
- featurefile.name = filename
- addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
- class Builder(object):
- supportedTables = frozenset(
- Tag(tag)
- for tag in [
- "BASE",
- "GDEF",
- "GPOS",
- "GSUB",
- "OS/2",
- "head",
- "hhea",
- "name",
- "vhea",
- "STAT",
- ]
- )
- def __init__(self, font, featurefile):
- self.font = font
- # 'featurefile' can be either a path or file object (in which case we
- # parse it into an AST), or a pre-parsed AST instance
- if isinstance(featurefile, FeatureFile):
- self.parseTree, self.file = featurefile, None
- else:
- self.parseTree, self.file = None, featurefile
- self.glyphMap = font.getReverseGlyphMap()
- self.varstorebuilder = None
- if "fvar" in font:
- self.axes = font["fvar"].axes
- self.varstorebuilder = OnlineVarStoreBuilder(
- [ax.axisTag for ax in self.axes]
- )
- self.default_language_systems_ = set()
- self.script_ = None
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- self.language_systems = set()
- self.seen_non_DFLT_script_ = False
- self.named_lookups_ = {}
- self.cur_lookup_ = None
- self.cur_lookup_name_ = None
- self.cur_feature_name_ = None
- self.lookups_ = []
- self.lookup_locations = {"GSUB": {}, "GPOS": {}}
- self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
- self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
- self.feature_variations_ = {}
- # for feature 'aalt'
- self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
- self.aalt_location_ = None
- self.aalt_alternates_ = {}
- # for 'featureNames'
- self.featureNames_ = set()
- self.featureNames_ids_ = {}
- # for 'cvParameters'
- self.cv_parameters_ = set()
- self.cv_parameters_ids_ = {}
- self.cv_num_named_params_ = {}
- self.cv_characters_ = defaultdict(list)
- # for feature 'size'
- self.size_parameters_ = None
- # for table 'head'
- self.fontRevision_ = None # 2.71
- # for table 'name'
- self.names_ = []
- # for table 'BASE'
- self.base_horiz_axis_ = None
- self.base_vert_axis_ = None
- # for table 'GDEF'
- self.attachPoints_ = {} # "a" --> {3, 7}
- self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600}
- self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7}
- self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column))
- self.markAttach_ = {} # "acute" --> (4, (file, line, column))
- self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4
- self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4
- # for table 'OS/2'
- self.os2_ = {}
- # for table 'hhea'
- self.hhea_ = {}
- # for table 'vhea'
- self.vhea_ = {}
- # for table 'STAT'
- self.stat_ = {}
- # for conditionsets
- self.conditionsets_ = {}
- # We will often use exactly the same locations (i.e. the font's masters)
- # for a large number of variable scalars. Instead of creating a model
- # for each, let's share the models.
- self.model_cache = {}
- def build(self, tables=None, debug=False):
- if self.parseTree is None:
- self.parseTree = Parser(self.file, self.glyphMap).parse()
- self.parseTree.build(self)
- # by default, build all the supported tables
- if tables is None:
- tables = self.supportedTables
- else:
- tables = frozenset(tables)
- unsupported = tables - self.supportedTables
- if unsupported:
- unsupported_string = ", ".join(sorted(unsupported))
- raise NotImplementedError(
- "The following tables were requested but are unsupported: "
- f"{unsupported_string}."
- )
- if "GSUB" in tables:
- self.build_feature_aalt_()
- if "head" in tables:
- self.build_head()
- if "hhea" in tables:
- self.build_hhea()
- if "vhea" in tables:
- self.build_vhea()
- if "name" in tables:
- self.build_name()
- if "OS/2" in tables:
- self.build_OS_2()
- if "STAT" in tables:
- self.build_STAT()
- for tag in ("GPOS", "GSUB"):
- if tag not in tables:
- continue
- table = self.makeTable(tag)
- if self.feature_variations_:
- self.makeFeatureVariations(table, tag)
- if (
- table.ScriptList.ScriptCount > 0
- or table.FeatureList.FeatureCount > 0
- or table.LookupList.LookupCount > 0
- ):
- fontTable = self.font[tag] = newTable(tag)
- fontTable.table = table
- elif tag in self.font:
- del self.font[tag]
- if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
- self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
- if "GDEF" in tables:
- gdef = self.buildGDEF()
- if gdef:
- self.font["GDEF"] = gdef
- elif "GDEF" in self.font:
- del self.font["GDEF"]
- if "BASE" in tables:
- base = self.buildBASE()
- if base:
- self.font["BASE"] = base
- elif "BASE" in self.font:
- del self.font["BASE"]
- if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
- self.buildDebg()
- def get_chained_lookup_(self, location, builder_class):
- result = builder_class(self.font, location)
- result.lookupflag = self.lookupflag_
- result.markFilterSet = self.lookupflag_markFilterSet_
- self.lookups_.append(result)
- return result
- def add_lookup_to_feature_(self, lookup, feature_name):
- for script, lang in self.language_systems:
- key = (script, lang, feature_name)
- self.features_.setdefault(key, []).append(lookup)
- def get_lookup_(self, location, builder_class):
- if (
- self.cur_lookup_
- and type(self.cur_lookup_) == builder_class
- and self.cur_lookup_.lookupflag == self.lookupflag_
- and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
- ):
- return self.cur_lookup_
- if self.cur_lookup_name_ and self.cur_lookup_:
- raise FeatureLibError(
- "Within a named lookup block, all rules must be of "
- "the same lookup type and flag",
- location,
- )
- self.cur_lookup_ = builder_class(self.font, location)
- self.cur_lookup_.lookupflag = self.lookupflag_
- self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
- self.lookups_.append(self.cur_lookup_)
- if self.cur_lookup_name_:
- # We are starting a lookup rule inside a named lookup block.
- self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
- if self.cur_feature_name_:
- # We are starting a lookup rule inside a feature. This includes
- # lookup rules inside named lookups inside features.
- self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
- return self.cur_lookup_
- def build_feature_aalt_(self):
- if not self.aalt_features_ and not self.aalt_alternates_:
- return
- alternates = {g: set(a) for g, a in self.aalt_alternates_.items()}
- for location, name in self.aalt_features_ + [(None, "aalt")]:
- feature = [
- (script, lang, feature, lookups)
- for (script, lang, feature), lookups in self.features_.items()
- if feature == name
- ]
- # "aalt" does not have to specify its own lookups, but it might.
- if not feature and name != "aalt":
- warnings.warn("%s: Feature %s has not been defined" % (location, name))
- continue
- for script, lang, feature, lookups in feature:
- for lookuplist in lookups:
- if not isinstance(lookuplist, list):
- lookuplist = [lookuplist]
- for lookup in lookuplist:
- for glyph, alts in lookup.getAlternateGlyphs().items():
- alternates.setdefault(glyph, set()).update(alts)
- single = {
- glyph: list(repl)[0] for glyph, repl in alternates.items() if len(repl) == 1
- }
- # TODO: Figure out the glyph alternate ordering used by makeotf.
- # https://github.com/fonttools/fonttools/issues/836
- multi = {
- glyph: sorted(repl, key=self.font.getGlyphID)
- for glyph, repl in alternates.items()
- if len(repl) > 1
- }
- if not single and not multi:
- return
- self.features_ = {
- (script, lang, feature): lookups
- for (script, lang, feature), lookups in self.features_.items()
- if feature != "aalt"
- }
- old_lookups = self.lookups_
- self.lookups_ = []
- self.start_feature(self.aalt_location_, "aalt")
- if single:
- single_lookup = self.get_lookup_(location, SingleSubstBuilder)
- single_lookup.mapping = single
- if multi:
- multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
- multi_lookup.alternates = multi
- self.end_feature()
- self.lookups_.extend(old_lookups)
- def build_head(self):
- if not self.fontRevision_:
- return
- table = self.font.get("head")
- if not table: # this only happens for unit tests
- table = self.font["head"] = newTable("head")
- table.decompile(b"\0" * 54, self.font)
- table.tableVersion = 1.0
- table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
- table.fontRevision = self.fontRevision_
- def build_hhea(self):
- if not self.hhea_:
- return
- table = self.font.get("hhea")
- if not table: # this only happens for unit tests
- table = self.font["hhea"] = newTable("hhea")
- table.decompile(b"\0" * 36, self.font)
- table.tableVersion = 0x00010000
- if "caretoffset" in self.hhea_:
- table.caretOffset = self.hhea_["caretoffset"]
- if "ascender" in self.hhea_:
- table.ascent = self.hhea_["ascender"]
- if "descender" in self.hhea_:
- table.descent = self.hhea_["descender"]
- if "linegap" in self.hhea_:
- table.lineGap = self.hhea_["linegap"]
- def build_vhea(self):
- if not self.vhea_:
- return
- table = self.font.get("vhea")
- if not table: # this only happens for unit tests
- table = self.font["vhea"] = newTable("vhea")
- table.decompile(b"\0" * 36, self.font)
- table.tableVersion = 0x00011000
- if "verttypoascender" in self.vhea_:
- table.ascent = self.vhea_["verttypoascender"]
- if "verttypodescender" in self.vhea_:
- table.descent = self.vhea_["verttypodescender"]
- if "verttypolinegap" in self.vhea_:
- table.lineGap = self.vhea_["verttypolinegap"]
- def get_user_name_id(self, table):
- # Try to find first unused font-specific name id
- nameIDs = [name.nameID for name in table.names]
- for user_name_id in range(256, 32767):
- if user_name_id not in nameIDs:
- return user_name_id
- def buildFeatureParams(self, tag):
- params = None
- if tag == "size":
- params = otTables.FeatureParamsSize()
- (
- params.DesignSize,
- params.SubfamilyID,
- params.RangeStart,
- params.RangeEnd,
- ) = self.size_parameters_
- if tag in self.featureNames_ids_:
- params.SubfamilyNameID = self.featureNames_ids_[tag]
- else:
- params.SubfamilyNameID = 0
- elif tag in self.featureNames_:
- if not self.featureNames_ids_:
- # name table wasn't selected among the tables to build; skip
- pass
- else:
- assert tag in self.featureNames_ids_
- params = otTables.FeatureParamsStylisticSet()
- params.Version = 0
- params.UINameID = self.featureNames_ids_[tag]
- elif tag in self.cv_parameters_:
- params = otTables.FeatureParamsCharacterVariants()
- params.Format = 0
- params.FeatUILabelNameID = self.cv_parameters_ids_.get(
- (tag, "FeatUILabelNameID"), 0
- )
- params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
- (tag, "FeatUITooltipTextNameID"), 0
- )
- params.SampleTextNameID = self.cv_parameters_ids_.get(
- (tag, "SampleTextNameID"), 0
- )
- params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
- params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
- (tag, "ParamUILabelNameID_0"), 0
- )
- params.CharCount = len(self.cv_characters_[tag])
- params.Character = self.cv_characters_[tag]
- return params
- def build_name(self):
- if not self.names_:
- return
- table = self.font.get("name")
- if not table: # this only happens for unit tests
- table = self.font["name"] = newTable("name")
- table.names = []
- for name in self.names_:
- nameID, platformID, platEncID, langID, string = name
- # For featureNames block, nameID is 'feature tag'
- # For cvParameters blocks, nameID is ('feature tag', 'block name')
- if not isinstance(nameID, int):
- tag = nameID
- if tag in self.featureNames_:
- if tag not in self.featureNames_ids_:
- self.featureNames_ids_[tag] = self.get_user_name_id(table)
- assert self.featureNames_ids_[tag] is not None
- nameID = self.featureNames_ids_[tag]
- elif tag[0] in self.cv_parameters_:
- if tag not in self.cv_parameters_ids_:
- self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
- assert self.cv_parameters_ids_[tag] is not None
- nameID = self.cv_parameters_ids_[tag]
- table.setName(string, nameID, platformID, platEncID, langID)
- table.names.sort()
- def build_OS_2(self):
- if not self.os2_:
- return
- table = self.font.get("OS/2")
- if not table: # this only happens for unit tests
- table = self.font["OS/2"] = newTable("OS/2")
- data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
- table.decompile(data, self.font)
- version = 0
- if "fstype" in self.os2_:
- table.fsType = self.os2_["fstype"]
- if "panose" in self.os2_:
- panose = getTableModule("OS/2").Panose()
- (
- panose.bFamilyType,
- panose.bSerifStyle,
- panose.bWeight,
- panose.bProportion,
- panose.bContrast,
- panose.bStrokeVariation,
- panose.bArmStyle,
- panose.bLetterForm,
- panose.bMidline,
- panose.bXHeight,
- ) = self.os2_["panose"]
- table.panose = panose
- if "typoascender" in self.os2_:
- table.sTypoAscender = self.os2_["typoascender"]
- if "typodescender" in self.os2_:
- table.sTypoDescender = self.os2_["typodescender"]
- if "typolinegap" in self.os2_:
- table.sTypoLineGap = self.os2_["typolinegap"]
- if "winascent" in self.os2_:
- table.usWinAscent = self.os2_["winascent"]
- if "windescent" in self.os2_:
- table.usWinDescent = self.os2_["windescent"]
- if "vendor" in self.os2_:
- table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
- if "weightclass" in self.os2_:
- table.usWeightClass = self.os2_["weightclass"]
- if "widthclass" in self.os2_:
- table.usWidthClass = self.os2_["widthclass"]
- if "unicoderange" in self.os2_:
- table.setUnicodeRanges(self.os2_["unicoderange"])
- if "codepagerange" in self.os2_:
- pages = self.build_codepages_(self.os2_["codepagerange"])
- table.ulCodePageRange1, table.ulCodePageRange2 = pages
- version = 1
- if "xheight" in self.os2_:
- table.sxHeight = self.os2_["xheight"]
- version = 2
- if "capheight" in self.os2_:
- table.sCapHeight = self.os2_["capheight"]
- version = 2
- if "loweropsize" in self.os2_:
- table.usLowerOpticalPointSize = self.os2_["loweropsize"]
- version = 5
- if "upperopsize" in self.os2_:
- table.usUpperOpticalPointSize = self.os2_["upperopsize"]
- version = 5
- def checkattr(table, attrs):
- for attr in attrs:
- if not hasattr(table, attr):
- setattr(table, attr, 0)
- table.version = max(version, table.version)
- # this only happens for unit tests
- if version >= 1:
- checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
- if version >= 2:
- checkattr(
- table,
- (
- "sxHeight",
- "sCapHeight",
- "usDefaultChar",
- "usBreakChar",
- "usMaxContext",
- ),
- )
- if version >= 5:
- checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
- def setElidedFallbackName(self, value, location):
- # ElidedFallbackName is a convenience method for setting
- # ElidedFallbackNameID so only one can be allowed
- for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
- if token in self.stat_:
- raise FeatureLibError(
- f"{token} is already set.",
- location,
- )
- if isinstance(value, int):
- self.stat_["ElidedFallbackNameID"] = value
- elif isinstance(value, list):
- self.stat_["ElidedFallbackName"] = value
- else:
- raise AssertionError(value)
- def addDesignAxis(self, designAxis, location):
- if "DesignAxes" not in self.stat_:
- self.stat_["DesignAxes"] = []
- if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
- raise FeatureLibError(
- f'DesignAxis already defined for tag "{designAxis.tag}".',
- location,
- )
- if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
- raise FeatureLibError(
- f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
- location,
- )
- self.stat_["DesignAxes"].append(designAxis)
- def addAxisValueRecord(self, axisValueRecord, location):
- if "AxisValueRecords" not in self.stat_:
- self.stat_["AxisValueRecords"] = []
- # Check for duplicate AxisValueRecords
- for record_ in self.stat_["AxisValueRecords"]:
- if (
- {n.asFea() for n in record_.names}
- == {n.asFea() for n in axisValueRecord.names}
- and {n.asFea() for n in record_.locations}
- == {n.asFea() for n in axisValueRecord.locations}
- and record_.flags == axisValueRecord.flags
- ):
- raise FeatureLibError(
- "An AxisValueRecord with these values is already defined.",
- location,
- )
- self.stat_["AxisValueRecords"].append(axisValueRecord)
- def build_STAT(self):
- if not self.stat_:
- return
- axes = self.stat_.get("DesignAxes")
- if not axes:
- raise FeatureLibError("DesignAxes not defined", None)
- axisValueRecords = self.stat_.get("AxisValueRecords")
- axisValues = {}
- format4_locations = []
- for tag in axes:
- axisValues[tag.tag] = []
- if axisValueRecords is not None:
- for avr in axisValueRecords:
- valuesDict = {}
- if avr.flags > 0:
- valuesDict["flags"] = avr.flags
- if len(avr.locations) == 1:
- location = avr.locations[0]
- values = location.values
- if len(values) == 1: # format1
- valuesDict.update({"value": values[0], "name": avr.names})
- if len(values) == 2: # format3
- valuesDict.update(
- {
- "value": values[0],
- "linkedValue": values[1],
- "name": avr.names,
- }
- )
- if len(values) == 3: # format2
- nominal, minVal, maxVal = values
- valuesDict.update(
- {
- "nominalValue": nominal,
- "rangeMinValue": minVal,
- "rangeMaxValue": maxVal,
- "name": avr.names,
- }
- )
- axisValues[location.tag].append(valuesDict)
- else:
- valuesDict.update(
- {
- "location": {i.tag: i.values[0] for i in avr.locations},
- "name": avr.names,
- }
- )
- format4_locations.append(valuesDict)
- designAxes = [
- {
- "ordering": a.axisOrder,
- "tag": a.tag,
- "name": a.names,
- "values": axisValues[a.tag],
- }
- for a in axes
- ]
- nameTable = self.font.get("name")
- if not nameTable: # this only happens for unit tests
- nameTable = self.font["name"] = newTable("name")
- nameTable.names = []
- if "ElidedFallbackNameID" in self.stat_:
- nameID = self.stat_["ElidedFallbackNameID"]
- name = nameTable.getDebugName(nameID)
- if not name:
- raise FeatureLibError(
- f"ElidedFallbackNameID {nameID} points "
- "to a nameID that does not exist in the "
- '"name" table',
- None,
- )
- elif "ElidedFallbackName" in self.stat_:
- nameID = self.stat_["ElidedFallbackName"]
- otl.buildStatTable(
- self.font,
- designAxes,
- locations=format4_locations,
- elidedFallbackName=nameID,
- )
- def build_codepages_(self, pages):
- pages2bits = {
- 1252: 0,
- 1250: 1,
- 1251: 2,
- 1253: 3,
- 1254: 4,
- 1255: 5,
- 1256: 6,
- 1257: 7,
- 1258: 8,
- 874: 16,
- 932: 17,
- 936: 18,
- 949: 19,
- 950: 20,
- 1361: 21,
- 869: 48,
- 866: 49,
- 865: 50,
- 864: 51,
- 863: 52,
- 862: 53,
- 861: 54,
- 860: 55,
- 857: 56,
- 855: 57,
- 852: 58,
- 775: 59,
- 737: 60,
- 708: 61,
- 850: 62,
- 437: 63,
- }
- bits = [pages2bits[p] for p in pages if p in pages2bits]
- pages = []
- for i in range(2):
- pages.append("")
- for j in range(i * 32, (i + 1) * 32):
- if j in bits:
- pages[i] += "1"
- else:
- pages[i] += "0"
- return [binary2num(p[::-1]) for p in pages]
- def buildBASE(self):
- if not self.base_horiz_axis_ and not self.base_vert_axis_:
- return None
- base = otTables.BASE()
- base.Version = 0x00010000
- base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
- base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
- result = newTable("BASE")
- result.table = base
- return result
- def buildBASEAxis(self, axis):
- if not axis:
- return
- bases, scripts = axis
- axis = otTables.Axis()
- axis.BaseTagList = otTables.BaseTagList()
- axis.BaseTagList.BaselineTag = bases
- axis.BaseTagList.BaseTagCount = len(bases)
- axis.BaseScriptList = otTables.BaseScriptList()
- axis.BaseScriptList.BaseScriptRecord = []
- axis.BaseScriptList.BaseScriptCount = len(scripts)
- for script in sorted(scripts):
- record = otTables.BaseScriptRecord()
- record.BaseScriptTag = script[0]
- record.BaseScript = otTables.BaseScript()
- record.BaseScript.BaseLangSysCount = 0
- record.BaseScript.BaseValues = otTables.BaseValues()
- record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
- record.BaseScript.BaseValues.BaseCoord = []
- record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
- for c in script[2]:
- coord = otTables.BaseCoord()
- coord.Format = 1
- coord.Coordinate = c
- record.BaseScript.BaseValues.BaseCoord.append(coord)
- axis.BaseScriptList.BaseScriptRecord.append(record)
- return axis
- def buildGDEF(self):
- gdef = otTables.GDEF()
- gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
- gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
- gdef.LigCaretList = otl.buildLigCaretList(
- self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
- )
- gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
- gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
- gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
- if self.varstorebuilder:
- store = self.varstorebuilder.finish()
- if store:
- gdef.Version = 0x00010003
- gdef.VarStore = store
- varidx_map = store.optimize()
- gdef.remap_device_varidxes(varidx_map)
- if "GPOS" in self.font:
- self.font["GPOS"].table.remap_device_varidxes(varidx_map)
- self.model_cache.clear()
- if any(
- (
- gdef.GlyphClassDef,
- gdef.AttachList,
- gdef.LigCaretList,
- gdef.MarkAttachClassDef,
- gdef.MarkGlyphSetsDef,
- )
- ) or hasattr(gdef, "VarStore"):
- result = newTable("GDEF")
- result.table = gdef
- return result
- else:
- return None
- def buildGDEFGlyphClassDef_(self):
- if self.glyphClassDefs_:
- classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
- else:
- classes = {}
- for lookup in self.lookups_:
- classes.update(lookup.inferGlyphClasses())
- for markClass in self.parseTree.markClasses.values():
- for markClassDef in markClass.definitions:
- for glyph in markClassDef.glyphSet():
- classes[glyph] = 3
- if classes:
- result = otTables.GlyphClassDef()
- result.classDefs = classes
- return result
- else:
- return None
- def buildGDEFMarkAttachClassDef_(self):
- classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
- if not classDefs:
- return None
- result = otTables.MarkAttachClassDef()
- result.classDefs = classDefs
- return result
- def buildGDEFMarkGlyphSetsDef_(self):
- sets = []
- for glyphs, id_ in sorted(
- self.markFilterSets_.items(), key=lambda item: item[1]
- ):
- sets.append(glyphs)
- return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
- def buildDebg(self):
- if "Debg" not in self.font:
- self.font["Debg"] = newTable("Debg")
- self.font["Debg"].data = {}
- self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
- def buildLookups_(self, tag):
- assert tag in ("GPOS", "GSUB"), tag
- for lookup in self.lookups_:
- lookup.lookup_index = None
- lookups = []
- for lookup in self.lookups_:
- if lookup.table != tag:
- continue
- lookup.lookup_index = len(lookups)
- self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
- location=str(lookup.location),
- name=self.get_lookup_name_(lookup),
- feature=None,
- )
- lookups.append(lookup)
- otLookups = []
- for l in lookups:
- try:
- otLookups.append(l.build())
- except OpenTypeLibError as e:
- raise FeatureLibError(str(e), e.location) from e
- except Exception as e:
- location = self.lookup_locations[tag][str(l.lookup_index)].location
- raise FeatureLibError(str(e), location) from e
- return otLookups
- def makeTable(self, tag):
- table = getattr(otTables, tag, None)()
- table.Version = 0x00010000
- table.ScriptList = otTables.ScriptList()
- table.ScriptList.ScriptRecord = []
- table.FeatureList = otTables.FeatureList()
- table.FeatureList.FeatureRecord = []
- table.LookupList = otTables.LookupList()
- table.LookupList.Lookup = self.buildLookups_(tag)
- # Build a table for mapping (tag, lookup_indices) to feature_index.
- # For example, ('liga', (2,3,7)) --> 23.
- feature_indices = {}
- required_feature_indices = {} # ('latn', 'DEU') --> 23
- scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24
- # Sort the feature table by feature tag:
- # https://github.com/fonttools/fonttools/issues/568
- sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
- for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
- script, lang, feature_tag = key
- # l.lookup_index will be None when a lookup is not needed
- # for the table under construction. For example, substitution
- # rules will have no lookup_index while building GPOS tables.
- lookup_indices = tuple(
- [l.lookup_index for l in lookups if l.lookup_index is not None]
- )
- size_feature = tag == "GPOS" and feature_tag == "size"
- force_feature = self.any_feature_variations(feature_tag, tag)
- if len(lookup_indices) == 0 and not size_feature and not force_feature:
- continue
- for ix in lookup_indices:
- try:
- self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
- str(ix)
- ]._replace(feature=key)
- except KeyError:
- warnings.warn(
- "feaLib.Builder subclass needs upgrading to "
- "stash debug information. See fonttools#2065."
- )
- feature_key = (feature_tag, lookup_indices)
- feature_index = feature_indices.get(feature_key)
- if feature_index is None:
- feature_index = len(table.FeatureList.FeatureRecord)
- frec = otTables.FeatureRecord()
- frec.FeatureTag = feature_tag
- frec.Feature = otTables.Feature()
- frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
- frec.Feature.LookupListIndex = list(lookup_indices)
- frec.Feature.LookupCount = len(lookup_indices)
- table.FeatureList.FeatureRecord.append(frec)
- feature_indices[feature_key] = feature_index
- scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
- if self.required_features_.get((script, lang)) == feature_tag:
- required_feature_indices[(script, lang)] = feature_index
- # Build ScriptList.
- for script, lang_features in sorted(scripts.items()):
- srec = otTables.ScriptRecord()
- srec.ScriptTag = script
- srec.Script = otTables.Script()
- srec.Script.DefaultLangSys = None
- srec.Script.LangSysRecord = []
- for lang, feature_indices in sorted(lang_features.items()):
- langrec = otTables.LangSysRecord()
- langrec.LangSys = otTables.LangSys()
- langrec.LangSys.LookupOrder = None
- req_feature_index = required_feature_indices.get((script, lang))
- if req_feature_index is None:
- langrec.LangSys.ReqFeatureIndex = 0xFFFF
- else:
- langrec.LangSys.ReqFeatureIndex = req_feature_index
- langrec.LangSys.FeatureIndex = [
- i for i in feature_indices if i != req_feature_index
- ]
- langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
- if lang == "dflt":
- srec.Script.DefaultLangSys = langrec.LangSys
- else:
- langrec.LangSysTag = lang
- srec.Script.LangSysRecord.append(langrec)
- srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
- table.ScriptList.ScriptRecord.append(srec)
- table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
- table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
- table.LookupList.LookupCount = len(table.LookupList.Lookup)
- return table
- def makeFeatureVariations(self, table, table_tag):
- feature_vars = {}
- has_any_variations = False
- # Sort out which lookups to build, gather their indices
- for (_, _, feature_tag), variations in self.feature_variations_.items():
- feature_vars[feature_tag] = []
- for conditionset, builders in variations.items():
- raw_conditionset = self.conditionsets_[conditionset]
- indices = []
- for b in builders:
- if b.table != table_tag:
- continue
- assert b.lookup_index is not None
- indices.append(b.lookup_index)
- has_any_variations = True
- feature_vars[feature_tag].append((raw_conditionset, indices))
- if has_any_variations:
- for feature_tag, conditions_and_lookups in feature_vars.items():
- addFeatureVariationsRaw(
- self.font, table, conditions_and_lookups, feature_tag
- )
- def any_feature_variations(self, feature_tag, table_tag):
- for (_, _, feature), variations in self.feature_variations_.items():
- if feature != feature_tag:
- continue
- for conditionset, builders in variations.items():
- if any(b.table == table_tag for b in builders):
- return True
- return False
- def get_lookup_name_(self, lookup):
- rev = {v: k for k, v in self.named_lookups_.items()}
- if lookup in rev:
- return rev[lookup]
- return None
- def add_language_system(self, location, script, language):
- # OpenType Feature File Specification, section 4.b.i
- if script == "DFLT" and language == "dflt" and self.default_language_systems_:
- raise FeatureLibError(
- 'If "languagesystem DFLT dflt" is present, it must be '
- "the first of the languagesystem statements",
- location,
- )
- if script == "DFLT":
- if self.seen_non_DFLT_script_:
- raise FeatureLibError(
- 'languagesystems using the "DFLT" script tag must '
- "precede all other languagesystems",
- location,
- )
- else:
- self.seen_non_DFLT_script_ = True
- if (script, language) in self.default_language_systems_:
- raise FeatureLibError(
- '"languagesystem %s %s" has already been specified'
- % (script.strip(), language.strip()),
- location,
- )
- self.default_language_systems_.add((script, language))
- def get_default_language_systems_(self):
- # OpenType Feature File specification, 4.b.i. languagesystem:
- # If no "languagesystem" statement is present, then the
- # implementation must behave exactly as though the following
- # statement were present at the beginning of the feature file:
- # languagesystem DFLT dflt;
- if self.default_language_systems_:
- return frozenset(self.default_language_systems_)
- else:
- return frozenset({("DFLT", "dflt")})
- def start_feature(self, location, name):
- self.language_systems = self.get_default_language_systems_()
- self.script_ = "DFLT"
- self.cur_lookup_ = None
- self.cur_feature_name_ = name
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- if name == "aalt":
- self.aalt_location_ = location
- def end_feature(self):
- assert self.cur_feature_name_ is not None
- self.cur_feature_name_ = None
- self.language_systems = None
- self.cur_lookup_ = None
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- def start_lookup_block(self, location, name):
- if name in self.named_lookups_:
- raise FeatureLibError(
- 'Lookup "%s" has already been defined' % name, location
- )
- if self.cur_feature_name_ == "aalt":
- raise FeatureLibError(
- "Lookup blocks cannot be placed inside 'aalt' features; "
- "move it out, and then refer to it with a lookup statement",
- location,
- )
- self.cur_lookup_name_ = name
- self.named_lookups_[name] = None
- self.cur_lookup_ = None
- if self.cur_feature_name_ is None:
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- def end_lookup_block(self):
- assert self.cur_lookup_name_ is not None
- self.cur_lookup_name_ = None
- self.cur_lookup_ = None
- if self.cur_feature_name_ is None:
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- def add_lookup_call(self, lookup_name):
- assert lookup_name in self.named_lookups_, lookup_name
- self.cur_lookup_ = None
- lookup = self.named_lookups_[lookup_name]
- if lookup is not None: # skip empty named lookup
- self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
- def set_font_revision(self, location, revision):
- self.fontRevision_ = revision
- def set_language(self, location, language, include_default, required):
- assert len(language) == 4
- if self.cur_feature_name_ in ("aalt", "size"):
- raise FeatureLibError(
- "Language statements are not allowed "
- 'within "feature %s"' % self.cur_feature_name_,
- location,
- )
- if self.cur_feature_name_ is None:
- raise FeatureLibError(
- "Language statements are not allowed "
- "within standalone lookup blocks",
- location,
- )
- self.cur_lookup_ = None
- key = (self.script_, language, self.cur_feature_name_)
- lookups = self.features_.get((key[0], "dflt", key[2]))
- if (language == "dflt" or include_default) and lookups:
- self.features_[key] = lookups[:]
- else:
- self.features_[key] = []
- self.language_systems = frozenset([(self.script_, language)])
- if required:
- key = (self.script_, language)
- if key in self.required_features_:
- raise FeatureLibError(
- "Language %s (script %s) has already "
- "specified feature %s as its required feature"
- % (
- language.strip(),
- self.script_.strip(),
- self.required_features_[key].strip(),
- ),
- location,
- )
- self.required_features_[key] = self.cur_feature_name_
- def getMarkAttachClass_(self, location, glyphs):
- glyphs = frozenset(glyphs)
- id_ = self.markAttachClassID_.get(glyphs)
- if id_ is not None:
- return id_
- id_ = len(self.markAttachClassID_) + 1
- self.markAttachClassID_[glyphs] = id_
- for glyph in glyphs:
- if glyph in self.markAttach_:
- _, loc = self.markAttach_[glyph]
- raise FeatureLibError(
- "Glyph %s already has been assigned "
- "a MarkAttachmentType at %s" % (glyph, loc),
- location,
- )
- self.markAttach_[glyph] = (id_, location)
- return id_
- def getMarkFilterSet_(self, location, glyphs):
- glyphs = frozenset(glyphs)
- id_ = self.markFilterSets_.get(glyphs)
- if id_ is not None:
- return id_
- id_ = len(self.markFilterSets_)
- self.markFilterSets_[glyphs] = id_
- return id_
- def set_lookup_flag(self, location, value, markAttach, markFilter):
- value = value & 0xFF
- if markAttach:
- markAttachClass = self.getMarkAttachClass_(location, markAttach)
- value = value | (markAttachClass << 8)
- if markFilter:
- markFilterSet = self.getMarkFilterSet_(location, markFilter)
- value = value | 0x10
- self.lookupflag_markFilterSet_ = markFilterSet
- else:
- self.lookupflag_markFilterSet_ = None
- self.lookupflag_ = value
- def set_script(self, location, script):
- if self.cur_feature_name_ in ("aalt", "size"):
- raise FeatureLibError(
- "Script statements are not allowed "
- 'within "feature %s"' % self.cur_feature_name_,
- location,
- )
- if self.cur_feature_name_ is None:
- raise FeatureLibError(
- "Script statements are not allowed " "within standalone lookup blocks",
- location,
- )
- if self.language_systems == {(script, "dflt")}:
- # Nothing to do.
- return
- self.cur_lookup_ = None
- self.script_ = script
- self.lookupflag_ = 0
- self.lookupflag_markFilterSet_ = None
- self.set_language(location, "dflt", include_default=True, required=False)
- def find_lookup_builders_(self, lookups):
- """Helper for building chain contextual substitutions
- Given a list of lookup names, finds the LookupBuilder for each name.
- If an input name is None, it gets mapped to a None LookupBuilder.
- """
- lookup_builders = []
- for lookuplist in lookups:
- if lookuplist is not None:
- lookup_builders.append(
- [self.named_lookups_.get(l.name) for l in lookuplist]
- )
- else:
- lookup_builders.append(None)
- return lookup_builders
- def add_attach_points(self, location, glyphs, contourPoints):
- for glyph in glyphs:
- self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
- def add_feature_reference(self, location, featureName):
- if self.cur_feature_name_ != "aalt":
- raise FeatureLibError(
- 'Feature references are only allowed inside "feature aalt"', location
- )
- self.aalt_features_.append((location, featureName))
- def add_featureName(self, tag):
- self.featureNames_.add(tag)
- def add_cv_parameter(self, tag):
- self.cv_parameters_.add(tag)
- def add_to_cv_num_named_params(self, tag):
- """Adds new items to ``self.cv_num_named_params_``
- or increments the count of existing items."""
- if tag in self.cv_num_named_params_:
- self.cv_num_named_params_[tag] += 1
- else:
- self.cv_num_named_params_[tag] = 1
- def add_cv_character(self, character, tag):
- self.cv_characters_[tag].append(character)
- def set_base_axis(self, bases, scripts, vertical):
- if vertical:
- self.base_vert_axis_ = (bases, scripts)
- else:
- self.base_horiz_axis_ = (bases, scripts)
- def set_size_parameters(
- self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
- ):
- if self.cur_feature_name_ != "size":
- raise FeatureLibError(
- "Parameters statements are not allowed "
- 'within "feature %s"' % self.cur_feature_name_,
- location,
- )
- self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
- for script, lang in self.language_systems:
- key = (script, lang, self.cur_feature_name_)
- self.features_.setdefault(key, [])
- # GSUB rules
- # GSUB 1
- def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
- if self.cur_feature_name_ == "aalt":
- for from_glyph, to_glyph in mapping.items():
- alts = self.aalt_alternates_.setdefault(from_glyph, set())
- alts.add(to_glyph)
- return
- if prefix or suffix or forceChain:
- self.add_single_subst_chained_(location, prefix, suffix, mapping)
- return
- lookup = self.get_lookup_(location, SingleSubstBuilder)
- for from_glyph, to_glyph in mapping.items():
- if from_glyph in lookup.mapping:
- if to_glyph == lookup.mapping[from_glyph]:
- log.info(
- "Removing duplicate single substitution from glyph"
- ' "%s" to "%s" at %s',
- from_glyph,
- to_glyph,
- location,
- )
- else:
- raise FeatureLibError(
- 'Already defined rule for replacing glyph "%s" by "%s"'
- % (from_glyph, lookup.mapping[from_glyph]),
- location,
- )
- lookup.mapping[from_glyph] = to_glyph
- # GSUB 2
- def add_multiple_subst(
- self, location, prefix, glyph, suffix, replacements, forceChain=False
- ):
- if prefix or suffix or forceChain:
- chain = self.get_lookup_(location, ChainContextSubstBuilder)
- sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
- sub.mapping[glyph] = replacements
- chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
- return
- lookup = self.get_lookup_(location, MultipleSubstBuilder)
- if glyph in lookup.mapping:
- if replacements == lookup.mapping[glyph]:
- log.info(
- "Removing duplicate multiple substitution from glyph"
- ' "%s" to %s%s',
- glyph,
- replacements,
- f" at {location}" if location else "",
- )
- else:
- raise FeatureLibError(
- 'Already defined substitution for glyph "%s"' % glyph, location
- )
- lookup.mapping[glyph] = replacements
- # GSUB 3
- def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
- if self.cur_feature_name_ == "aalt":
- alts = self.aalt_alternates_.setdefault(glyph, set())
- alts.update(replacement)
- return
- if prefix or suffix:
- chain = self.get_lookup_(location, ChainContextSubstBuilder)
- lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
- chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [lookup]))
- else:
- lookup = self.get_lookup_(location, AlternateSubstBuilder)
- if glyph in lookup.alternates:
- raise FeatureLibError(
- 'Already defined alternates for glyph "%s"' % glyph, location
- )
- # We allow empty replacement glyphs here.
- lookup.alternates[glyph] = replacement
- # GSUB 4
- def add_ligature_subst(
- self, location, prefix, glyphs, suffix, replacement, forceChain
- ):
- if prefix or suffix or forceChain:
- chain = self.get_lookup_(location, ChainContextSubstBuilder)
- lookup = self.get_chained_lookup_(location, LigatureSubstBuilder)
- chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup]))
- else:
- lookup = self.get_lookup_(location, LigatureSubstBuilder)
- if not all(glyphs):
- raise FeatureLibError("Empty glyph class in substitution", location)
- # OpenType feature file syntax, section 5.d, "Ligature substitution":
- # "Since the OpenType specification does not allow ligature
- # substitutions to be specified on target sequences that contain
- # glyph classes, the implementation software will enumerate
- # all specific glyph sequences if glyph classes are detected"
- for g in sorted(itertools.product(*glyphs)):
- lookup.ligatures[g] = replacement
- # GSUB 5/6
- def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
- if not all(glyphs) or not all(prefix) or not all(suffix):
- raise FeatureLibError(
- "Empty glyph class in contextual substitution", location
- )
- lookup = self.get_lookup_(location, ChainContextSubstBuilder)
- lookup.rules.append(
- ChainContextualRule(
- prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
- )
- )
- def add_single_subst_chained_(self, location, prefix, suffix, mapping):
- if not mapping or not all(prefix) or not all(suffix):
- raise FeatureLibError(
- "Empty glyph class in contextual substitution", location
- )
- # https://github.com/fonttools/fonttools/issues/512
- # https://github.com/fonttools/fonttools/issues/2150
- chain = self.get_lookup_(location, ChainContextSubstBuilder)
- sub = chain.find_chainable_single_subst(mapping)
- if sub is None:
- sub = self.get_chained_lookup_(location, SingleSubstBuilder)
- sub.mapping.update(mapping)
- chain.rules.append(
- ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub])
- )
- # GSUB 8
- def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
- if not mapping:
- raise FeatureLibError("Empty glyph class in substitution", location)
- lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
- lookup.rules.append((old_prefix, old_suffix, mapping))
- # GPOS rules
- # GPOS 1
- def add_single_pos(self, location, prefix, suffix, pos, forceChain):
- if prefix or suffix or forceChain:
- self.add_single_pos_chained_(location, prefix, suffix, pos)
- else:
- lookup = self.get_lookup_(location, SinglePosBuilder)
- for glyphs, value in pos:
- if not glyphs:
- raise FeatureLibError(
- "Empty glyph class in positioning rule", location
- )
- otValueRecord = self.makeOpenTypeValueRecord(
- location, value, pairPosContext=False
- )
- for glyph in glyphs:
- try:
- lookup.add_pos(location, glyph, otValueRecord)
- except OpenTypeLibError as e:
- raise FeatureLibError(str(e), e.location) from e
- # GPOS 2
- def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
- if not glyphclass1 or not glyphclass2:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- lookup = self.get_lookup_(location, PairPosBuilder)
- v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
- v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
- lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2)
- def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
- if not glyph1 or not glyph2:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- lookup = self.get_lookup_(location, PairPosBuilder)
- v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
- v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
- lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
- # GPOS 3
- def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
- if not glyphclass:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- lookup = self.get_lookup_(location, CursivePosBuilder)
- lookup.add_attachment(
- location,
- glyphclass,
- self.makeOpenTypeAnchor(location, entryAnchor),
- self.makeOpenTypeAnchor(location, exitAnchor),
- )
- # GPOS 4
- def add_mark_base_pos(self, location, bases, marks):
- builder = self.get_lookup_(location, MarkBasePosBuilder)
- self.add_marks_(location, builder, marks)
- if not bases:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- for baseAnchor, markClass in marks:
- otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
- for base in bases:
- builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
- # GPOS 5
- def add_mark_lig_pos(self, location, ligatures, components):
- builder = self.get_lookup_(location, MarkLigPosBuilder)
- componentAnchors = []
- if not ligatures:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- for marks in components:
- anchors = {}
- self.add_marks_(location, builder, marks)
- for ligAnchor, markClass in marks:
- anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
- componentAnchors.append(anchors)
- for glyph in ligatures:
- builder.ligatures[glyph] = componentAnchors
- # GPOS 6
- def add_mark_mark_pos(self, location, baseMarks, marks):
- builder = self.get_lookup_(location, MarkMarkPosBuilder)
- self.add_marks_(location, builder, marks)
- if not baseMarks:
- raise FeatureLibError("Empty glyph class in positioning rule", location)
- for baseAnchor, markClass in marks:
- otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
- for baseMark in baseMarks:
- builder.baseMarks.setdefault(baseMark, {})[
- markClass.name
- ] = otBaseAnchor
- # GPOS 7/8
- def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
- if not all(glyphs) or not all(prefix) or not all(suffix):
- raise FeatureLibError(
- "Empty glyph class in contextual positioning rule", location
- )
- lookup = self.get_lookup_(location, ChainContextPosBuilder)
- lookup.rules.append(
- ChainContextualRule(
- prefix, glyphs, suffix, self.find_lookup_builders_(lookups)
- )
- )
- def add_single_pos_chained_(self, location, prefix, suffix, pos):
- if not pos or not all(prefix) or not all(suffix):
- raise FeatureLibError(
- "Empty glyph class in contextual positioning rule", location
- )
- # https://github.com/fonttools/fonttools/issues/514
- chain = self.get_lookup_(location, ChainContextPosBuilder)
- targets = []
- for _, _, _, lookups in chain.rules:
- targets.extend(lookups)
- subs = []
- for glyphs, value in pos:
- if value is None:
- subs.append(None)
- continue
- otValue = self.makeOpenTypeValueRecord(
- location, value, pairPosContext=False
- )
- sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
- if sub is None:
- sub = self.get_chained_lookup_(location, SinglePosBuilder)
- targets.append(sub)
- for glyph in glyphs:
- sub.add_pos(location, glyph, otValue)
- subs.append(sub)
- assert len(pos) == len(subs), (pos, subs)
- chain.rules.append(
- ChainContextualRule(prefix, [g for g, v in pos], suffix, subs)
- )
- def add_marks_(self, location, lookupBuilder, marks):
- """Helper for add_mark_{base,liga,mark}_pos."""
- for _, markClass in marks:
- for markClassDef in markClass.definitions:
- for mark in markClassDef.glyphs.glyphSet():
- if mark not in lookupBuilder.marks:
- otMarkAnchor = self.makeOpenTypeAnchor(
- location, copy.deepcopy(markClassDef.anchor)
- )
- lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
- else:
- existingMarkClass = lookupBuilder.marks[mark][0]
- if markClass.name != existingMarkClass:
- raise FeatureLibError(
- "Glyph %s cannot be in both @%s and @%s"
- % (mark, existingMarkClass, markClass.name),
- location,
- )
- def add_subtable_break(self, location):
- self.cur_lookup_.add_subtable_break(location)
- def setGlyphClass_(self, location, glyph, glyphClass):
- oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
- if oldClass and oldClass != glyphClass:
- raise FeatureLibError(
- "Glyph %s was assigned to a different class at %s"
- % (glyph, oldLocation),
- location,
- )
- self.glyphClassDefs_[glyph] = (glyphClass, location)
- def add_glyphClassDef(
- self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
- ):
- for glyph in baseGlyphs:
- self.setGlyphClass_(location, glyph, 1)
- for glyph in ligatureGlyphs:
- self.setGlyphClass_(location, glyph, 2)
- for glyph in markGlyphs:
- self.setGlyphClass_(location, glyph, 3)
- for glyph in componentGlyphs:
- self.setGlyphClass_(location, glyph, 4)
- def add_ligatureCaretByIndex_(self, location, glyphs, carets):
- for glyph in glyphs:
- if glyph not in self.ligCaretPoints_:
- self.ligCaretPoints_[glyph] = carets
- def makeLigCaret(self, location, caret):
- if not isinstance(caret, VariableScalar):
- return caret
- default, device = self.makeVariablePos(location, caret)
- if device is not None:
- return (default, device)
- return default
- def add_ligatureCaretByPos_(self, location, glyphs, carets):
- carets = [self.makeLigCaret(location, caret) for caret in carets]
- for glyph in glyphs:
- if glyph not in self.ligCaretCoords_:
- self.ligCaretCoords_[glyph] = carets
- def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
- self.names_.append([nameID, platformID, platEncID, langID, string])
- def add_os2_field(self, key, value):
- self.os2_[key] = value
- def add_hhea_field(self, key, value):
- self.hhea_[key] = value
- def add_vhea_field(self, key, value):
- self.vhea_[key] = value
- def add_conditionset(self, location, key, value):
- if "fvar" not in self.font:
- raise FeatureLibError(
- "Cannot add feature variations to a font without an 'fvar' table",
- location,
- )
- # Normalize
- axisMap = {
- axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
- for axis in self.axes
- }
- value = {
- tag: (
- normalizeValue(bottom, axisMap[tag]),
- normalizeValue(top, axisMap[tag]),
- )
- for tag, (bottom, top) in value.items()
- }
- # NOTE: This might result in rounding errors (off-by-ones) compared to
- # rules in Designspace files, since we're working with what's in the
- # `avar` table rather than the original values.
- if "avar" in self.font:
- mapping = self.font["avar"].segments
- value = {
- axis: tuple(
- piecewiseLinearMap(v, mapping[axis]) if axis in mapping else v
- for v in condition_range
- )
- for axis, condition_range in value.items()
- }
- self.conditionsets_[key] = value
- def makeVariablePos(self, location, varscalar):
- if not self.varstorebuilder:
- raise FeatureLibError(
- "Can't define a variable scalar in a non-variable font", location
- )
- varscalar.axes = self.axes
- if not varscalar.does_vary:
- return varscalar.default, None
- default, index = varscalar.add_to_variation_store(
- self.varstorebuilder, self.model_cache, self.font.get("avar")
- )
- device = None
- if index is not None and index != 0xFFFFFFFF:
- device = buildVarDevTable(index)
- return default, device
- def makeOpenTypeAnchor(self, location, anchor):
- """ast.Anchor --> otTables.Anchor"""
- if anchor is None:
- return None
- variable = False
- deviceX, deviceY = None, None
- if anchor.xDeviceTable is not None:
- deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
- if anchor.yDeviceTable is not None:
- deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
- for dim in ("x", "y"):
- varscalar = getattr(anchor, dim)
- if not isinstance(varscalar, VariableScalar):
- continue
- if getattr(anchor, dim + "DeviceTable") is not None:
- raise FeatureLibError(
- "Can't define a device coordinate and variable scalar", location
- )
- default, device = self.makeVariablePos(location, varscalar)
- setattr(anchor, dim, default)
- if device is not None:
- if dim == "x":
- deviceX = device
- else:
- deviceY = device
- variable = True
- otlanchor = otl.buildAnchor(
- anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY
- )
- if variable:
- otlanchor.Format = 3
- return otlanchor
- _VALUEREC_ATTRS = {
- name[0].lower() + name[1:]: (name, isDevice)
- for _, name, isDevice, _ in otBase.valueRecordFormat
- if not name.startswith("Reserved")
- }
- def makeOpenTypeValueRecord(self, location, v, pairPosContext):
- """ast.ValueRecord --> otBase.ValueRecord"""
- if not v:
- return None
- vr = {}
- for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
- val = getattr(v, astName, None)
- if not val:
- continue
- if isDevice:
- vr[otName] = otl.buildDevice(dict(val))
- elif isinstance(val, VariableScalar):
- otDeviceName = otName[0:4] + "Device"
- feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
- if getattr(v, feaDeviceName):
- raise FeatureLibError(
- "Can't define a device coordinate and variable scalar", location
- )
- vr[otName], device = self.makeVariablePos(location, val)
- if device is not None:
- vr[otDeviceName] = device
- else:
- vr[otName] = val
- if pairPosContext and not vr:
- vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
- valRec = otl.buildValue(vr)
- return valRec
|