From 9034f4f3f105a811d30201eef68e7143ef739119 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 12 Apr 2017 17:14:22 -0700 Subject: [PATCH] [varLib] Rewrite axis configuration code We do not accept nonstandard axes if element is not present. Breaks one test. Needs updated expectations. Soon we'll be generating avar as well and all will be better, much better. --- Lib/fontTools/varLib/__init__.py | 191 ++++++++++++++------------- Tests/varLib/data/Build2.designspace | 148 --------------------- Tests/varLib/varLib_test.py | 11 +- 3 files changed, 104 insertions(+), 246 deletions(-) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 65119eb7c..c97192490 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -46,32 +46,26 @@ class VarLibError(Exception): # # Move to fvar table proper? -def _add_fvar(font, axes, instances, axis_map): +def _add_fvar(font, axes, instances): """ Add 'fvar' table to font. - axes is a dictionary mapping axis-id to axis (min,default,max) - coordinate values. + axes is an ordered dictionary of DesignspaceAxis objects. instances is list of dictionary objects with 'location', 'stylename', and possibly 'postscriptfontname' entries. - - axis_map is an ordered dictionary mapping axis-id to (axis-tag, axis-name-dict). """ assert "fvar" not in font font['fvar'] = fvar = newTable('fvar') nameTable = font['name'] - for iden in axis_map.keys(): - if iden not in axes: - continue + for a in axes.values(): axis = Axis() - axis.axisTag = Tag(axis_map[iden][0]) - axis.minValue, axis.defaultValue, axis.maxValue = axes[iden] + axis.axisTag = Tag(a.tag) + axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum # TODO: Add all languages: https://github.com/fonttools/fonttools/issues/921 - axisName = tounicode(axis_map[iden][1]['en']) - axis.axisNameID = nameTable.addName(axisName) + axis.axisNameID = nameTable.addName(tounicode(a.labelname['en'])) fvar.axes.append(axis) for instance in instances: @@ -84,7 +78,7 @@ def _add_fvar(font, axes, instances, axis_map): if psname is not None: psname = tounicode(psname) inst.postscriptNameID = nameTable.addName(psname) - inst.coordinates = {axis_map[k][0]:v for k,v in coordinates.items()} + inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()} fvar.instances.append(inst) return fvar @@ -368,12 +362,6 @@ def build(designspace_filename, master_finder=lambda s:s): raise VarLibError("no sources found in .designspace") instances = ds.get('instances', []) - log.info("Building variable font") - log.info("Loading master fonts") - basedir = os.path.dirname(designspace_filename) - master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] - master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] - standard_axis_map = OrderedDict([ ('weight', ('wght', {'en':'Weight'})), ('width', ('wdth', {'en':'Width'})), @@ -381,80 +369,103 @@ def build(designspace_filename, master_finder=lambda s:s): ('optical', ('opsz', {'en':'Optical Size'})), ]) + # Setup axes + class DesignspaceAxis(object): + pass + axis_objects = OrderedDict() if axes is not None: - # the designspace file loaded had an element. - # honor the order of the axes - axis_map = OrderedDict() - for axis in axes: - axis_name = axis['name'] - if axis_name in standard_axis_map: - axis_map[axis_name] = standard_axis_map[axis_name] - else: - tag = axis['tag'] - assert axis['labelname']['en'] - labels = axis['labelname'] - axis_map[axis_name] = (tag, labels) - else: - axis_map = standard_axis_map + for axis_dict in axes: + axis_name = axis_dict.get('name') + if not axis_name: + axis_name = axis_dict['name'] = axis_dict['tag'] + if axis_name in standard_axis_map: + if 'tag' not in axis_dict: + axis_dict['tag'] = standard_axis_map[axis_name][0] + if 'labelname' not in axis_dict: + axis_dict['labelname'] = standard_axis_map[axis_name][1].copy() + + axis = DesignspaceAxis() + for item in ['name', 'tag', 'labelname', 'minimum', 'default', 'maximum']: + assert item in axis_dict, 'Axis does not have "%s"' % item + axis.__dict__ = axis_dict + axis_objects[axis_name] = axis + else: + # No element. Guess things... + base_idx = None + for i,m in enumerate(masters): + if 'info' in m and m['info']['copy']: + assert base_idx is None + base_idx = i + assert base_idx is not None, "Cannot find 'base' master; Either add element to .designspace document, or add element to one of the sources in the .designspace document." + + master_locs = [o['location'] for o in masters] + base_loc = master_locs[base_idx] + axis_names = set(base_loc.keys()) + assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no element." + + for name,(tag,labelname) in standard_axis_map.items(): + if name not in axis_names: + continue + + axis = Axis() + axis.name = name + axis.tag = tag + axis.labelname = labelname.copy() + axis.default = base_loc[name] + axis.minimum = min(m[name] for m in master_locs if name in m) + axis.maximum = max(m[name] for m in master_locs if name in m) + axis_objects[name] = axis + del base_idx, base_loc, axis_names, master_locs + axes = axis_objects + del axis_objects + + axis_supports = {} + for axis in axes.values(): + axis_supports[axis.name] = (axis.minimum, axis.default, axis.maximum) + log.info("Axis supports:\n%s", pformat(axis_supports)) + + # TODO + # check all masters and instances locations are valid and fill in default items + master_locs = [o['location'] for o in masters] - - axis_names = set(master_locs[0].keys()) - assert all(axis_names == set(m.keys()) for m in master_locs) - - base_idx = None - for i,m in enumerate(masters): - if 'info' in m and m['info']['copy']: - assert base_idx is None - base_idx = i - - # Set up axes - axes_dict = {} - if axes is not None: - # the designspace file loaded had an element - for axis in axes: - default = axis['default'] - lower = axis['minimum'] - upper = axis['maximum'] - name = axis['name'] - axes_dict[name] = (lower, default, upper) - else: - - for name in axis_names: - default = master_locs[base_idx][name] - lower = min(m[name] for m in master_locs) - upper = max(m[name] for m in master_locs) - if default == lower == upper: - continue - axes_dict[name] = (lower, default, upper) - log.info("Axes:\n%s", pformat(axes_dict)) - assert all(name in axis_map for name in axes_dict.keys()) - - assert base_idx is not None, "Cannot find 'base' master; Either add element to .designspace document, or add element to one of the sources in the .designspace document." - log.info("Index of base master: %s", base_idx) - log.info("Master locations:\n%s", pformat(master_locs)) - # We can use the base font straight, but it's faster to load it again since - # then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables. - #gx = master_fonts[base_idx] - gx = TTFont(master_ttfs[base_idx]) + # Normalize master locations + master_locs = [models.normalizeLocation(m, axis_supports) for m in master_locs] + log.info("Normalized master locations:\n%s", pformat(master_locs)) + + # Find base master + base_idx = None + for i,m in enumerate(master_locs): + if all(v == 0 for v in m.values()): + assert base_idx is None + base_idx = i + assert base_idx is not None, "Base master not found; no master at default location?" + log.info("Index of base master: %s", base_idx) + + + + log.info("Building variable font") + log.info("Loading master fonts") + basedir = os.path.dirname(designspace_filename) + master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] + master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] + # Reload base font as target font + vf = TTFont(master_ttfs[base_idx]) # TODO append masters as named-instances as well; needs .designspace change. - fvar = _add_fvar(gx, axes_dict, instances, axis_map) - - - # Normalize master locations - master_locs = [models.normalizeLocation(m, axes_dict) for m in master_locs] - - log.info("Normalized master locations:\n%s", pformat(master_locs)) + fvar = _add_fvar(vf, axes, instances) # TODO Clean this up. del instances - del axes_dict - master_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in master_locs] - #instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs] + del axis_supports + + # Map from axis names to axis tags... + master_locs = [{axes[k].tag:v for k,v in loc.items()} for loc in master_locs] + #del axes + # From here on, we use fvar axes only axisTags = [axis.axisTag for axis in fvar.axes] # Assume single-model for now. @@ -462,13 +473,13 @@ def build(designspace_filename, master_finder=lambda s:s): assert 0 == model.mapping[base_idx] log.info("Building variations tables") - _add_MVAR(gx, model, master_fonts, axisTags) - if 'glyf' in gx: - _add_gvar(gx, model, master_fonts) - _add_HVAR(gx, model, master_fonts, axisTags) - _merge_OTL(gx, model, master_fonts, axisTags, base_idx) + _add_MVAR(vf, model, master_fonts, axisTags) + if 'glyf' in vf: + _add_gvar(vf, model, master_fonts) + _add_HVAR(vf, model, master_fonts, axisTags) + _merge_OTL(vf, model, master_fonts, axisTags, base_idx) - return gx, model, master_ttfs + return vf, model, master_ttfs def main(args=None): @@ -486,10 +497,10 @@ def main(args=None): finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf') outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' - gx, model, master_ttfs = build(designspace_filename, finder) + vf, model, master_ttfs = build(designspace_filename, finder) log.info("Saving variation font %s", outfile) - gx.save(outfile) + vf.save(outfile) if __name__ == "__main__": diff --git a/Tests/varLib/data/Build2.designspace b/Tests/varLib/data/Build2.designspace index 64f9de7b8..f3f98ab6c 100644 --- a/Tests/varLib/data/Build2.designspace +++ b/Tests/varLib/data/Build2.designspace @@ -13,31 +13,11 @@ - - - - - - - - - - - - - - - - - - - - @@ -45,7 +25,6 @@ - @@ -53,7 +32,6 @@ - @@ -61,7 +39,6 @@ - @@ -69,7 +46,6 @@ - @@ -77,49 +53,41 @@ - - - - - - - - @@ -131,157 +99,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py index f25f8d3e7..f1ad3e197 100644 --- a/Tests/varLib/varLib_test.py +++ b/Tests/varLib/varLib_test.py @@ -98,9 +98,7 @@ class BuildTest(unittest.TestCase): # ----- def test_varlib_build_ttf(self): - """Designspace file contains element. - build() is called without an axisMap parameter. - """ + """Designspace file contains element.""" suffix = '.ttf' ds_path = self.get_test_input('Build.designspace') ufo_dir = self.get_test_input('master_ufo') @@ -121,9 +119,7 @@ class BuildTest(unittest.TestCase): def test_varlib_build2_ttf(self): - """Designspace file does not contain an element. - build() is called with an axisMap parameter. - """ + """Designspace file does not contain an element.""" suffix = '.ttf' ds_path = self.get_test_input('Build2.designspace') ufo_dir = self.get_test_input('master_ufo') @@ -134,9 +130,8 @@ class BuildTest(unittest.TestCase): for path in ttx_paths: self.compile_font(path, suffix, self.tempdir) - axisMap = {'contrast': ('cntr', 'Contrast')} finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) - varfont, model, _ = build(ds_path, finder, axisMap) + varfont, model, _ = build(ds_path, finder) tables = ['GDEF', 'HVAR', 'fvar', 'gvar'] expected_ttx_path = self.get_test_output('Build.ttx')