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