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