Spaces:
Running
Running
"""\ | |
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()) | |