Merge pull request #864 from miguelsousa/pr857-followup

Followup to PR #857
This commit is contained in:
Cosimo Lupo 2017-02-26 16:44:18 +00:00 committed by GitHub
commit ae58d3872c
6 changed files with 187 additions and 82 deletions

View File

@ -29,7 +29,7 @@ from fontTools.ttLib.tables._g_v_a_r import TupleVariation
from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otTables as ot
from fontTools.varLib import builder, designspace, models from fontTools.varLib import builder, designspace, models
from fontTools.varLib.merger import VariationMerger from fontTools.varLib.merger import VariationMerger
import collections from collections import OrderedDict
import warnings import warnings
import os.path import os.path
import logging import logging
@ -42,7 +42,6 @@ log = logging.getLogger("fontTools.varLib")
# #
# Move to fvar table proper? # Move to fvar table proper?
# TODO how to provide axis order?
def _add_fvar(font, axes, instances, axis_map): def _add_fvar(font, axes, instances, axis_map):
""" """
Add 'fvar' table to font. Add 'fvar' table to font.
@ -53,16 +52,14 @@ def _add_fvar(font, axes, instances, axis_map):
instances is list of dictionary objects with 'location', 'stylename', instances is list of dictionary objects with 'location', 'stylename',
and possibly 'postscriptfontname' entries. and possibly 'postscriptfontname' entries.
axisMap is dictionary mapping axis-id to (axis-tag, axis-name). axis_map is dictionary mapping axis-id to (axis-tag, axis-name).
""" """
assert "fvar" not in font assert "fvar" not in font
font['fvar'] = fvar = newTable('fvar') font['fvar'] = fvar = newTable('fvar')
nameTable = font['name'] nameTable = font['name']
for iden in axis_map.keys(): for iden in sorted(axes.keys(), key=lambda i: axis_map.keys().index(i)):
if not iden in axes:
continue
axis = Axis() axis = Axis()
axis.axisTag = Tag(axis_map[iden][0]) axis.axisTag = Tag(axis_map[iden][0])
axis.minValue, axis.defaultValue, axis.maxValue = axes[iden] axis.minValue, axis.defaultValue, axis.maxValue = axes[iden]
@ -267,7 +264,11 @@ def build(designspace_filename, master_finder=lambda s:s, axisMap=None):
(axis-tag, axis-name). (axis-tag, axis-name).
""" """
masters, instances, axisMapDS = designspace.load(designspace_filename) ds = designspace.load(designspace_filename)
axes = ds['axes']
masters = ds['masters']
instances = ds['instances']
base_idx = None base_idx = None
for i,m in enumerate(masters): for i,m in enumerate(masters):
if 'info' in m and m['info']['copy']: if 'info' in m and m['info']['copy']:
@ -283,32 +284,62 @@ def build(designspace_filename, master_finder=lambda s:s, axisMap=None):
master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] 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] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
standard_axis_map = OrderedDict([
('weight', ('wght', 'Weight')),
('width', ('wdth', 'Width')),
('slant', ('slnt', 'Slant')),
('optical', ('opsz', 'Optical Size')),
('custom', ('xxxx', 'Custom'))
])
if axisMap: if axisMap:
axis_map = designspace.standard_axis_map.copy() # a dictionary mapping axis-id to (axis-tag, axis-name) was provided
axis_map = standard_axis_map.copy()
axis_map.update(axisMap) axis_map.update(axisMap)
elif axisMapDS: elif axes:
axis_map = axisMapDS # the designspace file loaded had an <axes> 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: else:
axis_map = designspace.standard_axis_map tag = axis['tag']
assert axis['labelname']['en']
label = axis['labelname']['en']
axis_map[axis_name] = (tag, label)
else:
axis_map = standard_axis_map
# TODO: For weight & width, use OS/2 values and setup 'avar' mapping. # TODO: For weight & width, use OS/2 values and setup 'avar' mapping.
master_locs = [o['location'] for o in masters] master_locs = [o['location'] for o in masters]
axis_tags = set(master_locs[0].keys()) axis_names = set(master_locs[0].keys())
assert all(axis_tags == set(m.keys()) for m in master_locs) assert all(axis_names == set(m.keys()) for m in master_locs)
# Set up axes # Set up axes
axes = {} axes_dict = {}
for tag in axis_tags: if axes:
default = master_locs[base_idx][tag] # the designspace file loaded had an <axes> element
lower = min(m[tag] for m in master_locs) for axis in axes:
upper = max(m[tag] for m in master_locs) 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: if default == lower == upper:
continue continue
axes[tag] = (lower, default, upper) axes_dict[name] = (lower, default, upper)
log.info("Axes:\n%s", pformat(axes)) log.info("Axes:\n%s", pformat(axes_dict))
assert all(name in axis_map for name in axes_dict.keys())
log.info("Master locations:\n%s", pformat(master_locs)) log.info("Master locations:\n%s", pformat(master_locs))
@ -318,17 +349,17 @@ def build(designspace_filename, master_finder=lambda s:s, axisMap=None):
gx = TTFont(master_ttfs[base_idx]) gx = TTFont(master_ttfs[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(gx, axes, instances, axis_map) fvar = _add_fvar(gx, axes_dict, instances, axis_map)
# Normalize master locations # Normalize master locations
master_locs = [models.normalizeLocation(m, axes) for m in master_locs] master_locs = [models.normalizeLocation(m, axes_dict) for m in master_locs]
log.info("Normalized master locations:\n%s", pformat(master_locs)) log.info("Normalized master locations:\n%s", pformat(master_locs))
# TODO Clean this up. # TODO Clean this up.
del instances del instances
del axes del axes_dict
master_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in master_locs] 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] #instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs]
axisTags = [axis.axisTag for axis in fvar.axes] axisTags = [axis.axisTag for axis in fvar.axes]

