• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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