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-01-04 12:41:55 +01:00
from fontTools . ttLib . tables . _g_v_a_r 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-18 16:48:02 -07:00
import warnings
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
#
# Move to fvar table proper?
2017-04-12 17:14:22 -07:00
def _add_fvar ( 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 .
"""
2016-04-14 18:27:44 -07:00
assert " fvar " not in font
2016-07-01 15:31:00 -07:00
font [ ' fvar ' ] = 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
2017-04-12 15:57:03 -07:00
# TODO: Add all languages: https://github.com/fonttools/fonttools/issues/921
2017-04-12 17:14:22 -07:00
axis . axisNameID = nameTable . addName ( tounicode ( a . labelname [ ' en ' ] ) )
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 17:14:22 -07:00
inst . coordinates = { axes [ k ] . tag : v for k , v in coordinates . items ( ) }
2016-04-14 18:27:44 -07:00
fvar . instances . append ( inst )
2016-09-08 09:17:45 -07:00
return fvar
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 ] )
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 )
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-04-03 18:13:54 +02:00
def _add_gvar ( font , model , master_ttfs , tolerance = .5 ) :
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 ) ) :
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
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 )
2016-07-01 15:31:00 -07:00
for i , ( delta , support ) in enumerate ( zip ( deltas [ 1 : ] , supports [ 1 : ] ) ) :
2017-04-03 18:04:26 +02:00
if not delta :
continue
2017-04-03 18:13:54 +02:00
if tolerance and max ( abs ( delta ) . array ) < = tolerance :
continue
2017-01-04 12:41:55 +01:00
var = TupleVariation ( support , delta )
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 )
assert " MVAR " not in font
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 )
2016-07-01 15:31:00 -07:00
2016-09-08 09:17:45 -07:00
def _merge_OTL ( font , model , master_fonts , axisTags , base_idx ) :
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-01-25 20:11:35 -08:00
merger . mergeTables ( font , master_fonts , axisTags , base_idx , [ ' 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-04-12 15:43:13 -07:00
def build ( designspace_filename , master_finder = lambda s : s ) :
2016-09-02 17:10:16 -07:00
"""
Build variation font from a designspace file .
2016-04-14 00:31:17 -07:00
2016-09-02 17:10:16 -07:00
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 ) .
"""
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 17:14:22 -07:00
# Setup axes
class DesignspaceAxis ( object ) :
pass
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-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 ( )
for item in [ ' name ' , ' tag ' , ' labelname ' , ' minimum ' , ' default ' , ' maximum ' ] :
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 )
axis_objects [ name ] = axis
del base_idx , base_loc , axis_names , master_locs
axes = axis_objects
del axis_objects
axis_supports = { }
for axis in axes . values ( ) :
axis_supports [ axis . name ] = ( axis . minimum , axis . default , axis . maximum )
log . info ( " Axis supports: \n %s " , pformat ( axis_supports ) )
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 ' ]
for name in loc . keys ( ) :
assert name in axes , " Location axis ' %s ' unknown for ' %s ' . " % ( name , obj_name )
for axis_name , axis in axes . items ( ) :
if axis_name not in loc :
loc [ axis_name ] = axis . default
else :
v = loc [ axis_name ]
assert axis . minimum < = v < = axis . maximum , " Location for axis ' %s ' ( %s ) out of range for ' %s ' " % ( name , v , obj_name )
2016-04-14 18:27:44 -07:00
2016-08-17 17:08:58 -07:00
master_locs = [ o [ ' location ' ] for o in masters ]
2017-04-12 17:14:22 -07:00
log . info ( " Master locations: \n %s " , pformat ( master_locs ) )
2016-04-14 18:27:44 -07:00
2017-04-12 17:14:22 -07:00
# Normalize master locations
master_locs = [ models . normalizeLocation ( m , axis_supports ) for m in master_locs ]
log . info ( " Normalized master locations: \n %s " , pformat ( master_locs ) )
2016-04-14 18:27:44 -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-04-12 17:14:22 -07:00
for i , m in enumerate ( master_locs ) :
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 )
2016-09-08 09:17:45 -07:00
2016-04-14 18:27:44 -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.
fvar = _add_fvar ( vf , axes , instances )
2016-08-15 16:29:21 -07:00
2016-09-07 14:23:22 -07:00
# TODO Clean this up.
2016-09-08 09:17:45 -07:00
del instances
2017-04-12 17:14:22 -07:00
del axis_supports
# Map from axis names to axis tags...
master_locs = [ { axes [ k ] . tag : v for k , v in loc . items ( ) } for loc in master_locs ]
#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.
model = models . VariationModel ( master_locs )
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 )
_merge_OTL ( vf , model , master_fonts , axisTags , base_idx )
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 ( ) )
2016-04-13 23:51:54 -07:00
import doctest , sys
sys . exit ( doctest . testmod ( ) . failed )