Merge pull request #2052 from simoncozens/fealib-debug

feaLib source debugging
This commit is contained in:
Simon Cozens 2020-09-17 20:13:23 +01:00 committed by GitHub
commit a18b6bfb6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 203 additions and 8 deletions

View File

@ -38,6 +38,12 @@ def main(args=None):
nargs="+", nargs="+",
help="Specify the table(s) to be built.", help="Specify the table(s) to be built.",
) )
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="Add source-level debugging information to font.",
)
parser.add_argument( parser.add_argument(
"-v", "-v",
"--verbose", "--verbose",
@ -58,7 +64,9 @@ def main(args=None):
font = TTFont(options.input_font) font = TTFont(options.input_font)
try: try:
addOpenTypeFeatures(font, options.input_fea, tables=options.tables) addOpenTypeFeatures(
font, options.input_fea, tables=options.tables, debug=options.debug
)
except FeatureLibError as e: except FeatureLibError as e:
if options.traceback: if options.traceback:
raise raise

View File

@ -2,6 +2,7 @@ from fontTools.misc.py23 import *
from fontTools.misc import sstruct from fontTools.misc import sstruct
from fontTools.misc.textTools import binary2num, safeEval from fontTools.misc.textTools import binary2num, safeEval
from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
from fontTools.feaLib.parser import Parser from fontTools.feaLib.parser import Parser
from fontTools.feaLib.ast import FeatureFile from fontTools.feaLib.ast import FeatureFile
from fontTools.otlLib import builder as otl from fontTools.otlLib import builder as otl
@ -34,7 +35,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def addOpenTypeFeatures(font, featurefile, tables=None): def addOpenTypeFeatures(font, featurefile, tables=None, debug=False):
"""Add features from a file to a font. Note that this replaces any features """Add features from a file to a font. Note that this replaces any features
currently present. currently present.
@ -44,13 +45,17 @@ def addOpenTypeFeatures(font, featurefile, tables=None):
parse it into an AST), or a pre-parsed AST instance. parse it into an AST), or a pre-parsed AST instance.
tables: If passed, restrict the set of affected tables to those in the tables: If passed, restrict the set of affected tables to those in the
list. list.
debug: Whether to add source debugging information to the font in the
``Debg`` table
""" """
builder = Builder(font, featurefile) builder = Builder(font, featurefile)
builder.build(tables=tables) builder.build(tables=tables, debug=debug)
def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): def addOpenTypeFeaturesFromString(
font, features, filename=None, tables=None, debug=False
):
"""Add features from a string to a font. Note that this replaces any """Add features from a string to a font. Note that this replaces any
features currently present. features currently present.
@ -62,13 +67,15 @@ def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
directory is assumed. directory is assumed.
tables: If passed, restrict the set of affected tables to those in the tables: If passed, restrict the set of affected tables to those in the
list. list.
debug: Whether to add source debugging information to the font in the
``Debg`` table
""" """
featurefile = UnicodeIO(tounicode(features)) featurefile = UnicodeIO(tounicode(features))
if filename: if filename:
featurefile.name = filename featurefile.name = filename
addOpenTypeFeatures(font, featurefile, tables=tables) addOpenTypeFeatures(font, featurefile, tables=tables, debug=debug)
class Builder(object): class Builder(object):
@ -108,6 +115,7 @@ class Builder(object):
self.cur_lookup_name_ = None self.cur_lookup_name_ = None
self.cur_feature_name_ = None self.cur_feature_name_ = None
self.lookups_ = [] self.lookups_ = []
self.lookup_locations = {"GSUB": {}, "GPOS": {}}
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
# for feature 'aalt' # for feature 'aalt'
@ -146,7 +154,7 @@ class Builder(object):
# for table 'vhea' # for table 'vhea'
self.vhea_ = {} self.vhea_ = {}
def build(self, tables=None): def build(self, tables=None, debug=False):
if self.parseTree is None: if self.parseTree is None:
self.parseTree = Parser(self.file, self.glyphMap).parse() self.parseTree = Parser(self.file, self.glyphMap).parse()
self.parseTree.build(self) self.parseTree.build(self)
@ -201,6 +209,8 @@ class Builder(object):
self.font["BASE"] = base self.font["BASE"] = base
elif "BASE" in self.font: elif "BASE" in self.font:
del self.font["BASE"] del self.font["BASE"]
if debug:
self.buildDebg()
def get_chained_lookup_(self, location, builder_class): def get_chained_lookup_(self, location, builder_class):
result = builder_class(self.font, location) result = builder_class(self.font, location)
@ -638,6 +648,12 @@ class Builder(object):
sets.append(glyphs) sets.append(glyphs)
return otl.buildMarkGlyphSetsDef(sets, self.glyphMap) return otl.buildMarkGlyphSetsDef(sets, self.glyphMap)
def buildDebg(self):
if "Debg" not in self.font:
self.font["Debg"] = newTable("Debg")
self.font["Debg"].data = {}
self.font["Debg"].data[LOOKUP_DEBUG_INFO_KEY] = self.lookup_locations
def buildLookups_(self, tag): def buildLookups_(self, tag):
assert tag in ("GPOS", "GSUB"), tag assert tag in ("GPOS", "GSUB"), tag
for lookup in self.lookups_: for lookup in self.lookups_:
@ -647,6 +663,11 @@ class Builder(object):
if lookup.table != tag: if lookup.table != tag:
continue continue
lookup.lookup_index = len(lookups) lookup.lookup_index = len(lookups)
self.lookup_locations[tag][str(lookup.lookup_index)] = LookupDebugInfo(
location=str(lookup.location),
name=self.get_lookup_name_(lookup),
feature=None,
)
lookups.append(lookup) lookups.append(lookup)
try: try:
otLookups = [l.build() for l in lookups] otLookups = [l.build() for l in lookups]
@ -685,6 +706,11 @@ class Builder(object):
if len(lookup_indices) == 0 and not size_feature: if len(lookup_indices) == 0 and not size_feature:
continue continue
for ix in lookup_indices:
self.lookup_locations[tag][str(ix)] = self.lookup_locations[tag][
str(ix)
]._replace(feature=key)
feature_key = (feature_tag, lookup_indices) feature_key = (feature_tag, lookup_indices)
feature_index = feature_indices.get(feature_key) feature_index = feature_indices.get(feature_key)
if feature_index is None: if feature_index is None:
@ -737,6 +763,12 @@ class Builder(object):
table.LookupList.LookupCount = len(table.LookupList.Lookup) table.LookupList.LookupCount = len(table.LookupList.Lookup)
return table return table
def get_lookup_name_(self, lookup):
rev = {v: k for k, v in self.named_lookups_.items()}
if lookup in rev:
return rev[lookup]
return None
def add_language_system(self, location, script, language): def add_language_system(self, location, script, language):
# OpenType Feature File Specification, section 4.b.i # OpenType Feature File Specification, section 4.b.i
if script == "DFLT" and language == "dflt" and self.default_language_systems_: if script == "DFLT" and language == "dflt" and self.default_language_systems_:

