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
2016-08-15 11:59:53 -07:00
MutatorMath . designspace files in the varLib . designspace module .
2016-04-15 13:56:37 -07:00
For now , if you run this file on a designspace file , it tries to find
2016-12-08 20:48:08 -08:00
ttf - interpolatable files for the masters and build a variable - font from
2016-04-15 13:56:37 -07:00
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
2016-12-08 20:48:08 -08:00
Then you can make a variable - font this way :
2016-04-15 13:56:37 -07:00
2017-02-22 14:46:23 -06:00
$ fonttools varLib master_ufo / NotoSansArabic . designspace
2016-04-15 13:56:37 -07:00
API * will * change in near future .
"""
2016-04-12 23:52:03 -07:00
from __future__ import print_function , division , absolute_import
2016-10-04 14:56:26 +01:00
from __future__ import unicode_literals
2016-04-12 23:52:03 -07:00
from fontTools . misc . py23 import *
2016-07-01 15:31:00 -07:00
from fontTools . ttLib import TTFont , newTable
2016-04-14 18:27:44 -07:00
from fontTools . ttLib . tables . _n_a_m_e import NameRecord
2016-07-01 15:31:00 -07:00
from fontTools . ttLib . tables . _f_v_a_r import Axis , NamedInstance
2016-04-27 00:21:46 -07:00
from fontTools . ttLib . tables . _g_l_y_f import GlyphCoordinates
2017-05-08 15:42:57 -06:00
from fontTools . ttLib . tables . TupleVariation import TupleVariation
2016-07-01 15:31:00 -07:00
from fontTools . ttLib . tables import otTables as ot
2017-02-22 20:18:05 +01:00
from fontTools . varLib import builder , designspace , models
2017-04-10 21:01:00 +02:00
from fontTools . varLib . merger import VariationMerger , _all_equal
2017-02-25 20:53:48 -08:00
from collections import OrderedDict
2016-04-14 18:27:44 -07:00
import os . path
2017-01-06 20:22:45 +01:00
import logging
from pprint import pformat
2017-01-11 11:58:17 +00:00
log = logging . getLogger ( " fontTools.varLib " )
2016-04-12 23:52:03 -07:00
2017-02-27 16:34:41 +00:00
class VarLibError ( Exception ) :
pass
2016-04-14 18:27:44 -07:00
#
# Creation routines
#
2017-04-12 21:52:29 -07:00
def _add_fvar_avar ( font , axes , instances ) :
2016-09-02 17:29:22 -07:00
"""
Add ' fvar ' table to font .
2017-04-12 17:14:22 -07:00
axes is an ordered dictionary of DesignspaceAxis objects .
2016-09-02 17:29:22 -07:00
instances is list of dictionary objects with ' location ' , ' stylename ' ,
and possibly ' postscriptfontname ' entries .
"""
2017-04-12 21:52:29 -07:00
assert axes
assert isinstance ( axes , OrderedDict )
log . info ( " Generating fvar / avar " )
fvar = newTable ( ' fvar ' )
2016-10-04 14:56:26 +01:00
nameTable = font [ ' name ' ]
2016-04-14 18:27:44 -07:00
2017-04-12 17:14:22 -07:00
for a in axes . values ( ) :
2016-04-14 18:27:44 -07:00
axis = Axis ( )
2017-04-12 17:14:22 -07:00
axis . axisTag = Tag ( a . tag )
axis . minValue , axis . defaultValue , axis . maxValue = a . minimum , a . default , a . maximum
axis . axisNameID = nameTable . addName ( tounicode ( a . labelname [ ' en ' ] ) )
2017-04-19 11:10:00 -07:00
# TODO:
# Replace previous line with the following when the following issues are resolved:
# https://github.com/fonttools/fonttools/issues/930
# https://github.com/fonttools/fonttools/issues/931
# axis.axisNameID = nameTable.addMultilingualName(a.labelname, font)
2016-04-14 18:27:44 -07:00
fvar . axes . append ( axis )
2016-09-02 17:29:22 -07:00
for instance in instances :
coordinates = instance [ ' location ' ]
2016-10-04 14:56:26 +01:00
name = tounicode ( instance [ ' stylename ' ] )
2016-09-02 18:12:14 -07:00
psname = instance . get ( ' postscriptfontname ' )
2016-09-02 17:29:22 -07:00
2016-04-14 18:27:44 -07:00
inst = NamedInstance ( )
2016-10-04 14:56:26 +01:00
inst . subfamilyNameID = nameTable . addName ( name )
if psname is not None :
psname = tounicode ( psname )
inst . postscriptNameID = nameTable . addName ( psname )
2017-04-12 21:52:29 -07:00
inst . coordinates = { axes [ k ] . tag : axes [ k ] . map_backward ( v ) for k , v in coordinates . items ( ) }
2016-04-14 18:27:44 -07:00
fvar . instances . append ( inst )
2017-04-12 21:52:29 -07:00
avar = newTable ( ' avar ' )
interesting = False
for axis in axes . values ( ) :
curve = avar . segments [ axis . tag ] = { }
if not axis . map or all ( k == v for k , v in axis . map . items ( ) ) :
continue
interesting = True
2017-07-28 16:25:03 +02:00
curve . update ( { - 1.0 : - 1.0 , 0.0 : 0.0 , 1.0 : 1.0 } )
2017-04-12 21:52:29 -07:00
items = sorted ( axis . map . items ( ) )
keys = [ item [ 0 ] for item in items ]
vals = [ item [ 1 ] for item in items ]
# Current avar requirements. We don't have to enforce
# these on the designer and can deduce some ourselves,
# but for now just enforce them.
assert axis . minimum == min ( keys )
assert axis . maximum == max ( keys )
assert axis . default in keys
# No duplicates
assert len ( set ( keys ) ) == len ( keys )
assert len ( set ( vals ) ) == len ( vals )
# Ascending values
assert sorted ( vals ) == vals
keys_triple = ( axis . minimum , axis . default , axis . maximum )
vals_triple = tuple ( axis . map_forward ( v ) for v in keys_triple )
keys = [ models . normalizeValue ( v , keys_triple ) for v in keys ]
vals = [ models . normalizeValue ( v , vals_triple ) for v in vals ]
curve . update ( zip ( keys , vals ) )
if not interesting :
log . info ( " No need for avar " )
avar = None
assert " fvar " not in font
font [ ' fvar ' ] = fvar
assert " avar " not in font
if avar :
font [ ' avar ' ] = avar
return fvar , avar
2016-09-08 09:17:45 -07:00
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-07-29 14:44:02 -07:00
coord = GlyphCoordinates ( [ ( getattr ( c , ' x ' , 0 ) , getattr ( c , ' y ' , 0 ) ) for c in glyph . components ] )
2017-05-08 15:42:57 -06:00
control = ( glyph . numberOfContours , [ 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 ]
2017-05-08 15:42:57 -06:00
control = ( glyph . numberOfContours , ) + allData [ 1 : ]
2016-04-14 23:55:11 -07:00
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 )
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 ) :
2016-07-29 14:44:02 -07:00
if hasattr ( comp , ' x ' ) :
comp . x , comp . y = p
2016-04-27 01:17:09 -07:00
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 )
2017-03-04 12:54:20 -08:00
horizontalAdvanceWidth = round ( rightSideX - leftSideX )
leftSideBearing = round ( glyph . xMin - leftSideX )
2016-06-07 15:51:54 -07:00
# XXX Handle vertical
2017-02-25 10:59:31 -08:00
font [ " hmtx " ] . metrics [ glyphName ] = horizontalAdvanceWidth , leftSideBearing
2016-06-07 15:51:54 -07:00
2016-08-09 20:53:19 -07:00
2017-05-31 19:00:33 -04:00
def _all_interpolatable_in_between ( deltas , coords , i , j , tolerance ) :
assert j - i > = 2
2017-05-28 23:40:24 -07:00
from fontTools . varLib . mutator import _iup_segment
2017-07-27 17:05:34 +01:00
interp = list ( _iup_segment ( coords [ i + 1 : j ] , coords [ i ] , deltas [ i ] , coords [ j ] , deltas [ j ] ) )
2017-05-31 19:00:33 -04:00
deltas = deltas [ i + 1 : j ]
2017-05-28 23:40:24 -07:00
assert len ( deltas ) == len ( interp )
2017-05-28 23:43:49 -07:00
return all ( abs ( complex ( x - p , y - q ) ) < = tolerance for ( x , y ) , ( p , q ) in zip ( deltas , interp ) )
2017-05-28 23:40:24 -07:00
2017-05-31 19:06:42 -04:00
def _iup_contour_bound_forced_set ( delta , coords , tolerance = 0 ) :
""" The forced set is a conservative set of points on the contour that must be encoded
explicitly ( ie . cannot be interpolated ) . Calculating this set allows for significantly
speeding up the dynamic - programming , as well as resolve circularity in DP .
2017-05-08 15:42:57 -06:00
2017-05-31 19:06:42 -04:00
The set is precise ; that is , if an index is in the returned set , then there is no way
that IUP can generate delta for that point , given coords and delta .
"""
assert len ( delta ) == len ( coords )
2017-05-29 14:24:09 -04:00
forced = set ( )
2017-05-29 16:13:46 -04:00
# Track "last" and "next" points on the contour as we sweep.
2017-05-29 14:24:09 -04:00
nd , nc = delta [ 0 ] , coords [ 0 ]
ld , lc = delta [ - 1 ] , coords [ - 1 ]
2017-05-31 19:06:42 -04:00
for i in range ( len ( delta ) - 1 , - 1 , - 1 ) :
2017-05-29 14:24:09 -04:00
d , c = ld , lc
ld , lc = delta [ i - 1 ] , coords [ i - 1 ]
2017-05-29 16:13:46 -04:00
for j in ( 0 , 1 ) : # For X and for Y
2017-07-26 14:13:18 -07:00
cj = c [ j ]
dj = d [ j ]
lcj = lc [ j ]
ldj = ld [ j ]
ncj = nc [ j ]
ndj = nd [ j ]
if lcj < = ncj :
c1 , c2 = lcj , ncj
d1 , d2 = ldj , ndj
else :
c1 , c2 = ncj , lcj
d1 , d2 = ndj , ldj
2017-05-29 16:13:46 -04:00
# If coordinate for current point is between coordinate of adjacent
# points on the two sides, but the delta for current point is NOT
# between delta for those adjacent points (considering tolerance
# allowance), then there is no way that current point can be IUP-ed.
# Mark it forced.
2017-07-26 14:13:18 -07:00
force = False
if c1 < = cj < = c2 :
if not ( min ( d1 , d2 ) - tolerance < = dj < = max ( d1 , d2 ) + tolerance ) :
force = True
else : # cj < c1 or c2 < cj
if c1 == c2 :
if d1 == d2 :
if abs ( dj - d1 ) > tolerance :
force = True
else :
if abs ( dj ) > tolerance :
# Disabled the following because the "d1 == d2" does
# check does not take tolerance into consideration...
pass # force = True
elif d1 != d2 :
if cj < c1 :
if dj != d1 and ( ( dj - tolerance < d1 ) != ( d1 < d2 ) ) :
force = True
else : # c2 < cj
if d2 != dj and ( ( d2 < dj + tolerance ) != ( d1 < d2 ) ) :
force = True
if force :
2017-05-29 14:24:09 -04:00
forced . add ( i )
break
nd , nc = d , c
2017-05-31 19:06:42 -04:00
return forced
2017-07-22 18:43:03 -07:00
def _iup_contour_optimize_dp ( delta , coords , forced = { } , tolerance = 0 , lookback = None ) :
2017-05-31 19:20:53 -04:00
""" Straightforward Dynamic-Programming. For each index i, find least-costly encoding of
points i to n - 1 where i is explicitly encoded . We find this by considering all next
explicit points j and check whether interpolation can fill points between i and j .
2017-05-31 19:06:42 -04:00
2017-05-31 19:20:53 -04:00
Note that solution always encodes last point explicitly . Higher - level is responsible
for removing that restriction .
2017-05-31 19:06:42 -04:00
2017-05-31 19:20:53 -04:00
As major speedup , we stop looking further whenever we see a " forced " point . """
2017-05-31 19:06:42 -04:00
2017-05-31 19:20:53 -04:00
n = len ( delta )
2017-07-22 18:43:03 -07:00
if lookback is None :
lookback = n
2017-05-31 19:14:17 -04:00
costs = { - 1 : 0 }
chain = { - 1 : None }
2017-05-31 19:20:53 -04:00
for i in range ( 0 , n ) :
2017-05-31 19:11:30 -04:00
best_cost = costs [ i - 1 ] + 1
2017-05-28 23:40:24 -07:00
2017-05-29 15:59:29 -04:00
costs [ i ] = best_cost
2017-05-31 19:11:30 -04:00
chain [ i ] = i - 1
2017-05-28 23:40:24 -07:00
2017-05-31 19:11:30 -04:00
if i - 1 in forced :
2017-05-29 15:29:20 -04:00
continue
2017-07-22 18:43:03 -07:00
for j in range ( i - 2 , max ( i - lookback , - 2 ) , - 1 ) :
2017-05-29 15:29:20 -04:00
cost = costs [ j ] + 1
2017-05-28 23:40:24 -07:00
2017-05-31 19:11:30 -04:00
if cost < best_cost and _all_interpolatable_in_between ( delta , coords , j , i , tolerance ) :
2017-05-29 15:59:29 -04:00
costs [ i ] = best_cost = cost
chain [ i ] = j
2017-05-28 23:40:24 -07:00
2017-05-29 15:29:20 -04:00
if j in forced :
break
2017-05-28 23:40:24 -07:00
2017-07-22 18:31:10 -07:00
return chain , costs
2017-05-31 19:44:35 -04:00
def _rot_list ( l , k ) :
""" Rotate list by k items forward. Ie. item at position 0 will be
at position k in returned list . Negative k is allowed . """
n = len ( l )
k % = n
2017-05-31 20:10:15 -04:00
if not k : return l
2017-05-31 19:44:35 -04:00
return l [ n - k : ] + l [ : n - k ]
def _rot_set ( s , k , n ) :
2017-05-31 20:10:15 -04:00
k % = n
if not k : return s
2017-05-31 19:44:35 -04:00
return { ( v + k ) % n for v in s }
2017-05-08 15:42:57 -06:00
2017-05-31 19:20:53 -04:00
def _iup_contour_optimize ( delta , coords , tolerance = 0. ) :
2017-05-08 15:42:57 -06:00
n = len ( delta )
2017-05-31 19:20:53 -04:00
# Get the easy cases out of the way:
# If all are within tolerance distance of 0, encode nothing:
if all ( abs ( complex ( * p ) ) < = tolerance for p in delta ) :
2017-05-18 13:28:21 -07:00
return [ None ] * n
2017-05-31 19:20:53 -04:00
# If there's exactly one point, return it:
2017-05-18 13:28:21 -07:00
if n == 1 :
return delta
2017-05-31 19:20:53 -04:00
# If all deltas are exactly the same, return just one (the first one):
2017-05-08 15:42:57 -06:00
d0 = delta [ 0 ]
if all ( d0 == d for d in delta ) :
return [ d0 ] + [ None ] * ( n - 1 )
2017-05-31 19:20:53 -04:00
# Else, solve the general problem using Dynamic Programming.
forced = _iup_contour_bound_forced_set ( delta , coords , tolerance )
2017-05-31 19:44:35 -04:00
# The _iup_contour_optimize_dp() routine returns the optimal encoding
# solution given the constraint that the last point is always encoded.
2017-07-22 18:31:10 -07:00
# To remove this constraint, we use two different methods, depending on
2017-05-31 19:44:35 -04:00
# whether forced set is non-empty or not:
if forced :
# Forced set is non-empty: rotate the contour start point
# such that the last point in the list is a forced point.
k = ( n - 1 ) - max ( forced )
assert k > = 0
2017-07-22 18:31:10 -07:00
delta = _rot_list ( delta , k )
coords = _rot_list ( coords , k )
forced = _rot_set ( forced , k , n )
chain , costs = _iup_contour_optimize_dp ( delta , coords , forced , tolerance )
# Assemble solution.
solution = set ( )
i = n - 1
while i is not None :
solution . add ( i )
i = chain [ i ]
assert forced < = solution , ( forced , solution )
delta = [ delta [ i ] if i in solution else None for i in range ( n ) ]
delta = _rot_list ( delta , - k )
2017-05-31 19:44:35 -04:00
else :
2017-07-22 18:48:23 -07:00
# Repeat the contour an extra time, solve the 2*n case, then look for solutions of the
# circular n-length problem in the solution for 2*n linear case. I cannot prove that
# this always produces the optimal solution...
2017-07-22 18:43:03 -07:00
chain , costs = _iup_contour_optimize_dp ( delta + delta , coords + coords , forced , tolerance , n )
2017-07-22 18:31:10 -07:00
best_sol , best_cost = None , n + 1
for start in range ( n - 1 , 2 * n - 1 ) :
# Assemble solution.
solution = set ( )
i = start
while i > start - n :
solution . add ( i % n )
i = chain [ i ]
if i == start - n :
cost = costs [ start ] - costs [ start - n ]
if cost < = best_cost :
best_sol , best_cost = solution , cost
delta = [ delta [ i ] if i in best_sol else None for i in range ( n ) ]
2017-05-18 15:49:57 -07:00
2017-05-08 15:42:57 -06:00
return delta
2017-05-31 19:06:42 -04:00
def _iup_delta_optimize ( delta , coords , ends , tolerance = 0. ) :
2017-05-18 13:28:52 -07:00
assert sorted ( ends ) == ends and len ( coords ) == ( ends [ - 1 ] + 1 if ends else 0 ) + 4
2017-05-08 15:42:57 -06:00
n = len ( coords )
ends = ends + [ n - 4 , n - 3 , n - 2 , n - 1 ]
out = [ ]
start = 0
for end in ends :
2017-05-31 19:06:42 -04:00
contour = _iup_contour_optimize ( delta [ start : end + 1 ] , coords [ start : end + 1 ] , tolerance )
2017-05-28 23:40:24 -07:00
assert len ( contour ) == end - start + 1
2017-05-08 15:42:57 -06:00
out . extend ( contour )
start = end + 1
2017-05-18 15:49:57 -07:00
return out
2017-05-08 15:42:57 -06:00
2017-05-28 23:40:24 -07:00
def _add_gvar ( font , model , master_ttfs , tolerance = 0.5 , optimize = True ) :
2016-08-09 20:53:19 -07:00
2017-05-18 16:02:58 -07:00
assert tolerance > = 0
2016-08-09 20:53:19 -07:00
2017-01-06 20:22:45 +01:00
log . info ( " Generating gvar " )
2016-04-14 23:55:11 -07:00
assert " gvar " not in font
2016-07-01 15:31:00 -07:00
gvar = font [ " gvar " ] = newTable ( ' gvar ' )
2016-04-14 18:27:44 -07:00
gvar . version = 1
gvar . reserved = 0
gvar . variations = { }
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 ) ) :
2017-05-24 09:49:37 +01:00
log . warning ( " 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
2016-07-01 15:31:00 -07:00
# Update gvar
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 )
2017-05-08 15:42:57 -06:00
# Prepare for IUP optimization
origCoords = deltas [ 0 ]
endPts = control [ 1 ] if control [ 0 ] > = 1 else list ( range ( len ( control [ 1 ] ) ) )
2016-07-01 15:31:00 -07:00
for i , ( delta , support ) in enumerate ( zip ( deltas [ 1 : ] , supports [ 1 : ] ) ) :
2017-05-18 16:02:58 -07:00
if all ( abs ( v ) < = tolerance for v in delta . array ) :
2017-04-03 18:13:54 +02:00
continue
2017-01-04 12:41:55 +01:00
var = TupleVariation ( support , delta )
2017-05-18 15:49:57 -07:00
if optimize :
2017-05-31 19:06:42 -04:00
delta_opt = _iup_delta_optimize ( delta , origCoords , endPts , tolerance = tolerance )
2017-05-18 15:49:57 -07:00
2017-05-18 16:08:36 -07:00
if None in delta_opt :
2017-05-18 15:49:57 -07:00
# Use "optimized" version only if smaller...
var_opt = TupleVariation ( support , delta_opt )
2017-05-18 16:08:36 -07:00
2017-05-18 15:49:57 -07:00
axis_tags = sorted ( support . keys ( ) ) # Shouldn't matter that this is different from fvar...?
tupleData , auxData = var . compile ( axis_tags , [ ] , None )
unoptimized_len = len ( tupleData ) + len ( auxData )
tupleData , auxData = var_opt . compile ( axis_tags , [ ] , None )
optimized_len = len ( tupleData ) + len ( auxData )
2017-05-18 16:08:36 -07:00
2017-05-18 15:49:57 -07:00
if optimized_len < unoptimized_len :
var = var_opt
2016-04-15 08:56:04 -07:00
gvar . variations [ glyph ] . append ( var )
2016-04-14 18:27:44 -07:00
2016-09-08 09:17:45 -07:00
def _add_HVAR ( font , model , master_ttfs , axisTags ) :
2016-07-01 15:31:00 -07:00
2017-01-06 20:22:45 +01:00
log . info ( " Generating HVAR " )
2016-08-09 20:53:19 -07:00
hAdvanceDeltas = { }
metricses = [ m [ " hmtx " ] . metrics for m in master_ttfs ]
for glyph in font . getGlyphOrder ( ) :
hAdvances = [ metrics [ glyph ] [ 0 ] for metrics in metricses ]
2016-08-15 16:25:35 -07:00
# TODO move round somewhere else?
hAdvanceDeltas [ glyph ] = tuple ( round ( d ) for d in model . getDeltas ( hAdvances ) [ 1 : ] )
2016-07-01 15:31:00 -07:00
# We only support the direct mapping right now.
supports = model . supports [ 1 : ]
2016-09-08 09:17:45 -07:00
varTupleList = builder . buildVarRegionList ( supports , axisTags )
2016-07-01 15:31:00 -07:00
varTupleIndexes = list ( range ( len ( supports ) ) )
n = len ( supports )
items = [ ]
zeroes = [ 0 ] * n
for glyphName in font . getGlyphOrder ( ) :
items . append ( hAdvanceDeltas . get ( glyphName , zeroes ) )
while items and items [ - 1 ] is zeroes :
del items [ - 1 ]
2016-08-11 01:02:52 -07:00
2016-08-11 01:35:56 -07:00
advanceMapping = None
# Add indirect mapping to save on duplicates
uniq = set ( items )
# TODO Improve heuristic
if ( len ( items ) - len ( uniq ) ) * len ( varTupleIndexes ) > len ( items ) :
newItems = sorted ( uniq )
mapper = { v : i for i , v in enumerate ( newItems ) }
mapping = [ mapper [ item ] for item in items ]
2016-08-11 21:42:31 -07:00
while len ( mapping ) > 1 and mapping [ - 1 ] == mapping [ - 2 ] :
del mapping [ - 1 ]
2016-08-11 01:35:56 -07:00
advanceMapping = builder . buildVarIdxMap ( mapping )
items = newItems
del mapper , mapping , newItems
del uniq
2016-08-11 01:02:52 -07:00
2016-08-10 03:15:38 -07:00
varData = builder . buildVarData ( varTupleIndexes , items )
varStore = builder . buildVarStore ( varTupleList , [ varData ] )
2016-07-01 15:31:00 -07:00
assert " HVAR " not in font
HVAR = font [ " HVAR " ] = newTable ( ' HVAR ' )
hvar = HVAR . table = ot . HVAR ( )
2016-09-04 20:58:46 -07:00
hvar . Version = 0x00010000
2016-07-01 15:31:00 -07:00
hvar . VarStore = varStore
2016-08-11 01:35:56 -07:00
hvar . AdvWidthMap = advanceMapping
hvar . LsbMap = hvar . RsbMap = None
2016-08-10 01:17:45 -07:00
2017-04-10 21:01:00 +02:00
_MVAR_entries = {
' hasc ' : ( ' OS/2 ' , ' sTypoAscender ' ) , # horizontal ascender
' hdsc ' : ( ' OS/2 ' , ' sTypoDescender ' ) , # horizontal descender
' hlgp ' : ( ' OS/2 ' , ' sTypoLineGap ' ) , # horizontal line gap
' hcla ' : ( ' OS/2 ' , ' usWinAscent ' ) , # horizontal clipping ascent
' hcld ' : ( ' OS/2 ' , ' usWinDescent ' ) , # horizontal clipping descent
' vasc ' : ( ' vhea ' , ' ascent ' ) , # vertical ascender
' vdsc ' : ( ' vhea ' , ' descent ' ) , # vertical descender
' vlgp ' : ( ' vhea ' , ' lineGap ' ) , # vertical line gap
' hcrs ' : ( ' hhea ' , ' caretSlopeRise ' ) , # horizontal caret rise
' hcrn ' : ( ' hhea ' , ' caretSlopeRun ' ) , # horizontal caret run
' hcof ' : ( ' hhea ' , ' caretOffset ' ) , # horizontal caret offset
' vcrs ' : ( ' vhea ' , ' caretSlopeRise ' ) , # vertical caret rise
' vcrn ' : ( ' vhea ' , ' caretSlopeRun ' ) , # vertical caret run
' vcof ' : ( ' vhea ' , ' caretOffset ' ) , # vertical caret offset
' xhgt ' : ( ' OS/2 ' , ' sxHeight ' ) , # x height
' cpht ' : ( ' OS/2 ' , ' sCapHeight ' ) , # cap height
' sbxs ' : ( ' OS/2 ' , ' ySubscriptXSize ' ) , # subscript em x size
' sbys ' : ( ' OS/2 ' , ' ySubscriptYSize ' ) , # subscript em y size
' sbxo ' : ( ' OS/2 ' , ' ySubscriptXOffset ' ) , # subscript em x offset
' sbyo ' : ( ' OS/2 ' , ' ySubscriptYOffset ' ) , # subscript em y offset
' spxs ' : ( ' OS/2 ' , ' ySuperscriptXSize ' ) , # superscript em x size
' spys ' : ( ' OS/2 ' , ' ySuperscriptYSize ' ) , # superscript em y size
' spxo ' : ( ' OS/2 ' , ' ySuperscriptXOffset ' ) , # superscript em x offset
' spyo ' : ( ' OS/2 ' , ' ySuperscriptYOffset ' ) , # superscript em y offset
' strs ' : ( ' OS/2 ' , ' yStrikeoutSize ' ) , # strikeout size
' stro ' : ( ' OS/2 ' , ' yStrikeoutPosition ' ) , # strikeout offset
' unds ' : ( ' post ' , ' underlineThickness ' ) , # underline size
' undo ' : ( ' post ' , ' underlinePosition ' ) , # underline offset
#'gsp0': ('gasp', 'gaspRange[0].rangeMaxPPEM'), # gaspRange[0]
#'gsp1': ('gasp', 'gaspRange[1].rangeMaxPPEM'), # gaspRange[1]
#'gsp2': ('gasp', 'gaspRange[2].rangeMaxPPEM'), # gaspRange[2]
#'gsp3': ('gasp', 'gaspRange[3].rangeMaxPPEM'), # gaspRange[3]
#'gsp4': ('gasp', 'gaspRange[4].rangeMaxPPEM'), # gaspRange[4]
#'gsp5': ('gasp', 'gaspRange[5].rangeMaxPPEM'), # gaspRange[5]
#'gsp6': ('gasp', 'gaspRange[6].rangeMaxPPEM'), # gaspRange[6]
#'gsp7': ('gasp', 'gaspRange[7].rangeMaxPPEM'), # gaspRange[7]
#'gsp8': ('gasp', 'gaspRange[8].rangeMaxPPEM'), # gaspRange[8]
#'gsp9': ('gasp', 'gaspRange[9].rangeMaxPPEM'), # gaspRange[9]
}
def _add_MVAR ( font , model , master_ttfs , axisTags ) :
log . info ( " Generating MVAR " )
store_builder = builder . OnlineVarStoreBuilder ( axisTags )
store_builder . setModel ( model )
records = [ ]
lastTableTag = None
fontTable = None
tables = None
for tag , ( tableTag , itemName ) in sorted ( _MVAR_entries . items ( ) , key = lambda kv : kv [ 1 ] ) :
if tableTag != lastTableTag :
tables = fontTable = None
if tableTag in font :
# TODO Check all masters have same table set?
fontTable = font [ tableTag ]
tables = [ master [ tableTag ] for master in master_ttfs ]
lastTableTag = tableTag
if tables is None :
continue
# TODO support gasp entries
master_values = [ getattr ( table , itemName ) for table in tables ]
if _all_equal ( master_values ) :
base , varIdx = master_values [ 0 ] , None
else :
base , varIdx = store_builder . storeMasters ( master_values )
setattr ( fontTable , itemName , base )
if varIdx is None :
continue
log . info ( ' %s : %s . %s %s ' , tag , tableTag , itemName , master_values )
rec = ot . MetricsValueRecord ( )
rec . ValueTag = tag
rec . VarIdx = varIdx
records . append ( rec )
2017-06-22 15:15:58 -07:00
assert " MVAR " not in font
2017-06-20 15:30:28 -07:00
if records :
2017-06-20 15:01:23 -07:00
MVAR = font [ " MVAR " ] = newTable ( ' MVAR ' )
mvar = MVAR . table = ot . MVAR ( )
mvar . Version = 0x00010000
mvar . Reserved = 0
mvar . VarStore = store_builder . finish ( )
mvar . ValueRecord = sorted ( records , key = lambda r : r . ValueTag )
2017-04-10 21:01:00 +02:00
2016-07-01 15:31:00 -07:00
2017-05-22 19:40:20 -07:00
def _merge_OTL ( font , model , master_fonts , axisTags ) :
2016-08-13 03:09:11 -07:00
2017-01-06 20:22:45 +01:00
log . info ( " Merging OpenType Layout tables " )
2016-10-12 16:11:20 -07:00
merger = VariationMerger ( model , axisTags , font )
2016-08-13 03:09:11 -07:00
2017-05-22 19:40:20 -07:00
merger . mergeTables ( font , master_fonts , [ ' GPOS ' ] )
2016-09-07 17:11:21 -07:00
store = merger . store_builder . finish ( )
2016-09-27 18:41:51 +02:00
try :
GDEF = font [ ' GDEF ' ] . table
assert GDEF . Version < = 0x00010002
except KeyError :
font [ ' GDEF ' ] = newTable ( ' GDEF ' )
GDEFTable = font [ " GDEF " ] = newTable ( ' GDEF ' )
GDEF = GDEFTable . table = ot . GDEF ( )
2016-09-07 17:11:21 -07:00
GDEF . Version = 0x00010003
GDEF . VarStore = store
2016-08-15 11:14:52 -07:00
2016-08-13 03:09:11 -07:00
2017-05-22 19:40:20 -07:00
def load_designspace ( designspace_filename ) :
2016-04-14 18:27:44 -07:00
2017-02-26 07:49:44 -08:00
ds = designspace . load ( designspace_filename )
2017-04-12 15:39:05 -07:00
axes = ds . get ( ' axes ' )
masters = ds . get ( ' sources ' )
if not masters :
raise VarLibError ( " no sources found in .designspace " )
instances = ds . get ( ' instances ' , [ ] )
2017-02-26 07:49:44 -08:00
2017-02-25 20:53:48 -08:00
standard_axis_map = OrderedDict ( [
2017-04-12 15:57:03 -07:00
( ' weight ' , ( ' wght ' , { ' en ' : ' Weight ' } ) ) ,
( ' width ' , ( ' wdth ' , { ' en ' : ' Width ' } ) ) ,
( ' slant ' , ( ' slnt ' , { ' en ' : ' Slant ' } ) ) ,
( ' optical ' , ( ' opsz ' , { ' en ' : ' Optical Size ' } ) ) ,
2017-02-25 20:53:48 -08:00
] )
2017-04-12 21:52:29 -07:00
2017-04-12 17:14:22 -07:00
# Setup axes
class DesignspaceAxis ( object ) :
2017-04-12 21:52:29 -07:00
2017-05-22 19:40:20 -07:00
def __repr__ ( self ) :
return repr ( self . __dict__ )
2017-04-12 21:52:29 -07:00
@staticmethod
def _map ( v , map ) :
keys = map . keys ( )
if not keys :
return v
if v in keys :
return map [ v ]
k = min ( keys )
if v < k :
return v + map [ k ] - k
k = max ( keys )
if v > k :
return v + map [ k ] - k
# Interpolate
a = max ( k for k in keys if k < v )
b = min ( k for k in keys if k > v )
va = map [ a ]
vb = map [ b ]
return va + ( vb - va ) * ( v - a ) / ( b - a )
def map_forward ( self , v ) :
if self . map is None : return v
return self . _map ( v , self . map )
def map_backward ( self , v ) :
if self . map is None : return v
map = { v : k for k , v in self . map . items ( ) }
return self . _map ( v , map )
2017-04-12 17:14:22 -07:00
axis_objects = OrderedDict ( )
2017-04-12 15:43:13 -07:00
if axes is not None :
2017-04-12 17:14:22 -07:00
for axis_dict in axes :
axis_name = axis_dict . get ( ' name ' )
if not axis_name :
axis_name = axis_dict [ ' name ' ] = axis_dict [ ' tag ' ]
2017-04-12 21:52:29 -07:00
if ' map ' not in axis_dict :
axis_dict [ ' map ' ] = None
else :
axis_dict [ ' map ' ] = { m [ ' input ' ] : m [ ' output ' ] for m in axis_dict [ ' map ' ] }
2017-04-12 17:14:22 -07:00
2017-02-25 20:53:48 -08:00
if axis_name in standard_axis_map :
2017-04-12 17:14:22 -07:00
if ' tag ' not in axis_dict :
axis_dict [ ' tag ' ] = standard_axis_map [ axis_name ] [ 0 ]
if ' labelname ' not in axis_dict :
axis_dict [ ' labelname ' ] = standard_axis_map [ axis_name ] [ 1 ] . copy ( )
axis = DesignspaceAxis ( )
2017-04-12 21:52:29 -07:00
for item in [ ' name ' , ' tag ' , ' labelname ' , ' minimum ' , ' default ' , ' maximum ' , ' map ' ] :
2017-04-12 17:14:22 -07:00
assert item in axis_dict , ' Axis does not have " %s " ' % item
axis . __dict__ = axis_dict
axis_objects [ axis_name ] = axis
2017-02-22 21:22:34 -08:00
else :
2017-04-12 17:14:22 -07:00
# No <axes> element. Guess things...
base_idx = None
for i , m in enumerate ( masters ) :
if ' info ' in m and m [ ' info ' ] [ ' copy ' ] :
assert base_idx is None
base_idx = i
assert base_idx is not None , " Cannot find ' base ' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document. "
master_locs = [ o [ ' location ' ] for o in masters ]
base_loc = master_locs [ base_idx ]
axis_names = set ( base_loc . keys ( ) )
assert all ( name in standard_axis_map for name in axis_names ) , " Non-standard axis found and there exist no <axes> element. "
for name , ( tag , labelname ) in standard_axis_map . items ( ) :
if name not in axis_names :
continue
2017-02-25 20:53:48 -08:00
2017-04-12 21:47:48 -07:00
axis = DesignspaceAxis ( )
2017-04-12 17:14:22 -07:00
axis . name = name
axis . tag = tag
axis . labelname = labelname . copy ( )
axis . default = base_loc [ name ]
axis . minimum = min ( m [ name ] for m in master_locs if name in m )
axis . maximum = max ( m [ name ] for m in master_locs if name in m )
2017-04-12 21:52:29 -07:00
axis . map = None
# TODO Fill in weight / width mapping from OS/2 table? Need loading fonts...
2017-04-12 17:14:22 -07:00
axis_objects [ name ] = axis
del base_idx , base_loc , axis_names , master_locs
axes = axis_objects
del axis_objects
2017-05-22 19:40:20 -07:00
log . info ( " Axes: \n %s " , pformat ( axes ) )
2017-04-12 17:14:22 -07:00
2017-04-12 17:21:24 -07:00
# Check all master and instance locations are valid and fill in defaults
for obj in masters + instances :
obj_name = obj . get ( ' name ' , obj . get ( ' stylename ' , ' ' ) )
loc = obj [ ' location ' ]
2017-05-18 13:08:50 -07:00
for axis_name in loc . keys ( ) :
assert axis_name in axes , " Location axis ' %s ' unknown for ' %s ' . " % ( axis_name , obj_name )
2017-04-12 17:21:24 -07:00
for axis_name , axis in axes . items ( ) :
if axis_name not in loc :
loc [ axis_name ] = axis . default
else :
2017-04-12 21:52:29 -07:00
v = axis . map_backward ( loc [ axis_name ] )
2017-05-18 13:08:50 -07:00
assert axis . minimum < = v < = axis . maximum , " Location for axis ' %s ' (mapped to %s ) out of range for ' %s ' [ %s .. %s ] " % ( axis_name , v , obj_name , axis . minimum , axis . maximum )
2016-04-14 18:27:44 -07:00
2017-04-12 17:14:22 -07:00
# Normalize master locations
2017-04-12 21:52:29 -07:00
2017-05-22 19:40:20 -07:00
normalized_master_locs = [ o [ ' location ' ] for o in masters ]
log . info ( " Internal master locations: \n %s " , pformat ( normalized_master_locs ) )
2017-04-12 21:52:29 -07:00
2017-04-12 21:56:49 -07:00
# TODO This mapping should ideally be moved closer to logic in _add_fvar_avar
2017-05-22 19:40:20 -07:00
internal_axis_supports = { }
2017-04-12 21:52:29 -07:00
for axis in axes . values ( ) :
triple = ( axis . minimum , axis . default , axis . maximum )
2017-05-22 19:40:20 -07:00
internal_axis_supports [ axis . name ] = [ axis . map_forward ( v ) for v in triple ]
log . info ( " Internal axis supports: \n %s " , pformat ( internal_axis_supports ) )
2017-04-12 21:52:29 -07:00
2017-05-22 19:40:20 -07:00
normalized_master_locs = [ models . normalizeLocation ( m , internal_axis_supports ) for m in normalized_master_locs ]
log . info ( " Normalized master locations: \n %s " , pformat ( normalized_master_locs ) )
2016-04-14 18:27:44 -07:00
2017-04-12 21:52:29 -07:00
2017-04-12 17:14:22 -07:00
# Find base master
2017-04-12 16:08:01 -07:00
base_idx = None
2017-05-22 19:40:20 -07:00
for i , m in enumerate ( normalized_master_locs ) :
2017-04-12 17:14:22 -07:00
if all ( v == 0 for v in m . values ( ) ) :
2017-04-12 16:08:01 -07:00
assert base_idx is None
base_idx = i
2017-04-12 17:14:22 -07:00
assert base_idx is not None , " Base master not found; no master at default location? "
2017-04-12 16:08:01 -07:00
log . info ( " Index of base master: %s " , base_idx )
2017-05-22 19:40:20 -07:00
return axes , internal_axis_supports , base_idx , normalized_master_locs , masters , instances
def build ( designspace_filename , master_finder = lambda s : s ) :
"""
Build variation font from a designspace file .
If master_finder is set , it should be a callable that takes master
filename as found in designspace file and map it to master font
binary as to be opened ( eg . . ttf or . otf ) .
"""
axes , internal_axis_supports , base_idx , normalized_master_locs , masters , instances = load_designspace ( designspace_filename )
2016-09-08 09:17:45 -07:00
2017-04-12 17:14:22 -07:00
log . info ( " Building variable font " )
log . info ( " Loading master fonts " )
basedir = os . path . dirname ( designspace_filename )
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 ]
# Reload base font as target font
vf = TTFont ( master_ttfs [ base_idx ] )
2016-09-05 19:14:40 -07:00
2017-04-12 17:14:22 -07:00
# TODO append masters as named-instances as well; needs .designspace change.
2017-04-12 21:52:29 -07:00
fvar , avar = _add_fvar_avar ( vf , axes , instances )
2016-09-08 09:17:45 -07:00
del instances
2017-04-12 17:14:22 -07:00
# Map from axis names to axis tags...
2017-05-22 19:40:20 -07:00
normalized_master_locs = [ { axes [ k ] . tag : v for k , v in loc . items ( ) } for loc in normalized_master_locs ]
2017-04-12 17:14:22 -07:00
#del axes
# From here on, we use fvar axes only
2016-09-08 09:17:45 -07:00
axisTags = [ axis . axisTag for axis in fvar . axes ]
2016-09-07 14:23:22 -07:00
2016-08-15 16:29:21 -07:00
# Assume single-model for now.
2017-05-22 19:40:20 -07:00
model = models . VariationModel ( normalized_master_locs )
2016-08-15 16:29:21 -07:00
assert 0 == model . mapping [ base_idx ]
2016-08-09 20:53:19 -07:00
2017-01-06 20:22:45 +01:00
log . info ( " Building variations tables " )
2017-04-12 17:14:22 -07:00
_add_MVAR ( vf , model , master_fonts , axisTags )
if ' glyf ' in vf :
_add_gvar ( vf , model , master_fonts )
_add_HVAR ( vf , model , master_fonts , axisTags )
2017-05-22 19:40:20 -07:00
_merge_OTL ( vf , model , master_fonts , axisTags )
2016-04-14 18:27:44 -07:00
2017-04-12 17:14:22 -07:00
return vf , model , master_ttfs
2016-09-02 17:10:16 -07:00
def main ( args = None ) :
2017-01-06 20:22:45 +01:00
from argparse import ArgumentParser
from fontTools import configLogger
2016-09-02 17:10:16 -07:00
2016-11-03 10:59:00 +00:00
parser = ArgumentParser ( prog = ' varLib ' )
parser . add_argument ( ' designspace ' )
2016-11-02 20:54:50 -07:00
options = parser . parse_args ( args )
2016-09-02 17:10:16 -07:00
2017-01-06 20:22:45 +01:00
# TODO: allow user to configure logging via command-line options
configLogger ( level = " INFO " )
2016-11-03 10:59:00 +00:00
designspace_filename = options . designspace
2016-09-02 17:10:16 -07:00
finder = lambda s : s . replace ( ' master_ufo ' , ' master_ttf_interpolatable ' ) . replace ( ' .ufo ' , ' .ttf ' )
2016-12-08 20:48:08 -08:00
outfile = os . path . splitext ( designspace_filename ) [ 0 ] + ' -VF.ttf '
2016-09-02 17:10:16 -07:00
2017-04-12 17:14:22 -07:00
vf , model , master_ttfs = build ( designspace_filename , finder )
2016-09-02 17:10:16 -07:00
2017-01-06 20:22:45 +01:00
log . info ( " Saving variation font %s " , outfile )
2017-04-12 17:14:22 -07:00
vf . 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 :
2017-01-11 12:10:58 +00:00
sys . exit ( main ( ) )
2017-05-22 19:40:20 -07:00
import doctest
2016-04-13 23:51:54 -07:00
sys . exit ( doctest . testmod ( ) . failed )