2016-04-15 13:56:37 -07:00
|
|
|
"""
|
|
|
|
Module for dealing with 'gvar'-style font variations, also known as run-time
|
|
|
|
interpolation.
|
2016-04-12 23:52:03 -07:00
|
|
|
|
2016-04-15 13:56:37 -07:00
|
|
|
The ideas here are very similar to MutatorMath. There is even code to read
|
|
|
|
MutatorMath .designspace files.
|
|
|
|
|
|
|
|
For now, if you run this file on a designspace file, it tries to find
|
|
|
|
ttf-interpolatable files for the masters and build a GX variation font from
|
|
|
|
them. Such ttf-interpolatable and designspace files can be generated from
|
|
|
|
a Glyphs source, eg., using noto-source as an example:
|
|
|
|
|
|
|
|
$ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs
|
|
|
|
|
|
|
|
Then you can make a GX font this way:
|
|
|
|
|
|
|
|
$ python fonttools/Lib/fontTools/varLib/__init__.py master_ufo/NotoSansArabic.designspace
|
|
|
|
|
|
|
|
API *will* change in near future.
|
|
|
|
"""
|
2016-04-12 23:52:03 -07:00
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from fontTools.misc.py23 import *
|
2016-04-14 18:27:44 -07:00
|
|
|
from fontTools.ttLib import TTFont
|
|
|
|
from fontTools.ttLib.tables._n_a_m_e import NameRecord
|
|
|
|
from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance
|
2016-04-27 00:21:46 -07:00
|
|
|
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
2016-04-14 18:27:44 -07:00
|
|
|
from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, GlyphVariation
|
2016-04-18 16:48:02 -07:00
|
|
|
import warnings
|
2016-04-28 09:56:21 +01:00
|
|
|
try:
|
|
|
|
import xml.etree.cElementTree as ET
|
|
|
|
except ImportError:
|
|
|
|
import xml.etree.ElementTree as ET
|
2016-04-14 18:27:44 -07:00
|
|
|
import os.path
|
2016-04-12 23:52:03 -07:00
|
|
|
|
|
|
|
|
2016-04-14 18:27:44 -07:00
|
|
|
#
|
|
|
|
# Variation space, aka design space, model
|
|
|
|
#
|
|
|
|
|
2016-04-27 00:15:07 -07:00
|
|
|
def supportScalar(location, support):
|
|
|
|
"""Returns the scalar multiplier at location, for a master
|
|
|
|
with support.
|
|
|
|
>>> 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
|
|
|
|
"""
|
|
|
|
scalar = 1.
|
|
|
|
for axis,(lower,peak,upper) in support.items():
|
|
|
|
if axis not in location:
|
|
|
|
scalar = 0.
|
|
|
|
break
|
|
|
|
v = location[axis]
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2016-04-13 23:46:12 -07:00
|
|
|
class VariationModel(object):
|
2016-04-13 00:33:42 -07:00
|
|
|
|
2016-04-12 23:52:03 -07:00
|
|
|
"""
|
|
|
|
Locations must be in normalized space. Ie. base master
|
|
|
|
is at origin (0).
|
2016-04-15 13:46:52 -07:00
|
|
|
>>> from pprint import pprint
|
2016-04-13 23:51:54 -07:00
|
|
|
>>> 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'])
|
2016-04-15 13:46:52 -07:00
|
|
|
>>> 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},
|
2016-04-27 00:15:07 -07:00
|
|
|
{0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.25},
|
2016-04-15 13:46:52 -07:00
|
|
|
{0: 1.0,
|
2016-04-27 00:15:07 -07:00
|
|
|
3: 0.75,
|
|
|
|
4: 0.25,
|
|
|
|
5: 0.6666666666666667,
|
|
|
|
6: 0.16666666666666669,
|
|
|
|
7: 0.6666666666666667}]
|
2016-04-12 23:52:03 -07:00
|
|
|
"""
|
|
|
|
|
2016-04-13 00:15:58 -07:00
|
|
|
def __init__(self, locations, axisOrder=[]):
|
2016-04-13 16:44:59 -07:00
|
|
|
locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations]
|
2016-04-13 00:33:42 -07:00
|
|
|
keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder)
|
2016-04-13 00:15:58 -07:00
|
|
|
axisPoints = keyFunc.axisPoints
|
2016-04-15 13:46:52 -07:00
|
|
|
self.locations = sorted(locations, key=keyFunc)
|
|
|
|
# TODO Assert that locations are unique.
|
|
|
|
self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order
|
|
|
|
self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above
|
2016-04-13 00:15:58 -07:00
|
|
|
|
|
|
|
self._computeMasterSupports(axisPoints)
|
|
|
|
|
2016-04-13 00:33:42 -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:
|
|
|
|
axisPoints[axis] = {0}
|
|
|
|
assert value not in axisPoints[axis]
|
|
|
|
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
|
|
|
|
|
2016-04-13 00:15:58 -07:00
|
|
|
@staticmethod
|
|
|
|
def lowerBound(value, lst):
|
|
|
|
if any(v < value for v in lst):
|
|
|
|
return max(v for v in lst if v < value)
|
|
|
|
else:
|
|
|
|
return value
|
|
|
|
@staticmethod
|
|
|
|
def upperBound(value, lst):
|
|
|
|
if any(v > value for v in lst):
|
|
|
|
return min(v for v in lst if v > value)
|
|
|
|
else:
|
|
|
|
return value
|
|
|
|
|
|
|
|
def _computeMasterSupports(self, axisPoints):
|
|
|
|
supports = []
|
2016-04-13 16:44:59 -07:00
|
|
|
deltaWeights = []
|
2016-04-15 13:46:52 -07:00
|
|
|
locations = self.locations
|
2016-04-13 13:17:59 -07:00
|
|
|
for i,loc in enumerate(locations):
|
2016-04-13 00:15:58 -07:00
|
|
|
box = {}
|
|
|
|
|
|
|
|
# Account for axisPoints first
|
|
|
|
for axis,values in axisPoints.items():
|
|
|
|
if not axis in loc:
|
|
|
|
continue
|
|
|
|
locV = loc[axis]
|
|
|
|
box[axis] = (self.lowerBound(locV, values), locV, self.upperBound(locV, values))
|
|
|
|
|
|
|
|
locAxes = set(loc.keys())
|
|
|
|
# Walk over previous masters now
|
2016-04-13 13:17:59 -07:00
|
|
|
for j,m in enumerate(locations[:i]):
|
2016-04-13 00:15:58 -07:00
|
|
|
# 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
|
|
|
|
for axis, (lower,_,upper) in box.items():
|
|
|
|
if axis in m and not (lower < m[axis] < upper):
|
|
|
|
relevant = False
|
|
|
|
break
|
|
|
|
if not relevant:
|
|
|
|
continue
|
|
|
|
# Split the box for new master
|
|
|
|
for axis,val in m.items():
|
|
|
|
assert axis in box
|
|
|
|
lower,locV,upper = box[axis]
|
|
|
|
if val < locV:
|
|
|
|
lower = val
|
|
|
|
elif locV < val:
|
|
|
|
upper = val
|
|
|
|
box[axis] = (lower,locV,upper)
|
|
|
|
supports.append(box)
|
|
|
|
|
2016-04-15 13:46:52 -07:00
|
|
|
deltaWeight = {}
|
2016-04-13 16:44:59 -07:00
|
|
|
# Walk over previous masters now, populate deltaWeight
|
2016-04-13 13:17:59 -07:00
|
|
|
for j,m in enumerate(locations[:i]):
|
2016-04-27 00:15:07 -07:00
|
|
|
scalar = supportScalar(loc, supports[j])
|
2016-04-15 13:46:52 -07:00
|
|
|
if scalar:
|
|
|
|
deltaWeight[j] = scalar
|
2016-04-13 16:44:59 -07:00
|
|
|
deltaWeights.append(deltaWeight)
|
2016-04-13 13:17:59 -07:00
|
|
|
|
2016-04-15 13:46:52 -07:00
|
|
|
self.supports = supports
|
|
|
|
self.deltaWeights = deltaWeights
|
2016-04-13 13:17:59 -07:00
|
|
|
|
2016-04-15 08:56:04 -07:00
|
|
|
def getDeltas(self, masterValues):
|
2016-04-15 13:46:52 -07:00
|
|
|
count = len(self.locations)
|
|
|
|
assert len(masterValues) == len(self.deltaWeights)
|
|
|
|
mapping = self.reverseMapping
|
|
|
|
out = []
|
|
|
|
for i,weights in enumerate(self.deltaWeights):
|
2016-04-27 00:15:07 -07:00
|
|
|
delta = masterValues[mapping[i]]
|
|
|
|
for j,weight in weights.items():
|
|
|
|
delta -= out[j] * weight
|
2016-04-15 13:46:52 -07:00
|
|
|
out.append(delta)
|
2016-04-15 08:56:04 -07:00
|
|
|
return out
|
|
|
|
|
2016-05-26 13:34:37 -07:00
|
|
|
def interpolateFromDeltas(self, loc, deltas):
|
|
|
|
v = None
|
|
|
|
supports = model.supports
|
|
|
|
assert len(deltas) == len(supports)
|
|
|
|
for i,(delta,support) in enumerate(zip(deltas, supports)):
|
|
|
|
scalar = supportScalar(loc, support)
|
|
|
|
if not scalar: continue
|
|
|
|
contribution = delta * scalar
|
|
|
|
if i == 0:
|
|
|
|
v = contribution
|
|
|
|
else:
|
|
|
|
v += contribution
|
|
|
|
return v
|
|
|
|
|
2016-06-07 16:21:43 -07:00
|
|
|
def interpolateFromMasters(self, loc, masterValues):
|
2016-05-26 13:34:37 -07:00
|
|
|
deltas = self.getDeltas(masterValues)
|
|
|
|
return self.interpolateFromDeltas(loc, deltas)
|
|
|
|
|
2016-04-14 18:27:44 -07:00
|
|
|
#
|
|
|
|
# .designspace routines
|
|
|
|
#
|
|
|
|
|
2016-04-14 00:31:17 -07:00
|
|
|
def _xmlParseLocation(et):
|
|
|
|
loc = {}
|
|
|
|
for dim in et.find('location'):
|
|
|
|
assert dim.tag == 'dimension'
|
|
|
|
name = dim.attrib['name']
|
2016-04-14 18:27:44 -07:00
|
|
|
value = float(dim.attrib['xvalue'])
|
2016-04-14 00:31:17 -07:00
|
|
|
assert name not in loc
|
|
|
|
loc[name] = value
|
|
|
|
return loc
|
|
|
|
|
|
|
|
def _designspace_load(et):
|
|
|
|
base_idx = None
|
|
|
|
masters = []
|
|
|
|
ds = et.getroot()
|
|
|
|
for master in ds.find('sources'):
|
|
|
|
name = master.attrib['name']
|
|
|
|
filename = master.attrib['filename']
|
|
|
|
isBase = master.find('info')
|
|
|
|
if isBase is not None:
|
|
|
|
assert base_idx is None
|
|
|
|
base_idx = len(masters)
|
|
|
|
loc = _xmlParseLocation(master)
|
|
|
|
masters.append((filename, loc, name))
|
|
|
|
|
|
|
|
instances = []
|
|
|
|
for instance in ds.find('instances'):
|
|
|
|
name = master.attrib['name']
|
|
|
|
family = instance.attrib['familyname']
|
|
|
|
style = instance.attrib['stylename']
|
|
|
|
filename = instance.attrib['filename']
|
|
|
|
loc = _xmlParseLocation(instance)
|
|
|
|
instances.append((filename, loc, name, family, style))
|
|
|
|
|
|
|
|
return masters, instances, base_idx
|
|
|
|
|
|
|
|
def designspace_load(filename):
|
|
|
|
return _designspace_load(ET.parse(filename))
|
|
|
|
|
|
|
|
def designspace_loads(string):
|
|
|
|
return _designspace_load(ET.fromstring(string))
|
|
|
|
|
2016-04-14 18:27:44 -07:00
|
|
|
|
|
|
|
#
|
|
|
|
# Creation routines
|
|
|
|
#
|
|
|
|
|
|
|
|
# TODO: Move to name table proper; also, is mac_roman ok for ASCII names?
|
|
|
|
def _AddName(font, name):
|
|
|
|
"""(font, "Bold") --> NameRecord"""
|
|
|
|
nameTable = font.get("name")
|
|
|
|
namerec = NameRecord()
|
|
|
|
namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256])
|
|
|
|
namerec.string = name.encode("mac_roman")
|
|
|
|
namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0)
|
|
|
|
nameTable.names.append(namerec)
|
|
|
|
return namerec
|
|
|
|
|
|
|
|
# Move to fvar table proper?
|
|
|
|
def _add_fvar(font, axes, instances):
|
|
|
|
assert "fvar" not in font
|
|
|
|
font['fvar'] = fvar = table__f_v_a_r()
|
|
|
|
|
|
|
|
for tag in sorted(axes.keys()):
|
|
|
|
axis = Axis()
|
|
|
|
axis.axisTag = tag
|
|
|
|
name, axis.minValue, axis.defaultValue, axis.maxValue = axes[tag]
|
|
|
|
axis.nameID = _AddName(font, name).nameID
|
|
|
|
fvar.axes.append(axis)
|
|
|
|
|
|
|
|
for name, coordinates in instances:
|
|
|
|
inst = NamedInstance()
|
|
|
|
inst.nameID = _AddName(font, name).nameID
|
|
|
|
inst.coordinates = coordinates
|
|
|
|
fvar.instances.append(inst)
|
|
|
|
|
2016-04-14 23:55:11 -07:00
|
|
|
# TODO Move to glyf or gvar table proper
|
|
|
|
def _GetCoordinates(font, glyphName):
|
2016-04-14 18:27:44 -07:00
|
|
|
"""font, glyphName --> glyph coordinates as expected by "gvar" table
|
|
|
|
|
|
|
|
The result includes four "phantom points" for the glyph metrics,
|
|
|
|
as mandated by the "gvar" spec.
|
|
|
|
"""
|
2016-04-14 23:55:11 -07:00
|
|
|
glyf = font["glyf"]
|
|
|
|
if glyphName not in glyf.glyphs: return None
|
|
|
|
glyph = glyf[glyphName]
|
2016-04-14 18:27:44 -07:00
|
|
|
if glyph.isComposite():
|
2016-04-27 01:17:09 -07:00
|
|
|
coord = GlyphCoordinates([(c.x,c.y) for c in glyph.components])
|
2016-04-14 23:55:11 -07:00
|
|
|
control = [c.glyphName for c in glyph.components]
|
2016-04-14 18:27:44 -07:00
|
|
|
else:
|
2016-04-14 23:55:11 -07:00
|
|
|
allData = glyph.getCoordinates(glyf)
|
|
|
|
coord = allData[0]
|
|
|
|
control = allData[1:]
|
|
|
|
|
2016-04-14 18:27:44 -07:00
|
|
|
# Add phantom points for (left, right, top, bottom) positions.
|
|
|
|
horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName]
|
|
|
|
if not hasattr(glyph, 'xMin'):
|
2016-04-14 23:55:11 -07:00
|
|
|
glyph.recalcBounds(glyf)
|
2016-04-14 18:27:44 -07:00
|
|
|
leftSideX = glyph.xMin - leftSideBearing
|
|
|
|
rightSideX = leftSideX + horizontalAdvanceWidth
|
|
|
|
# XXX these are incorrect. Load vmtx and fix.
|
|
|
|
topSideY = glyph.yMax
|
|
|
|
bottomSideY = -glyph.yMin
|
2016-04-27 00:25:31 -07:00
|
|
|
coord = coord.copy()
|
2016-04-14 18:27:44 -07:00
|
|
|
coord.extend([(leftSideX, 0),
|
|
|
|
(rightSideX, 0),
|
|
|
|
(0, topSideY),
|
|
|
|
(0, bottomSideY)])
|
2016-04-14 23:55:11 -07:00
|
|
|
|
|
|
|
return coord, control
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-27 01:17:09 -07:00
|
|
|
# TODO Move to glyf or gvar table proper
|
|
|
|
def _SetCoordinates(font, glyphName, coord):
|
|
|
|
glyf = font["glyf"]
|
|
|
|
assert glyphName in glyf.glyphs
|
|
|
|
glyph = glyf[glyphName]
|
|
|
|
|
|
|
|
# Handle phantom points for (left, right, top, bottom) positions.
|
|
|
|
assert len(coord) >= 4
|
|
|
|
if not hasattr(glyph, 'xMin'):
|
|
|
|
glyph.recalcBounds(glyf)
|
|
|
|
topSideY = glyph.yMax
|
|
|
|
bottomSideY = -glyph.yMin
|
|
|
|
leftSideX = coord[-4][0]
|
|
|
|
rightSideX = coord[-3][0]
|
|
|
|
topSideY = coord[-2][1]
|
|
|
|
bottomSideY = coord[-1][1]
|
|
|
|
|
|
|
|
for _ in range(4):
|
|
|
|
del coord[-1]
|
|
|
|
|
|
|
|
if glyph.isComposite():
|
|
|
|
assert len(coord) == len(glyph.components)
|
|
|
|
for p,comp in zip(coord, glyph.components):
|
|
|
|
comp.x,comp.y = p
|
|
|
|
elif glyph.numberOfContours is 0:
|
|
|
|
assert len(coord) == 0
|
|
|
|
else:
|
|
|
|
assert len(coord) == len(glyph.coordinates)
|
|
|
|
glyph.coordinates = coord
|
|
|
|
|
2016-06-07 15:51:54 -07:00
|
|
|
glyph.recalcBounds(glyf)
|
|
|
|
|
|
|
|
horizontalAdvanceWidth = rightSideX - leftSideX
|
|
|
|
leftSideBearing = glyph.xMin - leftSideX
|
|
|
|
# XXX Handle vertical
|
|
|
|
# XXX Remove the round when https://github.com/behdad/fonttools/issues/593 is fixed
|
|
|
|
font["hmtx"].metrics[glyphName] = int(round(horizontalAdvanceWidth)), int(round(leftSideBearing))
|
|
|
|
|
2016-04-15 08:56:04 -07:00
|
|
|
def _add_gvar(font, axes, master_ttfs, master_locs, base_idx):
|
2016-04-14 18:27:44 -07:00
|
|
|
|
|
|
|
# Make copies for modification
|
|
|
|
master_ttfs = master_ttfs[:]
|
2016-04-15 08:56:04 -07:00
|
|
|
master_locs = [l.copy() for l in master_locs]
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-14 23:55:11 -07:00
|
|
|
axis_tags = axes.keys()
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-15 13:46:52 -07:00
|
|
|
# Normalize master locations. TODO Move to a separate function.
|
2016-04-14 23:55:11 -07:00
|
|
|
for tag,(name,lower,default,upper) in axes.items():
|
2016-04-15 08:56:04 -07:00
|
|
|
for l in master_locs:
|
2016-04-14 18:27:44 -07:00
|
|
|
v = l[tag]
|
2016-04-14 23:55:11 -07:00
|
|
|
if v == default:
|
2016-04-14 18:27:44 -07:00
|
|
|
v = 0
|
2016-04-14 23:55:11 -07:00
|
|
|
elif v < default:
|
|
|
|
v = (v - default) / (default - lower)
|
2016-04-14 18:27:44 -07:00
|
|
|
else:
|
2016-04-14 23:55:11 -07:00
|
|
|
v = (v - default) / (upper - default)
|
2016-04-14 18:27:44 -07:00
|
|
|
l[tag] = v
|
|
|
|
# Locations are normalized now
|
|
|
|
|
|
|
|
print("Normalized master positions:")
|
2016-04-15 13:46:52 -07:00
|
|
|
print(master_locs)
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-14 23:55:11 -07:00
|
|
|
print("Generating gvar")
|
|
|
|
assert "gvar" not in font
|
|
|
|
gvar = font["gvar"] = table__g_v_a_r()
|
2016-04-14 18:27:44 -07:00
|
|
|
gvar.version = 1
|
|
|
|
gvar.reserved = 0
|
|
|
|
gvar.variations = {}
|
|
|
|
|
2016-04-15 08:56:04 -07:00
|
|
|
# Assume single-model for now.
|
|
|
|
model = VariationModel(master_locs)
|
2016-04-15 13:46:52 -07:00
|
|
|
model_base_idx = model.mapping[base_idx]
|
|
|
|
assert 0 == model_base_idx
|
2016-04-15 08:56:04 -07:00
|
|
|
|
2016-04-14 23:55:11 -07:00
|
|
|
for glyph in font.getGlyphOrder():
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-14 23:55:11 -07:00
|
|
|
allData = [_GetCoordinates(m, glyph) for m in master_ttfs]
|
|
|
|
allCoords = [d[0] for d in allData]
|
|
|
|
allControls = [d[1] for d in allData]
|
|
|
|
control = allControls[0]
|
|
|
|
if (any(c != control for c in allControls)):
|
|
|
|
warnings.warn("glyph %s has incompatible masters; skipping" % glyph)
|
2016-04-14 18:27:44 -07:00
|
|
|
continue
|
2016-04-14 23:55:11 -07:00
|
|
|
del allControls
|
2016-04-14 18:27:44 -07:00
|
|
|
|
|
|
|
gvar.variations[glyph] = []
|
|
|
|
|
2016-04-15 08:56:04 -07:00
|
|
|
deltas = model.getDeltas(allCoords)
|
|
|
|
supports = model.supports
|
|
|
|
assert len(deltas) == len(supports)
|
|
|
|
for i,(delta,support) in enumerate(zip(deltas, supports)):
|
2016-04-15 13:46:52 -07:00
|
|
|
if i == model_base_idx:
|
2016-04-15 08:56:04 -07:00
|
|
|
continue
|
|
|
|
var = GlyphVariation(support, delta)
|
|
|
|
gvar.variations[glyph].append(var)
|
2016-04-14 18:27:44 -07:00
|
|
|
|
2016-04-14 00:31:17 -07:00
|
|
|
def main(args=None):
|
|
|
|
|
|
|
|
import sys
|
|
|
|
if args is None:
|
|
|
|
args = sys.argv[1:]
|
|
|
|
|
2016-04-14 18:27:44 -07:00
|
|
|
(designspace_filename,) = args
|
|
|
|
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
|
|
|
|
axisMap = None # dict mapping axis id to (axis tag, axis name)
|
|
|
|
outfile = os.path.splitext(designspace_filename)[0] + '-GX.ttf'
|
|
|
|
|
|
|
|
masters, instances, base_idx = designspace_load(designspace_filename)
|
2016-04-17 11:53:20 -07:00
|
|
|
assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document."
|
2016-04-14 00:31:17 -07:00
|
|
|
|
2016-04-15 13:46:52 -07:00
|
|
|
from pprint import pprint
|
2016-04-14 18:27:44 -07:00
|
|
|
print("Masters:")
|
2016-04-14 00:31:17 -07:00
|
|
|
pprint(masters)
|
2016-04-14 18:27:44 -07:00
|
|
|
print("Instances:")
|
2016-04-14 00:31:17 -07:00
|
|
|
pprint(instances)
|
2016-04-14 18:27:44 -07:00
|
|
|
print("Index of base master:", base_idx)
|
|
|
|
|
|
|
|
print("Building GX")
|
|
|
|
print("Loading TTF masters")
|
|
|
|
basedir = os.path.dirname(designspace_filename)
|
|
|
|
master_ttfs = [finder(os.path.join(basedir, m[0])) for m in masters]
|
|
|
|
master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs]
|
|
|
|
|
|
|
|
standard_axis_map = {
|
|
|
|
'weight': ('wght', 'Weight'),
|
|
|
|
'width': ('wdth', 'Width'),
|
|
|
|
'slant': ('slnt', 'Slant'),
|
|
|
|
'optical': ('opsz', 'Optical Size'),
|
|
|
|
}
|
|
|
|
|
|
|
|
axis_map = standard_axis_map
|
|
|
|
if axisMap:
|
|
|
|
axis_map = axis_map.copy()
|
|
|
|
axis_map.update(axisMap)
|
|
|
|
|
|
|
|
# TODO: For weight & width, use OS/2 values and setup 'avar' mapping.
|
|
|
|
|
|
|
|
# Set up master locations
|
|
|
|
master_locs = []
|
|
|
|
instance_locs = []
|
|
|
|
out = []
|
|
|
|
for loc in [m[1] for m in masters+instances]:
|
|
|
|
# Apply modifications for default axes; and apply tags
|
|
|
|
l = {}
|
|
|
|
for axis,value in loc.items():
|
|
|
|
tag,name = axis_map[axis]
|
|
|
|
l[tag] = value
|
|
|
|
out.append(l)
|
|
|
|
master_locs = out[:len(masters)]
|
|
|
|
instance_locs = out[len(masters):]
|
|
|
|
|
|
|
|
axis_tags = set(master_locs[0].keys())
|
|
|
|
assert all(axis_tags == set(m.keys()) for m in master_locs)
|
|
|
|
print("Axis tags:", axis_tags)
|
|
|
|
print("Master positions:")
|
|
|
|
pprint(master_locs)
|
|
|
|
|
|
|
|
# Set up axes
|
|
|
|
axes = {}
|
|
|
|
axis_names = {}
|
|
|
|
for tag,name in axis_map.values():
|
|
|
|
if tag not in axis_tags: continue
|
|
|
|
axis_names[tag] = name
|
|
|
|
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)
|
|
|
|
name = axis_names[tag]
|
|
|
|
axes[tag] = (name, lower, default, upper)
|
|
|
|
print("Axes:")
|
|
|
|
pprint(axes)
|
|
|
|
|
|
|
|
# Set up named instances
|
|
|
|
instance_list = []
|
|
|
|
for loc,instance in zip(instance_locs,instances):
|
|
|
|
style = instance[4]
|
|
|
|
instance_list.append((style, loc))
|
|
|
|
# TODO append masters as named-instances as well; needs .designspace change.
|
|
|
|
|
|
|
|
gx = TTFont(master_ttfs[base_idx])
|
|
|
|
|
|
|
|
_add_fvar(gx, axes, instance_list)
|
|
|
|
|
|
|
|
print("Setting up glyph variations")
|
2016-04-14 23:55:11 -07:00
|
|
|
_add_gvar(gx, axes, master_fonts, master_locs, base_idx)
|
2016-04-14 18:27:44 -07:00
|
|
|
|
|
|
|
print("Saving GX font", outfile)
|
|
|
|
gx.save(outfile)
|
2016-04-14 00:31:17 -07:00
|
|
|
|
2016-04-13 23:51:54 -07:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2016-04-14 00:31:17 -07:00
|
|
|
import sys
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
main()
|
2016-04-15 13:46:52 -07:00
|
|
|
#sys.exit(0)
|
2016-04-13 23:51:54 -07:00
|
|
|
import doctest, sys
|
|
|
|
sys.exit(doctest.testmod().failed)
|