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