builder.py 67 KB

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