Merge pull request #864 from miguelsousa/pr857-followup
Followup to PR #857
This commit is contained in:
commit
ae58d3872c
@ -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]
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
3
NEWS.rst
3
NEWS.rst
@ -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)
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
@ -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/>
|
||||||
|
@ -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'}}]
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user