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