View File

@ -0,0 +1,10 @@
from typing import NamedTuple
LOOKUP_DEBUG_INFO_KEY = "com.github.fonttools.feaLib"
class LookupDebugInfo(NamedTuple):
"""Information about where a lookup came from, to be embedded in a font"""
location: str
name: str
feature: list

View File

@ -0,0 +1,17 @@
import json
from . import DefaultTable
class table_D__e_b_g(DefaultTable.DefaultTable):
def decompile(self, data, ttFont):
self.data = json.loads(data)
def compile(self, ttFont):
return json.dumps(self.data).encode("utf-8")
def toXML(self, writer, ttFont):
writer.writecdata(json.dumps(self.data))
def fromXML(self, name, attrs, content, ttFont):
self.data = json.loads(content)

View File

@ -12,6 +12,7 @@ from fontTools.misc.py23 import *
from fontTools.misc.fixedTools import otRound from fontTools.misc.fixedTools import otRound
from fontTools.misc.textTools import pad, safeEval from fontTools.misc.textTools import pad, safeEval
from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord, CountReference
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
import logging import logging
import struct import struct
@ -1187,6 +1188,44 @@ class COLR(BaseTable):
} }
class LookupList(BaseTable):
@property
def table(self):
for l in self.Lookup:
for st in l.SubTable:
if type(st).__name__.endswith("Subst"):
return "GSUB"
if type(st).__name__.endswith("Pos"):
return "GPOS"
raise ValueError
def toXML2(self, xmlWriter, font):
if not font or "Debg" not in font or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data:
return super().toXML2(xmlWriter, font)
debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
for conv in self.getConverters():
if conv.repeat:
value = getattr(self, conv.name, [])
for lookupIndex, item in enumerate(value):
if str(lookupIndex) in debugData:
info = LookupDebugInfo(*debugData[str(lookupIndex)])
tag = info.location
if info.name:
tag = f'{info.name}: {tag}'
if info.feature:
script,language,feature = info.feature
tag = f'{tag} in {feature} ({script}/{language})'
xmlWriter.comment(tag)
xmlWriter.newline()
conv.xmlWrite(xmlWriter, font, item, conv.name,
[("index", lookupIndex)])
else:
if conv.aux and not eval(conv.aux, None, vars(self)):
continue
value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None!
conv.xmlWrite(xmlWriter, font, value, conv.name, [])
class BaseGlyphRecordArray(BaseTable): class BaseGlyphRecordArray(BaseTable):
def preWrite(self, font): def preWrite(self, font):

