123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- """\
- MS VOLT ``.vtp`` to AFDKO ``.fea`` OpenType Layout converter.
- Usage
- -----
- To convert a VTP project file:
- $ fonttools voltLib.voltToFea input.vtp output.fea
- It is also possible convert font files with `TSIV` table (as saved from Volt),
- in this case the glyph names used in the Volt project will be mapped to the
- actual glyph names in the font files when written to the feature file:
- $ fonttools voltLib.voltToFea input.ttf output.fea
- The ``--quiet`` option can be used to suppress warnings.
- The ``--traceback`` can be used to get Python traceback in case of exceptions,
- instead of suppressing the traceback.
- Limitations
- -----------
- * Not all VOLT features are supported, the script will error if it it
- encounters something it does not understand. Please report an issue if this
- happens.
- * AFDKO feature file syntax for mark positioning is awkward and does not allow
- setting the mark coverage. It also defines mark anchors globally, as a result
- some mark positioning lookups might cover many marks than what was in the VOLT
- file. This should not be an issue in practice, but if it is then the only way
- is to modify the VOLT file or the generated feature file manually to use unique
- mark anchors for each lookup.
- * VOLT allows subtable breaks in any lookup type, but AFDKO feature file
- implementations vary in their support; currently AFDKO’s makeOTF supports
- subtable breaks in pair positioning lookups only, while FontTools’ feaLib
- support it for most substitution lookups and only some positioning lookups.
- """
- import logging
- import re
- from io import StringIO
- from fontTools.feaLib import ast
- from fontTools.ttLib import TTFont, TTLibError
- from fontTools.voltLib import ast as VAst
- from fontTools.voltLib.parser import Parser as VoltParser
- log = logging.getLogger("fontTools.voltLib.voltToFea")
- TABLES = ["GDEF", "GSUB", "GPOS"]
- class MarkClassDefinition(ast.MarkClassDefinition):
- def asFea(self, indent=""):
- res = ""
- if not getattr(self, "used", False):
- res += "#"
- res += ast.MarkClassDefinition.asFea(self, indent)
- return res
- # For sorting voltLib.ast.GlyphDefinition, see its use below.
- class Group:
- def __init__(self, group):
- self.name = group.name.lower()
- self.groups = [
- x.group.lower() for x in group.enum.enum if isinstance(x, VAst.GroupName)
- ]
- def __lt__(self, other):
- if self.name in other.groups:
- return True
- if other.name in self.groups:
- return False
- if self.groups and not other.groups:
- return False
- if not self.groups and other.groups:
- return True
- class VoltToFea:
- _NOT_LOOKUP_NAME_RE = re.compile(r"[^A-Za-z_0-9.]")
- _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]")
- def __init__(self, file_or_path, font=None):
- self._file_or_path = file_or_path
- self._font = font
- self._glyph_map = {}
- self._glyph_order = None
- self._gdef = {}
- self._glyphclasses = {}
- self._features = {}
- self._lookups = {}
- self._marks = set()
- self._ligatures = {}
- self._markclasses = {}
- self._anchors = {}
- self._settings = {}
- self._lookup_names = {}
- self._class_names = {}
- def _lookupName(self, name):
- if name not in self._lookup_names:
- res = self._NOT_LOOKUP_NAME_RE.sub("_", name)
- while res in self._lookup_names.values():
- res += "_"
- self._lookup_names[name] = res
- return self._lookup_names[name]
- def _className(self, name):
- if name not in self._class_names:
- res = self._NOT_CLASS_NAME_RE.sub("_", name)
- while res in self._class_names.values():
- res += "_"
- self._class_names[name] = res
- return self._class_names[name]
- def _collectStatements(self, doc, tables):
- # Collect and sort group definitions first, to make sure a group
- # definition that references other groups comes after them since VOLT
- # does not enforce such ordering, and feature file require it.
- groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)]
- for statement in sorted(groups, key=lambda x: Group(x)):
- self._groupDefinition(statement)
- for statement in doc.statements:
- if isinstance(statement, VAst.GlyphDefinition):
- self._glyphDefinition(statement)
- elif isinstance(statement, VAst.AnchorDefinition):
- if "GPOS" in tables:
- self._anchorDefinition(statement)
- elif isinstance(statement, VAst.SettingDefinition):
- self._settingDefinition(statement)
- elif isinstance(statement, VAst.GroupDefinition):
- pass # Handled above
- elif isinstance(statement, VAst.ScriptDefinition):
- self._scriptDefinition(statement)
- elif not isinstance(statement, VAst.LookupDefinition):
- raise NotImplementedError(statement)
- # Lookup definitions need to be handled last as they reference glyph
- # and mark classes that might be defined after them.
- for statement in doc.statements:
- if isinstance(statement, VAst.LookupDefinition):
- if statement.pos and "GPOS" not in tables:
- continue
- if statement.sub and "GSUB" not in tables:
- continue
- self._lookupDefinition(statement)
- def _buildFeatureFile(self, tables):
- doc = ast.FeatureFile()
- statements = doc.statements
- if self._glyphclasses:
- statements.append(ast.Comment("# Glyph classes"))
- statements.extend(self._glyphclasses.values())
- if self._markclasses:
- statements.append(ast.Comment("\n# Mark classes"))
- statements.extend(c[1] for c in sorted(self._markclasses.items()))
- if self._lookups:
- statements.append(ast.Comment("\n# Lookups"))
- for lookup in self._lookups.values():
- statements.extend(getattr(lookup, "targets", []))
- statements.append(lookup)
- # Prune features
- features = self._features.copy()
- for ftag in features:
- scripts = features[ftag]
- for stag in scripts:
- langs = scripts[stag]
- for ltag in langs:
- langs[ltag] = [l for l in langs[ltag] if l.lower() in self._lookups]
- scripts[stag] = {t: l for t, l in langs.items() if l}
- features[ftag] = {t: s for t, s in scripts.items() if s}
- features = {t: f for t, f in features.items() if f}
- if features:
- statements.append(ast.Comment("# Features"))
- for ftag, scripts in features.items():
- feature = ast.FeatureBlock(ftag)
- stags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1)
- for stag in stags:
- feature.statements.append(ast.ScriptStatement(stag))
- ltags = sorted(scripts[stag], key=lambda k: 0 if k == "dflt" else 1)
- for ltag in ltags:
- include_default = True if ltag == "dflt" else False
- feature.statements.append(
- ast.LanguageStatement(ltag, include_default=include_default)
- )
- for name in scripts[stag][ltag]:
- lookup = self._lookups[name.lower()]
- lookupref = ast.LookupReferenceStatement(lookup)
- feature.statements.append(lookupref)
- statements.append(feature)
- if self._gdef and "GDEF" in tables:
- classes = []
- for name in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
- if name in self._gdef:
- classname = "GDEF_" + name.lower()
- glyphclass = ast.GlyphClassDefinition(classname, self._gdef[name])
- statements.append(glyphclass)
- classes.append(ast.GlyphClassName(glyphclass))
- else:
- classes.append(None)
- gdef = ast.TableBlock("GDEF")
- gdef.statements.append(ast.GlyphClassDefStatement(*classes))
- statements.append(gdef)
- return doc
- def convert(self, tables=None):
- doc = VoltParser(self._file_or_path).parse()
- if tables is None:
- tables = TABLES
- if self._font is not None:
- self._glyph_order = self._font.getGlyphOrder()
- self._collectStatements(doc, tables)
- fea = self._buildFeatureFile(tables)
- return fea.asFea()
- def _glyphName(self, glyph):
- try:
- name = glyph.glyph
- except AttributeError:
- name = glyph
- return ast.GlyphName(self._glyph_map.get(name, name))
- def _groupName(self, group):
- try:
- name = group.group
- except AttributeError:
- name = group
- return ast.GlyphClassName(self._glyphclasses[name.lower()])
- def _coverage(self, coverage):
- items = []
- for item in coverage:
- if isinstance(item, VAst.GlyphName):
- items.append(self._glyphName(item))
- elif isinstance(item, VAst.GroupName):
- items.append(self._groupName(item))
- elif isinstance(item, VAst.Enum):
- items.append(self._enum(item))
- elif isinstance(item, VAst.Range):
- items.append((item.start, item.end))
- else:
- raise NotImplementedError(item)
- return items
- def _enum(self, enum):
- return ast.GlyphClass(self._coverage(enum.enum))
- def _context(self, context):
- out = []
- for item in context:
- coverage = self._coverage(item)
- if not isinstance(coverage, (tuple, list)):
- coverage = [coverage]
- out.extend(coverage)
- return out
- def _groupDefinition(self, group):
- name = self._className(group.name)
- glyphs = self._enum(group.enum)
- glyphclass = ast.GlyphClassDefinition(name, glyphs)
- self._glyphclasses[group.name.lower()] = glyphclass
- def _glyphDefinition(self, glyph):
- try:
- self._glyph_map[glyph.name] = self._glyph_order[glyph.id]
- except TypeError:
- pass
- if glyph.type in ("BASE", "MARK", "LIGATURE", "COMPONENT"):
- if glyph.type not in self._gdef:
- self._gdef[glyph.type] = ast.GlyphClass()
- self._gdef[glyph.type].glyphs.append(self._glyphName(glyph.name))
- if glyph.type == "MARK":
- self._marks.add(glyph.name)
- elif glyph.type == "LIGATURE":
- self._ligatures[glyph.name] = glyph.components
- def _scriptDefinition(self, script):
- stag = script.tag
- for lang in script.langs:
- ltag = lang.tag
- for feature in lang.features:
- lookups = {l.split("\\")[0]: True for l in feature.lookups}
- ftag = feature.tag
- if ftag not in self._features:
- self._features[ftag] = {}
- if stag not in self._features[ftag]:
- self._features[ftag][stag] = {}
- assert ltag not in self._features[ftag][stag]
- self._features[ftag][stag][ltag] = lookups.keys()
- def _settingDefinition(self, setting):
- if setting.name.startswith("COMPILER_"):
- self._settings[setting.name] = setting.value
- else:
- log.warning(f"Unsupported setting ignored: {setting.name}")
- def _adjustment(self, adjustment):
- adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
- adv_device = adv_adjust_by and adv_adjust_by.items() or None
- dx_device = dx_adjust_by and dx_adjust_by.items() or None
- dy_device = dy_adjust_by and dy_adjust_by.items() or None
- return ast.ValueRecord(
- xPlacement=dx,
- yPlacement=dy,
- xAdvance=adv,
- xPlaDevice=dx_device,
- yPlaDevice=dy_device,
- xAdvDevice=adv_device,
- )
- def _anchor(self, adjustment):
- adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by = adjustment
- assert not adv_adjust_by
- dx_device = dx_adjust_by and dx_adjust_by.items() or None
- dy_device = dy_adjust_by and dy_adjust_by.items() or None
- return ast.Anchor(
- dx or 0,
- dy or 0,
- xDeviceTable=dx_device or None,
- yDeviceTable=dy_device or None,
- )
- def _anchorDefinition(self, anchordef):
- anchorname = anchordef.name
- glyphname = anchordef.glyph_name
- anchor = self._anchor(anchordef.pos)
- if anchorname.startswith("MARK_"):
- name = "_".join(anchorname.split("_")[1:])
- markclass = ast.MarkClass(self._className(name))
- glyph = self._glyphName(glyphname)
- markdef = MarkClassDefinition(markclass, anchor, glyph)
- self._markclasses[(glyphname, anchorname)] = markdef
- else:
- if glyphname not in self._anchors:
- self._anchors[glyphname] = {}
- if anchorname not in self._anchors[glyphname]:
- self._anchors[glyphname][anchorname] = {}
- self._anchors[glyphname][anchorname][anchordef.component] = anchor
- def _gposLookup(self, lookup, fealookup):
- statements = fealookup.statements
- pos = lookup.pos
- if isinstance(pos, VAst.PositionAdjustPairDefinition):
- for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
- coverage_1 = pos.coverages_1[idx1 - 1]
- coverage_2 = pos.coverages_2[idx2 - 1]
- # If not both are groups, use “enum pos” otherwise makeotf will
- # fail.
- enumerated = False
- for item in coverage_1 + coverage_2:
- if not isinstance(item, VAst.GroupName):
- enumerated = True
- glyphs1 = self._coverage(coverage_1)
- glyphs2 = self._coverage(coverage_2)
- record1 = self._adjustment(pos1)
- record2 = self._adjustment(pos2)
- assert len(glyphs1) == 1
- assert len(glyphs2) == 1
- statements.append(
- ast.PairPosStatement(
- glyphs1[0], record1, glyphs2[0], record2, enumerated=enumerated
- )
- )
- elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
- for a, b in pos.adjust_single:
- glyphs = self._coverage(a)
- record = self._adjustment(b)
- assert len(glyphs) == 1
- statements.append(
- ast.SinglePosStatement([(glyphs[0], record)], [], [], False)
- )
- elif isinstance(pos, VAst.PositionAttachDefinition):
- anchors = {}
- for marks, classname in pos.coverage_to:
- for mark in marks:
- # Set actually used mark classes. Basically a hack to get
- # around the feature file syntax limitation of making mark
- # classes global and not allowing mark positioning to
- # specify mark coverage.
- for name in mark.glyphSet():
- key = (name, "MARK_" + classname)
- self._markclasses[key].used = True
- markclass = ast.MarkClass(self._className(classname))
- for base in pos.coverage:
- for name in base.glyphSet():
- if name not in anchors:
- anchors[name] = []
- if classname not in anchors[name]:
- anchors[name].append(classname)
- for name in anchors:
- components = 1
- if name in self._ligatures:
- components = self._ligatures[name]
- marks = []
- for mark in anchors[name]:
- markclass = ast.MarkClass(self._className(mark))
- for component in range(1, components + 1):
- if len(marks) < component:
- marks.append([])
- anchor = None
- if component in self._anchors[name][mark]:
- anchor = self._anchors[name][mark][component]
- marks[component - 1].append((anchor, markclass))
- base = self._glyphName(name)
- if name in self._marks:
- mark = ast.MarkMarkPosStatement(base, marks[0])
- elif name in self._ligatures:
- mark = ast.MarkLigPosStatement(base, marks)
- else:
- mark = ast.MarkBasePosStatement(base, marks[0])
- statements.append(mark)
- elif isinstance(pos, VAst.PositionAttachCursiveDefinition):
- # Collect enter and exit glyphs
- enter_coverage = []
- for coverage in pos.coverages_enter:
- for base in coverage:
- for name in base.glyphSet():
- enter_coverage.append(name)
- exit_coverage = []
- for coverage in pos.coverages_exit:
- for base in coverage:
- for name in base.glyphSet():
- exit_coverage.append(name)
- # Write enter anchors, also check if the glyph has exit anchor and
- # write it, too.
- for name in enter_coverage:
- glyph = self._glyphName(name)
- entry = self._anchors[name]["entry"][1]
- exit = None
- if name in exit_coverage:
- exit = self._anchors[name]["exit"][1]
- exit_coverage.pop(exit_coverage.index(name))
- statements.append(ast.CursivePosStatement(glyph, entry, exit))
- # Write any remaining exit anchors.
- for name in exit_coverage:
- glyph = self._glyphName(name)
- exit = self._anchors[name]["exit"][1]
- statements.append(ast.CursivePosStatement(glyph, None, exit))
- else:
- raise NotImplementedError(pos)
- def _gposContextLookup(
- self, lookup, prefix, suffix, ignore, fealookup, targetlookup
- ):
- statements = fealookup.statements
- assert not lookup.reversal
- pos = lookup.pos
- if isinstance(pos, VAst.PositionAdjustPairDefinition):
- for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items():
- glyphs1 = self._coverage(pos.coverages_1[idx1 - 1])
- glyphs2 = self._coverage(pos.coverages_2[idx2 - 1])
- assert len(glyphs1) == 1
- assert len(glyphs2) == 1
- glyphs = (glyphs1[0], glyphs2[0])
- if ignore:
- statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
- else:
- lookups = (targetlookup, targetlookup)
- statement = ast.ChainContextPosStatement(
- prefix, glyphs, suffix, lookups
- )
- statements.append(statement)
- elif isinstance(pos, VAst.PositionAdjustSingleDefinition):
- glyphs = [ast.GlyphClass()]
- for a, b in pos.adjust_single:
- glyph = self._coverage(a)
- glyphs[0].extend(glyph)
- if ignore:
- statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
- else:
- statement = ast.ChainContextPosStatement(
- prefix, glyphs, suffix, [targetlookup]
- )
- statements.append(statement)
- elif isinstance(pos, VAst.PositionAttachDefinition):
- glyphs = [ast.GlyphClass()]
- for coverage, _ in pos.coverage_to:
- glyphs[0].extend(self._coverage(coverage))
- if ignore:
- statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)])
- else:
- statement = ast.ChainContextPosStatement(
- prefix, glyphs, suffix, [targetlookup]
- )
- statements.append(statement)
- else:
- raise NotImplementedError(pos)
- def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup):
- statements = fealookup.statements
- sub = lookup.sub
- for key, val in sub.mapping.items():
- if not key or not val:
- path, line, column = sub.location
- log.warning(f"{path}:{line}:{column}: Ignoring empty substitution")
- continue
- statement = None
- glyphs = self._coverage(key)
- replacements = self._coverage(val)
- if ignore:
- chain_context = (prefix, glyphs, suffix)
- statement = ast.IgnoreSubstStatement([chain_context])
- elif isinstance(sub, VAst.SubstitutionSingleDefinition):
- assert len(glyphs) == 1
- assert len(replacements) == 1
- statement = ast.SingleSubstStatement(
- glyphs, replacements, prefix, suffix, chain
- )
- elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition):
- assert len(glyphs) == 1
- assert len(replacements) == 1
- statement = ast.ReverseChainSingleSubstStatement(
- prefix, suffix, glyphs, replacements
- )
- elif isinstance(sub, VAst.SubstitutionMultipleDefinition):
- assert len(glyphs) == 1
- statement = ast.MultipleSubstStatement(
- prefix, glyphs[0], suffix, replacements, chain
- )
- elif isinstance(sub, VAst.SubstitutionLigatureDefinition):
- assert len(replacements) == 1
- statement = ast.LigatureSubstStatement(
- prefix, glyphs, suffix, replacements[0], chain
- )
- else:
- raise NotImplementedError(sub)
- statements.append(statement)
- def _lookupDefinition(self, lookup):
- mark_attachement = None
- mark_filtering = None
- flags = 0
- if lookup.direction == "RTL":
- flags |= 1
- if not lookup.process_base:
- flags |= 2
- # FIXME: Does VOLT support this?
- # if not lookup.process_ligatures:
- # flags |= 4
- if not lookup.process_marks:
- flags |= 8
- elif isinstance(lookup.process_marks, str):
- mark_attachement = self._groupName(lookup.process_marks)
- elif lookup.mark_glyph_set is not None:
- mark_filtering = self._groupName(lookup.mark_glyph_set)
- lookupflags = None
- if flags or mark_attachement is not None or mark_filtering is not None:
- lookupflags = ast.LookupFlagStatement(
- flags, mark_attachement, mark_filtering
- )
- if "\\" in lookup.name:
- # Merge sub lookups as subtables (lookups named “base\sub”),
- # makeotf/feaLib will issue a warning and ignore the subtable
- # statement if it is not a pairpos lookup, though.
- name = lookup.name.split("\\")[0]
- if name.lower() not in self._lookups:
- fealookup = ast.LookupBlock(self._lookupName(name))
- if lookupflags is not None:
- fealookup.statements.append(lookupflags)
- fealookup.statements.append(ast.Comment("# " + lookup.name))
- else:
- fealookup = self._lookups[name.lower()]
- fealookup.statements.append(ast.SubtableStatement())
- fealookup.statements.append(ast.Comment("# " + lookup.name))
- self._lookups[name.lower()] = fealookup
- else:
- fealookup = ast.LookupBlock(self._lookupName(lookup.name))
- if lookupflags is not None:
- fealookup.statements.append(lookupflags)
- self._lookups[lookup.name.lower()] = fealookup
- if lookup.comments is not None:
- fealookup.statements.append(ast.Comment("# " + lookup.comments))
- contexts = []
- if lookup.context:
- for context in lookup.context:
- prefix = self._context(context.left)
- suffix = self._context(context.right)
- ignore = context.ex_or_in == "EXCEPT_CONTEXT"
- contexts.append([prefix, suffix, ignore, False])
- # It seems that VOLT will create contextual substitution using
- # only the input if there is no other contexts in this lookup.
- if ignore and len(lookup.context) == 1:
- contexts.append([[], [], False, True])
- else:
- contexts.append([[], [], False, False])
- targetlookup = None
- for prefix, suffix, ignore, chain in contexts:
- if lookup.sub is not None:
- self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup)
- if lookup.pos is not None:
- if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"):
- fealookup.use_extension = True
- if prefix or suffix or chain or ignore:
- if not ignore and targetlookup is None:
- targetname = self._lookupName(lookup.name + " target")
- targetlookup = ast.LookupBlock(targetname)
- fealookup.targets = getattr(fealookup, "targets", [])
- fealookup.targets.append(targetlookup)
- self._gposLookup(lookup, targetlookup)
- self._gposContextLookup(
- lookup, prefix, suffix, ignore, fealookup, targetlookup
- )
- else:
- self._gposLookup(lookup, fealookup)
- def main(args=None):
- """Convert MS VOLT to AFDKO feature files."""
- import argparse
- from pathlib import Path
- from fontTools import configLogger
- parser = argparse.ArgumentParser(
- "fonttools voltLib.voltToFea", description=main.__doc__
- )
- parser.add_argument(
- "input", metavar="INPUT", type=Path, help="input font/VTP file to process"
- )
- parser.add_argument(
- "featurefile", metavar="OUTPUT", type=Path, help="output feature file"
- )
- parser.add_argument(
- "-t",
- "--table",
- action="append",
- choices=TABLES,
- dest="tables",
- help="List of tables to write, by default all tables are written",
- )
- parser.add_argument(
- "-q", "--quiet", action="store_true", help="Suppress non-error messages"
- )
- parser.add_argument(
- "--traceback", action="store_true", help="Don’t catch exceptions"
- )
- options = parser.parse_args(args)
- configLogger(level=("ERROR" if options.quiet else "INFO"))
- file_or_path = options.input
- font = None
- try:
- font = TTFont(file_or_path)
- if "TSIV" in font:
- file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
- else:
- log.error('"TSIV" table is missing, font was not saved from VOLT?')
- return 1
- except TTLibError:
- pass
- converter = VoltToFea(file_or_path, font)
- try:
- fea = converter.convert(options.tables)
- except NotImplementedError as e:
- if options.traceback:
- raise
- location = getattr(e.args[0], "location", None)
- message = f'"{e}" is not supported'
- if location:
- path, line, column = location
- log.error(f"{path}:{line}:{column}: {message}")
- else:
- log.error(message)
- return 1
- with open(options.featurefile, "w") as feafile:
- feafile.write(fea)
- if __name__ == "__main__":
- import sys
- sys.exit(main())
|