[varLib] Generate avar table

Fixes https://github.com/fonttools/fonttools/issues/916
This commit is contained in:
Behdad Esfahbod 2017-04-12 21:52:29 -07:00
parent 191cea5965
commit 968c25dd7f
3 changed files with 136 additions and 41 deletions

View File

@ -45,8 +45,7 @@ class VarLibError(Exception):
# Creation routines
#
# Move to fvar table proper?
def _add_fvar(font, axes, instances):
def _add_fvar_avar(font, axes, instances):
"""
Add 'fvar' table to font.
@ -56,8 +55,12 @@ def _add_fvar(font, axes, instances):
and possibly 'postscriptfontname' entries.
"""
assert "fvar" not in font
font['fvar'] = fvar = newTable('fvar')
assert axes
assert isinstance(axes, OrderedDict)
log.info("Generating fvar / avar")
fvar = newTable('fvar')
nameTable = font['name']
for a in axes.values():
@ -78,10 +81,51 @@ def _add_fvar(font, axes, instances):
if psname is not None:
psname = tounicode(psname)
inst.postscriptNameID = nameTable.addName(psname)
inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()}
inst.coordinates = {axes[k].tag:axes[k].map_backward(v) for k,v in coordinates.items()}
fvar.instances.append(inst)
return fvar
avar = newTable('avar')
interesting = False
for axis in axes.values():
curve = avar.segments[axis.tag] = {}
if not axis.map or all(k==v for k,v in axis.map.items()):
continue
interesting = True
items = sorted(axis.map.items())
keys = [item[0] for item in items]
vals = [item[1] for item in items]
# Current avar requirements. We don't have to enforce
# these on the designer and can deduce some ourselves,
# but for now just enforce them.
assert axis.minimum == min(keys)
assert axis.maximum == max(keys)
assert axis.default in keys
# No duplicates
assert len(set(keys)) == len(keys)
assert len(set(vals)) == len(vals)
# Ascending values
assert sorted(vals) == vals
keys_triple = (axis.minimum, axis.default, axis.maximum)
vals_triple = tuple(axis.map_forward(v) for v in keys_triple)
keys = [models.normalizeValue(v, keys_triple) for v in keys]
vals = [models.normalizeValue(v, vals_triple) for v in vals]
curve.update(zip(keys, vals))
if not interesting:
log.info("No need for avar")
avar = None
assert "fvar" not in font
font['fvar'] = fvar
assert "avar" not in font
if avar:
font['avar'] = avar
return fvar,avar
# TODO Move to glyf or gvar table proper
def _GetCoordinates(font, glyphName):
@ -369,16 +413,49 @@ def build(designspace_filename, master_finder=lambda s:s):
('optical', ('opsz', {'en':'Optical Size'})),
])
# Setup axes
class DesignspaceAxis(object):
pass
@staticmethod
def _map(v, map):
keys = map.keys()
if not keys:
return v
if v in keys:
return map[v]
k = min(keys)
if v < k:
return v + map[k] - k
k = max(keys)
if v > k:
return v + map[k] - k
# Interpolate
a = max(k for k in keys if k < v)
b = min(k for k in keys if k > v)
va = map[a]
vb = map[b]
return va + (vb - va) * (v - a) / (b - a)
def map_forward(self, v):
if self.map is None: return v
return self._map(v, self.map)
def map_backward(self, v):
if self.map is None: return v
map = {v:k for k,v in self.map.items()}
return self._map(v, map)
axis_objects = OrderedDict()
if axes is not None:
for axis_dict in axes:
axis_name = axis_dict.get('name')
if not axis_name:
axis_name = axis_dict['name'] = axis_dict['tag']
if 'map' not in axis_dict:
axis_dict['map'] = None
else:
axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']}
if axis_name in standard_axis_map:
if 'tag' not in axis_dict:
@ -387,7 +464,7 @@ def build(designspace_filename, master_finder=lambda s:s):
axis_dict['labelname'] = standard_axis_map[axis_name][1].copy()
axis = DesignspaceAxis()
for item in ['name', 'tag', 'labelname', 'minimum', 'default', 'maximum']:
for item in ['name', 'tag', 'labelname', 'minimum', 'default', 'maximum', 'map']:
assert item in axis_dict, 'Axis does not have "%s"' % item
axis.__dict__ = axis_dict
axis_objects[axis_name] = axis
@ -416,15 +493,13 @@ def build(designspace_filename, master_finder=lambda s:s):
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.map = None
# TODO Fill in weight / width mapping from OS/2 table? Need loading fonts...
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))
# Check all master and instance locations are valid and fill in defaults
for obj in masters+instances:
@ -436,16 +511,25 @@ def build(designspace_filename, master_finder=lambda s:s):
if axis_name not in loc:
loc[axis_name] = axis.default
else:
v = loc[axis_name]
assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (%s) out of range for '%s'" % (name, v, obj_name)
v = axis.map_backward(loc[axis_name])
assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (name, v, obj_name, axis.minimum, axis.maximum)
master_locs = [o['location'] for o in masters]
log.info("Master locations:\n%s", pformat(master_locs))
# Normalize master locations
master_locs = [o['location'] for o in masters]
log.info("Internal master locations:\n%s", pformat(master_locs))
axis_supports = {}
for axis in axes.values():
triple = (axis.minimum, axis.default, axis.maximum)
axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
log.info("Internal axis supports:\n%s", pformat(axis_supports))
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):
@ -456,7 +540,6 @@ def build(designspace_filename, master_finder=lambda s:s):
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)
@ -466,7 +549,7 @@ def build(designspace_filename, master_finder=lambda s:s):
vf = TTFont(master_ttfs[base_idx])
# TODO append masters as named-instances as well; needs .designspace change.
fvar = _add_fvar(vf, axes, instances)
fvar,avar = _add_fvar_avar(vf, axes, instances)
# TODO Clean this up.
del instances

