undo DesignSpaceDocument context manager changes, and explicitly (but opt-out) close master_fonts within varLib.build()

This commit is contained in:
justvanrossum 2020-01-29 11:55:39 +01:00
parent 703c2272bd
commit 49507a9de8
3 changed files with 223 additions and 225 deletions

View File

@ -1322,14 +1322,3 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
loaded[source.path] = source.font loaded[source.path] = source.font
fonts.append(source.font) fonts.append(source.font)
return fonts return fonts
def closeSourceFonts(self):
for source in self.sources:
if source.font is not None and hasattr(source.font, "close"):
source.font.close()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.closeSourceFonts()

View File

@ -821,7 +821,7 @@ def set_default_weight_width_slant(font, location):
font["post"].italicAngle = italicAngle font["post"].italicAngle = italicAngle
def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True): def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True, close_source_fonts=True):
""" """
Build variation font from a designspace file. Build variation font from a designspace file.
@ -840,59 +840,64 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
log.info("Loading master fonts") log.info("Loading master fonts")
master_fonts = load_masters(designspace, master_finder) master_fonts = load_masters(designspace, master_finder)
# TODO: 'master_ttfs' is unused except for return value, remove later try:
master_ttfs = [] # TODO: 'master_ttfs' is unused except for return value, remove later
for master in master_fonts: master_ttfs = []
try: for master in master_fonts:
master_ttfs.append(master.reader.file.name) try:
except AttributeError: master_ttfs.append(master.reader.file.name)
master_ttfs.append(None) # in-memory fonts have no path except AttributeError:
master_ttfs.append(None) # in-memory fonts have no path
# Copy the base master to work from it # Copy the base master to work from it
vf = deepcopy(master_fonts[ds.base_idx]) vf = deepcopy(master_fonts[ds.base_idx])
# TODO append masters as named-instances as well; needs .designspace change. # TODO append masters as named-instances as well; needs .designspace change.
fvar = _add_fvar(vf, ds.axes, ds.instances) fvar = _add_fvar(vf, ds.axes, ds.instances)
if 'STAT' not in exclude: if 'STAT' not in exclude:
_add_stat(vf, ds.axes) _add_stat(vf, ds.axes)
if 'avar' not in exclude: if 'avar' not in exclude:
_add_avar(vf, ds.axes) _add_avar(vf, ds.axes)
# Map from axis names to axis tags... # Map from axis names to axis tags...
normalized_master_locs = [ normalized_master_locs = [
{ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs {ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs
] ]
# From here on, we use fvar axes only # From here on, we use fvar axes only
axisTags = [axis.axisTag for axis in fvar.axes] axisTags = [axis.axisTag for axis in fvar.axes]
# Assume single-model for now. # Assume single-model for now.
model = models.VariationModel(normalized_master_locs, axisOrder=axisTags) model = models.VariationModel(normalized_master_locs, axisOrder=axisTags)
assert 0 == model.mapping[ds.base_idx] assert 0 == model.mapping[ds.base_idx]
log.info("Building variations tables") log.info("Building variations tables")
if 'MVAR' not in exclude: if 'MVAR' not in exclude:
_add_MVAR(vf, model, master_fonts, axisTags) _add_MVAR(vf, model, master_fonts, axisTags)
if 'HVAR' not in exclude: if 'HVAR' not in exclude:
_add_HVAR(vf, model, master_fonts, axisTags) _add_HVAR(vf, model, master_fonts, axisTags)
if 'VVAR' not in exclude and 'vmtx' in vf: if 'VVAR' not in exclude and 'vmtx' in vf:
_add_VVAR(vf, model, master_fonts, axisTags) _add_VVAR(vf, model, master_fonts, axisTags)
if 'GDEF' not in exclude or 'GPOS' not in exclude: if 'GDEF' not in exclude or 'GPOS' not in exclude:
_merge_OTL(vf, model, master_fonts, axisTags) _merge_OTL(vf, model, master_fonts, axisTags)
if 'gvar' not in exclude and 'glyf' in vf: if 'gvar' not in exclude and 'glyf' in vf:
_add_gvar(vf, model, master_fonts, optimize=optimize) _add_gvar(vf, model, master_fonts, optimize=optimize)
if 'cvar' not in exclude and 'glyf' in vf: if 'cvar' not in exclude and 'glyf' in vf:
_merge_TTHinting(vf, model, master_fonts) _merge_TTHinting(vf, model, master_fonts)
if 'GSUB' not in exclude and ds.rules: if 'GSUB' not in exclude and ds.rules:
_add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules, ds.rulesProcessingLast) _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules, ds.rulesProcessingLast)
if 'CFF2' not in exclude and 'CFF ' in vf: if 'CFF2' not in exclude and 'CFF ' in vf:
_add_CFF2(vf, model, master_fonts) _add_CFF2(vf, model, master_fonts)
if "post" in vf: if "post" in vf:
# set 'post' to format 2 to keep the glyph names dropped from CFF2 # set 'post' to format 2 to keep the glyph names dropped from CFF2
post = vf["post"] post = vf["post"]
if post.formatType != 2.0: if post.formatType != 2.0:
post.formatType = 2.0 post.formatType = 2.0
post.extraNames = [] post.extraNames = []
post.mapping = {} post.mapping = {}
finally:
if close_source_fonts:
for master in master_fonts:
master.close()
set_default_weight_width_slant( set_default_weight_width_slant(
vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes} vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}