View File

@ -114,12 +114,16 @@ class BuilderTest(unittest.TestCase):
lines.append(line.rstrip() + os.linesep) lines.append(line.rstrip() + os.linesep)
return lines return lines
def expect_ttx(self, font, expected_ttx): def expect_ttx(self, font, expected_ttx, replace=None):
path = self.temp_path(suffix=".ttx") path = self.temp_path(suffix=".ttx")
font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB', font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
'GPOS', 'OS/2', 'hhea', 'vhea']) 'GPOS', 'OS/2', 'hhea', 'vhea'])
actual = self.read_ttx(path) actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx) expected = self.read_ttx(expected_ttx)
if replace:
for i in range(len(expected)):
for k, v in replace.items():
expected[i] = expected[i].replace(k, v)
if actual != expected: if actual != expected:
for line in difflib.unified_diff( for line in difflib.unified_diff(
expected, actual, fromfile=expected_ttx, tofile=path): expected, actual, fromfile=expected_ttx, tofile=path):
@ -133,12 +137,17 @@ class BuilderTest(unittest.TestCase):
def check_feature_file(self, name): def check_feature_file(self, name):
font = makeTTFont() font = makeTTFont()
addOpenTypeFeatures(font, self.getpath("%s.fea" % name)) feapath = self.getpath("%s.fea" % name)
addOpenTypeFeatures(font, feapath)
self.expect_ttx(font, self.getpath("%s.ttx" % name)) self.expect_ttx(font, self.getpath("%s.ttx" % name))
# Make sure we can produce binary OpenType tables, not just XML. # Make sure we can produce binary OpenType tables, not just XML.
for tag in ('GDEF', 'GSUB', 'GPOS'): for tag in ('GDEF', 'GSUB', 'GPOS'):
if tag in font: if tag in font:
font[tag].compile(font) font[tag].compile(font)
debugttx = self.getpath("%s-debug.ttx" % name)
if os.path.exists(debugttx):
addOpenTypeFeatures(font, feapath, debug=True)
self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath})
def check_fea2fea_file(self, name, base=None, parser=Parser): def check_fea2fea_file(self, name, base=None, parser=Parser):
font = makeTTFont() font = makeTTFont()

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=4 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
<FeatureIndex index="2" value="2"/>
<FeatureIndex index="3" value="3"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=4 -->
<FeatureRecord index="0">
<FeatureTag value="tst1"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="tst2"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="2">
<FeatureTag value="tst3"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="1"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="3">
<FeatureTag value="tst4"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="1"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=2 -->
<!-- SomeLookup: __PATH__:4:5 in tst2 (DFLT/dflt) -->
<Lookup index="0">
<LookupType value="4"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<LigatureSubst index="0">
<LigatureSet glyph="f">
<Ligature components="f,i" glyph="f_f_i"/>
<Ligature components="i" glyph="f_i"/>
</LigatureSet>
</LigatureSubst>
</Lookup>
<!-- EmbeddedLookup: __PATH__:18:9 in tst4 (DFLT/dflt) -->
<Lookup index="1">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="A" out="A.sc"/>
</SingleSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>