View File

@ -9,13 +9,7 @@ except ImportError:
__all__ = ['load', 'loads'] __all__ = ['load', 'loads']
standard_axis_map = collections.OrderedDict( namespaces = {'xml': '{http://www.w3.org/XML/1998/namespace}'}
[['weight', ('wght', 'Weight')],
['width', ('wdth', 'Width')],
['slant', ('slnt', 'Slant')],
['optical', ('opsz', 'Optical Size')],
['custom',('xxxx', 'Custom')]]
)
def _xmlParseLocation(et): def _xmlParseLocation(et):
loc = {} loc = {}
@ -40,37 +34,61 @@ def _loadItem(et):
item[elt.tag] = value item[elt.tag] = value
return item return item
def _xmlParseAxisOrMap(elt):
dic = {}
for name in elt.attrib:
if name in ['name', 'tag']:
dic[name] = elt.attrib[name]
else:
dic[name] = float(elt.attrib[name])
return dic
def _loadAxis(et):
item = dict(_xmlParseAxisOrMap(et))
maps = []
labelnames = {}
for elt in et:
assert elt.tag in ['labelname', 'map']
if elt.tag == 'labelname':
lang = elt.attrib["{0}lang".format(namespaces['xml'])]
labelnames[lang] = elt.text
elif elt.tag == 'map':
maps.append(_xmlParseAxisOrMap(elt))
if labelnames:
item['labelname'] = labelnames
if maps:
item['map'] = maps
return item
def _load(et): def _load(et):
designspace = {}
ds = et.getroot() ds = et.getroot()
axisMap = collections.OrderedDict() axes = []
axesET = ds.find('axes') ds_axes = ds.find('axes')
if axesET: if ds_axes:
axisList = axesET.findall('axis') for et in ds_axes:
for axisET in axisList: axes.append(_loadAxis(et))
axisName = axisET.attrib["name"] designspace['axes'] = axes
labelET = axisET.find('labelname')
if (None == labelET):
# If the designpsace file axes is a std axes, the label name may be omitted.
tag, label = standard_axis_map[axisName]
else:
label = labelET.text
tag = axisET.attrib["tag"]
axisMap[axisName] = (tag, label)
masters = [] masters = []
for et in ds.find('sources'): for et in ds.find('sources'):
masters.append(_loadItem(et)) masters.append(_loadItem(et))
designspace['masters'] = masters
instances = [] instances = []
for et in ds.find('instances'): for et in ds.find('instances'):
instances.append(_loadItem(et)) instances.append(_loadItem(et))
designspace['instances'] = instances
return masters, instances, axisMap return designspace
def load(filename): def load(filename):
"""Load designspace from a file name or object. Returns two items: """Load designspace from a file name or object.
list of masters (aka sources) and list of instances.""" Returns a dictionary containing three items:
- list of axes
- list of masters (aka sources)
- list of instances"""
return _load(ET.parse(filename)) return _load(ET.parse(filename))
def loads(string): def loads(string):

