builder.py 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838
  1. from __future__ import annotations
  2. from fontTools.misc import sstruct
  3. from fontTools.misc.textTools import Tag, tostr, binary2num, safeEval
  4. from fontTools.feaLib.error import FeatureLibError
  5. from fontTools.feaLib.lookupDebugInfo import (
  6. LookupDebugInfo,
  7. LOOKUP_DEBUG_INFO_KEY,
  8. LOOKUP_DEBUG_ENV_VAR,
  9. )
  10. from fontTools.feaLib.parser import Parser
  11. from fontTools.feaLib.ast import FeatureFile
  12. from fontTools.feaLib.variableScalar import VariableScalar, VariableScalarBuilder
  13. from fontTools.otlLib import builder as otl
  14. from fontTools.otlLib.maxContextCalc import maxCtxFont
  15. from fontTools.ttLib import newTable, getTableModule
  16. from fontTools.ttLib.tables import otBase, otTables
  17. from fontTools.otlLib.builder import (
  18. AlternateSubstBuilder,
  19. ChainContextPosBuilder,
  20. ChainContextSubstBuilder,
  21. LigatureSubstBuilder,
  22. MultipleSubstBuilder,
  23. CursivePosBuilder,
  24. MarkBasePosBuilder,
  25. MarkLigPosBuilder,
  26. MarkMarkPosBuilder,
  27. ReverseChainSingleSubstBuilder,
  28. SingleSubstBuilder,
  29. ClassPairPosSubtableBuilder,
  30. PairPosBuilder,
  31. SinglePosBuilder,
  32. ChainContextualRule,
  33. AnySubstBuilder,
  34. )
  35. from fontTools.otlLib.error import OpenTypeLibError
  36. from fontTools.varLib.errors import VarLibError
  37. from fontTools.varLib.varStore import OnlineVarStoreBuilder
  38. from fontTools.varLib.builder import buildVarDevTable
  39. from fontTools.varLib.featureVars import addFeatureVariationsRaw
  40. from fontTools.varLib.models import normalizeValue, piecewiseLinearMap
  41. from collections import defaultdict
  42. import copy
  43. import itertools
  44. from io import StringIO
  45. import logging
  46. import warnings
  47. import os
  48. log = logging.getLogger(__name__)
  49. def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
  50. """Add features from a file to a font. Note that this replaces any features
  51. currently present.
  52. Args:
  53. font (feaLib.ttLib.TTFont): The font object.
  54. featurefile: Either a path or file object (in which case we
  55. parse it into an AST), or a pre-parsed AST instance.
  56. tables: If passed, restrict the set of affected tables to those in the
  57. list.
  58. debug: Whether to add source debugging information to the font in the
  59. ``Debg`` table
  60. """
  61. builder = Builder(font, featurefile)
  62. builder.build(tables=tables, debug=debug)
  63. def addOpenTypeFeaturesFromString(
  64. font, features, filename=None, tables=None, debug=False
  65. ):
  66. """Add features from a string to a font. Note that this replaces any
  67. features currently present.
  68. Args:
  69. font (feaLib.ttLib.TTFont): The font object.
  70. features: A string containing feature code.
  71. filename: The directory containing ``filename`` is used as the root of
  72. relative ``include()`` paths; if ``None`` is provided, the current
  73. directory is assumed.
  74. tables: If passed, restrict the set of affected tables to those in the
  75. list.
  76. debug: Whether to add source debugging information to the font in the
  77. ``Debg`` table
  78. """
  79. featurefile = StringIO(tostr(features))
  80. if filename:
  81. featurefile.name = filename
  82. addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
  83. class Builder(object):
  84. supportedTables = frozenset(
  85. Tag(tag)
  86. for tag in [
  87. "BASE",
  88. "GDEF",
  89. "GPOS",
  90. "GSUB",
  91. "OS/2",
  92. "head",
  93. "hhea",
  94. "name",
  95. "vhea",
  96. "STAT",
  97. ]
  98. )
  99. def __init__(self, font, featurefile):
  100. self.font = font
  101. # 'featurefile' can be either a path or file object (in which case we
  102. # parse it into an AST), or a pre-parsed AST instance
  103. if isinstance(featurefile, FeatureFile):
  104. self.parseTree, self.file = featurefile, None
  105. else:
  106. self.parseTree, self.file = None, featurefile
  107. self.glyphMap = font.getReverseGlyphMap()
  108. self.varstorebuilder = None
  109. if "fvar" in font:
  110. self.axes = font["fvar"].axes
  111. self.varstorebuilder = OnlineVarStoreBuilder(
  112. [ax.axisTag for ax in self.axes]
  113. )
  114. self.scalar_builder = VariableScalarBuilder.from_ttf(font)
  115. self.default_language_systems_ = set()
  116. self.script_ = None
  117. self.lookupflag_ = 0
  118. self.lookupflag_markFilterSet_ = None
  119. self.use_extension_ = False
  120. self.language_systems = set()
  121. self.seen_non_DFLT_script_ = False
  122. self.named_lookups_ = {}
  123. self.cur_lookup_ = None
  124. self.cur_lookup_name_ = None
  125. self.cur_feature_name_ = None
  126. self.lookups_ = []
  127. self.lookup_locations = {"GSUB": {}, "GPOS": {}}
  128. self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
  129. self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
  130. self.feature_variations_ = {}
  131. # for feature 'aalt'
  132. self.aalt_features_ = [] # [(location, featureName)*], for 'aalt'
  133. self.aalt_location_ = None
  134. self.aalt_alternates_ = {}
  135. self.aalt_use_extension_ = False
  136. # for 'featureNames'
  137. self.featureNames_ = set()
  138. self.featureNames_ids_ = {}
  139. # for 'cvParameters'
  140. self.cv_parameters_ = set()
  141. self.cv_parameters_ids_ = {}
  142. self.cv_num_named_params_ = {}
  143. self.cv_characters_ = defaultdict(list)
  144. # for feature 'size'
  145. self.size_parameters_ = None
  146. # for table 'head'
  147. self.fontRevision_ = None # 2.71
  148. # for table 'name'
  149. self.names_ = []
  150. # for table 'BASE'
  151. self.base_horiz_axis_ = None
  152. self.base_vert_axis_ = None
  153. # for table 'GDEF'
  154. self.attachPoints_ = {} # "a" --> {3, 7}
  155. self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600}
  156. self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7}
  157. self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column))
  158. self.markAttach_ = {} # "acute" --> (4, (file, line, column))
  159. self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4
  160. self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4
  161. # for table 'OS/2'
  162. self.os2_ = {}
  163. # for table 'hhea'
  164. self.hhea_ = {}
  165. # for table 'vhea'
  166. self.vhea_ = {}
  167. # for table 'STAT'
  168. self.stat_ = {}
  169. # for conditionsets
  170. self.conditionsets_ = {}
  171. def build(self, tables=None, debug=False):
  172. if self.parseTree is None:
  173. self.parseTree = Parser(self.file, self.glyphMap).parse()
  174. self.parseTree.build(self)
  175. # by default, build all the supported tables
  176. if tables is None:
  177. tables = self.supportedTables
  178. else:
  179. tables = frozenset(tables)
  180. unsupported = tables - self.supportedTables
  181. if unsupported:
  182. unsupported_string = ", ".join(sorted(unsupported))
  183. raise NotImplementedError(
  184. "The following tables were requested but are unsupported: "
  185. f"{unsupported_string}."
  186. )
  187. if "GSUB" in tables:
  188. self.build_feature_aalt_()
  189. if "head" in tables:
  190. self.build_head()
  191. if "hhea" in tables:
  192. self.build_hhea()
  193. if "vhea" in tables:
  194. self.build_vhea()
  195. if "name" in tables:
  196. self.build_name()
  197. if "OS/2" in tables:
  198. self.build_OS_2()
  199. if "STAT" in tables:
  200. self.build_STAT()
  201. for tag in ("GPOS", "GSUB"):
  202. if tag not in tables:
  203. continue
  204. table = self.makeTable(tag)
  205. if self.feature_variations_:
  206. self.makeFeatureVariations(table, tag)
  207. if (
  208. table.ScriptList.ScriptCount > 0
  209. or table.FeatureList.FeatureCount > 0
  210. or table.LookupList.LookupCount > 0
  211. ):
  212. fontTable = self.font[tag] = newTable(tag)
  213. fontTable.table = table
  214. elif tag in self.font:
  215. del self.font[tag]
  216. if any(tag in self.font for tag in ("GPOS", "GSUB")) and "OS/2" in self.font:
  217. self.font["OS/2"].usMaxContext = maxCtxFont(self.font)
  218. if "GDEF" in tables:
  219. gdef = self.buildGDEF()
  220. if gdef:
  221. self.font["GDEF"] = gdef
  222. elif "GDEF" in self.font:
  223. del self.font["GDEF"]
  224. if "BASE" in tables:
  225. base = self.buildBASE()
  226. if base:
  227. self.font["BASE"] = base
  228. elif "BASE" in self.font:
  229. del self.font["BASE"]
  230. if debug or os.environ.get(LOOKUP_DEBUG_ENV_VAR):
  231. self.buildDebg()
  232. def get_chained_lookup_(self, location, builder_class):
  233. result = builder_class(self.font, location)
  234. result.lookupflag = self.lookupflag_
  235. result.markFilterSet = self.lookupflag_markFilterSet_
  236. result.extension = self.use_extension_
  237. self.lookups_.append(result)
  238. return result
  239. def add_lookup_to_feature_(self, lookup, feature_name):
  240. for script, lang in self.language_systems:
  241. key = (script, lang, feature_name)
  242. self.features_.setdefault(key, []).append(lookup)
  243. def get_lookup_(self, location, builder_class, mapping=None):
  244. if (
  245. self.cur_lookup_
  246. and type(self.cur_lookup_) == builder_class
  247. and self.cur_lookup_.lookupflag == self.lookupflag_
  248. and self.cur_lookup_.markFilterSet == self.lookupflag_markFilterSet_
  249. and self.cur_lookup_.can_add_mapping(mapping)
  250. ):
  251. return self.cur_lookup_
  252. if self.cur_lookup_name_ and self.cur_lookup_:
  253. raise FeatureLibError(
  254. "Within a named lookup block, all rules must be of "
  255. "the same lookup type and flag",
  256. location,
  257. )
  258. self.cur_lookup_ = builder_class(self.font, location)
  259. self.cur_lookup_.lookupflag = self.lookupflag_
  260. self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_
  261. self.cur_lookup_.extension = self.use_extension_
  262. self.lookups_.append(self.cur_lookup_)
  263. if self.cur_lookup_name_:
  264. # We are starting a lookup rule inside a named lookup block.
  265. self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
  266. if self.cur_feature_name_:
  267. # We are starting a lookup rule inside a feature. This includes
  268. # lookup rules inside named lookups inside features.
  269. self.add_lookup_to_feature_(self.cur_lookup_, self.cur_feature_name_)
  270. return self.cur_lookup_
  271. def build_feature_aalt_(self):
  272. if not self.aalt_features_ and not self.aalt_alternates_:
  273. return
  274. # > alternate glyphs will be sorted in the order that the source features
  275. # > are named in the aalt definition, not the order of the feature definitions
  276. # > in the file. Alternates defined explicitly ... will precede all others.
  277. # https://github.com/fonttools/fonttools/issues/836
  278. alternates = {g: list(a) for g, a in self.aalt_alternates_.items()}
  279. for location, name in self.aalt_features_ + [(None, "aalt")]:
  280. feature = [
  281. (script, lang, feature, lookups)
  282. for (script, lang, feature), lookups in self.features_.items()
  283. if feature == name
  284. ]
  285. # "aalt" does not have to specify its own lookups, but it might.
  286. if not feature and name != "aalt":
  287. warnings.warn("%s: Feature %s has not been defined" % (location, name))
  288. continue
  289. for script, lang, feature, lookups in feature:
  290. for lookuplist in lookups:
  291. if not isinstance(lookuplist, list):
  292. lookuplist = [lookuplist]
  293. for lookup in lookuplist:
  294. for glyph, alts in lookup.getAlternateGlyphs().items():
  295. alts_for_glyph = alternates.setdefault(glyph, [])
  296. alts_for_glyph.extend(
  297. g for g in alts if g not in alts_for_glyph
  298. )
  299. single = {
  300. glyph: repl[0] for glyph, repl in alternates.items() if len(repl) == 1
  301. }
  302. multi = {glyph: repl for glyph, repl in alternates.items() if len(repl) > 1}
  303. if not single and not multi:
  304. return
  305. self.features_ = {
  306. (script, lang, feature): lookups
  307. for (script, lang, feature), lookups in self.features_.items()
  308. if feature != "aalt"
  309. }
  310. old_lookups = self.lookups_
  311. self.lookups_ = []
  312. self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_)
  313. if single:
  314. single_lookup = self.get_lookup_(location, SingleSubstBuilder)
  315. single_lookup.mapping = single
  316. if multi:
  317. multi_lookup = self.get_lookup_(location, AlternateSubstBuilder)
  318. multi_lookup.alternates = multi
  319. self.end_feature()
  320. self.lookups_.extend(old_lookups)
  321. def build_head(self):
  322. if not self.fontRevision_:
  323. return
  324. table = self.font.get("head")
  325. if not table: # this only happens for unit tests
  326. table = self.font["head"] = newTable("head")
  327. table.decompile(b"\0" * 54, self.font)
  328. table.tableVersion = 1.0
  329. table.magicNumber = 0x5F0F3CF5
  330. table.created = table.modified = 3406620153 # 2011-12-13 11:22:33
  331. table.fontRevision = self.fontRevision_
  332. def build_hhea(self):
  333. if not self.hhea_:
  334. return
  335. table = self.font.get("hhea")
  336. if not table: # this only happens for unit tests
  337. table = self.font["hhea"] = newTable("hhea")
  338. table.decompile(b"\0" * 36, self.font)
  339. table.tableVersion = 0x00010000
  340. if "caretoffset" in self.hhea_:
  341. table.caretOffset = self.hhea_["caretoffset"]
  342. if "ascender" in self.hhea_:
  343. table.ascent = self.hhea_["ascender"]
  344. if "descender" in self.hhea_:
  345. table.descent = self.hhea_["descender"]
  346. if "linegap" in self.hhea_:
  347. table.lineGap = self.hhea_["linegap"]
  348. def build_vhea(self):
  349. if not self.vhea_:
  350. return
  351. table = self.font.get("vhea")
  352. if not table: # this only happens for unit tests
  353. table = self.font["vhea"] = newTable("vhea")
  354. table.decompile(b"\0" * 36, self.font)
  355. table.tableVersion = 0x00011000
  356. if "verttypoascender" in self.vhea_:
  357. table.ascent = self.vhea_["verttypoascender"]
  358. if "verttypodescender" in self.vhea_:
  359. table.descent = self.vhea_["verttypodescender"]
  360. if "verttypolinegap" in self.vhea_:
  361. table.lineGap = self.vhea_["verttypolinegap"]
  362. def get_user_name_id(self, table):
  363. # Try to find first unused font-specific name id
  364. nameIDs = [name.nameID for name in table.names]
  365. for user_name_id in range(256, 32767):
  366. if user_name_id not in nameIDs:
  367. return user_name_id
  368. def buildFeatureParams(self, tag):
  369. # by convention, a missing name ID is represented by 0xffff.
  370. # the spec says that these fields can be 'NULL', but 'NULL' is not
  371. # well defined for the purpose of nameIDs?
  372. NO_NAME_ID = 0xFFFF
  373. params = None
  374. if tag == "size":
  375. params = otTables.FeatureParamsSize()
  376. (
  377. params.DesignSize,
  378. params.SubfamilyID,
  379. params.RangeStart,
  380. params.RangeEnd,
  381. ) = self.size_parameters_
  382. if tag in self.featureNames_ids_:
  383. params.SubfamilyNameID = self.featureNames_ids_[tag]
  384. else:
  385. params.SubfamilyNameID = 0
  386. elif tag in self.featureNames_:
  387. if not self.featureNames_ids_:
  388. # name table wasn't selected among the tables to build; skip
  389. pass
  390. else:
  391. assert tag in self.featureNames_ids_
  392. params = otTables.FeatureParamsStylisticSet()
  393. params.Version = 0
  394. params.UINameID = self.featureNames_ids_[tag]
  395. elif tag in self.cv_parameters_:
  396. params = otTables.FeatureParamsCharacterVariants()
  397. params.Format = 0
  398. params.FeatUILabelNameID = self.cv_parameters_ids_.get(
  399. (tag, "FeatUILabelNameID"), NO_NAME_ID
  400. )
  401. params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
  402. (tag, "FeatUITooltipTextNameID"), NO_NAME_ID
  403. )
  404. params.SampleTextNameID = self.cv_parameters_ids_.get(
  405. (tag, "SampleTextNameID"), NO_NAME_ID
  406. )
  407. params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
  408. params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
  409. (tag, "ParamUILabelNameID_0"), NO_NAME_ID
  410. )
  411. params.CharCount = len(self.cv_characters_[tag])
  412. params.Character = self.cv_characters_[tag]
  413. return params
  414. def build_name(self):
  415. if not self.names_:
  416. return
  417. table = self.font.get("name")
  418. if not table: # this only happens for unit tests
  419. table = self.font["name"] = newTable("name")
  420. table.names = []
  421. for name in self.names_:
  422. nameID, platformID, platEncID, langID, string = name
  423. # For featureNames block, nameID is 'feature tag'
  424. # For cvParameters blocks, nameID is ('feature tag', 'block name')
  425. if not isinstance(nameID, int):
  426. tag = nameID
  427. if tag in self.featureNames_:
  428. if tag not in self.featureNames_ids_:
  429. self.featureNames_ids_[tag] = self.get_user_name_id(table)
  430. assert self.featureNames_ids_[tag] is not None
  431. nameID = self.featureNames_ids_[tag]
  432. elif tag[0] in self.cv_parameters_:
  433. if tag not in self.cv_parameters_ids_:
  434. self.cv_parameters_ids_[tag] = self.get_user_name_id(table)
  435. assert self.cv_parameters_ids_[tag] is not None
  436. nameID = self.cv_parameters_ids_[tag]
  437. table.setName(string, nameID, platformID, platEncID, langID)
  438. table.names.sort()
  439. def build_OS_2(self):
  440. if not self.os2_:
  441. return
  442. table = self.font.get("OS/2")
  443. if not table: # this only happens for unit tests
  444. table = self.font["OS/2"] = newTable("OS/2")
  445. data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0)
  446. table.decompile(data, self.font)
  447. version = 0
  448. if "fstype" in self.os2_:
  449. table.fsType = self.os2_["fstype"]
  450. if "panose" in self.os2_:
  451. panose = getTableModule("OS/2").Panose()
  452. (
  453. panose.bFamilyType,
  454. panose.bSerifStyle,
  455. panose.bWeight,
  456. panose.bProportion,
  457. panose.bContrast,
  458. panose.bStrokeVariation,
  459. panose.bArmStyle,
  460. panose.bLetterForm,
  461. panose.bMidline,
  462. panose.bXHeight,
  463. ) = self.os2_["panose"]
  464. table.panose = panose
  465. if "typoascender" in self.os2_:
  466. table.sTypoAscender = self.os2_["typoascender"]
  467. if "typodescender" in self.os2_:
  468. table.sTypoDescender = self.os2_["typodescender"]
  469. if "typolinegap" in self.os2_:
  470. table.sTypoLineGap = self.os2_["typolinegap"]
  471. if "winascent" in self.os2_:
  472. table.usWinAscent = self.os2_["winascent"]
  473. if "windescent" in self.os2_:
  474. table.usWinDescent = self.os2_["windescent"]
  475. if "vendor" in self.os2_:
  476. table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''")
  477. if "weightclass" in self.os2_:
  478. table.usWeightClass = self.os2_["weightclass"]
  479. if "widthclass" in self.os2_:
  480. table.usWidthClass = self.os2_["widthclass"]
  481. if "unicoderange" in self.os2_:
  482. table.setUnicodeRanges(self.os2_["unicoderange"])
  483. if "codepagerange" in self.os2_:
  484. pages = self.build_codepages_(self.os2_["codepagerange"])
  485. table.ulCodePageRange1, table.ulCodePageRange2 = pages
  486. version = 1
  487. if "xheight" in self.os2_:
  488. table.sxHeight = self.os2_["xheight"]
  489. version = 2
  490. if "capheight" in self.os2_:
  491. table.sCapHeight = self.os2_["capheight"]
  492. version = 2
  493. if "loweropsize" in self.os2_:
  494. table.usLowerOpticalPointSize = self.os2_["loweropsize"]
  495. version = 5
  496. if "upperopsize" in self.os2_:
  497. table.usUpperOpticalPointSize = self.os2_["upperopsize"]
  498. version = 5
  499. def checkattr(table, attrs):
  500. for attr in attrs:
  501. if not hasattr(table, attr):
  502. setattr(table, attr, 0)
  503. table.version = max(version, table.version)
  504. # this only happens for unit tests
  505. if version >= 1:
  506. checkattr(table, ("ulCodePageRange1", "ulCodePageRange2"))
  507. if version >= 2:
  508. checkattr(
  509. table,
  510. (
  511. "sxHeight",
  512. "sCapHeight",
  513. "usDefaultChar",
  514. "usBreakChar",
  515. "usMaxContext",
  516. ),
  517. )
  518. if version >= 5:
  519. checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
  520. def setElidedFallbackName(self, value, location):
  521. # ElidedFallbackName is a convenience method for setting
  522. # ElidedFallbackNameID so only one can be allowed
  523. for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
  524. if token in self.stat_:
  525. raise FeatureLibError(
  526. f"{token} is already set.",
  527. location,
  528. )
  529. if isinstance(value, int):
  530. self.stat_["ElidedFallbackNameID"] = value
  531. elif isinstance(value, list):
  532. self.stat_["ElidedFallbackName"] = value
  533. else:
  534. raise AssertionError(value)
  535. def addDesignAxis(self, designAxis, location):
  536. if "DesignAxes" not in self.stat_:
  537. self.stat_["DesignAxes"] = []
  538. if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
  539. raise FeatureLibError(
  540. f'DesignAxis already defined for tag "{designAxis.tag}".',
  541. location,
  542. )
  543. if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
  544. raise FeatureLibError(
  545. f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
  546. location,
  547. )
  548. self.stat_["DesignAxes"].append(designAxis)
  549. def addAxisValueRecord(self, axisValueRecord, location):
  550. if "AxisValueRecords" not in self.stat_:
  551. self.stat_["AxisValueRecords"] = []
  552. # Check for duplicate AxisValueRecords
  553. for record_ in self.stat_["AxisValueRecords"]:
  554. if (
  555. {n.asFea() for n in record_.names}
  556. == {n.asFea() for n in axisValueRecord.names}
  557. and {n.asFea() for n in record_.locations}
  558. == {n.asFea() for n in axisValueRecord.locations}
  559. and record_.flags == axisValueRecord.flags
  560. ):
  561. raise FeatureLibError(
  562. "An AxisValueRecord with these values is already defined.",
  563. location,
  564. )
  565. self.stat_["AxisValueRecords"].append(axisValueRecord)
  566. def build_STAT(self):
  567. if not self.stat_:
  568. return
  569. axes = self.stat_.get("DesignAxes")
  570. if not axes:
  571. raise FeatureLibError("DesignAxes not defined", None)
  572. axisValueRecords = self.stat_.get("AxisValueRecords")
  573. axisValues = {}
  574. format4_locations = []
  575. for tag in axes:
  576. axisValues[tag.tag] = []
  577. if axisValueRecords is not None:
  578. for avr in axisValueRecords:
  579. valuesDict = {}
  580. if avr.flags > 0:
  581. valuesDict["flags"] = avr.flags
  582. if len(avr.locations) == 1:
  583. location = avr.locations[0]
  584. values = location.values
  585. if len(values) == 1: # format1
  586. valuesDict.update({"value": values[0], "name": avr.names})
  587. if len(values) == 2: # format3
  588. valuesDict.update(
  589. {
  590. "value": values[0],
  591. "linkedValue": values[1],
  592. "name": avr.names,
  593. }
  594. )
  595. if len(values) == 3: # format2
  596. nominal, minVal, maxVal = values
  597. valuesDict.update(
  598. {
  599. "nominalValue": nominal,
  600. "rangeMinValue": minVal,
  601. "rangeMaxValue": maxVal,
  602. "name": avr.names,
  603. }
  604. )
  605. axisValues[location.tag].append(valuesDict)
  606. else:
  607. valuesDict.update(
  608. {
  609. "location": {i.tag: i.values[0] for i in avr.locations},
  610. "name": avr.names,
  611. }
  612. )
  613. format4_locations.append(valuesDict)
  614. designAxes = [
  615. {
  616. "ordering": a.axisOrder,
  617. "tag": a.tag,
  618. "name": a.names,
  619. "values": axisValues[a.tag],
  620. }
  621. for a in axes
  622. ]
  623. nameTable = self.font.get("name")
  624. if not nameTable: # this only happens for unit tests
  625. nameTable = self.font["name"] = newTable("name")
  626. nameTable.names = []
  627. if "ElidedFallbackNameID" in self.stat_:
  628. nameID = self.stat_["ElidedFallbackNameID"]
  629. name = nameTable.getDebugName(nameID)
  630. if not name:
  631. raise FeatureLibError(
  632. f"ElidedFallbackNameID {nameID} points "
  633. "to a nameID that does not exist in the "
  634. '"name" table',
  635. None,
  636. )
  637. elif "ElidedFallbackName" in self.stat_:
  638. nameID = self.stat_["ElidedFallbackName"]
  639. otl.buildStatTable(
  640. self.font,
  641. designAxes,
  642. locations=format4_locations,
  643. elidedFallbackName=nameID,
  644. )
  645. def build_codepages_(self, pages):
  646. pages2bits = {
  647. 1252: 0,
  648. 1250: 1,
  649. 1251: 2,
  650. 1253: 3,
  651. 1254: 4,
  652. 1255: 5,
  653. 1256: 6,
  654. 1257: 7,
  655. 1258: 8,
  656. 874: 16,
  657. 932: 17,
  658. 936: 18,
  659. 949: 19,
  660. 950: 20,
  661. 1361: 21,
  662. 869: 48,
  663. 866: 49,
  664. 865: 50,
  665. 864: 51,
  666. 863: 52,
  667. 862: 53,
  668. 861: 54,
  669. 860: 55,
  670. 857: 56,
  671. 855: 57,
  672. 852: 58,
  673. 775: 59,
  674. 737: 60,
  675. 708: 61,
  676. 850: 62,
  677. 437: 63,
  678. }
  679. bits = [pages2bits[p] for p in pages if p in pages2bits]
  680. pages = []
  681. for i in range(2):
  682. pages.append("")
  683. for j in range(i * 32, (i + 1) * 32):
  684. if j in bits:
  685. pages[i] += "1"
  686. else:
  687. pages[i] += "0"
  688. return [binary2num(p[::-1]) for p in pages]
  689. def buildBASE(self):
  690. if not self.base_horiz_axis_ and not self.base_vert_axis_:
  691. return None
  692. base = otTables.BASE()
  693. base.Version = 0x00010000
  694. base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_)
  695. base.VertAxis = self.buildBASEAxis(self.base_vert_axis_)
  696. result = newTable("BASE")
  697. result.table = base
  698. return result
  699. def buildBASECoord(self, c):
  700. coord = otTables.BaseCoord()
  701. coord.Format = 1
  702. coord.Coordinate = c
  703. return coord
  704. def buildBASEAxis(self, axis):
  705. if not axis:
  706. return
  707. bases, scripts, minmax = axis
  708. axis = otTables.Axis()
  709. axis.BaseTagList = otTables.BaseTagList()
  710. axis.BaseTagList.BaselineTag = bases
  711. axis.BaseTagList.BaseTagCount = len(bases)
  712. axis.BaseScriptList = otTables.BaseScriptList()
  713. axis.BaseScriptList.BaseScriptRecord = []
  714. axis.BaseScriptList.BaseScriptCount = len(scripts)
  715. for script in sorted(scripts):
  716. minmax_for_script = [
  717. record[1:] for record in minmax if record[0] == script[0]
  718. ]
  719. record = otTables.BaseScriptRecord()
  720. record.BaseScriptTag = script[0]
  721. record.BaseScript = otTables.BaseScript()
  722. record.BaseScript.BaseValues = otTables.BaseValues()
  723. record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1])
  724. record.BaseScript.BaseValues.BaseCoord = []
  725. record.BaseScript.BaseValues.BaseCoordCount = len(script[2])
  726. record.BaseScript.BaseLangSysRecord = []
  727. for c in script[2]:
  728. record.BaseScript.BaseValues.BaseCoord.append(self.buildBASECoord(c))
  729. for language, min_coord, max_coord in sorted(minmax_for_script):
  730. minmax_record = otTables.MinMax()
  731. minmax_record.MinCoord = self.buildBASECoord(min_coord)
  732. minmax_record.MaxCoord = self.buildBASECoord(max_coord)
  733. minmax_record.FeatMinMaxCount = 0
  734. if language == "dflt":
  735. record.BaseScript.DefaultMinMax = minmax_record
  736. else:
  737. lang_record = otTables.BaseLangSysRecord()
  738. lang_record.BaseLangSysTag = language
  739. lang_record.MinMax = minmax_record
  740. record.BaseScript.BaseLangSysRecord.append(lang_record)
  741. record.BaseScript.BaseLangSysCount = len(
  742. record.BaseScript.BaseLangSysRecord
  743. )
  744. axis.BaseScriptList.BaseScriptRecord.append(record)
  745. return axis
  746. def buildGDEF(self):
  747. gdef = otTables.GDEF()
  748. gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_()
  749. gdef.AttachList = otl.buildAttachList(self.attachPoints_, self.glyphMap)
  750. gdef.LigCaretList = otl.buildLigCaretList(
  751. self.ligCaretCoords_, self.ligCaretPoints_, self.glyphMap
  752. )
  753. gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_()
  754. gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_()
  755. gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
  756. if self.varstorebuilder:
  757. store = self.varstorebuilder.finish()
  758. if store:
  759. gdef.Version = 0x00010003
  760. gdef.VarStore = store
  761. varidx_map = store.optimize()
  762. gdef.remap_device_varidxes(varidx_map)
  763. if "GPOS" in self.font:
  764. self.font["GPOS"].table.remap_device_varidxes(varidx_map)
  765. if any(
  766. (
  767. gdef.GlyphClassDef,
  768. gdef.AttachList,
  769. gdef.LigCaretList,
  770. gdef.MarkAttachClassDef,
  771. gdef.MarkGlyphSetsDef,
  772. )
  773. ) or hasattr(gdef, "VarStore"):
  774. result = newTable("GDEF")
  775. result.table = gdef
  776. return result
  777. else:
  778. return None
  779. def buildGDEFGlyphClassDef_(self):
  780. if self.glyphClassDefs_:
  781. classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()}
  782. else:
  783. classes = {}
  784. for lookup in self.lookups_:
  785. classes.update(lookup.inferGlyphClasses())
  786. for markClass in self.parseTree.markClasses.values():
  787. for markClassDef in markClass.definitions:
  788. for glyph in markClassDef.glyphSet():
  789. classes[glyph] = 3
  790. if classes:
  791. result = otTables.GlyphClassDef()
  792. result.classDefs = classes
  793. return result
  794. else:
  795. return None
  796. def buildGDEFMarkAttachClassDef_(self):
  797. classDefs = {g: c for g, (c, _) in self.markAttach_.items()}
  798. if not classDefs:
  799. return None
  800. result = otTables.MarkAttachClassDef()
  801. result.classDefs = classDefs
  802. return result
  803. def buildGDEFMarkGlyphSetsDef_(self):
  804. sets = []
  805. for glyphs, id_ in sorted(
  806. self.markFilterSets_.items(), key=lambda item: item[1]
  807. ):
  808. sets.append(glyphs)
  809. return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
  810. def buildDebg(self):
  811. if "Debg" not in self.font:
  812. self.font["Debg"] = newTable("Debg")
  813. self.font["Debg"].data = {}
  814. self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
  815. def buildLookups_(self, tag):
  816. assert tag in ("GPOS", "GSUB"), tag
  817. for lookup in self.lookups_:
  818. lookup.lookup_index = None
  819. lookups = []
  820. for lookup in self.lookups_:
  821. if lookup.table != tag:
  822. continue
  823. name = self.get_lookup_name_(lookup)
  824. resolved = lookup.promote_lookup_type(is_named_lookup=name is not None)
  825. if resolved is None:
  826. raise FeatureLibError(
  827. "Within a named lookup block, all rules must be of "
  828. "the same lookup type and flag",
  829. lookup.location,
  830. )
  831. for l in resolved:
  832. lookup.lookup_index = len(lookups)
  833. self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
  834. location=str(lookup.location),
  835. name=name,
  836. feature=None,
  837. )
  838. lookups.append(l)
  839. otLookups = []
  840. for l in lookups:
  841. try:
  842. otLookups.append(l.build())
  843. except OpenTypeLibError as e:
  844. raise FeatureLibError(str(e), e.location) from e
  845. except Exception as e:
  846. location = self.lookup_locations[tag][str(l.lookup_index)].location
  847. raise FeatureLibError(str(e), location) from e
  848. return otLookups
  849. def makeTable(self, tag):
  850. table = getattr(otTables, tag, None)()
  851. table.Version = 0x00010000
  852. table.ScriptList = otTables.ScriptList()
  853. table.ScriptList.ScriptRecord = []
  854. table.FeatureList = otTables.FeatureList()
  855. table.FeatureList.FeatureRecord = []
  856. table.LookupList = otTables.LookupList()
  857. table.LookupList.Lookup = self.buildLookups_(tag)
  858. # Build a table for mapping (tag, lookup_indices) to feature_index.
  859. # For example, ('liga', (2,3,7)) --> 23.
  860. feature_indices = {}
  861. required_feature_indices = {} # ('latn', 'DEU') --> 23
  862. scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24
  863. # Sort the feature table by feature tag:
  864. # https://github.com/fonttools/fonttools/issues/568
  865. sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1])
  866. for key, lookups in sorted(self.features_.items(), key=sortFeatureTag):
  867. script, lang, feature_tag = key
  868. # l.lookup_index will be None when a lookup is not needed
  869. # for the table under construction. For example, substitution
  870. # rules will have no lookup_index while building GPOS tables.
  871. # We also deduplicate lookup indices, as they only get applied once
  872. # within a given feature:
  873. # https://github.com/fonttools/fonttools/issues/2946
  874. lookup_indices = tuple(
  875. dict.fromkeys(
  876. l.lookup_index for l in lookups if l.lookup_index is not None
  877. )
  878. )
  879. # order doesn't matter, but lookup_indices preserves it.
  880. # We want to combine identical sets of lookups (order doesn't matter)
  881. # but also respect the order provided by the user (although there's
  882. # a reasonable argument to just sort and dedupe, which fontc does)
  883. lookup_key = frozenset(lookup_indices)
  884. size_feature = tag == "GPOS" and feature_tag == "size"
  885. force_feature = self.any_feature_variations(feature_tag, tag)
  886. if len(lookup_indices) == 0 and not size_feature and not force_feature:
  887. continue
  888. for ix in lookup_indices:
  889. try:
  890. self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
  891. str(ix)
  892. ]._replace(feature=key)
  893. except KeyError:
  894. warnings.warn(
  895. "feaLib.Builder subclass needs upgrading to "
  896. "stash debug information. See fonttools#2065."
  897. )
  898. feature_key = (feature_tag, lookup_key)
  899. feature_index = feature_indices.get(feature_key)
  900. if feature_index is None:
  901. feature_index = len(table.FeatureList.FeatureRecord)
  902. frec = otTables.FeatureRecord()
  903. frec.FeatureTag = feature_tag
  904. frec.Feature = otTables.Feature()
  905. frec.Feature.FeatureParams = self.buildFeatureParams(feature_tag)
  906. frec.Feature.LookupListIndex = list(lookup_indices)
  907. frec.Feature.LookupCount = len(lookup_indices)
  908. table.FeatureList.FeatureRecord.append(frec)
  909. feature_indices[feature_key] = feature_index
  910. scripts.setdefault(script, {}).setdefault(lang, []).append(feature_index)
  911. if self.required_features_.get((script, lang)) == feature_tag:
  912. required_feature_indices[(script, lang)] = feature_index
  913. # Build ScriptList.
  914. for script, lang_features in sorted(scripts.items()):
  915. srec = otTables.ScriptRecord()
  916. srec.ScriptTag = script
  917. srec.Script = otTables.Script()
  918. srec.Script.DefaultLangSys = None
  919. srec.Script.LangSysRecord = []
  920. for lang, feature_indices in sorted(lang_features.items()):
  921. langrec = otTables.LangSysRecord()
  922. langrec.LangSys = otTables.LangSys()
  923. langrec.LangSys.LookupOrder = None
  924. req_feature_index = required_feature_indices.get((script, lang))
  925. if req_feature_index is None:
  926. langrec.LangSys.ReqFeatureIndex = 0xFFFF
  927. else:
  928. langrec.LangSys.ReqFeatureIndex = req_feature_index
  929. langrec.LangSys.FeatureIndex = [
  930. i for i in feature_indices if i != req_feature_index
  931. ]
  932. langrec.LangSys.FeatureCount = len(langrec.LangSys.FeatureIndex)
  933. if lang == "dflt":
  934. srec.Script.DefaultLangSys = langrec.LangSys
  935. else:
  936. langrec.LangSysTag = lang
  937. srec.Script.LangSysRecord.append(langrec)
  938. srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
  939. table.ScriptList.ScriptRecord.append(srec)
  940. table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
  941. table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
  942. table.LookupList.LookupCount = len(table.LookupList.Lookup)
  943. return table
  944. def makeFeatureVariations(self, table, table_tag):
  945. feature_vars = {}
  946. has_any_variations = False
  947. # Sort out which lookups to build, gather their indices
  948. for (_, _, feature_tag), variations in self.feature_variations_.items():
  949. feature_vars[feature_tag] = []
  950. for conditionset, builders in variations.items():
  951. raw_conditionset = self.conditionsets_[conditionset]
  952. indices = []
  953. for b in builders:
  954. if b.table != table_tag:
  955. continue
  956. assert b.lookup_index is not None
  957. indices.append(b.lookup_index)
  958. has_any_variations = True
  959. feature_vars[feature_tag].append((raw_conditionset, indices))
  960. if has_any_variations:
  961. for feature_tag, conditions_and_lookups in feature_vars.items():
  962. addFeatureVariationsRaw(
  963. self.font, table, conditions_and_lookups, feature_tag
  964. )
  965. def any_feature_variations(self, feature_tag, table_tag):
  966. for (_, _, feature), variations in self.feature_variations_.items():
  967. if feature != feature_tag:
  968. continue
  969. for conditionset, builders in variations.items():
  970. if any(b.table == table_tag for b in builders):
  971. return True
  972. return False
  973. def get_lookup_name_(self, lookup):
  974. rev = {v: k for k, v in self.named_lookups_.items()}
  975. if lookup in rev:
  976. return rev[lookup]
  977. return None
  978. def add_language_system(self, location, script, language):
  979. # OpenType Feature File Specification, section 4.b.i
  980. if script == "DFLT" and language == "dflt" and self.default_language_systems_:
  981. raise FeatureLibError(
  982. 'If "languagesystem DFLT dflt" is present, it must be '
  983. "the first of the languagesystem statements",
  984. location,
  985. )
  986. if script == "DFLT":
  987. if self.seen_non_DFLT_script_:
  988. raise FeatureLibError(
  989. 'languagesystems using the "DFLT" script tag must '
  990. "precede all other languagesystems",
  991. location,
  992. )
  993. else:
  994. self.seen_non_DFLT_script_ = True
  995. if (script, language) in self.default_language_systems_:
  996. raise FeatureLibError(
  997. '"languagesystem %s %s" has already been specified'
  998. % (script.strip(), language.strip()),
  999. location,
  1000. )
  1001. self.default_language_systems_.add((script, language))
  1002. def get_default_language_systems_(self):
  1003. # OpenType Feature File specification, 4.b.i. languagesystem:
  1004. # If no "languagesystem" statement is present, then the
  1005. # implementation must behave exactly as though the following
  1006. # statement were present at the beginning of the feature file:
  1007. # languagesystem DFLT dflt;
  1008. if self.default_language_systems_:
  1009. return frozenset(self.default_language_systems_)
  1010. else:
  1011. return frozenset({("DFLT", "dflt")})
  1012. def start_feature(self, location, name, use_extension=False):
  1013. if use_extension and name != "aalt":
  1014. raise FeatureLibError(
  1015. "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature",
  1016. location,
  1017. )
  1018. self.language_systems = self.get_default_language_systems_()
  1019. self.script_ = "DFLT"
  1020. self.cur_lookup_ = None
  1021. self.cur_feature_name_ = name
  1022. self.lookupflag_ = 0
  1023. self.lookupflag_markFilterSet_ = None
  1024. self.use_extension_ = use_extension
  1025. if name == "aalt":
  1026. self.aalt_location_ = location
  1027. self.aalt_use_extension_ = use_extension
  1028. def end_feature(self):
  1029. assert self.cur_feature_name_ is not None
  1030. self.cur_feature_name_ = None
  1031. self.language_systems = None
  1032. self.cur_lookup_ = None
  1033. self.lookupflag_ = 0
  1034. self.lookupflag_markFilterSet_ = None
  1035. self.use_extension_ = False
  1036. def start_lookup_block(self, location, name, use_extension=False):
  1037. if name in self.named_lookups_:
  1038. raise FeatureLibError(
  1039. 'Lookup "%s" has already been defined' % name, location
  1040. )
  1041. if self.cur_feature_name_ == "aalt":
  1042. raise FeatureLibError(
  1043. "Lookup blocks cannot be placed inside 'aalt' features; "
  1044. "move it out, and then refer to it with a lookup statement",
  1045. location,
  1046. )
  1047. self.cur_lookup_name_ = name
  1048. self.named_lookups_[name] = None
  1049. self.cur_lookup_ = None
  1050. self.use_extension_ = use_extension
  1051. if self.cur_feature_name_ is None:
  1052. self.lookupflag_ = 0
  1053. self.lookupflag_markFilterSet_ = None
  1054. def end_lookup_block(self):
  1055. assert self.cur_lookup_name_ is not None
  1056. self.cur_lookup_name_ = None
  1057. self.cur_lookup_ = None
  1058. self.use_extension_ = False
  1059. if self.cur_feature_name_ is None:
  1060. self.lookupflag_ = 0
  1061. self.lookupflag_markFilterSet_ = None
  1062. def add_lookup_call(self, lookup_name):
  1063. assert lookup_name in self.named_lookups_, lookup_name
  1064. self.cur_lookup_ = None
  1065. lookup = self.named_lookups_[lookup_name]
  1066. if lookup is not None: # skip empty named lookup
  1067. self.add_lookup_to_feature_(lookup, self.cur_feature_name_)
  1068. def set_font_revision(self, location, revision):
  1069. self.fontRevision_ = revision
  1070. def set_language(self, location, language, include_default, required):
  1071. assert len(language) == 4
  1072. if self.cur_feature_name_ in ("aalt", "size"):
  1073. raise FeatureLibError(
  1074. "Language statements are not allowed "
  1075. 'within "feature %s"' % self.cur_feature_name_,
  1076. location,
  1077. )
  1078. if self.cur_feature_name_ is None:
  1079. raise FeatureLibError(
  1080. "Language statements are not allowed "
  1081. "within standalone lookup blocks",
  1082. location,
  1083. )
  1084. self.cur_lookup_ = None
  1085. key = (self.script_, language, self.cur_feature_name_)
  1086. lookups = self.features_.get((key[0], "dflt", key[2]))
  1087. if (language == "dflt" or include_default) and lookups:
  1088. self.features_[key] = lookups[:]
  1089. else:
  1090. # if we aren't including default we need to manually remove the
  1091. # default lookups, which were added to all declared langsystems
  1092. # as they were encountered (we don't remove all lookups because
  1093. # we want to allow duplicate script/lang statements;
  1094. # see https://github.com/fonttools/fonttools/issues/3748
  1095. cur_lookups = self.features_.get(key, [])
  1096. self.features_[key] = [x for x in cur_lookups if x not in lookups]
  1097. self.language_systems = frozenset([(self.script_, language)])
  1098. if required:
  1099. key = (self.script_, language)
  1100. if key in self.required_features_:
  1101. raise FeatureLibError(
  1102. "Language %s (script %s) has already "
  1103. "specified feature %s as its required feature"
  1104. % (
  1105. language.strip(),
  1106. self.script_.strip(),
  1107. self.required_features_[key].strip(),
  1108. ),
  1109. location,
  1110. )
  1111. self.required_features_[key] = self.cur_feature_name_
  1112. def getMarkAttachClass_(self, location, glyphs):
  1113. glyphs = frozenset(glyphs)
  1114. id_ = self.markAttachClassID_.get(glyphs)
  1115. if id_ is not None:
  1116. return id_
  1117. id_ = len(self.markAttachClassID_) + 1
  1118. self.markAttachClassID_[glyphs] = id_
  1119. for glyph in glyphs:
  1120. if glyph in self.markAttach_:
  1121. _, loc = self.markAttach_[glyph]
  1122. raise FeatureLibError(
  1123. "Glyph %s already has been assigned "
  1124. "a MarkAttachmentType at %s" % (glyph, loc),
  1125. location,
  1126. )
  1127. self.markAttach_[glyph] = (id_, location)
  1128. return id_
  1129. def getMarkFilterSet_(self, location, glyphs):
  1130. glyphs = frozenset(glyphs)
  1131. id_ = self.markFilterSets_.get(glyphs)
  1132. if id_ is not None:
  1133. return id_
  1134. id_ = len(self.markFilterSets_)
  1135. self.markFilterSets_[glyphs] = id_
  1136. return id_
  1137. def set_lookup_flag(self, location, value, markAttach, markFilter):
  1138. value = value & 0xFF
  1139. if markAttach is not None:
  1140. markAttachClass = self.getMarkAttachClass_(location, markAttach)
  1141. value = value | (markAttachClass << 8)
  1142. if markFilter is not None:
  1143. markFilterSet = self.getMarkFilterSet_(location, markFilter)
  1144. value = value | 0x10
  1145. self.lookupflag_markFilterSet_ = markFilterSet
  1146. else:
  1147. self.lookupflag_markFilterSet_ = None
  1148. self.lookupflag_ = value
  1149. def set_script(self, location, script):
  1150. if self.cur_feature_name_ in ("aalt", "size"):
  1151. raise FeatureLibError(
  1152. "Script statements are not allowed "
  1153. 'within "feature %s"' % self.cur_feature_name_,
  1154. location,
  1155. )
  1156. if self.cur_feature_name_ is None:
  1157. raise FeatureLibError(
  1158. "Script statements are not allowed " "within standalone lookup blocks",
  1159. location,
  1160. )
  1161. if self.language_systems == {(script, "dflt")}:
  1162. # Nothing to do.
  1163. return
  1164. self.cur_lookup_ = None
  1165. self.script_ = script
  1166. self.lookupflag_ = 0
  1167. self.lookupflag_markFilterSet_ = None
  1168. self.set_language(location, "dflt", include_default=True, required=False)
  1169. def find_lookup_builders_(self, lookups):
  1170. """Helper for building chain contextual substitutions
  1171. Given a list of lookup names, finds the LookupBuilder for each name.
  1172. If an input name is None, it gets mapped to a None LookupBuilder.
  1173. """
  1174. lookup_builders = []
  1175. for lookuplist in lookups:
  1176. if lookuplist is not None:
  1177. lookup_builders.append(
  1178. [self.named_lookups_.get(l.name) for l in lookuplist]
  1179. )
  1180. else:
  1181. lookup_builders.append(None)
  1182. return lookup_builders
  1183. def add_attach_points(self, location, glyphs, contourPoints):
  1184. for glyph in glyphs:
  1185. self.attachPoints_.setdefault(glyph, set()).update(contourPoints)
  1186. def add_feature_reference(self, location, featureName):
  1187. if self.cur_feature_name_ != "aalt":
  1188. raise FeatureLibError(
  1189. 'Feature references are only allowed inside "feature aalt"', location
  1190. )
  1191. self.aalt_features_.append((location, featureName))
  1192. def add_featureName(self, tag):
  1193. self.featureNames_.add(tag)
  1194. def add_cv_parameter(self, tag):
  1195. self.cv_parameters_.add(tag)
  1196. def add_to_cv_num_named_params(self, tag):
  1197. """Adds new items to ``self.cv_num_named_params_``
  1198. or increments the count of existing items."""
  1199. if tag in self.cv_num_named_params_:
  1200. self.cv_num_named_params_[tag] += 1
  1201. else:
  1202. self.cv_num_named_params_[tag] = 1
  1203. def add_cv_character(self, character, tag):
  1204. self.cv_characters_[tag].append(character)
  1205. def set_base_axis(self, bases, scripts, vertical, minmax=[]):
  1206. if vertical:
  1207. self.base_vert_axis_ = (bases, scripts, minmax)
  1208. else:
  1209. self.base_horiz_axis_ = (bases, scripts, minmax)
  1210. def set_size_parameters(
  1211. self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd
  1212. ):
  1213. if self.cur_feature_name_ != "size":
  1214. raise FeatureLibError(
  1215. "Parameters statements are not allowed "
  1216. 'within "feature %s"' % self.cur_feature_name_,
  1217. location,
  1218. )
  1219. self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd]
  1220. for script, lang in self.language_systems:
  1221. key = (script, lang, self.cur_feature_name_)
  1222. self.features_.setdefault(key, [])
  1223. # GSUB rules
  1224. def add_any_subst_(self, location, mapping):
  1225. lookup = self.get_lookup_(location, AnySubstBuilder, mapping=mapping)
  1226. for key, value in mapping.items():
  1227. if key in lookup.mapping:
  1228. if value == lookup.mapping[key]:
  1229. log.info(
  1230. 'Removing duplicate substitution from "%s" to "%s" at %s',
  1231. ", ".join(key),
  1232. ", ".join(value),
  1233. location,
  1234. )
  1235. else:
  1236. raise FeatureLibError(
  1237. 'Already defined substitution for "%s"' % ", ".join(key),
  1238. location,
  1239. )
  1240. lookup.mapping[key] = value
  1241. # GSUB 1
  1242. def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
  1243. if self.cur_feature_name_ == "aalt":
  1244. for from_glyph, to_glyph in mapping.items():
  1245. alts = self.aalt_alternates_.setdefault(from_glyph, [])
  1246. if to_glyph not in alts:
  1247. alts.append(to_glyph)
  1248. return
  1249. if prefix or suffix or forceChain:
  1250. self.add_single_subst_chained_(location, prefix, suffix, mapping)
  1251. return
  1252. self.add_any_subst_(
  1253. location,
  1254. {(key,): (value,) for key, value in mapping.items()},
  1255. )
  1256. # GSUB 2
  1257. def add_multiple_subst(
  1258. self, location, prefix, glyph, suffix, replacements, forceChain=False
  1259. ):
  1260. if prefix or suffix or forceChain:
  1261. self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements)
  1262. return
  1263. self.add_any_subst_(
  1264. location,
  1265. {(glyph,): tuple(replacements)},
  1266. )
  1267. # GSUB 3
  1268. def add_alternate_subst(self, location, prefix, glyph, suffix, replacement):
  1269. if self.cur_feature_name_ == "aalt":
  1270. alts = self.aalt_alternates_.setdefault(glyph, [])
  1271. alts.extend(g for g in replacement if g not in alts)
  1272. return
  1273. if prefix or suffix:
  1274. chain = self.get_lookup_(location, ChainContextSubstBuilder)
  1275. lookup = chain.find_chainable_alternate_subst(glyph)
  1276. if lookup is None:
  1277. lookup = self.get_chained_lookup_(location, AlternateSubstBuilder)
  1278. self._add_contextual_rule(chain, prefix, [{glyph}], suffix, [lookup])
  1279. else:
  1280. lookup = self.get_lookup_(location, AlternateSubstBuilder)
  1281. if glyph in lookup.alternates:
  1282. raise FeatureLibError(
  1283. 'Already defined alternates for glyph "%s"' % glyph, location
  1284. )
  1285. # We allow empty replacement glyphs here.
  1286. lookup.alternates[glyph] = replacement
  1287. # GSUB 4
  1288. def add_ligature_subst(
  1289. self, location, prefix, glyphs, suffix, replacement, forceChain
  1290. ):
  1291. if prefix or suffix or forceChain:
  1292. self.add_ligature_subst_chained_(
  1293. location, prefix, glyphs, suffix, replacement
  1294. )
  1295. return
  1296. if not all(glyphs):
  1297. raise FeatureLibError("Empty glyph class in substitution", location)
  1298. # OpenType feature file syntax, section 5.d, "Ligature substitution":
  1299. # "Since the OpenType specification does not allow ligature
  1300. # substitutions to be specified on target sequences that contain
  1301. # glyph classes, the implementation software will enumerate
  1302. # all specific glyph sequences if glyph classes are detected"
  1303. self.add_any_subst_(
  1304. location,
  1305. {g: (replacement,) for g in itertools.product(*glyphs)},
  1306. )
  1307. @staticmethod
  1308. def _add_contextual_rule(chain, prefix, glyphs, suffix, lookups):
  1309. """Add a contextual rule, merging with the last rule if possible.
  1310. Consecutive rules that share the same prefix, suffix, lookups, and have
  1311. a single input position can be merged into one rule with broader input
  1312. coverage. This produces more compact binary tables (often Format 3).
  1313. """
  1314. if len(glyphs) == 1 and chain.rules and not chain.rules[-1].is_subtable_break:
  1315. last = chain.rules[-1]
  1316. if (
  1317. len(last.glyphs) == 1
  1318. and last.prefix == prefix
  1319. and last.suffix == suffix
  1320. and last.lookups == lookups
  1321. ):
  1322. if not isinstance(last.glyphs[0], set):
  1323. last.glyphs[0] = set(last.glyphs[0])
  1324. last.glyphs[0].update(glyphs[0])
  1325. return
  1326. chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, lookups))
  1327. # GSUB 5/6
  1328. def add_chain_context_subst(self, location, prefix, glyphs, suffix, lookups):
  1329. if not all(glyphs) or not all(prefix) or not all(suffix):
  1330. raise FeatureLibError(
  1331. "Empty glyph class in contextual substitution", location
  1332. )
  1333. lookup = self.get_lookup_(location, ChainContextSubstBuilder)
  1334. resolved = self.find_lookup_builders_(lookups)
  1335. self._add_contextual_rule(lookup, prefix, glyphs, suffix, resolved)
  1336. def add_single_subst_chained_(self, location, prefix, suffix, mapping):
  1337. if not mapping or not all(prefix) or not all(suffix):
  1338. raise FeatureLibError(
  1339. "Empty glyph class in contextual substitution", location
  1340. )
  1341. # https://github.com/fonttools/fonttools/issues/512
  1342. # https://github.com/fonttools/fonttools/issues/2150
  1343. chain = self.get_lookup_(location, ChainContextSubstBuilder)
  1344. sub = chain.find_chainable_subst(mapping, SingleSubstBuilder)
  1345. if sub is None:
  1346. sub = self.get_chained_lookup_(location, SingleSubstBuilder)
  1347. sub.mapping.update(mapping)
  1348. keys = set(mapping.keys())
  1349. self._add_contextual_rule(chain, prefix, [keys], suffix, [sub])
  1350. def add_multi_subst_chained_(self, location, prefix, glyph, suffix, replacements):
  1351. if not all(prefix) or not all(suffix):
  1352. raise FeatureLibError(
  1353. "Empty glyph class in contextual substitution", location
  1354. )
  1355. # https://github.com/fonttools/fonttools/issues/3551
  1356. chain = self.get_lookup_(location, ChainContextSubstBuilder)
  1357. sub = chain.find_chainable_subst({glyph: replacements}, MultipleSubstBuilder)
  1358. if sub is None:
  1359. sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
  1360. sub.mapping[glyph] = replacements
  1361. # https://github.com/fonttools/fonttools/issues/4016
  1362. self._add_contextual_rule(chain, prefix, [{glyph}], suffix, [sub])
  1363. def add_ligature_subst_chained_(
  1364. self, location, prefix, glyphs, suffix, replacement
  1365. ):
  1366. # https://github.com/fonttools/fonttools/issues/3701
  1367. if not all(prefix) or not all(suffix):
  1368. raise FeatureLibError(
  1369. "Empty glyph class in contextual substitution", location
  1370. )
  1371. chain = self.get_lookup_(location, ChainContextSubstBuilder)
  1372. sub = chain.find_chainable_ligature_subst(glyphs, replacement)
  1373. if sub is None:
  1374. sub = self.get_chained_lookup_(location, LigatureSubstBuilder)
  1375. for g in itertools.product(*glyphs):
  1376. existing = sub.ligatures.get(g, replacement)
  1377. if existing != replacement:
  1378. raise FeatureLibError(
  1379. f"Conflicting ligature sub rules: '{g}' maps to '{existing}' and '{replacement}'",
  1380. location,
  1381. )
  1382. sub.ligatures[g] = replacement
  1383. chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub]))
  1384. # GSUB 8
  1385. def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping):
  1386. if not mapping:
  1387. raise FeatureLibError("Empty glyph class in substitution", location)
  1388. lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder)
  1389. lookup.rules.append((old_prefix, old_suffix, mapping))
  1390. # GPOS rules
  1391. # GPOS 1
  1392. def add_single_pos(self, location, prefix, suffix, pos, forceChain):
  1393. if prefix or suffix or forceChain:
  1394. self.add_single_pos_chained_(location, prefix, suffix, pos)
  1395. else:
  1396. lookup = self.get_lookup_(location, SinglePosBuilder)
  1397. for glyphs, value in pos:
  1398. if not glyphs:
  1399. raise FeatureLibError(
  1400. "Empty glyph class in positioning rule", location
  1401. )
  1402. otValueRecord = self.makeOpenTypeValueRecord(
  1403. location, value, pairPosContext=False
  1404. )
  1405. for glyph in glyphs:
  1406. try:
  1407. lookup.add_pos(location, glyph, otValueRecord)
  1408. except OpenTypeLibError as e:
  1409. raise FeatureLibError(str(e), e.location) from e
  1410. # GPOS 2
  1411. def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2):
  1412. if not glyphclass1 or not glyphclass2:
  1413. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1414. lookup = self.get_lookup_(location, PairPosBuilder)
  1415. v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
  1416. v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
  1417. cls1 = tuple(sorted(set(glyphclass1)))
  1418. cls2 = tuple(sorted(set(glyphclass2)))
  1419. lookup.addClassPair(location, cls1, v1, cls2, v2)
  1420. def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
  1421. if not glyph1 or not glyph2:
  1422. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1423. lookup = self.get_lookup_(location, PairPosBuilder)
  1424. v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True)
  1425. v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True)
  1426. lookup.addGlyphPair(location, glyph1, v1, glyph2, v2)
  1427. # GPOS 3
  1428. def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
  1429. if not glyphclass:
  1430. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1431. lookup = self.get_lookup_(location, CursivePosBuilder)
  1432. lookup.add_attachment(
  1433. location,
  1434. glyphclass,
  1435. self.makeOpenTypeAnchor(location, entryAnchor),
  1436. self.makeOpenTypeAnchor(location, exitAnchor),
  1437. )
  1438. # GPOS 4
  1439. def add_mark_base_pos(self, location, bases, marks):
  1440. builder = self.get_lookup_(location, MarkBasePosBuilder)
  1441. self.add_marks_(location, builder, marks)
  1442. if not bases:
  1443. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1444. for baseAnchor, markClass in marks:
  1445. otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
  1446. for base in bases:
  1447. builder.bases.setdefault(base, {})[markClass.name] = otBaseAnchor
  1448. # GPOS 5
  1449. def add_mark_lig_pos(self, location, ligatures, components):
  1450. builder = self.get_lookup_(location, MarkLigPosBuilder)
  1451. componentAnchors = []
  1452. if not ligatures:
  1453. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1454. for marks in components:
  1455. anchors = {}
  1456. self.add_marks_(location, builder, marks)
  1457. for ligAnchor, markClass in marks:
  1458. anchors[markClass.name] = self.makeOpenTypeAnchor(location, ligAnchor)
  1459. componentAnchors.append(anchors)
  1460. for glyph in ligatures:
  1461. builder.ligatures[glyph] = componentAnchors
  1462. # GPOS 6
  1463. def add_mark_mark_pos(self, location, baseMarks, marks):
  1464. builder = self.get_lookup_(location, MarkMarkPosBuilder)
  1465. self.add_marks_(location, builder, marks)
  1466. if not baseMarks:
  1467. raise FeatureLibError("Empty glyph class in positioning rule", location)
  1468. for baseAnchor, markClass in marks:
  1469. otBaseAnchor = self.makeOpenTypeAnchor(location, baseAnchor)
  1470. for baseMark in baseMarks:
  1471. builder.baseMarks.setdefault(baseMark, {})[
  1472. markClass.name
  1473. ] = otBaseAnchor
  1474. # GPOS 7/8
  1475. def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups):
  1476. if not all(glyphs) or not all(prefix) or not all(suffix):
  1477. raise FeatureLibError(
  1478. "Empty glyph class in contextual positioning rule", location
  1479. )
  1480. lookup = self.get_lookup_(location, ChainContextPosBuilder)
  1481. resolved = self.find_lookup_builders_(lookups)
  1482. self._add_contextual_rule(lookup, prefix, glyphs, suffix, resolved)
  1483. def add_single_pos_chained_(self, location, prefix, suffix, pos):
  1484. if not pos or not all(prefix) or not all(suffix):
  1485. raise FeatureLibError(
  1486. "Empty glyph class in contextual positioning rule", location
  1487. )
  1488. # https://github.com/fonttools/fonttools/issues/514
  1489. chain = self.get_lookup_(location, ChainContextPosBuilder)
  1490. targets = []
  1491. for _, _, _, lookups in chain.rules:
  1492. targets.extend(lookups)
  1493. subs = []
  1494. for glyphs, value in pos:
  1495. if value is None:
  1496. subs.append(None)
  1497. continue
  1498. otValue = self.makeOpenTypeValueRecord(
  1499. location, value, pairPosContext=False
  1500. )
  1501. sub = chain.find_chainable_single_pos(targets, glyphs, otValue)
  1502. if sub is None:
  1503. sub = self.get_chained_lookup_(location, SinglePosBuilder)
  1504. targets.append(sub)
  1505. for glyph in glyphs:
  1506. sub.add_pos(location, glyph, otValue)
  1507. subs.append(sub)
  1508. assert len(pos) == len(subs), (pos, subs)
  1509. glyphs = [g for g, v in pos]
  1510. self._add_contextual_rule(chain, prefix, glyphs, suffix, subs)
  1511. def add_marks_(self, location, lookupBuilder, marks):
  1512. """Helper for add_mark_{base,liga,mark}_pos."""
  1513. for _, markClass in marks:
  1514. for markClassDef in markClass.definitions:
  1515. for mark in markClassDef.glyphs.glyphSet():
  1516. if mark not in lookupBuilder.marks:
  1517. otMarkAnchor = self.makeOpenTypeAnchor(
  1518. location, copy.deepcopy(markClassDef.anchor)
  1519. )
  1520. lookupBuilder.marks[mark] = (markClass.name, otMarkAnchor)
  1521. else:
  1522. existingMarkClass = lookupBuilder.marks[mark][0]
  1523. if markClass.name != existingMarkClass:
  1524. raise FeatureLibError(
  1525. "Glyph %s cannot be in both @%s and @%s"
  1526. % (mark, existingMarkClass, markClass.name),
  1527. location,
  1528. )
  1529. def add_subtable_break(self, location):
  1530. self.cur_lookup_.add_subtable_break(location)
  1531. def setGlyphClass_(self, location, glyph, glyphClass):
  1532. oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
  1533. if oldClass and oldClass != glyphClass:
  1534. raise FeatureLibError(
  1535. "Glyph %s was assigned to a different class at %s"
  1536. % (glyph, oldLocation),
  1537. location,
  1538. )
  1539. self.glyphClassDefs_[glyph] = (glyphClass, location)
  1540. def add_glyphClassDef(
  1541. self, location, baseGlyphs, ligatureGlyphs, markGlyphs, componentGlyphs
  1542. ):
  1543. for glyph in baseGlyphs:
  1544. self.setGlyphClass_(location, glyph, 1)
  1545. for glyph in ligatureGlyphs:
  1546. self.setGlyphClass_(location, glyph, 2)
  1547. for glyph in markGlyphs:
  1548. self.setGlyphClass_(location, glyph, 3)
  1549. for glyph in componentGlyphs:
  1550. self.setGlyphClass_(location, glyph, 4)
  1551. def add_ligatureCaretByIndex_(self, location, glyphs, carets):
  1552. for glyph in glyphs:
  1553. if glyph not in self.ligCaretPoints_:
  1554. self.ligCaretPoints_[glyph] = carets
  1555. def makeLigCaret(self, location, caret):
  1556. if not isinstance(caret, VariableScalar):
  1557. return caret
  1558. default, device = self.makeVariablePos(location, caret)
  1559. if device is not None:
  1560. return (default, device)
  1561. return default
  1562. def add_ligatureCaretByPos_(self, location, glyphs, carets):
  1563. carets = [self.makeLigCaret(location, caret) for caret in carets]
  1564. for glyph in glyphs:
  1565. if glyph not in self.ligCaretCoords_:
  1566. self.ligCaretCoords_[glyph] = carets
  1567. def add_name_record(self, location, nameID, platformID, platEncID, langID, string):
  1568. self.names_.append([nameID, platformID, platEncID, langID, string])
  1569. def add_os2_field(self, key, value):
  1570. self.os2_[key] = value
  1571. def add_hhea_field(self, key, value):
  1572. self.hhea_[key] = value
  1573. def add_vhea_field(self, key, value):
  1574. self.vhea_[key] = value
  1575. def add_conditionset(self, location, key, value):
  1576. if "fvar" not in self.font:
  1577. raise FeatureLibError(
  1578. "Cannot add feature variations to a font without an 'fvar' table",
  1579. location,
  1580. )
  1581. if key in self.conditionsets_:
  1582. raise FeatureLibError(
  1583. f"Condition set '{key}' has the same name as a previous condition set",
  1584. location,
  1585. )
  1586. # Normalize
  1587. axisMap = {
  1588. axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
  1589. for axis in self.axes
  1590. }
  1591. value = {
  1592. tag: (
  1593. normalizeValue(bottom, axisMap[tag]),
  1594. normalizeValue(top, axisMap[tag]),
  1595. )
  1596. for tag, (bottom, top) in value.items()
  1597. }
  1598. # NOTE: This might result in rounding errors (off-by-ones) compared to
  1599. # rules in Designspace files, since we're working with what's in the
  1600. # `avar` table rather than the original values.
  1601. if "avar" in self.font:
  1602. mapping = self.font["avar"].segments
  1603. value = {
  1604. axis: tuple(
  1605. piecewiseLinearMap(v, mapping[axis]) if axis in mapping else v
  1606. for v in condition_range
  1607. )
  1608. for axis, condition_range in value.items()
  1609. }
  1610. self.conditionsets_[key] = value
  1611. def makeVariablePos(
  1612. self, location, varscalar: VariableScalar
  1613. ) -> tuple[int, int | None]:
  1614. """Make a pos statement from a VariableScalar, returning the default
  1615. value, and optionally the variation index if the scalar genuinely
  1616. requires variation too."""
  1617. if self.varstorebuilder is None or self.scalar_builder is None:
  1618. raise FeatureLibError(
  1619. "Can't define a variable scalar in a non-variable font", location
  1620. )
  1621. if not varscalar.does_vary:
  1622. return self.scalar_builder.default_value(varscalar), None
  1623. try:
  1624. default, index = self.scalar_builder.add_to_variation_store(
  1625. varscalar, self.varstorebuilder
  1626. )
  1627. except VarLibError as e:
  1628. raise FeatureLibError(
  1629. "Failed to compute deltas for variable scalar", location
  1630. ) from e
  1631. device = None
  1632. if index is not None and index != 0xFFFFFFFF:
  1633. device = buildVarDevTable(index)
  1634. return default, device
  1635. def makeAnchorPos(self, varscalar, deviceTable, location):
  1636. device = None
  1637. if not isinstance(varscalar, VariableScalar):
  1638. if deviceTable is not None:
  1639. device = otl.buildDevice(dict(deviceTable))
  1640. return varscalar, device
  1641. default, device = self.makeVariablePos(location, varscalar)
  1642. if device is not None and deviceTable is not None:
  1643. raise FeatureLibError(
  1644. "Can't define a device coordinate and variable scalar", location
  1645. )
  1646. return default, device
  1647. def makeOpenTypeAnchor(self, location, anchor):
  1648. """ast.Anchor --> otTables.Anchor"""
  1649. if anchor is None:
  1650. return None
  1651. deviceX, deviceY = None, None
  1652. if anchor.xDeviceTable is not None:
  1653. deviceX = otl.buildDevice(dict(anchor.xDeviceTable))
  1654. if anchor.yDeviceTable is not None:
  1655. deviceY = otl.buildDevice(dict(anchor.yDeviceTable))
  1656. x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location)
  1657. y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location)
  1658. otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY)
  1659. return otlanchor
  1660. _VALUEREC_ATTRS = {
  1661. name[0].lower() + name[1:]: (name, isDevice)
  1662. for _, name, isDevice, _ in otBase.valueRecordFormat
  1663. if not name.startswith("Reserved")
  1664. }
  1665. def makeOpenTypeValueRecord(self, location, v, pairPosContext):
  1666. """ast.ValueRecord --> otBase.ValueRecord"""
  1667. if not v:
  1668. return None
  1669. vr = {}
  1670. for astName, (otName, isDevice) in self._VALUEREC_ATTRS.items():
  1671. val = getattr(v, astName, None)
  1672. if not val:
  1673. continue
  1674. if isDevice:
  1675. vr[otName] = otl.buildDevice(dict(val))
  1676. elif isinstance(val, VariableScalar):
  1677. otDeviceName = otName[0:4] + "Device"
  1678. feaDeviceName = otDeviceName[0].lower() + otDeviceName[1:]
  1679. if getattr(v, feaDeviceName):
  1680. raise FeatureLibError(
  1681. "Can't define a device coordinate and variable scalar", location
  1682. )
  1683. vr[otName], device = self.makeVariablePos(location, val)
  1684. if device is not None:
  1685. vr[otDeviceName] = device
  1686. else:
  1687. vr[otName] = val
  1688. if pairPosContext and not vr:
  1689. vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0}
  1690. valRec = otl.buildValue(vr)
  1691. return valRec