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