View File

@ -282,18 +282,18 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'TestNonMarkingCFF2_'): for path in self.get_file_list(ttx_dir, '.ttx', 'TestNonMarkingCFF2_'):
self.compile_font(path, ".otf", self.tempdir) self.compile_font(path, ".otf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = ["CFF2"] tables = ["CFF2"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_CFF2(self): def test_varlib_build_CFF2(self):
ds_path = self.get_test_input('TestCFF2.designspace') ds_path = self.get_test_input('TestCFF2.designspace')
@ -304,18 +304,18 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'TestCFF2_'): for path in self.get_file_list(ttx_dir, '.ttx', 'TestCFF2_'):
self.compile_font(path, ".otf", self.tempdir) self.compile_font(path, ".otf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = ["fvar", "CFF2"] tables = ["fvar", "CFF2"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_sparse_CFF2(self): def test_varlib_build_sparse_CFF2(self):
ds_path = self.get_test_input('TestSparseCFF2VF.designspace') ds_path = self.get_test_input('TestSparseCFF2VF.designspace')
@ -326,18 +326,18 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'): for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'):
self.compile_font(path, ".otf", self.tempdir) self.compile_font(path, ".otf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = ["fvar", "CFF2"] tables = ["fvar", "CFF2"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_vpal(self): def test_varlib_build_vpal(self):
ds_path = self.get_test_input('test_vpal.designspace') ds_path = self.get_test_input('test_vpal.designspace')
@ -348,18 +348,18 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'master_vpal_test_'): for path in self.get_file_list(ttx_dir, '.ttx', 'master_vpal_test_'):
self.compile_font(path, ".otf", self.tempdir) self.compile_font(path, ".otf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = ["GPOS"] tables = ["GPOS"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_main_ttf(self): def test_varlib_main_ttf(self):
"""Mostly for testing varLib.main() """Mostly for testing varLib.main()
@ -416,20 +416,20 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
self.compile_font(path, ".ttf", self.tempdir) self.compile_font(path, ".ttf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
filename = os.path.join( filename = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
) )
source.font = TTFont( source.font = TTFont(
filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True
) )
source.filename = None # Make sure no file path gets into build() source.filename = None # Make sure no file path gets into build()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_from_ttf_paths(self): def test_varlib_build_from_ttf_paths(self):
ds_path = self.get_test_input("Build.designspace") ds_path = self.get_test_input("Build.designspace")
@ -440,34 +440,34 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
self.compile_font(path, ".ttf", self.tempdir) self.compile_font(path, ".ttf", self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_from_ttx_paths(self): def test_varlib_build_from_ttx_paths(self):
ds_path = self.get_test_input("Build.designspace") ds_path = self.get_test_input("Build.designspace")
ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
expected_ttx_path = self.get_test_output("BuildMain.ttx") expected_ttx_path = self.get_test_output("BuildMain.ttx")
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx") ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx")
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_sparse_masters(self): def test_varlib_build_sparse_masters(self):
ds_path = self.get_test_input("SparseMasters.designspace") ds_path = self.get_test_input("SparseMasters.designspace")
@ -482,9 +482,10 @@ class BuildTest(unittest.TestCase):
import fontTools.varLib.mvar import fontTools.varLib.mvar
ds_path = self.get_test_input("SparseMasters.designspace") ds_path = self.get_test_input("SparseMasters.designspace")
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
load_masters(ds) master_fonts = load_masters(ds)
try:
# Trigger MVAR generation so varLib is forced to create deltas with a # Trigger MVAR generation so varLib is forced to create deltas with a
# sparse master inbetween. # sparse master inbetween.
font_0_os2 = ds.sources[0].font["OS/2"] font_0_os2 = ds.sources[0].font["OS/2"]
@ -558,6 +559,9 @@ class BuildTest(unittest.TestCase):
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
mvar_tags = [vr.ValueTag for vr in varfont["MVAR"].table.ValueRecord] mvar_tags = [vr.ValueTag for vr in varfont["MVAR"].table.ValueRecord]
assert all(tag in mvar_tags for tag in fontTools.varLib.mvar.MVAR_ENTRIES) assert all(tag in mvar_tags for tag in fontTools.varLib.mvar.MVAR_ENTRIES)
finally:
for master in master_fonts:
master.close()
def test_varlib_build_VVAR_CFF2(self): def test_varlib_build_VVAR_CFF2(self):
ds_path = self.get_test_input('TestVVAR.designspace') ds_path = self.get_test_input('TestVVAR.designspace')
@ -569,20 +573,20 @@ class BuildTest(unittest.TestCase):
for path in self.get_file_list(ttx_dir, '.ttx', 'TestVVAR'): for path in self.get_file_list(ttx_dir, '.ttx', 'TestVVAR'):
font, savepath = self.compile_font(path, suffix, self.tempdir) font, savepath = self.compile_font(path, suffix, self.tempdir)
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
source.path = os.path.join( source.path = os.path.join(
self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix) self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix)
) )
ds.updatePaths() ds.updatePaths()
varfont, _, _ = build(ds) varfont, _, _ = build(ds)
varfont = reload_font(varfont) varfont = reload_font(varfont)
expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
tables = ["VVAR"] tables = ["VVAR"]
self.expect_ttx(varfont, expected_ttx_path, tables) self.expect_ttx(varfont, expected_ttx_path, tables)
self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
def test_varlib_build_single_master(self): def test_varlib_build_single_master(self):
self._run_varlib_build_test( self._run_varlib_build_test(
@ -603,87 +607,87 @@ class BuildTest(unittest.TestCase):
ds_path = self.get_test_input("KerningMerging.designspace") ds_path = self.get_test_input("KerningMerging.designspace")
ttx_dir = self.get_test_input("master_kerning_merging") ttx_dir = self.get_test_input("master_kerning_merging")
with DesignSpaceDocument.fromfile(ds_path) as ds: ds = DesignSpaceDocument.fromfile(ds_path)
for source in ds.sources: for source in ds.sources:
ttx_dump = TTFont() ttx_dump = TTFont()
ttx_dump.importXML( ttx_dump.importXML(
os.path.join( os.path.join(
ttx_dir, os.path.basename(source.filename).replace(".ttf", ".ttx") ttx_dir, os.path.basename(source.filename).replace(".ttf", ".ttx")
)
) )
source.font = reload_font(ttx_dump)
varfont, _, _ = build(ds)
varfont = reload_font(varfont)
class_kerning_tables = [
t
for l in varfont["GPOS"].table.LookupList.Lookup
for t in l.SubTable
if t.Format == 2
]
assert len(class_kerning_tables) == 1
class_kerning_table = class_kerning_tables[0]
# Test that no class kerned against class zero (containing all glyphs not
# classed) has a `XAdvDevice` table attached, which in the variable font
# context is a "VariationIndex" table and points to kerning deltas in the GDEF
# table. Variation deltas of any kerning class against class zero should
# probably never exist.
for class1_record in class_kerning_table.Class1Record:
class2_zero = class1_record.Class2Record[0]
assert getattr(class2_zero.Value1, "XAdvDevice", None) is None
# Assert the variable font's kerning table (without deltas) is equal to the
# default font's kerning table. The bug fixed in
# https://github.com/fonttools/fonttools/pull/1638 caused rogue kerning
# values to be written to the variable font.
assert _extract_flat_kerning(varfont, class_kerning_table) == {
("A", ".notdef"): 0,
("A", "A"): 0,
("A", "B"): -20,
("A", "C"): 0,
("A", "D"): -20,
("B", ".notdef"): 0,
("B", "A"): 0,
("B", "B"): 0,
("B", "C"): 0,
("B", "D"): 0,
}
instance_thin = instantiateVariableFont(varfont, {"wght": 100})
instance_thin_kerning_table = (
instance_thin["GPOS"].table.LookupList.Lookup[0].SubTable[0]
) )
assert _extract_flat_kerning(instance_thin, instance_thin_kerning_table) == { source.font = reload_font(ttx_dump)
("A", ".notdef"): 0,
("A", "A"): 0,
("A", "B"): 0,
("A", "C"): 10,
("A", "D"): 0,
("B", ".notdef"): 0,
("B", "A"): 0,
("B", "B"): 0,
("B", "C"): 10,
("B", "D"): 0,
}
instance_black = instantiateVariableFont(varfont, {"wght": 900}) varfont, _, _ = build(ds)
instance_black_kerning_table = ( varfont = reload_font(varfont)
instance_black["GPOS"].table.LookupList.Lookup[0].SubTable[0]
) class_kerning_tables = [
assert _extract_flat_kerning(instance_black, instance_black_kerning_table) == { t
("A", ".notdef"): 0, for l in varfont["GPOS"].table.LookupList.Lookup
("A", "A"): 0, for t in l.SubTable
("A", "B"): 0, if t.Format == 2
("A", "C"): 0, ]
("A", "D"): 40, assert len(class_kerning_tables) == 1
("B", ".notdef"): 0, class_kerning_table = class_kerning_tables[0]
("B", "A"): 0,
("B", "B"): 0, # Test that no class kerned against class zero (containing all glyphs not
("B", "C"): 0, # classed) has a `XAdvDevice` table attached, which in the variable font
("B", "D"): 40, # context is a "VariationIndex" table and points to kerning deltas in the GDEF
} # table. Variation deltas of any kerning class against class zero should
# probably never exist.
for class1_record in class_kerning_table.Class1Record:
class2_zero = class1_record.Class2Record[0]
assert getattr(class2_zero.Value1, "XAdvDevice", None) is None
# Assert the variable font's kerning table (without deltas) is equal to the
# default font's kerning table. The bug fixed in
# https://github.com/fonttools/fonttools/pull/1638 caused rogue kerning
# values to be written to the variable font.
assert _extract_flat_kerning(varfont, class_kerning_table) == {
("A", ".notdef"): 0,
("A", "A"): 0,
("A", "B"): -20,
("A", "C"): 0,
("A", "D"): -20,
("B", ".notdef"): 0,
("B", "A"): 0,
("B", "B"): 0,
("B", "C"): 0,
("B", "D"): 0,
}
instance_thin = instantiateVariableFont(varfont, {"wght": 100})
instance_thin_kerning_table = (
instance_thin["GPOS"].table.LookupList.Lookup[0].SubTable[0]
)
assert _extract_flat_kerning(instance_thin, instance_thin_kerning_table) == {
("A", ".notdef"): 0,
("A", "A"): 0,
("A", "B"): 0,
("A", "C"): 10,
("A", "D"): 0,
("B", ".notdef"): 0,
("B", "A"): 0,
("B", "B"): 0,
("B", "C"): 10,
("B", "D"): 0,
}
instance_black = instantiateVariableFont(varfont, {"wght": 900})
instance_black_kerning_table = (
instance_black["GPOS"].table.LookupList.Lookup[0].SubTable[0]
)
assert _extract_flat_kerning(instance_black, instance_black_kerning_table) == {
("A", ".notdef"): 0,
("A", "A"): 0,
("A", "B"): 0,
("A", "C"): 0,
("A", "D"): 40,
("B", ".notdef"): 0,
("B", "A"): 0,
("B", "B"): 0,
("B", "C"): 0,
("B", "D"): 40,
}
def test_load_masters_layerName_without_required_font(): def test_load_masters_layerName_without_required_font():