View File

@ -12,7 +12,11 @@ import os.path
def interpolate_layout(designspace_filename, loc, finder): def interpolate_layout(designspace_filename, loc, finder):
masters, instances, axisMap = designspace.load(designspace_filename) ds = designspace.load(designspace_filename)
axes = ds['axes']
masters = ds['masters']
instances = ds['instances']
base_idx = None base_idx = None
for i,m in enumerate(masters): for i,m in enumerate(masters):
if 'info' in m and m['info']['copy']: if 'info' in m and m['info']['copy']:
@ -34,26 +38,37 @@ def interpolate_layout(designspace_filename, loc, finder):
master_locs = [o['location'] for o in masters] master_locs = [o['location'] for o in masters]
axis_tags = set(master_locs[0].keys()) axis_names = set(master_locs[0].keys())
assert all(axis_tags == set(m.keys()) for m in master_locs) assert all(axis_names == set(m.keys()) for m in master_locs)
# Set up axes # Set up axes
axes = {} axes_dict = {}
for tag in axis_tags: if axes:
# the designspace file loaded had an <axes> 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 tag in axis_names:
default = master_locs[base_idx][tag] default = master_locs[base_idx][tag]
lower = min(m[tag] for m in master_locs) lower = min(m[tag] for m in master_locs)
upper = max(m[tag] for m in master_locs) upper = max(m[tag] for m in master_locs)
axes[tag] = (lower, default, upper) if default == lower == upper:
continue
axes_dict[tag] = (lower, default, upper)
print("Axes:") print("Axes:")
pprint(axes) pprint(axes_dict)
print("Location:", loc) print("Location:", loc)
print("Master locations:") print("Master locations:")
pprint(master_locs) pprint(master_locs)
# Normalize locations # Normalize locations
loc = models.normalizeLocation(loc, axes) loc = models.normalizeLocation(loc, axes_dict)
master_locs = [models.normalizeLocation(m, axes) for m in master_locs] master_locs = [models.normalizeLocation(m, axes_dict) for m in master_locs]
print("Normalized location:", loc) print("Normalized location:", loc)
print("Normalized master locations:") print("Normalized master locations:")
@ -66,7 +81,7 @@ def interpolate_layout(designspace_filename, loc, finder):
merger = InstancerMerger(font, model, loc) merger = InstancerMerger(font, model, loc)
print("Building variations tables") print("Building variations tables")
merger.mergeTables(font, master_fonts, axes, base_idx, ['GPOS']) merger.mergeTables(font, master_fonts, axes_dict, base_idx, ['GPOS'])
return font return font

View File

