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.varLib import builder, designspace, models
from fontTools.varLib.merger import VariationMerger
import collections
from collections import OrderedDict
import warnings
import os.path
import logging
@ -42,7 +42,6 @@ log = logging.getLogger("fontTools.varLib")
#
# Move to fvar table proper?
# TODO how to provide axis order?
def _add_fvar(font, axes, instances, axis_map):
"""
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',
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
font['fvar'] = fvar = newTable('fvar')
nameTable = font['name']
for iden in axis_map.keys():
if not iden in axes:
continue
for iden in sorted(axes.keys(), key=lambda i: axis_map.keys().index(i)):
axis = Axis()
axis.axisTag = Tag(axis_map[iden][0])
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).
"""
masters, instances, axisMapDS = designspace.load(designspace_filename)
ds = designspace.load(designspace_filename)
axes = ds['axes']
masters = ds['masters']
instances = ds['instances']
base_idx = None
for i,m in enumerate(masters):
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_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:
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)
elif axisMapDS:
axis_map = axisMapDS
elif axes:
# 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:
tag = axis['tag']
assert axis['labelname']['en']
label = axis['labelname']['en']
axis_map[axis_name] = (tag, label)
else:
axis_map = designspace.standard_axis_map
axis_map = standard_axis_map
# TODO: For weight & width, use OS/2 values and setup 'avar' mapping.
master_locs = [o['location'] for o in masters]
axis_tags = set(master_locs[0].keys())
assert all(axis_tags == set(m.keys()) for m in master_locs)
axis_names = set(master_locs[0].keys())
assert all(axis_names == set(m.keys()) for m in master_locs)
# Set up axes
axes = {}
for tag in axis_tags:
default = master_locs[base_idx][tag]
lower = min(m[tag] for m in master_locs)
upper = max(m[tag] for m in master_locs)
if default == lower == upper:
continue
axes[tag] = (lower, default, upper)
log.info("Axes:\n%s", pformat(axes))
axes_dict = {}
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 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())
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])
# 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
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))
# TODO Clean this up.
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]
#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]

View File

@ -9,13 +9,7 @@ except ImportError:
__all__ = ['load', 'loads']
standard_axis_map = collections.OrderedDict(
[['weight', ('wght', 'Weight')],
['width', ('wdth', 'Width')],
['slant', ('slnt', 'Slant')],
['optical', ('opsz', 'Optical Size')],
['custom',('xxxx', 'Custom')]]
)
namespaces = {'xml': '{http://www.w3.org/XML/1998/namespace}'}
def _xmlParseLocation(et):
loc = {}
@ -40,37 +34,61 @@ def _loadItem(et):
item[elt.tag] = value
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):
designspace = {}
ds = et.getroot()
axisMap = collections.OrderedDict()
axesET = ds.find('axes')
if axesET:
axisList = axesET.findall('axis')
for axisET in axisList:
axisName = axisET.attrib["name"]
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)
axes = []
ds_axes = ds.find('axes')
if ds_axes:
for et in ds_axes:
axes.append(_loadAxis(et))
designspace['axes'] = axes
masters = []
for et in ds.find('sources'):
masters.append(_loadItem(et))
designspace['masters'] = masters
instances = []
for et in ds.find('instances'):
instances.append(_loadItem(et))
designspace['instances'] = instances
return masters, instances, axisMap
return designspace
def load(filename):
"""Load designspace from a file name or object. Returns two items:
list of masters (aka sources) and list of instances."""
"""Load designspace from a file name or object.
Returns a dictionary containing three items:
- list of axes
- list of masters (aka sources)
- list of instances"""
return _load(ET.parse(filename))
def loads(string):

View File

@ -12,7 +12,11 @@ import os.path
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
for i,m in enumerate(masters):
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]
axis_tags = set(master_locs[0].keys())
assert all(axis_tags == set(m.keys()) for m in master_locs)
axis_names = set(master_locs[0].keys())
assert all(axis_names == set(m.keys()) for m in master_locs)
# Set up axes
axes = {}
for tag in axis_tags:
default = master_locs[base_idx][tag]
lower = min(m[tag] for m in master_locs)
upper = max(m[tag] for m in master_locs)
axes[tag] = (lower, default, upper)
axes_dict = {}
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]
lower = min(m[tag] for m in master_locs)
upper = max(m[tag] for m in master_locs)
if default == lower == upper:
continue
axes_dict[tag] = (lower, default, upper)
print("Axes:")
pprint(axes)
pprint(axes_dict)
print("Location:", loc)
print("Master locations:")
pprint(master_locs)
# Normalize locations
loc = models.normalizeLocation(loc, axes)
master_locs = [models.normalizeLocation(m, axes) for m in master_locs]
loc = models.normalizeLocation(loc, axes_dict)
master_locs = [models.normalizeLocation(m, axes_dict) for m in master_locs]
print("Normalized location:", loc)
print("Normalized master locations:")
@ -66,7 +81,7 @@ def interpolate_layout(designspace_filename, loc, finder):
merger = InstancerMerger(font, model, loc)
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

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)
---------------------------

View File

@ -1,24 +1,36 @@
<?xml version="1.0"?>
<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>
<source filename="VarLibTest-Light.ufo" name="master_1">
<lib copy="1"/>
<groups copy="1"/>
<info copy="1"/>
<location>
<dimension name="weight" xvalue="0.000000"/>
<dimension name="weight" xvalue="0.0"/>
</location>
</source>
<source filename="VarLibTest-Bold.ufo" name="master_2">
<location>
<dimension name="weight" xvalue="1.000000"/>
<dimension name="weight" xvalue="1.0"/>
</location>
</source>
</sources>
<instances>
<instance familyname="VarLibTest" filename="instance/VarLibTest-Medium.ufo" stylename="Medium">
<location>
<dimension name="weight" xvalue="0.500000"/>
<dimension name="weight" xvalue="0.5"/>
</location>
<info/>
<kerning/>

View File

@ -9,21 +9,47 @@ class DesignspaceTest(unittest.TestCase):
def test_load(self):
self.assertEqual(
designspace.load(_getpath("VarLibTest.designspace")),
([{'filename': 'VarLibTest-Light.ufo',
'groups': {'copy': True},
'info': {'copy': True},
'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',
'stylename': 'Medium',
'info': {},
'kerning': {}}])
{'instances':
[{'info': {},
'familyname': 'VarLibTest',
'filename': 'instance/VarLibTest-Medium.ufo',
'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'}}]
}
)