Merge pull request #2052 from simoncozens/fealib-debug
feaLib source debugging
This commit is contained in:
commit
a18b6bfb6c
@ -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
|
||||||
|
@ -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_:
|
||||||
|
10
Lib/fontTools/feaLib/lookupDebugInfo.py
Normal file
10
Lib/fontTools/feaLib/lookupDebugInfo.py
Normal 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
|
17
Lib/fontTools/ttLib/tables/D__e_b_g.py
Normal file
17
Lib/fontTools/ttLib/tables/D__e_b_g.py
Normal 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)
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
80
Tests/feaLib/data/lookup-debug.ttx
Normal file
80
Tests/feaLib/data/lookup-debug.ttx
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user