xref: /aosp_15_r20/external/fonttools/Lib/fontTools/voltLib/voltToFea.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""\
2*e1fe3e4aSElliott HughesMS VOLT ``.vtp`` to AFDKO ``.fea`` OpenType Layout converter.
3*e1fe3e4aSElliott Hughes
4*e1fe3e4aSElliott HughesUsage
5*e1fe3e4aSElliott Hughes-----
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott HughesTo convert a VTP project file:
8*e1fe3e4aSElliott Hughes
9*e1fe3e4aSElliott Hughes
10*e1fe3e4aSElliott Hughes    $ fonttools voltLib.voltToFea input.vtp output.fea
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott HughesIt is also possible convert font files with `TSIV` table (as saved from Volt),
13*e1fe3e4aSElliott Hughesin this case the glyph names used in the Volt project will be mapped to the
14*e1fe3e4aSElliott Hughesactual glyph names in the font files when written to the feature file:
15*e1fe3e4aSElliott Hughes
16*e1fe3e4aSElliott Hughes    $ fonttools voltLib.voltToFea input.ttf output.fea
17*e1fe3e4aSElliott Hughes
18*e1fe3e4aSElliott HughesThe ``--quiet`` option can be used to suppress warnings.
19*e1fe3e4aSElliott Hughes
20*e1fe3e4aSElliott HughesThe ``--traceback`` can be used to get Python traceback in case of exceptions,
21*e1fe3e4aSElliott Hughesinstead of suppressing the traceback.
22*e1fe3e4aSElliott Hughes
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott HughesLimitations
25*e1fe3e4aSElliott Hughes-----------
26*e1fe3e4aSElliott Hughes
27*e1fe3e4aSElliott Hughes* Not all VOLT features are supported, the script will error if it it
28*e1fe3e4aSElliott Hughes  encounters something it does not understand. Please report an issue if this
29*e1fe3e4aSElliott Hughes  happens.
30*e1fe3e4aSElliott Hughes* AFDKO feature file syntax for mark positioning is awkward and does not allow
31*e1fe3e4aSElliott Hughes  setting the mark coverage. It also defines mark anchors globally, as a result
32*e1fe3e4aSElliott Hughes  some mark positioning lookups might cover many marks than what was in the VOLT
33*e1fe3e4aSElliott Hughes  file. This should not be an issue in practice, but if it is then the only way
34*e1fe3e4aSElliott Hughes  is to modify the VOLT file or the generated feature file manually to use unique
35*e1fe3e4aSElliott Hughes  mark anchors for each lookup.
36*e1fe3e4aSElliott Hughes* VOLT allows subtable breaks in any lookup type, but AFDKO feature file
37*e1fe3e4aSElliott Hughes  implementations vary in their support; currently AFDKO’s makeOTF supports
38*e1fe3e4aSElliott Hughes  subtable breaks in pair positioning lookups only, while FontTools’ feaLib
39*e1fe3e4aSElliott Hughes  support it for most substitution lookups and only some positioning lookups.
40*e1fe3e4aSElliott Hughes"""
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughesimport logging
43*e1fe3e4aSElliott Hughesimport re
44*e1fe3e4aSElliott Hughesfrom io import StringIO
45*e1fe3e4aSElliott Hughes
46*e1fe3e4aSElliott Hughesfrom fontTools.feaLib import ast
47*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont, TTLibError
48*e1fe3e4aSElliott Hughesfrom fontTools.voltLib import ast as VAst
49*e1fe3e4aSElliott Hughesfrom fontTools.voltLib.parser import Parser as VoltParser
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.voltLib.voltToFea")
52*e1fe3e4aSElliott Hughes
53*e1fe3e4aSElliott HughesTABLES = ["GDEF", "GSUB", "GPOS"]
54*e1fe3e4aSElliott Hughes
55*e1fe3e4aSElliott Hughes
56*e1fe3e4aSElliott Hughesclass MarkClassDefinition(ast.MarkClassDefinition):
57*e1fe3e4aSElliott Hughes    def asFea(self, indent=""):
58*e1fe3e4aSElliott Hughes        res = ""
59*e1fe3e4aSElliott Hughes        if not getattr(self, "used", False):
60*e1fe3e4aSElliott Hughes            res += "#"
61*e1fe3e4aSElliott Hughes        res += ast.MarkClassDefinition.asFea(self, indent)
62*e1fe3e4aSElliott Hughes        return res
63*e1fe3e4aSElliott Hughes
64*e1fe3e4aSElliott Hughes
65*e1fe3e4aSElliott Hughes# For sorting voltLib.ast.GlyphDefinition, see its use below.
66*e1fe3e4aSElliott Hughesclass Group:
67*e1fe3e4aSElliott Hughes    def __init__(self, group):
68*e1fe3e4aSElliott Hughes        self.name = group.name.lower()
69*e1fe3e4aSElliott Hughes        self.groups = [
70*e1fe3e4aSElliott Hughes            x.group.lower() for x in group.enum.enum if isinstance(x, VAst.GroupName)
71*e1fe3e4aSElliott Hughes        ]
72*e1fe3e4aSElliott Hughes
73*e1fe3e4aSElliott Hughes    def __lt__(self, other):
74*e1fe3e4aSElliott Hughes        if self.name in other.groups:
75*e1fe3e4aSElliott Hughes            return True
76*e1fe3e4aSElliott Hughes        if other.name in self.groups:
77*e1fe3e4aSElliott Hughes            return False
78*e1fe3e4aSElliott Hughes        if self.groups and not other.groups:
79*e1fe3e4aSElliott Hughes            return False
80*e1fe3e4aSElliott Hughes        if not self.groups and other.groups:
81*e1fe3e4aSElliott Hughes            return True
82*e1fe3e4aSElliott Hughes
83*e1fe3e4aSElliott Hughes
84*e1fe3e4aSElliott Hughesclass VoltToFea:
85*e1fe3e4aSElliott Hughes    _NOT_LOOKUP_NAME_RE = re.compile(r"[^A-Za-z_0-9.]")
86*e1fe3e4aSElliott Hughes    _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]")
87*e1fe3e4aSElliott Hughes
88*e1fe3e4aSElliott Hughes    def __init__(self, file_or_path, font=None):
89*e1fe3e4aSElliott Hughes        self._file_or_path = file_or_path
90*e1fe3e4aSElliott Hughes        self._font = font
91*e1fe3e4aSElliott Hughes
92*e1fe3e4aSElliott Hughes        self._glyph_map = {}
93*e1fe3e4aSElliott Hughes        self._glyph_order = None
94*e1fe3e4aSElliott Hughes
95*e1fe3e4aSElliott Hughes        self._gdef = {}
96*e1fe3e4aSElliott Hughes        self._glyphclasses = {}
97*e1fe3e4aSElliott Hughes        self._features = {}
98*e1fe3e4aSElliott Hughes        self._lookups = {}
99*e1fe3e4aSElliott Hughes
100*e1fe3e4aSElliott Hughes        self._marks = set()
101*e1fe3e4aSElliott Hughes        self._ligatures = {}
102*e1fe3e4aSElliott Hughes
103*e1fe3e4aSElliott Hughes        self._markclasses = {}
104*e1fe3e4aSElliott Hughes        self._anchors = {}
105*e1fe3e4aSElliott Hughes
106*e1fe3e4aSElliott Hughes        self._settings = {}
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughes        self._lookup_names = {}
109*e1fe3e4aSElliott Hughes        self._class_names = {}
110*e1fe3e4aSElliott Hughes
111*e1fe3e4aSElliott Hughes    def _lookupName(self, name):
112*e1fe3e4aSElliott Hughes        if name not in self._lookup_names:
113*e1fe3e4aSElliott Hughes            res = self._NOT_LOOKUP_NAME_RE.sub("_", name)
114*e1fe3e4aSElliott Hughes            while res in self._lookup_names.values():
115*e1fe3e4aSElliott Hughes                res += "_"
116*e1fe3e4aSElliott Hughes            self._lookup_names[name] = res
117*e1fe3e4aSElliott Hughes        return self._lookup_names[name]
118*e1fe3e4aSElliott Hughes
119*e1fe3e4aSElliott Hughes    def _className(self, name):
120*e1fe3e4aSElliott Hughes        if name not in self._class_names:
121*e1fe3e4aSElliott Hughes            res = self._NOT_CLASS_NAME_RE.sub("_", name)
122*e1fe3e4aSElliott Hughes            while res in self._class_names.values():
123*e1fe3e4aSElliott Hughes                res += "_"
124*e1fe3e4aSElliott Hughes            self._class_names[name] = res
125*e1fe3e4aSElliott Hughes        return self._class_names[name]
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes    def _collectStatements(self, doc, tables):
128*e1fe3e4aSElliott Hughes        # Collect and sort group definitions first, to make sure a group
129*e1fe3e4aSElliott Hughes        # definition that references other groups comes after them since VOLT
130*e1fe3e4aSElliott Hughes        # does not enforce such ordering, and feature file require it.
131*e1fe3e4aSElliott Hughes        groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)]
132*e1fe3e4aSElliott Hughes        for statement in sorted(groups, key=lambda x: Group(x)):
133*e1fe3e4aSElliott Hughes            self._groupDefinition(statement)
134*e1fe3e4aSElliott Hughes
135*e1fe3e4aSElliott Hughes        for statement in doc.statements:
136*e1fe3e4aSElliott Hughes            if isinstance(statement, VAst.GlyphDefinition):
137*e1fe3e4aSElliott Hughes                self._glyphDefinition(statement)
138*e1fe3e4aSElliott Hughes            elif isinstance(statement, VAst.AnchorDefinition):
139*e1fe3e4aSElliott Hughes                if "GPOS" in tables:
140*e1fe3e4aSElliott Hughes                    self._anchorDefinition(statement)
141*e1fe3e4aSElliott Hughes            elif isinstance(statement, VAst.SettingDefinition):
142*e1fe3e4aSElliott Hughes                self._settingDefinition(statement)
143*e1fe3e4aSElliott Hughes            elif isinstance(statement, VAst.GroupDefinition):
144*e1fe3e4aSElliott Hughes                pass  # Handled above
145*e1fe3e4aSElliott Hughes            elif isinstance(statement, VAst.ScriptDefinition):
146*e1fe3e4aSElliott Hughes                self._scriptDefinition(statement)
147*e1fe3e4aSElliott Hughes            elif not isinstance(statement, VAst.LookupDefinition):
148*e1fe3e4aSElliott Hughes                raise NotImplementedError(statement)
149*e1fe3e4aSElliott Hughes
150*e1fe3e4aSElliott Hughes        # Lookup definitions need to be handled last as they reference glyph
151*e1fe3e4aSElliott Hughes        # and mark classes that might be defined after them.
152*e1fe3e4aSElliott Hughes        for statement in doc.statements:
153*e1fe3e4aSElliott Hughes            if isinstance(statement, VAst.LookupDefinition):
154*e1fe3e4aSElliott Hughes                if statement.pos and "GPOS" not in tables:
155*e1fe3e4aSElliott Hughes                    continue
156*e1fe3e4aSElliott Hughes                if statement.sub and "GSUB" not in tables:
157*e1fe3e4aSElliott Hughes                    continue
158*e1fe3e4aSElliott Hughes                self._lookupDefinition(statement)
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughes    def _buildFeatureFile(self, tables):
161*e1fe3e4aSElliott Hughes        doc = ast.FeatureFile()
162*e1fe3e4aSElliott Hughes        statements = doc.statements
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes        if self._glyphclasses:
165*e1fe3e4aSElliott Hughes            statements.append(ast.Comment("# Glyph classes"))
166*e1fe3e4aSElliott Hughes            statements.extend(self._glyphclasses.values())
167*e1fe3e4aSElliott Hughes
168*e1fe3e4aSElliott Hughes        if self._markclasses:
169*e1fe3e4aSElliott Hughes            statements.append(ast.Comment("\n# Mark classes"))
170*e1fe3e4aSElliott Hughes            statements.extend(c[1] for c in sorted(self._markclasses.items()))
171*e1fe3e4aSElliott Hughes
172*e1fe3e4aSElliott Hughes        if self._lookups:
173*e1fe3e4aSElliott Hughes            statements.append(ast.Comment("\n# Lookups"))
174*e1fe3e4aSElliott Hughes            for lookup in self._lookups.values():
175*e1fe3e4aSElliott Hughes                statements.extend(getattr(lookup, "targets", []))
176*e1fe3e4aSElliott Hughes                statements.append(lookup)
177*e1fe3e4aSElliott Hughes
178*e1fe3e4aSElliott Hughes        # Prune features
179*e1fe3e4aSElliott Hughes        features = self._features.copy()
180*e1fe3e4aSElliott Hughes        for ftag in features:
181*e1fe3e4aSElliott Hughes            scripts = features[ftag]
182*e1fe3e4aSElliott Hughes            for stag in scripts:
183*e1fe3e4aSElliott Hughes                langs = scripts[stag]
184*e1fe3e4aSElliott Hughes                for ltag in langs:
185*e1fe3e4aSElliott Hughes                    langs[ltag] = [l for l in langs[ltag] if l.lower() in self._lookups]
186*e1fe3e4aSElliott Hughes                scripts[stag] = {t: l for t, l in langs.items() if l}
187*e1fe3e4aSElliott Hughes            features[ftag] = {t: s for t, s in scripts.items() if s}
188*e1fe3e4aSElliott Hughes        features = {t: f for t, f in features.items() if f}
189*e1fe3e4aSElliott Hughes
190*e1fe3e4aSElliott Hughes        if features:
191*e1fe3e4aSElliott Hughes            statements.append(ast.Comment("# Features"))
192*e1fe3e4aSElliott Hughes            for ftag, scripts in features.items():
193*e1fe3e4aSElliott Hughes                feature = ast.FeatureBlock(ftag)
194*e1fe3e4aSElliott Hughes                stags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1)
195*e1fe3e4aSElliott Hughes                for stag in stags:
196*e1fe3e4aSElliott Hughes                    feature.statements.append(ast.ScriptStatement(stag))
197*e1fe3e4aSElliott Hughes                    ltags = sorted(scripts[stag], key=lambda k: 0 if k == "dflt" else 1)
198*e1fe3e4aSElliott Hughes                    for ltag in ltags:
199*e1fe3e4aSElliott Hughes                        include_default = True if ltag == "dflt" else False
200*e1fe3e4aSElliott Hughes                        feature.statements.append(
201*e1fe3e4aSElliott Hughes                            ast.LanguageStatement(ltag, include_default=include_default)
202*e1fe3e4aSElliott Hughes                        )
203*e1fe3e4aSElliott Hughes                        for name in scripts[stag][ltag]:
204*e1fe3e4aSElliott Hughes                            lookup = self._lookups[name.lower()]
205*e1fe3e4aSElliott Hughes                            lookupref = ast.LookupReferenceStatement(lookup)
206*e1fe3e4aSElliott Hughes                            feature.statements.append(lookupref)
207*e1fe3e4aSElliott Hughes                statements.append(feature)
208*e1fe3e4aSElliott Hughes
209*e1fe3e4aSElliott Hughes        if self._gdef and "GDEF" in tables:
210*e1fe3e4aSElliott Hughes            classes = []
211*e1fe3e4aSElliott Hughes            for name in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
212*e1fe3e4aSElliott Hughes                if name in self._gdef:
213*e1fe3e4aSElliott Hughes                    classname = "GDEF_" + name.lower()
214*e1fe3e4aSElliott Hughes                    glyphclass = ast.GlyphClassDefinition(classname, self._gdef[name])
215*e1fe3e4aSElliott Hughes                    statements.append(glyphclass)
216*e1fe3e4aSElliott Hughes                    classes.append(ast.GlyphClassName(glyphclass))
217*e1fe3e4aSElliott Hughes                else:
218*e1fe3e4aSElliott Hughes                    classes.append(None)
219*e1fe3e4aSElliott Hughes
220*e1fe3e4aSElliott Hughes            gdef = ast.TableBlock("GDEF")
221*e1fe3e4aSElliott Hughes            gdef.statements.append(ast.GlyphClassDefStatement(*classes))
222*e1fe3e4aSElliott Hughes            statements.append(gdef)
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughes        return doc
225*e1fe3e4aSElliott Hughes
226*e1fe3e4aSElliott Hughes    def convert(self, tables=None):
227*e1fe3e4aSElliott Hughes        doc = VoltParser(self._file_or_path).parse()
228*e1fe3e4aSElliott Hughes
229*e1fe3e4aSElliott Hughes        if tables is None:
230*e1fe3e4aSElliott Hughes            tables = TABLES
231*e1fe3e4aSElliott Hughes        if self._font is not None:
232*e1fe3e4aSElliott Hughes            self._glyph_order = self._font.getGlyphOrder()
233*e1fe3e4aSElliott Hughes
234*e1fe3e4aSElliott Hughes        self._collectStatements(doc, tables)
235*e1fe3e4aSElliott Hughes        fea = self._buildFeatureFile(tables)
236*e1fe3e4aSElliott Hughes        return fea.asFea()
237*e1fe3e4aSElliott Hughes
238*e1fe3e4aSElliott Hughes    def _glyphName(self, glyph):
239*e1fe3e4aSElliott Hughes        try:
240*e1fe3e4aSElliott Hughes            name = glyph.glyph
241*e1fe3e4aSElliott Hughes        except AttributeError:
242*e1fe3e4aSElliott Hughes            name = glyph
243*e1fe3e4aSElliott Hughes        return ast.GlyphName(self._glyph_map.get(name, name))
244*e1fe3e4aSElliott Hughes
245*e1fe3e4aSElliott Hughes    def _groupName(self, group):
246*e1fe3e4aSElliott Hughes        try:
247*e1fe3e4aSElliott Hughes            name = group.group
248*e1fe3e4aSElliott Hughes        except AttributeError:
249*e1fe3e4aSElliott Hughes            name = group
250*e1fe3e4aSElliott Hughes        return ast.GlyphClassName(self._glyphclasses[name.lower()])
251*e1fe3e4aSElliott Hughes
252*e1fe3e4aSElliott Hughes    def _coverage(self, coverage):
253*e1fe3e4aSElliott Hughes        items = []
254*e1fe3e4aSElliott Hughes        for item in coverage:
255*e1fe3e4aSElliott Hughes            if isinstance(item, VAst.GlyphName):
256*e1fe3e4aSElliott Hughes                items.append(self._glyphName(item))
257*e1fe3e4aSElliott Hughes            elif isinstance(item, VAst.GroupName):
258*e1fe3e4aSElliott Hughes                items.append(self._groupName(item))
259*e1fe3e4aSElliott Hughes            elif isinstance(item, VAst.Enum):
260*e1fe3e4aSElliott Hughes                items.append(self._enum(item))
261*e1fe3e4aSElliott Hughes            elif isinstance(item, VAst.Range):
262*e1fe3e4aSElliott Hughes                items.append((item.start, item.end))
263*e1fe3e4aSElliott Hughes            else:
264*e1fe3e4aSElliott Hughes                raise NotImplementedError(item)
265*e1fe3e4aSElliott Hughes        return items
266*e1fe3e4aSElliott Hughes
267*e1fe3e4aSElliott Hughes    def _enum(self, enum):
268*e1fe3e4aSElliott Hughes        return ast.GlyphClass(self._coverage(enum.enum))
269*e1fe3e4aSElliott Hughes
270*e1fe3e4aSElliott Hughes    def _context(self, context):
271*e1fe3e4aSElliott Hughes        out = []
272*e1fe3e4aSElliott Hughes        for item in context:
273*e1fe3e4aSElliott Hughes            coverage = self._coverage(item)
274*e1fe3e4aSElliott Hughes            if not isinstance(coverage, (tuple, list)):
275*e1fe3e4aSElliott Hughes                coverage = [coverage]
276*e1fe3e4aSElliott Hughes            out.extend(coverage)
277*e1fe3e4aSElliott Hughes        return out
278*e1fe3e4aSElliott Hughes
279*e1fe3e4aSElliott Hughes    def _groupDefinition(self, group):
280*e1fe3e4aSElliott Hughes        name = self._className(group.name)
281*e1fe3e4aSElliott Hughes        glyphs = self._enum(group.enum)
282*e1fe3e4aSElliott Hughes        glyphclass = ast.GlyphClassDefinition(name, glyphs)
283*e1fe3e4aSElliott Hughes
284*e1fe3e4aSElliott Hughes        self._glyphclasses[group.name.lower()] = glyphclass
285*e1fe3e4aSElliott Hughes
286*e1fe3e4aSElliott Hughes    def _glyphDefinition(self, glyph):
287*e1fe3e4aSElliott Hughes        try:
288*e1fe3e4aSElliott Hughes            self._glyph_map[glyph.name] = self._glyph_order[glyph.id]
289*e1fe3e4aSElliott Hughes        except TypeError:
290*e1fe3e4aSElliott Hughes            pass
291*e1fe3e4aSElliott Hughes
292*e1fe3e4aSElliott Hughes        if glyph.type in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
293*e1fe3e4aSElliott Hughes            if glyph.type not in self._gdef:
294*e1fe3e4aSElliott Hughes                self._gdef[glyph.type] = ast.GlyphClass()
295*e1fe3e4aSElliott Hughes            self._gdef[glyph.type].glyphs.append(self._glyphName(glyph.name))
296*e1fe3e4aSElliott Hughes
297*e1fe3e4aSElliott Hughes        if glyph.type == "MARK":
298*e1fe3e4aSElliott Hughes            self._marks.add(glyph.name)
299*e1fe3e4aSElliott Hughes        elif glyph.type == "LIGATURE":
300*e1fe3e4aSElliott Hughes            self._ligatures[glyph.name] = glyph.components
301*e1fe3e4aSElliott Hughes
302*e1fe3e4aSElliott Hughes    def _scriptDefinition(self, script):
303*e1fe3e4aSElliott Hughes        stag = script.tag
304*e1fe3e4aSElliott Hughes        for lang in script.langs:
305*e1fe3e4aSElliott Hughes            ltag = lang.tag
306*e1fe3e4aSElliott Hughes            for feature in lang.features:
307*e1fe3e4aSElliott Hughes                lookups = {l.split("\\")[0]: True for l in feature.lookups}
308*e1fe3e4aSElliott Hughes                ftag = feature.tag
309*e1fe3e4aSElliott Hughes                if ftag not in self._features:
310*e1fe3e4aSElliott Hughes                    self._features[ftag] = {}
311*e1fe3e4aSElliott Hughes                if stag not in self._features[ftag]:
312*e1fe3e4aSElliott Hughes                    self._features[ftag][stag] = {}
313*e1fe3e4aSElliott Hughes                assert ltag not in self._features[ftag][stag]
314*e1fe3e4aSElliott Hughes                self._features[ftag][stag][ltag] = lookups.keys()
315*e1fe3e4aSElliott Hughes
316*e1fe3e4aSElliott Hughes    def _settingDefinition(self, setting):
317*e1fe3e4aSElliott Hughes        if setting.name.startswith("COMPILER_"):
318*e1fe3e4aSElliott Hughes            self._settings[setting.name] = setting.value
319*e1fe3e4aSElliott Hughes        else:
320*e1fe3e4aSElliott Hughes            log.warning(f"Unsupported setting ignored: {setting.name}")
321*e1fe3e4aSElliott Hughes
322*e1fe3e4aSElliott Hughes    def _adjustment(self, adjustment):
323*e1fe3e4aSElliott Hughes        adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
324*e1fe3e4aSElliott Hughes
325*e1fe3e4aSElliott Hughes        adv_device = adv_adjust_by and adv_adjust_by.items() or None
326*e1fe3e4aSElliott Hughes        dx_device = dx_adjust_by and dx_adjust_by.items() or None
327*e1fe3e4aSElliott Hughes        dy_device = dy_adjust_by and dy_adjust_by.items() or None
328*e1fe3e4aSElliott Hughes
329*e1fe3e4aSElliott Hughes        return ast.ValueRecord(
330*e1fe3e4aSElliott Hughes            xPlacement=dx,
331*e1fe3e4aSElliott Hughes            yPlacement=dy,
332*e1fe3e4aSElliott Hughes            xAdvance=adv,
333*e1fe3e4aSElliott Hughes            xPlaDevice=dx_device,
334*e1fe3e4aSElliott Hughes            yPlaDevice=dy_device,
335*e1fe3e4aSElliott Hughes            xAdvDevice=adv_device,
336*e1fe3e4aSElliott Hughes        )
337*e1fe3e4aSElliott Hughes
338*e1fe3e4aSElliott Hughes    def _anchor(self, adjustment):
339*e1fe3e4aSElliott Hughes        adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
340*e1fe3e4aSElliott Hughes
341*e1fe3e4aSElliott Hughes        assert not adv_adjust_by
342*e1fe3e4aSElliott Hughes        dx_device = dx_adjust_by and dx_adjust_by.items() or None
343*e1fe3e4aSElliott Hughes        dy_device = dy_adjust_by and dy_adjust_by.items() or None
344*e1fe3e4aSElliott Hughes
345*e1fe3e4aSElliott Hughes        return ast.Anchor(
346*e1fe3e4aSElliott Hughes            dx or 0,
347*e1fe3e4aSElliott Hughes            dy or 0,
348*e1fe3e4aSElliott Hughes            xDeviceTable=dx_device or None,
349*e1fe3e4aSElliott Hughes            yDeviceTable=dy_device or None,
350*e1fe3e4aSElliott Hughes        )
351*e1fe3e4aSElliott Hughes
352*e1fe3e4aSElliott Hughes    def _anchorDefinition(self, anchordef):
353*e1fe3e4aSElliott Hughes        anchorname = anchordef.name
354*e1fe3e4aSElliott Hughes        glyphname = anchordef.glyph_name
355*e1fe3e4aSElliott Hughes        anchor = self._anchor(anchordef.pos)
356*e1fe3e4aSElliott Hughes
357*e1fe3e4aSElliott Hughes        if anchorname.startswith("MARK_"):
358*e1fe3e4aSElliott Hughes            name = "_".join(anchorname.split("_")[1:])
359*e1fe3e4aSElliott Hughes            markclass = ast.MarkClass(self._className(name))
360*e1fe3e4aSElliott Hughes            glyph = self._glyphName(glyphname)
361*e1fe3e4aSElliott Hughes            markdef = MarkClassDefinition(markclass, anchor, glyph)
362*e1fe3e4aSElliott Hughes            self._markclasses[(glyphname, anchorname)] = markdef
363*e1fe3e4aSElliott Hughes        else:
364*e1fe3e4aSElliott Hughes            if glyphname not in self._anchors:
365*e1fe3e4aSElliott Hughes                self._anchors[glyphname] = {}
366*e1fe3e4aSElliott Hughes            if anchorname not in self._anchors[glyphname]:
367*e1fe3e4aSElliott Hughes                self._anchors[glyphname][anchorname] = {}
368*e1fe3e4aSElliott Hughes            self._anchors[glyphname][anchorname][anchordef.component] = anchor
369*e1fe3e4aSElliott Hughes
370*e1fe3e4aSElliott Hughes    def _gposLookup(self, lookup, fealookup):
371*e1fe3e4aSElliott Hughes        statements = fealookup.statements
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes        pos = lookup.pos
374*e1fe3e4aSElliott Hughes        if isinstance(pos, VAst.PositionAdjustPairDefinition):
375*e1fe3e4aSElliott Hughes            for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
376*e1fe3e4aSElliott Hughes                coverage_1 = pos.coverages_1[idx1 - 1]
377*e1fe3e4aSElliott Hughes                coverage_2 = pos.coverages_2[idx2 - 1]
378*e1fe3e4aSElliott Hughes
379*e1fe3e4aSElliott Hughes                # If not both are groups, use “enum pos” otherwise makeotf will
380*e1fe3e4aSElliott Hughes                # fail.
381*e1fe3e4aSElliott Hughes                enumerated = False
382*e1fe3e4aSElliott Hughes                for item in coverage_1 + coverage_2:
383*e1fe3e4aSElliott Hughes                    if not isinstance(item, VAst.GroupName):
384*e1fe3e4aSElliott Hughes                        enumerated = True
385*e1fe3e4aSElliott Hughes
386*e1fe3e4aSElliott Hughes                glyphs1 = self._coverage(coverage_1)
387*e1fe3e4aSElliott Hughes                glyphs2 = self._coverage(coverage_2)
388*e1fe3e4aSElliott Hughes                record1 = self._adjustment(pos1)
389*e1fe3e4aSElliott Hughes                record2 = self._adjustment(pos2)
390*e1fe3e4aSElliott Hughes                assert len(glyphs1) == 1
391*e1fe3e4aSElliott Hughes                assert len(glyphs2) == 1
392*e1fe3e4aSElliott Hughes                statements.append(
393*e1fe3e4aSElliott Hughes                    ast.PairPosStatement(
394*e1fe3e4aSElliott Hughes                        glyphs1[0], record1, glyphs2[0], record2, enumerated=enumerated
395*e1fe3e4aSElliott Hughes                    )
396*e1fe3e4aSElliott Hughes                )
397*e1fe3e4aSElliott Hughes        elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
398*e1fe3e4aSElliott Hughes            for a, b in pos.adjust_single:
399*e1fe3e4aSElliott Hughes                glyphs = self._coverage(a)
400*e1fe3e4aSElliott Hughes                record = self._adjustment(b)
401*e1fe3e4aSElliott Hughes                assert len(glyphs) == 1
402*e1fe3e4aSElliott Hughes                statements.append(
403*e1fe3e4aSElliott Hughes                    ast.SinglePosStatement([(glyphs[0], record)], [], [], False)
404*e1fe3e4aSElliott Hughes                )
405*e1fe3e4aSElliott Hughes        elif isinstance(pos, VAst.PositionAttachDefinition):
406*e1fe3e4aSElliott Hughes            anchors = {}
407*e1fe3e4aSElliott Hughes            for marks, classname in pos.coverage_to:
408*e1fe3e4aSElliott Hughes                for mark in marks:
409*e1fe3e4aSElliott Hughes                    # Set actually used mark classes. Basically a hack to get
410*e1fe3e4aSElliott Hughes                    # around the feature file syntax limitation of making mark
411*e1fe3e4aSElliott Hughes                    # classes global and not allowing mark positioning to
412*e1fe3e4aSElliott Hughes                    # specify mark coverage.
413*e1fe3e4aSElliott Hughes                    for name in mark.glyphSet():
414*e1fe3e4aSElliott Hughes                        key = (name, "MARK_" + classname)
415*e1fe3e4aSElliott Hughes                        self._markclasses[key].used = True
416*e1fe3e4aSElliott Hughes                markclass = ast.MarkClass(self._className(classname))
417*e1fe3e4aSElliott Hughes                for base in pos.coverage:
418*e1fe3e4aSElliott Hughes                    for name in base.glyphSet():
419*e1fe3e4aSElliott Hughes                        if name not in anchors:
420*e1fe3e4aSElliott Hughes                            anchors[name] = []
421*e1fe3e4aSElliott Hughes                        if classname not in anchors[name]:
422*e1fe3e4aSElliott Hughes                            anchors[name].append(classname)
423*e1fe3e4aSElliott Hughes
424*e1fe3e4aSElliott Hughes            for name in anchors:
425*e1fe3e4aSElliott Hughes                components = 1
426*e1fe3e4aSElliott Hughes                if name in self._ligatures:
427*e1fe3e4aSElliott Hughes                    components = self._ligatures[name]
428*e1fe3e4aSElliott Hughes
429*e1fe3e4aSElliott Hughes                marks = []
430*e1fe3e4aSElliott Hughes                for mark in anchors[name]:
431*e1fe3e4aSElliott Hughes                    markclass = ast.MarkClass(self._className(mark))
432*e1fe3e4aSElliott Hughes                    for component in range(1, components + 1):
433*e1fe3e4aSElliott Hughes                        if len(marks) < component:
434*e1fe3e4aSElliott Hughes                            marks.append([])
435*e1fe3e4aSElliott Hughes                        anchor = None
436*e1fe3e4aSElliott Hughes                        if component in self._anchors[name][mark]:
437*e1fe3e4aSElliott Hughes                            anchor = self._anchors[name][mark][component]
438*e1fe3e4aSElliott Hughes                        marks[component - 1].append((anchor, markclass))
439*e1fe3e4aSElliott Hughes
440*e1fe3e4aSElliott Hughes                base = self._glyphName(name)
441*e1fe3e4aSElliott Hughes                if name in self._marks:
442*e1fe3e4aSElliott Hughes                    mark = ast.MarkMarkPosStatement(base, marks[0])
443*e1fe3e4aSElliott Hughes                elif name in self._ligatures:
444*e1fe3e4aSElliott Hughes                    mark = ast.MarkLigPosStatement(base, marks)
445*e1fe3e4aSElliott Hughes                else:
446*e1fe3e4aSElliott Hughes                    mark = ast.MarkBasePosStatement(base, marks[0])
447*e1fe3e4aSElliott Hughes                statements.append(mark)
448*e1fe3e4aSElliott Hughes        elif isinstance(pos, VAst.PositionAttachCursiveDefinition):
449*e1fe3e4aSElliott Hughes            # Collect enter and exit glyphs
450*e1fe3e4aSElliott Hughes            enter_coverage = []
451*e1fe3e4aSElliott Hughes            for coverage in pos.coverages_enter:
452*e1fe3e4aSElliott Hughes                for base in coverage:
453*e1fe3e4aSElliott Hughes                    for name in base.glyphSet():
454*e1fe3e4aSElliott Hughes                        enter_coverage.append(name)
455*e1fe3e4aSElliott Hughes            exit_coverage = []
456*e1fe3e4aSElliott Hughes            for coverage in pos.coverages_exit:
457*e1fe3e4aSElliott Hughes                for base in coverage:
458*e1fe3e4aSElliott Hughes                    for name in base.glyphSet():
459*e1fe3e4aSElliott Hughes                        exit_coverage.append(name)
460*e1fe3e4aSElliott Hughes
461*e1fe3e4aSElliott Hughes            # Write enter anchors, also check if the glyph has exit anchor and
462*e1fe3e4aSElliott Hughes            # write it, too.
463*e1fe3e4aSElliott Hughes            for name in enter_coverage:
464*e1fe3e4aSElliott Hughes                glyph = self._glyphName(name)
465*e1fe3e4aSElliott Hughes                entry = self._anchors[name]["entry"][1]
466*e1fe3e4aSElliott Hughes                exit = None
467*e1fe3e4aSElliott Hughes                if name in exit_coverage:
468*e1fe3e4aSElliott Hughes                    exit = self._anchors[name]["exit"][1]
469*e1fe3e4aSElliott Hughes                    exit_coverage.pop(exit_coverage.index(name))
470*e1fe3e4aSElliott Hughes                statements.append(ast.CursivePosStatement(glyph, entry, exit))
471*e1fe3e4aSElliott Hughes
472*e1fe3e4aSElliott Hughes            # Write any remaining exit anchors.
473*e1fe3e4aSElliott Hughes            for name in exit_coverage:
474*e1fe3e4aSElliott Hughes                glyph = self._glyphName(name)
475*e1fe3e4aSElliott Hughes                exit = self._anchors[name]["exit"][1]
476*e1fe3e4aSElliott Hughes                statements.append(ast.CursivePosStatement(glyph, None, exit))
477*e1fe3e4aSElliott Hughes        else:
478*e1fe3e4aSElliott Hughes            raise NotImplementedError(pos)
479*e1fe3e4aSElliott Hughes
480*e1fe3e4aSElliott Hughes    def _gposContextLookup(
481*e1fe3e4aSElliott Hughes        self, lookup, prefix, suffix, ignore, fealookup, targetlookup
482*e1fe3e4aSElliott Hughes    ):
483*e1fe3e4aSElliott Hughes        statements = fealookup.statements
484*e1fe3e4aSElliott Hughes
485*e1fe3e4aSElliott Hughes        assert not lookup.reversal
486*e1fe3e4aSElliott Hughes
487*e1fe3e4aSElliott Hughes        pos = lookup.pos
488*e1fe3e4aSElliott Hughes        if isinstance(pos, VAst.PositionAdjustPairDefinition):
489*e1fe3e4aSElliott Hughes            for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
490*e1fe3e4aSElliott Hughes                glyphs1 = self._coverage(pos.coverages_1[idx1 - 1])
491*e1fe3e4aSElliott Hughes                glyphs2 = self._coverage(pos.coverages_2[idx2 - 1])
492*e1fe3e4aSElliott Hughes                assert len(glyphs1) == 1
493*e1fe3e4aSElliott Hughes                assert len(glyphs2) == 1
494*e1fe3e4aSElliott Hughes                glyphs = (glyphs1[0], glyphs2[0])
495*e1fe3e4aSElliott Hughes
496*e1fe3e4aSElliott Hughes                if ignore:
497*e1fe3e4aSElliott Hughes                    statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
498*e1fe3e4aSElliott Hughes                else:
499*e1fe3e4aSElliott Hughes                    lookups = (targetlookup, targetlookup)
500*e1fe3e4aSElliott Hughes                    statement = ast.ChainContextPosStatement(
501*e1fe3e4aSElliott Hughes                        prefix, glyphs, suffix, lookups
502*e1fe3e4aSElliott Hughes                    )
503*e1fe3e4aSElliott Hughes                statements.append(statement)
504*e1fe3e4aSElliott Hughes        elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
505*e1fe3e4aSElliott Hughes            glyphs = [ast.GlyphClass()]
506*e1fe3e4aSElliott Hughes            for a, b in pos.adjust_single:
507*e1fe3e4aSElliott Hughes                glyph = self._coverage(a)
508*e1fe3e4aSElliott Hughes                glyphs[0].extend(glyph)
509*e1fe3e4aSElliott Hughes
510*e1fe3e4aSElliott Hughes            if ignore:
511*e1fe3e4aSElliott Hughes                statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
512*e1fe3e4aSElliott Hughes            else:
513*e1fe3e4aSElliott Hughes                statement = ast.ChainContextPosStatement(
514*e1fe3e4aSElliott Hughes                    prefix, glyphs, suffix, [targetlookup]
515*e1fe3e4aSElliott Hughes                )
516*e1fe3e4aSElliott Hughes            statements.append(statement)
517*e1fe3e4aSElliott Hughes        elif isinstance(pos, VAst.PositionAttachDefinition):
518*e1fe3e4aSElliott Hughes            glyphs = [ast.GlyphClass()]
519*e1fe3e4aSElliott Hughes            for coverage, _ in pos.coverage_to:
520*e1fe3e4aSElliott Hughes                glyphs[0].extend(self._coverage(coverage))
521*e1fe3e4aSElliott Hughes
522*e1fe3e4aSElliott Hughes            if ignore:
523*e1fe3e4aSElliott Hughes                statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
524*e1fe3e4aSElliott Hughes            else:
525*e1fe3e4aSElliott Hughes                statement = ast.ChainContextPosStatement(
526*e1fe3e4aSElliott Hughes                    prefix, glyphs, suffix, [targetlookup]
527*e1fe3e4aSElliott Hughes                )
528*e1fe3e4aSElliott Hughes            statements.append(statement)
529*e1fe3e4aSElliott Hughes        else:
530*e1fe3e4aSElliott Hughes            raise NotImplementedError(pos)
531*e1fe3e4aSElliott Hughes
532*e1fe3e4aSElliott Hughes    def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup):
533*e1fe3e4aSElliott Hughes        statements = fealookup.statements
534*e1fe3e4aSElliott Hughes
535*e1fe3e4aSElliott Hughes        sub = lookup.sub
536*e1fe3e4aSElliott Hughes        for key, val in sub.mapping.items():
537*e1fe3e4aSElliott Hughes            if not key or not val:
538*e1fe3e4aSElliott Hughes                path, line, column = sub.location
539*e1fe3e4aSElliott Hughes                log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
540*e1fe3e4aSElliott Hughes                continue
541*e1fe3e4aSElliott Hughes            statement = None
542*e1fe3e4aSElliott Hughes            glyphs = self._coverage(key)
543*e1fe3e4aSElliott Hughes            replacements = self._coverage(val)
544*e1fe3e4aSElliott Hughes            if ignore:
545*e1fe3e4aSElliott Hughes                chain_context = (prefix, glyphs, suffix)
546*e1fe3e4aSElliott Hughes                statement = ast.IgnoreSubstStatement([chain_context])
547*e1fe3e4aSElliott Hughes            elif isinstance(sub, VAst.SubstitutionSingleDefinition):
548*e1fe3e4aSElliott Hughes                assert len(glyphs) == 1
549*e1fe3e4aSElliott Hughes                assert len(replacements) == 1
550*e1fe3e4aSElliott Hughes                statement = ast.SingleSubstStatement(
551*e1fe3e4aSElliott Hughes                    glyphs, replacements, prefix, suffix, chain
552*e1fe3e4aSElliott Hughes                )
553*e1fe3e4aSElliott Hughes            elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
554*e1fe3e4aSElliott Hughes                assert len(glyphs) == 1
555*e1fe3e4aSElliott Hughes                assert len(replacements) == 1
556*e1fe3e4aSElliott Hughes                statement = ast.ReverseChainSingleSubstStatement(
557*e1fe3e4aSElliott Hughes                    prefix, suffix, glyphs, replacements
558*e1fe3e4aSElliott Hughes                )
559*e1fe3e4aSElliott Hughes            elif isinstance(sub, VAst.SubstitutionMultipleDefinition):
560*e1fe3e4aSElliott Hughes                assert len(glyphs) == 1
561*e1fe3e4aSElliott Hughes                statement = ast.MultipleSubstStatement(
562*e1fe3e4aSElliott Hughes                    prefix, glyphs[0], suffix, replacements, chain
563*e1fe3e4aSElliott Hughes                )
564*e1fe3e4aSElliott Hughes            elif isinstance(sub, VAst.SubstitutionLigatureDefinition):
565*e1fe3e4aSElliott Hughes                assert len(replacements) == 1
566*e1fe3e4aSElliott Hughes                statement = ast.LigatureSubstStatement(
567*e1fe3e4aSElliott Hughes                    prefix, glyphs, suffix, replacements[0], chain
568*e1fe3e4aSElliott Hughes                )
569*e1fe3e4aSElliott Hughes            else:
570*e1fe3e4aSElliott Hughes                raise NotImplementedError(sub)
571*e1fe3e4aSElliott Hughes            statements.append(statement)
572*e1fe3e4aSElliott Hughes
573*e1fe3e4aSElliott Hughes    def _lookupDefinition(self, lookup):
574*e1fe3e4aSElliott Hughes        mark_attachement = None
575*e1fe3e4aSElliott Hughes        mark_filtering = None
576*e1fe3e4aSElliott Hughes
577*e1fe3e4aSElliott Hughes        flags = 0
578*e1fe3e4aSElliott Hughes        if lookup.direction == "RTL":
579*e1fe3e4aSElliott Hughes            flags |= 1
580*e1fe3e4aSElliott Hughes        if not lookup.process_base:
581*e1fe3e4aSElliott Hughes            flags |= 2
582*e1fe3e4aSElliott Hughes        # FIXME: Does VOLT support this?
583*e1fe3e4aSElliott Hughes        # if not lookup.process_ligatures:
584*e1fe3e4aSElliott Hughes        #     flags |= 4
585*e1fe3e4aSElliott Hughes        if not lookup.process_marks:
586*e1fe3e4aSElliott Hughes            flags |= 8
587*e1fe3e4aSElliott Hughes        elif isinstance(lookup.process_marks, str):
588*e1fe3e4aSElliott Hughes            mark_attachement = self._groupName(lookup.process_marks)
589*e1fe3e4aSElliott Hughes        elif lookup.mark_glyph_set is not None:
590*e1fe3e4aSElliott Hughes            mark_filtering = self._groupName(lookup.mark_glyph_set)
591*e1fe3e4aSElliott Hughes
592*e1fe3e4aSElliott Hughes        lookupflags = None
593*e1fe3e4aSElliott Hughes        if flags or mark_attachement is not None or mark_filtering is not None:
594*e1fe3e4aSElliott Hughes            lookupflags = ast.LookupFlagStatement(
595*e1fe3e4aSElliott Hughes                flags, mark_attachement, mark_filtering
596*e1fe3e4aSElliott Hughes            )
597*e1fe3e4aSElliott Hughes        if "\\" in lookup.name:
598*e1fe3e4aSElliott Hughes            # Merge sub lookups as subtables (lookups named “base\sub”),
599*e1fe3e4aSElliott Hughes            # makeotf/feaLib will issue a warning and ignore the subtable
600*e1fe3e4aSElliott Hughes            # statement if it is not a pairpos lookup, though.
601*e1fe3e4aSElliott Hughes            name = lookup.name.split("\\")[0]
602*e1fe3e4aSElliott Hughes            if name.lower() not in self._lookups:
603*e1fe3e4aSElliott Hughes                fealookup = ast.LookupBlock(self._lookupName(name))
604*e1fe3e4aSElliott Hughes                if lookupflags is not None:
605*e1fe3e4aSElliott Hughes                    fealookup.statements.append(lookupflags)
606*e1fe3e4aSElliott Hughes                fealookup.statements.append(ast.Comment("# " + lookup.name))
607*e1fe3e4aSElliott Hughes            else:
608*e1fe3e4aSElliott Hughes                fealookup = self._lookups[name.lower()]
609*e1fe3e4aSElliott Hughes                fealookup.statements.append(ast.SubtableStatement())
610*e1fe3e4aSElliott Hughes                fealookup.statements.append(ast.Comment("# " + lookup.name))
611*e1fe3e4aSElliott Hughes            self._lookups[name.lower()] = fealookup
612*e1fe3e4aSElliott Hughes        else:
613*e1fe3e4aSElliott Hughes            fealookup = ast.LookupBlock(self._lookupName(lookup.name))
614*e1fe3e4aSElliott Hughes            if lookupflags is not None:
615*e1fe3e4aSElliott Hughes                fealookup.statements.append(lookupflags)
616*e1fe3e4aSElliott Hughes            self._lookups[lookup.name.lower()] = fealookup
617*e1fe3e4aSElliott Hughes
618*e1fe3e4aSElliott Hughes        if lookup.comments is not None:
619*e1fe3e4aSElliott Hughes            fealookup.statements.append(ast.Comment("# " + lookup.comments))
620*e1fe3e4aSElliott Hughes
621*e1fe3e4aSElliott Hughes        contexts = []
622*e1fe3e4aSElliott Hughes        if lookup.context:
623*e1fe3e4aSElliott Hughes            for context in lookup.context:
624*e1fe3e4aSElliott Hughes                prefix = self._context(context.left)
625*e1fe3e4aSElliott Hughes                suffix = self._context(context.right)
626*e1fe3e4aSElliott Hughes                ignore = context.ex_or_in == "EXCEPT_CONTEXT"
627*e1fe3e4aSElliott Hughes                contexts.append([prefix, suffix, ignore, False])
628*e1fe3e4aSElliott Hughes                # It seems that VOLT will create contextual substitution using
629*e1fe3e4aSElliott Hughes                # only the input if there is no other contexts in this lookup.
630*e1fe3e4aSElliott Hughes                if ignore and len(lookup.context) == 1:
631*e1fe3e4aSElliott Hughes                    contexts.append([[], [], False, True])
632*e1fe3e4aSElliott Hughes        else:
633*e1fe3e4aSElliott Hughes            contexts.append([[], [], False, False])
634*e1fe3e4aSElliott Hughes
635*e1fe3e4aSElliott Hughes        targetlookup = None
636*e1fe3e4aSElliott Hughes        for prefix, suffix, ignore, chain in contexts:
637*e1fe3e4aSElliott Hughes            if lookup.sub is not None:
638*e1fe3e4aSElliott Hughes                self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup)
639*e1fe3e4aSElliott Hughes
640*e1fe3e4aSElliott Hughes            if lookup.pos is not None:
641*e1fe3e4aSElliott Hughes                if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"):
642*e1fe3e4aSElliott Hughes                    fealookup.use_extension = True
643*e1fe3e4aSElliott Hughes                if prefix or suffix or chain or ignore:
644*e1fe3e4aSElliott Hughes                    if not ignore and targetlookup is None:
645*e1fe3e4aSElliott Hughes                        targetname = self._lookupName(lookup.name + " target")
646*e1fe3e4aSElliott Hughes                        targetlookup = ast.LookupBlock(targetname)
647*e1fe3e4aSElliott Hughes                        fealookup.targets = getattr(fealookup, "targets", [])
648*e1fe3e4aSElliott Hughes                        fealookup.targets.append(targetlookup)
649*e1fe3e4aSElliott Hughes                        self._gposLookup(lookup, targetlookup)
650*e1fe3e4aSElliott Hughes                    self._gposContextLookup(
651*e1fe3e4aSElliott Hughes                        lookup, prefix, suffix, ignore, fealookup, targetlookup
652*e1fe3e4aSElliott Hughes                    )
653*e1fe3e4aSElliott Hughes                else:
654*e1fe3e4aSElliott Hughes                    self._gposLookup(lookup, fealookup)
655*e1fe3e4aSElliott Hughes
656*e1fe3e4aSElliott Hughes
657*e1fe3e4aSElliott Hughesdef main(args=None):
658*e1fe3e4aSElliott Hughes    """Convert MS VOLT to AFDKO feature files."""
659*e1fe3e4aSElliott Hughes
660*e1fe3e4aSElliott Hughes    import argparse
661*e1fe3e4aSElliott Hughes    from pathlib import Path
662*e1fe3e4aSElliott Hughes
663*e1fe3e4aSElliott Hughes    from fontTools import configLogger
664*e1fe3e4aSElliott Hughes
665*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
666*e1fe3e4aSElliott Hughes        "fonttools voltLib.voltToFea", description=main.__doc__
667*e1fe3e4aSElliott Hughes    )
668*e1fe3e4aSElliott Hughes    parser.add_argument(
669*e1fe3e4aSElliott Hughes        "input", metavar="INPUT", type=Path, help="input font/VTP file to process"
670*e1fe3e4aSElliott Hughes    )
671*e1fe3e4aSElliott Hughes    parser.add_argument(
672*e1fe3e4aSElliott Hughes        "featurefile", metavar="OUTPUT", type=Path, help="output feature file"
673*e1fe3e4aSElliott Hughes    )
674*e1fe3e4aSElliott Hughes    parser.add_argument(
675*e1fe3e4aSElliott Hughes        "-t",
676*e1fe3e4aSElliott Hughes        "--table",
677*e1fe3e4aSElliott Hughes        action="append",
678*e1fe3e4aSElliott Hughes        choices=TABLES,
679*e1fe3e4aSElliott Hughes        dest="tables",
680*e1fe3e4aSElliott Hughes        help="List of tables to write, by default all tables are written",
681*e1fe3e4aSElliott Hughes    )
682*e1fe3e4aSElliott Hughes    parser.add_argument(
683*e1fe3e4aSElliott Hughes        "-q", "--quiet", action="store_true", help="Suppress non-error messages"
684*e1fe3e4aSElliott Hughes    )
685*e1fe3e4aSElliott Hughes    parser.add_argument(
686*e1fe3e4aSElliott Hughes        "--traceback", action="store_true", help="Don’t catch exceptions"
687*e1fe3e4aSElliott Hughes    )
688*e1fe3e4aSElliott Hughes
689*e1fe3e4aSElliott Hughes    options = parser.parse_args(args)
690*e1fe3e4aSElliott Hughes
691*e1fe3e4aSElliott Hughes    configLogger(level=("ERROR" if options.quiet else "INFO"))
692*e1fe3e4aSElliott Hughes
693*e1fe3e4aSElliott Hughes    file_or_path = options.input
694*e1fe3e4aSElliott Hughes    font = None
695*e1fe3e4aSElliott Hughes    try:
696*e1fe3e4aSElliott Hughes        font = TTFont(file_or_path)
697*e1fe3e4aSElliott Hughes        if "TSIV" in font:
698*e1fe3e4aSElliott Hughes            file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
699*e1fe3e4aSElliott Hughes        else:
700*e1fe3e4aSElliott Hughes            log.error('"TSIV" table is missing, font was not saved from VOLT?')
701*e1fe3e4aSElliott Hughes            return 1
702*e1fe3e4aSElliott Hughes    except TTLibError:
703*e1fe3e4aSElliott Hughes        pass
704*e1fe3e4aSElliott Hughes
705*e1fe3e4aSElliott Hughes    converter = VoltToFea(file_or_path, font)
706*e1fe3e4aSElliott Hughes    try:
707*e1fe3e4aSElliott Hughes        fea = converter.convert(options.tables)
708*e1fe3e4aSElliott Hughes    except NotImplementedError as e:
709*e1fe3e4aSElliott Hughes        if options.traceback:
710*e1fe3e4aSElliott Hughes            raise
711*e1fe3e4aSElliott Hughes        location = getattr(e.args[0], "location", None)
712*e1fe3e4aSElliott Hughes        message = f'"{e}" is not supported'
713*e1fe3e4aSElliott Hughes        if location:
714*e1fe3e4aSElliott Hughes            path, line, column = location
715*e1fe3e4aSElliott Hughes            log.error(f"{path}:{line}:{column}: {message}")
716*e1fe3e4aSElliott Hughes        else:
717*e1fe3e4aSElliott Hughes            log.error(message)
718*e1fe3e4aSElliott Hughes        return 1
719*e1fe3e4aSElliott Hughes    with open(options.featurefile, "w") as feafile:
720*e1fe3e4aSElliott Hughes        feafile.write(fea)
721*e1fe3e4aSElliott Hughes
722*e1fe3e4aSElliott Hughes
723*e1fe3e4aSElliott Hughesif __name__ == "__main__":
724*e1fe3e4aSElliott Hughes    import sys
725*e1fe3e4aSElliott Hughes
726*e1fe3e4aSElliott Hughes    sys.exit(main())
727