@ -1,3 +1,6 @@
- [varLib] designspace.load() now returns a dictionary, instead of a tuple,
and supports <axes> element (#864)
3.7.2 (released 2017-02-17) 3.7.2 (released 2017-02-17)
--------------------------- ---------------------------

View File

@ -1,24 +1,36 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<designspace format="3"> <designspace format="3">
<axes>
<axis default="0.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght">
<map input="0.0" output="10.0" />
<map input="401.0" output="66.0" />
<map input="1000.0" output="990.0" />
</axis>
<axis default="250.0" maximum="1000.0" minimum="0.0" name="width" tag="wdth" />
<axis default="0.0" maximum="100.0" minimum="0.0" name="contrast" tag="cntr">
<labelname xml:lang="en">Contrast</labelname>
<labelname xml:lang="de">Kontrast</labelname>
</axis>
</axes>
<sources> <sources>
<source filename="VarLibTest-Light.ufo" name="master_1"> <source filename="VarLibTest-Light.ufo" name="master_1">
<lib copy="1"/> <lib copy="1"/>
<groups copy="1"/> <groups copy="1"/>
<info copy="1"/> <info copy="1"/>
<location> <location>
<dimension name="weight" xvalue="0.000000"/> <dimension name="weight" xvalue="0.0"/>
</location> </location>
</source> </source>
<source filename="VarLibTest-Bold.ufo" name="master_2"> <source filename="VarLibTest-Bold.ufo" name="master_2">
<location> <location>
<dimension name="weight" xvalue="1.000000"/> <dimension name="weight" xvalue="1.0"/>
</location> </location>
</source> </source>
</sources> </sources>
<instances> <instances>
<instance familyname="VarLibTest" filename="instance/VarLibTest-Medium.ufo" stylename="Medium"> <instance familyname="VarLibTest" filename="instance/VarLibTest-Medium.ufo" stylename="Medium">
<location> <location>
<dimension name="weight" xvalue="0.500000"/> <dimension name="weight" xvalue="0.5"/>
</location> </location>
<info/> <info/>
<kerning/> <kerning/>

View File

@ -9,21 +9,47 @@ class DesignspaceTest(unittest.TestCase):
def test_load(self): def test_load(self):
self.assertEqual( self.assertEqual(
designspace.load(_getpath("VarLibTest.designspace")), designspace.load(_getpath("VarLibTest.designspace")),
([{'filename': 'VarLibTest-Light.ufo',
'groups': {'copy': True}, {'instances':
'info': {'copy': True}, [{'info': {},
'lib': {'copy': True},
'location': {'weight': 0.0},
'name': 'master_1'},
{'filename': 'VarLibTest-Bold.ufo',
'location': {'weight': 1.0},
'name': 'master_2'}],
[{'filename': 'instance/VarLibTest-Medium.ufo',
'location': {'weight': 0.5},
'familyname': 'VarLibTest', 'familyname': 'VarLibTest',
'stylename': 'Medium', 'filename': 'instance/VarLibTest-Medium.ufo',
'info': {}, 'kerning': {},
'kerning': {}}]) 'location': {'weight': 0.5},
'stylename': 'Medium'}],
'masters':
[{'info': {'copy': True},
'name': 'master_1',
'lib': {'copy': True},
'filename': 'VarLibTest-Light.ufo',
'location': {'weight': 0.0},
'groups': {'copy': True}},
{'location': {'weight': 1.0},
'name': 'master_2',
'filename': 'VarLibTest-Bold.ufo'}],
'axes':
[{'map': [{'input': 0.0, 'output': 10.0},
{'input': 401.0, 'output': 66.0},
{'input': 1000.0, 'output': 990.0}],
'name': 'weight',
'default': 0.0,
'tag': 'wght',
'maximum': 1000.0,
'minimum': 0.0},
{'default': 250.0,
'minimum': 0.0,
'tag': 'wdth',
'maximum': 1000.0,
'name': 'width'},
{'name': 'contrast',
'default': 0.0,
'tag': 'cntr',
'maximum': 100.0,
'minimum': 0.0,
'labelname': {'de': 'Kontrast', 'en': 'Contrast'}}]
}
) )