2016-08-15 13:56:13 -07:00
|
|
|
"""Variation fonts interpolation models."""
|
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from fontTools.misc.py23 import *
|
|
|
|
|
2018-11-10 15:05:26 -05:00
|
|
|
__all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList',
|
2018-11-07 22:49:15 -05:00
|
|
|
'normalizeValue', 'normalizeLocation',
|
|
|
|
'supportScalar',
|
|
|
|
'VariationModel']
|
2017-04-12 21:52:29 -07:00
|
|
|
|
2018-03-26 20:46:26 -07:00
|
|
|
|
2018-11-07 22:49:15 -05:00
|
|
|
def nonNone(lst):
|
|
|
|
return [l for l in lst if l is not None]
|
|
|
|
|
2018-11-09 09:44:34 -05:00
|
|
|
def allNone(lst):
|
|
|
|
return all(l is None for l in lst)
|
|
|
|
|
2018-11-10 15:05:26 -05:00
|
|
|
def allEqualTo(ref, lst, mapper=None):
|
2018-11-09 09:49:56 -05:00
|
|
|
if mapper is None:
|
|
|
|
return all(ref == item for item in lst)
|
|
|
|
else:
|
|
|
|
mapped = mapper(ref)
|
|
|
|
return all(mapped == mapper(item) for item in lst)
|
2018-11-08 23:41:09 -05:00
|
|
|
|
2018-11-10 15:05:26 -05:00
|
|
|
def allEqual(lst, mapper=None):
|
2018-11-08 11:35:15 -05:00
|
|
|
if not lst:
|
|
|
|
return True
|
|
|
|
it = iter(lst)
|
|
|
|
first = next(it)
|
2018-11-10 15:05:26 -05:00
|
|
|
return allEqualTo(first, it, mapper=mapper)
|
2018-11-08 11:35:15 -05:00
|
|
|
|
2018-11-07 22:37:06 -05:00
|
|
|
def subList(truth, lst):
|
|
|
|
assert len(truth) == len(lst)
|
|
|
|
return [l for l,t in zip(lst,truth) if t]
|
|
|
|
|
2017-04-12 21:52:29 -07:00
|
|
|
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
|
2017-08-05 20:37:37 +02:00
|
|
|
assert lower <= default <= upper, "invalid axis values: %3.3f, %3.3f %3.3f"%(lower, default, upper)
|
2017-04-12 21:52:29 -07:00
|
|
|
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
|
2016-08-15 13:56:13 -07:00
|
|
|
|
|
|
|
def normalizeLocation(location, axes):
|
2016-11-29 14:38:46 +01:00
|
|
|
"""Normalizes location based on axis min/default/max values from axes.
|
|
|
|
>>> axes = {"wght": (100, 400, 900)}
|
|
|
|
>>> normalizeLocation({"wght": 400}, axes)
|
2017-04-12 21:52:29 -07:00
|
|
|
{'wght': 0.0}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": 100}, axes)
|
|
|
|
{'wght': -1.0}
|
|
|
|
>>> normalizeLocation({"wght": 900}, axes)
|
|
|
|
{'wght': 1.0}
|
|
|
|
>>> normalizeLocation({"wght": 650}, axes)
|
|
|
|
{'wght': 0.5}
|
|
|
|
>>> normalizeLocation({"wght": 1000}, axes)
|
|
|
|
{'wght': 1.0}
|
|
|
|
>>> normalizeLocation({"wght": 0}, axes)
|
|
|
|
{'wght': -1.0}
|
|
|
|
>>> axes = {"wght": (0, 0, 1000)}
|
|
|
|
>>> normalizeLocation({"wght": 0}, axes)
|
2017-04-12 21:52:29 -07:00
|
|
|
{'wght': 0.0}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": -1}, axes)
|
2017-04-12 21:52:29 -07:00
|
|
|
{'wght': 0.0}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": 1000}, axes)
|
|
|
|
{'wght': 1.0}
|
2016-11-29 14:42:20 +01:00
|
|
|
>>> normalizeLocation({"wght": 500}, axes)
|
|
|
|
{'wght': 0.5}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": 1001}, axes)
|
|
|
|
{'wght': 1.0}
|
|
|
|
>>> axes = {"wght": (0, 1000, 1000)}
|
|
|
|
>>> normalizeLocation({"wght": 0}, axes)
|
|
|
|
{'wght': -1.0}
|
|
|
|
>>> normalizeLocation({"wght": -1}, axes)
|
|
|
|
{'wght': -1.0}
|
2016-11-29 14:42:20 +01:00
|
|
|
>>> normalizeLocation({"wght": 500}, axes)
|
|
|
|
{'wght': -0.5}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": 1000}, axes)
|
2017-04-12 21:52:29 -07:00
|
|
|
{'wght': 0.0}
|
2016-11-29 14:38:46 +01:00
|
|
|
>>> normalizeLocation({"wght": 1001}, axes)
|
2017-04-12 21:52:29 -07:00
|
|
|
{'wght': 0.0}
|
2016-11-29 14:38:46 +01:00
|
|
|
"""
|
2016-08-15 13:56:13 -07:00
|
|
|
out = {}
|
2017-04-12 21:52:29 -07:00
|
|
|
for tag,triple in axes.items():
|
|
|
|
v = location.get(tag, triple[1])
|
|
|
|
out[tag] = normalizeValue(v, triple)
|
2016-08-15 13:56:13 -07:00
|
|
|
return out
|
|
|
|
|
2017-10-18 10:04:42 +02:00
|
|
|
def supportScalar(location, support, ot=True):
|
2016-08-15 13:56:13 -07:00
|
|
|
"""Returns the scalar multiplier at location, for a master
|
2017-08-07 17:04:42 -07:00
|
|
|
with support. If ot is True, then a peak value of zero
|
|
|
|
for support of an axis means "axis does not participate". That
|
|
|
|
is how OpenType Variation Font technology works.
|
2016-08-15 13:56:13 -07:00
|
|
|
>>> supportScalar({}, {})
|
|
|
|
1.0
|
|
|
|
>>> supportScalar({'wght':.2}, {})
|
|
|
|
1.0
|
|
|
|
>>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
|
|
|
|
0.1
|
|
|
|
>>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
|
|
|
|
0.75
|
2017-08-07 17:04:42 -07:00
|
|
|
>>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
|
|
|
|
0.75
|
2017-10-18 10:29:19 +02:00
|
|
|
>>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False)
|
2017-08-07 17:04:42 -07:00
|
|
|
0.375
|
2017-10-18 10:29:19 +02:00
|
|
|
>>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
|
2017-08-07 17:04:42 -07:00
|
|
|
0.75
|
2017-10-18 10:29:19 +02:00
|
|
|
>>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
|
2017-08-07 17:04:42 -07:00
|
|
|
0.75
|
2016-08-15 13:56:13 -07:00
|
|
|
"""
|
|
|
|
scalar = 1.
|
|
|
|
for axis,(lower,peak,upper) in support.items():
|
2017-08-07 17:04:42 -07:00
|
|
|
if ot:
|
|
|
|
# OpenType-specific case handling
|
|
|
|
if peak == 0.:
|
|
|
|
continue
|
|
|
|
if lower > peak or peak > upper:
|
|
|
|
continue
|
|
|
|
if lower < 0. and upper > 0.:
|
|
|
|
continue
|
|
|
|
v = location.get(axis, 0.)
|
|
|
|
else:
|
|
|
|
assert axis in location
|
|
|
|
v = location[axis]
|
2016-08-15 13:56:13 -07:00
|
|
|
if v == peak:
|
|
|
|
continue
|
|
|
|
if v <= lower or upper <= v:
|
|
|
|
scalar = 0.
|
|
|
|
break;
|
|
|
|
if v < peak:
|
|
|
|
scalar *= (v - lower) / (peak - lower)
|
|
|
|
else: # v > peak
|
|
|
|
scalar *= (v - upper) / (peak - upper)
|
|
|
|
return scalar
|
|
|
|
|
|
|
|
|
|
|
|
class VariationModel(object):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Locations must be in normalized space. Ie. base master
|
|
|
|
is at origin (0).
|
|
|
|
>>> from pprint import pprint
|
|
|
|
>>> locations = [ \
|
|
|
|
{'wght':100}, \
|
|
|
|
{'wght':-100}, \
|
|
|
|
{'wght':-180}, \
|
|
|
|
{'wdth':+.3}, \
|
|
|
|
{'wght':+120,'wdth':.3}, \
|
|
|
|
{'wght':+120,'wdth':.2}, \
|
|
|
|
{}, \
|
|
|
|
{'wght':+180,'wdth':.3}, \
|
|
|
|
{'wght':+180}, \
|
|
|
|
]
|
|
|
|
>>> model = VariationModel(locations, axisOrder=['wght'])
|
|
|
|
>>> pprint(model.locations)
|
|
|
|
[{},
|
|
|
|
{'wght': -100},
|
|
|
|
{'wght': -180},
|
|
|
|
{'wght': 100},
|
|
|
|
{'wght': 180},
|
|
|
|
{'wdth': 0.3},
|
|
|
|
{'wdth': 0.3, 'wght': 180},
|
|
|
|
{'wdth': 0.3, 'wght': 120},
|
|
|
|
{'wdth': 0.2, 'wght': 120}]
|
|
|
|
>>> pprint(model.deltaWeights)
|
|
|
|
[{},
|
|
|
|
{0: 1.0},
|
|
|
|
{0: 1.0},
|
|
|
|
{0: 1.0},
|
|
|
|
{0: 1.0},
|
|
|
|
{0: 1.0},
|
|
|
|
{0: 1.0, 4: 1.0, 5: 1.0},
|
[varLib] Tweak support-resolution algorithm
Improve varLib model algorithm.
This, basically means any varfont built that had an unusual master
configuration will change when rebuilt.
Here's a good test: a two-axis with 8 masters at unusual locations:
2-----------------5----------3
| |
| 7 |
| |
| 6 |
| |
| |
| |
0-----------4----------------1
Previously, the reach of master 3 (Black Extended) would
have started from A=.4, ie, the A position of master 4.
It now correctly starts from 0. Same thing with masters
after it. Ie, master 5 gets a span on the A axis from
0 to 1, whereas before it was getting from .4 to 1.
Previously, the on-axis masters always cut the space. They
don't anymore. That's more consistent, and fixes the main
issue Erik showed at TYPO Labs 2017. Same issue was also
causing the reach of master 3 to be limited, which I think
is the issue being discussed in the linked issue. Both should
be fixed.
It's hard to describe exactly what happened before / after.
Best to read the actual support values:
Before:
Sorted locations:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
TODO: We should add this as a test case.
There's another improvement I want to make, but that's separate.
2018-03-26 20:31:06 -07:00
|
|
|
{0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666},
|
2016-08-15 13:56:13 -07:00
|
|
|
{0: 1.0,
|
|
|
|
3: 0.75,
|
|
|
|
4: 0.25,
|
|
|
|
5: 0.6666666666666667,
|
[varLib] Tweak support-resolution algorithm
Improve varLib model algorithm.
This, basically means any varfont built that had an unusual master
configuration will change when rebuilt.
Here's a good test: a two-axis with 8 masters at unusual locations:
2-----------------5----------3
| |
| 7 |
| |
| 6 |
| |
| |
| |
0-----------4----------------1
Previously, the reach of master 3 (Black Extended) would
have started from A=.4, ie, the A position of master 4.
It now correctly starts from 0. Same thing with masters
after it. Ie, master 5 gets a span on the A axis from
0 to 1, whereas before it was getting from .4 to 1.
Previously, the on-axis masters always cut the space. They
don't anymore. That's more consistent, and fixes the main
issue Erik showed at TYPO Labs 2017. Same issue was also
causing the reach of master 3 to be limited, which I think
is the issue being discussed in the linked issue. Both should
be fixed.
It's hard to describe exactly what happened before / after.
Best to read the actual support values:
Before:
Sorted locations:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
TODO: We should add this as a test case.
There's another improvement I want to make, but that's separate.
2018-03-26 20:31:06 -07:00
|
|
|
6: 0.4444444444444445,
|
2016-08-15 13:56:13 -07:00
|
|
|
7: 0.6666666666666667}]
|
|
|
|
"""
|
|
|
|
|
2019-03-04 10:58:47 -08:00
|
|
|
def __init__(self, locations, axisOrder=None):
|
2019-03-04 11:42:54 -08:00
|
|
|
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
|
|
|
|
raise ValueError("locations must be unique")
|
|
|
|
|
2018-11-07 22:37:06 -05:00
|
|
|
self.origLocations = locations
|
2019-03-04 16:51:53 -08:00
|
|
|
self.axisOrder = axisOrder if axisOrder is not None else []
|
2018-11-07 22:37:06 -05:00
|
|
|
|
2016-08-15 13:56:13 -07:00
|
|
|
locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations]
|
2019-03-04 10:58:47 -08:00
|
|
|
keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=self.axisOrder)
|
2016-08-15 13:56:13 -07:00
|
|
|
self.locations = sorted(locations, key=keyFunc)
|
2019-03-04 11:42:54 -08:00
|
|
|
|
|
|
|
# Mapping from user's master order to our master order
|
|
|
|
self.mapping = [self.locations.index(l) for l in locations]
|
|
|
|
self.reverseMapping = [locations.index(l) for l in self.locations]
|
2016-08-15 13:56:13 -07:00
|
|
|
|
2019-03-04 16:58:15 -08:00
|
|
|
self._computeMasterSupports(keyFunc.axisPoints)
|
2018-11-07 22:37:06 -05:00
|
|
|
self._subModels = {}
|
|
|
|
|
2018-11-08 10:01:47 -05:00
|
|
|
def getSubModel(self, items):
|
2018-11-08 12:29:07 -05:00
|
|
|
if None not in items:
|
2018-11-07 22:37:06 -05:00
|
|
|
return self, items
|
|
|
|
key = tuple(v is not None for v in items)
|
|
|
|
subModel = self._subModels.get(key)
|
|
|
|
if subModel is None:
|
|
|
|
subModel = VariationModel(subList(key, self.origLocations), self.axisOrder)
|
|
|
|
self._subModels[key] = subModel
|
|
|
|
return subModel, subList(key, items)
|
2016-08-15 13:56:13 -07:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
|
|
|
|
assert {} in locations, "Base master not found."
|
|
|
|
axisPoints = {}
|
|
|
|
for loc in locations:
|
|
|
|
if len(loc) != 1:
|
|
|
|
continue
|
|
|
|
axis = next(iter(loc))
|
|
|
|
value = loc[axis]
|
|
|
|
if axis not in axisPoints:
|
2017-08-16 16:35:57 +01:00
|
|
|
axisPoints[axis] = {0.}
|
2018-06-14 15:34:27 +01:00
|
|
|
assert value not in axisPoints[axis], (
|
|
|
|
'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints)
|
|
|
|
)
|
2016-08-15 13:56:13 -07:00
|
|
|
axisPoints[axis].add(value)
|
|
|
|
|
|
|
|
def getKey(axisPoints, axisOrder):
|
|
|
|
def sign(v):
|
|
|
|
return -1 if v < 0 else +1 if v > 0 else 0
|
|
|
|
def key(loc):
|
|
|
|
rank = len(loc)
|
|
|
|
onPointAxes = [axis for axis,value in loc.items() if value in axisPoints[axis]]
|
|
|
|
orderedAxes = [axis for axis in axisOrder if axis in loc]
|
|
|
|
orderedAxes.extend([axis for axis in sorted(loc.keys()) if axis not in axisOrder])
|
|
|
|
return (
|
|
|
|
rank, # First, order by increasing rank
|
|
|
|
-len(onPointAxes), # Next, by decreasing number of onPoint axes
|
|
|
|
tuple(axisOrder.index(axis) if axis in axisOrder else 0x10000 for axis in orderedAxes), # Next, by known axes
|
|
|
|
tuple(orderedAxes), # Next, by all axes
|
|
|
|
tuple(sign(loc[axis]) for axis in orderedAxes), # Next, by signs of axis values
|
|
|
|
tuple(abs(loc[axis]) for axis in orderedAxes), # Next, by absolute value of axis values
|
|
|
|
)
|
|
|
|
return key
|
|
|
|
|
|
|
|
ret = getKey(axisPoints, axisOrder)
|
|
|
|
ret.axisPoints = axisPoints
|
|
|
|
return ret
|
|
|
|
|
2018-11-30 10:37:34 -08:00
|
|
|
def reorderMasters(self, master_list, mapping):
|
|
|
|
# For changing the master data order without
|
|
|
|
# recomputing supports and deltaWeights.
|
|
|
|
new_list = [master_list[idx] for idx in mapping]
|
|
|
|
self.origLocations = [self.origLocations[idx] for idx in mapping]
|
2018-11-30 21:46:16 -05:00
|
|
|
locations = [{k:v for k,v in loc.items() if v != 0.}
|
|
|
|
for loc in self.origLocations]
|
2018-11-30 10:37:34 -08:00
|
|
|
self.mapping = [self.locations.index(l) for l in locations]
|
|
|
|
self.reverseMapping = [locations.index(l) for l in self.locations]
|
2018-11-30 09:12:27 -08:00
|
|
|
self._subModels = {}
|
|
|
|
return new_list
|
2018-11-30 21:46:16 -05:00
|
|
|
|
2019-03-04 16:58:15 -08:00
|
|
|
def _computeMasterSupports(self, axisPoints):
|
2016-08-15 13:56:13 -07:00
|
|
|
supports = []
|
|
|
|
deltaWeights = []
|
|
|
|
locations = self.locations
|
[varLib] Take total bounding box into account when resolving model
Umm. Not sure how useful this is, but helps with cases where there
were not masters on extremes and people expected this to work.
Happens in Higher-Order Interpolation since the axis extremes
are not interesting.
Fixes https://github.com/googlei18n/fontmake/issues/473
Test case (in case someone wants to add it to test suite!):
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 0.5)},
{'B': (0, 0.5, 0.5)},
{'A': (0, 0.5, 0.5), 'B': (0, 0.5, 0.5)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
2018-10-25 19:45:21 -07:00
|
|
|
# Compute min/max across each axis, use it as total range.
|
|
|
|
# TODO Take this as input from outside?
|
|
|
|
minV = {}
|
|
|
|
maxV = {}
|
|
|
|
for l in locations:
|
|
|
|
for k,v in l.items():
|
2018-10-26 11:07:12 +01:00
|
|
|
minV[k] = min(v, minV.get(k, v))
|
|
|
|
maxV[k] = max(v, maxV.get(k, v))
|
2016-08-15 13:56:13 -07:00
|
|
|
for i,loc in enumerate(locations):
|
|
|
|
box = {}
|
[varLib] Take total bounding box into account when resolving model
Umm. Not sure how useful this is, but helps with cases where there
were not masters on extremes and people expected this to work.
Happens in Higher-Order Interpolation since the axis extremes
are not interesting.
Fixes https://github.com/googlei18n/fontmake/issues/473
Test case (in case someone wants to add it to test suite!):
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 0.5)},
{'B': (0, 0.5, 0.5)},
{'A': (0, 0.5, 0.5), 'B': (0, 0.5, 0.5)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
2018-10-25 19:45:21 -07:00
|
|
|
for axis,locV in loc.items():
|
[varLib] Tweak support-resolution algorithm
Improve varLib model algorithm.
This, basically means any varfont built that had an unusual master
configuration will change when rebuilt.
Here's a good test: a two-axis with 8 masters at unusual locations:
2-----------------5----------3
| |
| 7 |
| |
| 6 |
| |
| |
| |
0-----------4----------------1
Previously, the reach of master 3 (Black Extended) would
have started from A=.4, ie, the A position of master 4.
It now correctly starts from 0. Same thing with masters
after it. Ie, master 5 gets a span on the A axis from
0 to 1, whereas before it was getting from .4 to 1.
Previously, the on-axis masters always cut the space. They
don't anymore. That's more consistent, and fixes the main
issue Erik showed at TYPO Labs 2017. Same issue was also
causing the reach of master 3 to be limited, which I think
is the issue being discussed in the linked issue. Both should
be fixed.
It's hard to describe exactly what happened before / after.
Best to read the actual support values:
Before:
Sorted locations:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
TODO: We should add this as a test case.
There's another improvement I want to make, but that's separate.
2018-03-26 20:31:06 -07:00
|
|
|
if locV > 0:
|
[varLib] Take total bounding box into account when resolving model
Umm. Not sure how useful this is, but helps with cases where there
were not masters on extremes and people expected this to work.
Happens in Higher-Order Interpolation since the axis extremes
are not interesting.
Fixes https://github.com/googlei18n/fontmake/issues/473
Test case (in case someone wants to add it to test suite!):
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 0.5)},
{'B': (0, 0.5, 0.5)},
{'A': (0, 0.5, 0.5), 'B': (0, 0.5, 0.5)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
2018-10-25 19:45:21 -07:00
|
|
|
box[axis] = (0, locV, maxV[axis])
|
[varLib] Tweak support-resolution algorithm
Improve varLib model algorithm.
This, basically means any varfont built that had an unusual master
configuration will change when rebuilt.
Here's a good test: a two-axis with 8 masters at unusual locations:
2-----------------5----------3
| |
| 7 |
| |
| 6 |
| |
| |
| |
0-----------4----------------1
Previously, the reach of master 3 (Black Extended) would
have started from A=.4, ie, the A position of master 4.
It now correctly starts from 0. Same thing with masters
after it. Ie, master 5 gets a span on the A axis from
0 to 1, whereas before it was getting from .4 to 1.
Previously, the on-axis masters always cut the space. They
don't anymore. That's more consistent, and fixes the main
issue Erik showed at TYPO Labs 2017. Same issue was also
causing the reach of master 3 to be limited, which I think
is the issue being discussed in the linked issue. Both should
be fixed.
It's hard to describe exactly what happened before / after.
Best to read the actual support values:
Before:
Sorted locations:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.7
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.7}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.7, 1.0)}]
TODO: We should add this as a test case.
There's another improvement I want to make, but that's separate.
2018-03-26 20:31:06 -07:00
|
|
|
else:
|
[varLib] Take total bounding box into account when resolving model
Umm. Not sure how useful this is, but helps with cases where there
were not masters on extremes and people expected this to work.
Happens in Higher-Order Interpolation since the axis extremes
are not interesting.
Fixes https://github.com/googlei18n/fontmake/issues/473
Test case (in case someone wants to add it to test suite!):
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 0.5)},
{'B': (0, 0.5, 0.5)},
{'A': (0, 0.5, 0.5), 'B': (0, 0.5, 0.5)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
2018-10-25 19:45:21 -07:00
|
|
|
box[axis] = (minV[axis], locV, 0)
|
2016-08-15 13:56:13 -07:00
|
|
|
|
|
|
|
locAxes = set(loc.keys())
|
|
|
|
# Walk over previous masters now
|
|
|
|
for j,m in enumerate(locations[:i]):
|
|
|
|
# Master with extra axes do not participte
|
|
|
|
if not set(m.keys()).issubset(locAxes):
|
|
|
|
continue
|
|
|
|
# If it's NOT in the current box, it does not participate
|
|
|
|
relevant = True
|
2018-06-16 14:07:18 -04:00
|
|
|
for axis, (lower,peak,upper) in box.items():
|
|
|
|
if axis not in m or not (m[axis] == peak or lower < m[axis] < upper):
|
2016-08-15 13:56:13 -07:00
|
|
|
relevant = False
|
|
|
|
break
|
|
|
|
if not relevant:
|
|
|
|
continue
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
|
[varLib.models] Refine modeling one last time
In a523a697164a4 I changed the model such that when computing master
supports and we hit another master, cut the box in one dimension only,
to avoid interferring with other master. The axis to cut against was
chosen as axis that minimizes volume loss. That was fine in a sense,
but missed a great amount of information resulting from relative
positioning of the two masters. As such, it could not distinguish
between the following two situations:
behdad:fonttools 0 (master*)$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .5,.5 .55,.75
Sorted locations:
[{},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.55, 'B': 0.75}]
Supports:
[{},
{'A': (0, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.55, 1.0), 'B': (0.5, 0.75, 1.0)}]
behdad:fonttools 0 (02616ab...)$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .5,.5 .75,.55
Sorted locations:
[{},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.75, 'B': 0.55}]
Supports:
[{},
{'A': (0, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.75, 1.0), 'B': (0.5, 0.55, 1.0)}]
Check the last line. In both cases the box was cut against the second axis.
After this change, we get:
behdad:fonttools 0 (master)$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .5,.5 .55,.75
Sorted locations:
[{},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.55, 'B': 0.75}]
Supports:
[{},
{'A': (0, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.55, 1.0), 'B': (0.5, 0.75, 1.0)}]
behdad:fonttools 0 (master)$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .5,.5 .75,.55
Sorted locations:
[{},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.75, 'B': 0.55}]
Supports:
[{},
{'A': (0, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.75, 1.0), 'B': (0, 0.55, 1.0)}]
Just draw the boxes on a piece of napkin and convince yourself that this
is a good change.
If the ratios are equal, we cut against the first axis in the axisOrder:
behdad:fonttools 0 (master)$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .5,.5 .75,.75
Sorted locations:
[{},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.75, 'B': 0.75}]
Supports:
[{},
{'A': (0, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.75, 1.0), 'B': (0, 0.75, 1.0)}]
2018-04-27 16:47:46 -07:00
|
|
|
# Split the box for new master; split in whatever direction
|
[varLib] If multiple axes have the same range ratio, cut across both
The symmetry is desired.
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
Note the last line assymetry.
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0.5, 1.0, 1.0)}]
https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
2018-10-28 09:28:42 -07:00
|
|
|
# that has largest range ratio.
|
|
|
|
#
|
|
|
|
# For symmetry, we actually cut across multiple axes
|
|
|
|
# if they have the largest, equal, ratio.
|
|
|
|
# https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
|
|
|
|
|
|
|
|
bestAxes = {}
|
2018-06-17 18:47:53 -04:00
|
|
|
bestRatio = -1
|
[varLib] If multiple axes have the same range ratio, cut across both
The symmetry is desired.
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
Note the last line assymetry.
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0.5, 1.0, 1.0)}]
https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
2018-10-28 09:28:42 -07:00
|
|
|
for axis in m.keys():
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
val = m[axis]
|
2016-08-15 13:56:13 -07:00
|
|
|
assert axis in box
|
|
|
|
lower,locV,upper = box[axis]
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
newLower, newUpper = lower, upper
|
2016-08-15 13:56:13 -07:00
|
|
|
if val < locV:
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
newLower = val
|
2018-06-17 18:47:53 -04:00
|
|
|
ratio = (val - locV) / (lower - locV)
|
2016-08-15 13:56:13 -07:00
|
|
|
elif locV < val:
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
newUpper = val
|
2018-06-17 18:47:53 -04:00
|
|
|
ratio = (val - locV) / (upper - locV)
|
2018-06-16 14:17:44 -04:00
|
|
|
else: # val == locV
|
|
|
|
# Can't split box in this direction.
|
|
|
|
continue
|
2018-06-17 18:47:53 -04:00
|
|
|
if ratio > bestRatio:
|
[varLib] If multiple axes have the same range ratio, cut across both
The symmetry is desired.
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
Note the last line assymetry.
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0.5, 1.0, 1.0)}]
https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
2018-10-28 09:28:42 -07:00
|
|
|
bestAxes = {}
|
2018-06-17 18:47:53 -04:00
|
|
|
bestRatio = ratio
|
[varLib] If multiple axes have the same range ratio, cut across both
The symmetry is desired.
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
Note the last line assymetry.
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0.5, 1.0, 1.0)}]
https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
2018-10-28 09:28:42 -07:00
|
|
|
if ratio == bestRatio:
|
|
|
|
bestAxes[axis] = (newLower, locV, newUpper)
|
[varLib.models] Improve model
When adding a master and computing its support, when hitting another master,
we don't need to limit our box in every direction; we just need to limit in
one so we don't hit the older master with our support. Implement heuristic
that takes the first axis that minimizes the area loss.
This is not perfect, but a good improvement.
Before:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
After:
$ fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5 .7,.9
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5},
{'A': 0.7, 'B': 0.9}]
Supports:
[{},
{'A': (0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0, 1.0, 1.0)},
{'A': (0, 1.0, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.6, 1.0), 'B': (0, 1.0, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0, 0.7, 1.0), 'B': (0.5, 0.9, 1.0)}]
(Note the last line.)
2018-03-28 02:00:41 -07:00
|
|
|
|
[varLib] If multiple axes have the same range ratio, cut across both
The symmetry is desired.
Before:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0, 1.0, 1.0)}]
Note the last line assymetry.
After:
$ ./fonttools varLib.models 0,0 .5,0 0,.5 .5,.5 1,1
Sorted locations:
[{}, {'A': 0.5}, {'B': 0.5}, {'A': 0.5, 'B': 0.5}, {'A': 1.0, 'B': 1.0}]
Supports:
[{},
{'A': (0, 0.5, 1.0)},
{'B': (0, 0.5, 1.0)},
{'A': (0, 0.5, 1.0), 'B': (0, 0.5, 1.0)},
{'A': (0.5, 1.0, 1.0), 'B': (0.5, 1.0, 1.0)}]
https://github.com/fonttools/fonttools/commit/7ee81c8821671157968b097f3e55309a1faa511e#commitcomment-31054804
2018-10-28 09:28:42 -07:00
|
|
|
for axis,triple in bestAxes.items ():
|
|
|
|
box[axis] = triple
|
2016-08-15 13:56:13 -07:00
|
|
|
supports.append(box)
|
|
|
|
|
|
|
|
deltaWeight = {}
|
|
|
|
# Walk over previous masters now, populate deltaWeight
|
|
|
|
for j,m in enumerate(locations[:i]):
|
2017-10-19 10:06:20 -07:00
|
|
|
scalar = supportScalar(loc, supports[j])
|
2016-08-15 13:56:13 -07:00
|
|
|
if scalar:
|
|
|
|
deltaWeight[j] = scalar
|
|
|
|
deltaWeights.append(deltaWeight)
|
|
|
|
|
|
|
|
self.supports = supports
|
|
|
|
self.deltaWeights = deltaWeights
|
|
|
|
|
|
|
|
def getDeltas(self, masterValues):
|
|
|
|
assert len(masterValues) == len(self.deltaWeights)
|
|
|
|
mapping = self.reverseMapping
|
|
|
|
out = []
|
|
|
|
for i,weights in enumerate(self.deltaWeights):
|
|
|
|
delta = masterValues[mapping[i]]
|
|
|
|
for j,weight in weights.items():
|
2018-04-03 12:24:56 +01:00
|
|
|
delta -= out[j] * weight
|
2016-08-15 13:56:13 -07:00
|
|
|
out.append(delta)
|
|
|
|
return out
|
|
|
|
|
2018-11-08 10:05:18 -05:00
|
|
|
def getDeltasAndSupports(self, items):
|
2018-11-08 12:29:07 -05:00
|
|
|
model, items = self.getSubModel(items)
|
2018-11-08 10:05:18 -05:00
|
|
|
return model.getDeltas(items), model.supports
|
|
|
|
|
2017-05-23 00:42:30 -07:00
|
|
|
def getScalars(self, loc):
|
|
|
|
return [supportScalar(loc, support) for support in self.supports]
|
|
|
|
|
2017-05-23 01:01:07 -07:00
|
|
|
@staticmethod
|
|
|
|
def interpolateFromDeltasAndScalars(deltas, scalars):
|
2016-08-15 13:56:13 -07:00
|
|
|
v = None
|
2017-05-23 00:36:41 -07:00
|
|
|
assert len(deltas) == len(scalars)
|
|
|
|
for i,(delta,scalar) in enumerate(zip(deltas, scalars)):
|
2016-08-15 13:56:13 -07:00
|
|
|
if not scalar: continue
|
|
|
|
contribution = delta * scalar
|
2017-05-23 00:54:23 -07:00
|
|
|
if v is None:
|
2016-08-15 13:56:13 -07:00
|
|
|
v = contribution
|
|
|
|
else:
|
|
|
|
v += contribution
|
|
|
|
return v
|
|
|
|
|
2017-05-23 00:45:53 -07:00
|
|
|
def interpolateFromDeltas(self, loc, deltas):
|
|
|
|
scalars = self.getScalars(loc)
|
|
|
|
return self.interpolateFromDeltasAndScalars(deltas, scalars)
|
|
|
|
|
2016-08-15 13:56:13 -07:00
|
|
|
def interpolateFromMasters(self, loc, masterValues):
|
|
|
|
deltas = self.getDeltas(masterValues)
|
|
|
|
return self.interpolateFromDeltas(loc, deltas)
|
2016-11-29 14:38:46 +01:00
|
|
|
|
2017-05-23 00:51:05 -07:00
|
|
|
def interpolateFromMastersAndScalars(self, masterValues, scalars):
|
|
|
|
deltas = self.getDeltas(masterValues)
|
|
|
|
return self.interpolateFromDeltasAndScalars(deltas, scalars)
|
|
|
|
|
2016-11-29 14:38:46 +01:00
|
|
|
|
2018-09-11 18:06:06 +02:00
|
|
|
def piecewiseLinearMap(v, mapping):
|
|
|
|
keys = mapping.keys()
|
|
|
|
if not keys:
|
|
|
|
return v
|
|
|
|
if v in keys:
|
|
|
|
return mapping[v]
|
|
|
|
k = min(keys)
|
|
|
|
if v < k:
|
|
|
|
return v + mapping[k] - k
|
|
|
|
k = max(keys)
|
|
|
|
if v > k:
|
|
|
|
return v + mapping[k] - k
|
|
|
|
# Interpolate
|
|
|
|
a = max(k for k in keys if k < v)
|
|
|
|
b = min(k for k in keys if k > v)
|
|
|
|
va = mapping[a]
|
|
|
|
vb = mapping[b]
|
|
|
|
return va + (vb - va) * (v - a) / (b - a)
|
|
|
|
|
|
|
|
|
[varLib.models] Add main()
Takes positions and prints model results:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)}]
2018-03-26 20:10:15 -07:00
|
|
|
def main(args):
|
|
|
|
from fontTools import configLogger
|
|
|
|
|
|
|
|
args = args[1:]
|
|
|
|
|
|
|
|
# TODO: allow user to configure logging via command-line options
|
|
|
|
configLogger(level="INFO")
|
|
|
|
|
2018-06-16 13:53:31 -04:00
|
|
|
if len(args) < 1:
|
|
|
|
print("usage: fonttools varLib.models source.designspace", file=sys.stderr)
|
|
|
|
print(" or")
|
|
|
|
print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr)
|
|
|
|
sys.exit(1)
|
[varLib.models] Add main()
Takes positions and prints model results:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)}]
2018-03-26 20:10:15 -07:00
|
|
|
|
|
|
|
from pprint import pprint
|
2018-06-16 13:53:31 -04:00
|
|
|
|
|
|
|
if len(args) == 1 and args[0].endswith('.designspace'):
|
|
|
|
from fontTools.designspaceLib import DesignSpaceDocument
|
|
|
|
doc = DesignSpaceDocument()
|
|
|
|
doc.read(args[0])
|
|
|
|
locs = [s.location for s in doc.sources]
|
|
|
|
print("Original locations:")
|
|
|
|
pprint(locs)
|
|
|
|
doc.normalize()
|
|
|
|
print("Normalized locations:")
|
2019-03-01 13:04:52 -08:00
|
|
|
locs = [s.location for s in doc.sources]
|
2018-06-16 13:53:31 -04:00
|
|
|
pprint(locs)
|
|
|
|
else:
|
|
|
|
axes = [chr(c) for c in range(ord('A'), ord('Z')+1)]
|
|
|
|
locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args]
|
|
|
|
|
|
|
|
model = VariationModel(locs)
|
[varLib.models] Add main()
Takes positions and prints model results:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)}]
2018-03-26 20:10:15 -07:00
|
|
|
print("Sorted locations:")
|
|
|
|
pprint(model.locations)
|
|
|
|
print("Supports:")
|
|
|
|
pprint(model.supports)
|
|
|
|
|
2016-11-29 14:38:46 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
import doctest, sys
|
[varLib.models] Add main()
Takes positions and prints model results:
$ ./fonttools varLib.models 0,0 0,1 1,0 1,1 .4,0 .6,1 .5,.5
Sorted locations:
[{},
{'A': 0.4},
{'A': 1.0},
{'B': 1.0},
{'A': 1.0, 'B': 1.0},
{'A': 0.6, 'B': 1.0},
{'A': 0.5, 'B': 0.5}]
Supports:
[{},
{'A': (0.0, 0.4, 1.0)},
{'A': (0.4, 1.0, 1.0)},
{'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 1.0, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.6, 1.0), 'B': (0.0, 1.0, 1.0)},
{'A': (0.4, 0.5, 1.0), 'B': (0.0, 0.5, 1.0)}]
2018-03-26 20:10:15 -07:00
|
|
|
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
sys.exit(main(sys.argv))
|
|
|
|
|
2016-11-29 14:38:46 +01:00
|
|
|
sys.exit(doctest.testmod().failed)
|