View File

@ -2,13 +2,33 @@
from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import *
__all__ = ['normalizeLocation', 'supportScalar', 'VariationModel']
__all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel']
def normalizeValue(v, triple):
"""Normalizes value based on a min/default/max triple.
>>> normalizeValue(400, (100, 400, 900))
0.0
>>> normalizeValue(100, (100, 400, 900))
-1.0
>>> normalizeValue(650, (100, 400, 900))
0.5
"""
lower, default, upper = triple
assert lower <= default <= upper, "invalid axis values"
v = max(min(v, upper), lower)
if v == default:
v = 0.
elif v < default:
v = (v - default) / (default - lower)
else:
v = (v - default) / (upper - default)
return v
def normalizeLocation(location, axes):
"""Normalizes location based on axis min/default/max values from axes.
>>> axes = {"wght": (100, 400, 900)}
>>> normalizeLocation({"wght": 400}, axes)
{'wght': 0}
{'wght': 0.0}
>>> normalizeLocation({"wght": 100}, axes)
{'wght': -1.0}
>>> normalizeLocation({"wght": 900}, axes)
@ -21,9 +41,9 @@ def normalizeLocation(location, axes):
{'wght': -1.0}
>>> axes = {"wght": (0, 0, 1000)}
>>> normalizeLocation({"wght": 0}, axes)
{'wght': 0}
{'wght': 0.0}
>>> normalizeLocation({"wght": -1}, axes)
{'wght': 0}
{'wght': 0.0}
>>> normalizeLocation({"wght": 1000}, axes)
{'wght': 1.0}
>>> normalizeLocation({"wght": 500}, axes)
@ -38,22 +58,14 @@ def normalizeLocation(location, axes):
>>> normalizeLocation({"wght": 500}, axes)
{'wght': -0.5}
>>> normalizeLocation({"wght": 1000}, axes)
{'wght': 0}
{'wght': 0.0}
>>> normalizeLocation({"wght": 1001}, axes)
{'wght': 0}
{'wght': 0.0}
"""
out = {}
for tag,(lower,default,upper) in axes.items():
assert lower <= default <= upper, "invalid axis values"
v = location.get(tag, default)
v = max(min(v, upper), lower)
if v == default:
v = 0
elif v < default:
v = (v - default) / (default - lower)
else:
v = (v - default) / (upper - default)
out[tag] = v
for tag,triple in axes.items():
v = location.get(tag, triple[1])
out[tag] = normalizeValue(v, triple)
return out
def supportScalar(location, support):

View File

@ -6,7 +6,7 @@ from fontTools.varLib.models import (
def test_normalizeLocation():
axes = {"wght": (100, 400, 900)}
assert normalizeLocation({"wght": 400}, axes) == {'wght': 0}
assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0}
assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0}
assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0}
assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5}
@ -14,8 +14,8 @@ def test_normalizeLocation():
assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
axes = {"wght": (0, 0, 1000)}
assert normalizeLocation({"wght": 0}, axes) == {'wght': 0}
assert normalizeLocation({"wght": -1}, axes) == {'wght': 0}
assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0}
assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0}
assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5}
assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0}
@ -24,8 +24,8 @@ def test_normalizeLocation():
assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0}
assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5}
assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0}
assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0}
assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0}
assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0}
def test_supportScalar():