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