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 *
2017-10-10 12:43:15 +02:00
from fontTools . misc . arrayTools import Vector
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-10-12 10:40:40 +02:00
from fontTools . ttLib . tables . ttProgram import Program
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
2018-02-21 01:22:59 -08:00
from fontTools . ttLib . tables . otBase import OTTableWriter
2017-10-20 11:32:15 -04:00
from fontTools . varLib import builder , designspace , models , varStore
2017-04-10 21:01:00 +02:00
from fontTools . varLib . merger import VariationMerger , _all_equal
2017-10-22 12:19:24 +01:00
from fontTools . varLib . mvar import MVAR_ENTRIES
2017-10-15 18:16:01 -04:00
from fontTools . varLib . iup import iup_delta_optimize
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-08-03 12:38:23 +01: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 .
"""
2017-04-12 21:52:29 -07:00
assert axes
assert isinstance ( axes , OrderedDict )
2017-08-03 12:38:23 +01:00
log . info ( " Generating fvar " )
2017-04-12 21:52:29 -07:00
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 )
2017-11-13 21:01:27 -08:00
# TODO Skip axes that have no variation.
2017-04-12 17:14:22 -07:00
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 ( ) }
2017-08-01 04:24:59 +01: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 )
2017-08-03 12:38:23 +01:00
assert " fvar " not in font
font [ ' fvar ' ] = fvar
return fvar
2017-08-03 14:57:02 +01:00
def _add_avar ( font , axes ) :
2017-08-03 12:38:23 +01:00
"""
Add ' avar ' table to font .
axes is an ordered dictionary of DesignspaceAxis objects .
"""
assert axes
assert isinstance ( axes , OrderedDict )
log . info ( " Generating avar " )
2017-04-12 21:52:29 -07:00
avar = newTable ( ' avar ' )
2017-08-03 12:38:23 +01:00
2017-04-12 21:52:29 -07:00
interesting = False
for axis in axes . values ( ) :
2017-08-16 16:30:11 +01:00
# Currently, some rasterizers require that the default value maps
# (-1 to -1, 0 to 0, and 1 to 1) be present for all the segment
# maps, even when the default normalization mapping for the axis
# was not modified.
# https://github.com/googlei18n/fontmake/issues/295
# https://github.com/fonttools/fonttools/issues/1011
# TODO(anthrotype) revert this (and 19c4b37) when issue is fixed
curve = avar . segments [ axis . tag ] = { - 1.0 : - 1.0 , 0.0 : 0.0 , 1.0 : 1.0 }
2017-08-01 12:08:44 +01:00
if not axis . map :
2017-04-12 21:52:29 -07:00
continue
items = sorted ( axis . map . items ( ) )
2017-08-01 12:08:44 +01:00
keys = [ item [ 0 ] for item in items ]
2017-04-12 21:52:29 -07:00
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 ]
2017-08-01 12:08:44 +01:00
if all ( k == v for k , v in zip ( keys , vals ) ) :
continue
interesting = True
2017-04-12 21:52:29 -07:00
curve . update ( zip ( keys , vals ) )
2017-07-28 15:30:40 +01:00
assert 0.0 in curve and curve [ 0.0 ] == 0.0
assert - 1.0 not in curve or curve [ - 1.0 ] == - 1.0
assert + 1.0 not in curve or curve [ + 1.0 ] == + 1.0
2017-08-02 15:12:26 +01:00
# curve.update({-1.0: -1.0, 0.0: 0.0, 1.0: 1.0})
2017-07-28 15:30:40 +01:00
2017-08-03 12:38:23 +01:00
assert " avar " not in font
2017-04-12 21:52:29 -07:00
if not interesting :
log . info ( " No need for avar " )
avar = None
2017-08-03 12:38:23 +01:00
else :
2017-04-12 21:52:29 -07:00
font [ ' avar ' ] = avar
2017-08-03 12:38:23 +01:00
return avar
2016-09-08 09:17:45 -07:00
2017-11-13 21:25:04 -08:00
def _add_stat ( font , axes ) :
2018-04-16 12:35:30 +02:00
# for now we just get the axis tags and nameIDs from the fvar,
# so we can reuse the same nameIDs which were defined in there.
# TODO make use of 'axes' once it adds style attributes info:
# https://github.com/LettError/designSpaceDocument/issues/8
2017-11-13 21:25:04 -08:00
2018-02-05 12:51:01 +00:00
if " STAT " in font :
return
2018-04-16 12:35:30 +02:00
fvarTable = font [ ' fvar ' ]
2017-11-13 21:25:04 -08:00
STAT = font [ " STAT " ] = newTable ( ' STAT ' )
stat = STAT . table = ot . STAT ( )
2018-04-16 12:35:30 +02:00
stat . Version = 0x00010002
2017-11-13 21:25:04 -08:00
axisRecords = [ ]
2018-04-16 12:35:30 +02:00
for i , a in enumerate ( fvarTable . axes ) :
2017-11-13 21:25:04 -08:00
axis = ot . AxisRecord ( )
2018-04-16 12:35:30 +02:00
axis . AxisTag = Tag ( a . axisTag )
axis . AxisNameID = a . axisNameID
2017-11-13 21:25:04 -08:00
axis . AxisOrdering = i
axisRecords . append ( axis )
axisRecordArray = ot . AxisRecordArray ( )
axisRecordArray . Axis = axisRecords
# XXX these should not be hard-coded but computed automatically
stat . DesignAxisRecordSize = 8
stat . DesignAxisCount = len ( axisRecords )
stat . DesignAxisRecord = axisRecordArray
2018-04-16 12:35:30 +02:00
# for the elided fallback name, we default to the base style name.
# TODO make this user-configurable via designspace document
stat . ElidedFallbackNameID = 2
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 )
2018-02-28 15:13:59 +00:00
if horizontalAdvanceWidth < 0 :
# unlikely, but it can happen, see:
# https://github.com/fonttools/fonttools/pull/1198
horizontalAdvanceWidth = 0
2017-03-04 12:54:20 -08:00
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
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-10-15 18:16:01 -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...?
2017-07-20 16:02:15 +02:00
tupleData , auxData , _ = var . compile ( axis_tags , [ ] , None )
2017-05-18 15:49:57 -07:00
unoptimized_len = len ( tupleData ) + len ( auxData )
2017-07-20 16:02:15 +02:00
tupleData , auxData , _ = var_opt . compile ( axis_tags , [ ] , None )
2017-05-18 15:49:57 -07:00
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
2017-10-12 10:40:40 +02:00
def _remove_TTHinting ( font ) :
for tag in ( " cvar " , " cvt " , " fpgm " , " prep " ) :
if tag in font :
del font [ tag ]
for attr in ( " maxTwilightPoints " , " maxStorage " , " maxFunctionDefs " , " maxInstructionDefs " , " maxStackElements " , " maxSizeOfInstructions " ) :
setattr ( font [ " maxp " ] , attr , 0 )
font [ " maxp " ] . maxZones = 1
font [ " glyf " ] . removeHinting ( )
# TODO: Modify gasp table to deactivate gridfitting for all ranges?
2017-10-09 13:08:55 +02:00
2017-10-12 10:40:40 +02:00
def _merge_TTHinting ( font , model , master_ttfs , tolerance = 0.5 ) :
log . info ( " Merging TT hinting " )
2017-10-09 13:08:55 +02:00
assert " cvar " not in font
2017-10-12 10:40:40 +02:00
# Check that the existing hinting is compatible
# fpgm and prep table
for tag in ( " fpgm " , " prep " ) :
all_pgms = [ m [ tag ] . program for m in master_ttfs if tag in m ]
if len ( all_pgms ) == 0 :
continue
if tag in font :
font_pgm = font [ tag ] . program
else :
font_pgm = Program ( )
if any ( pgm != font_pgm for pgm in all_pgms ) :
log . warning ( " Masters have incompatible %s tables, hinting is discarded. " % tag )
_remove_TTHinting ( font )
return
# glyf table
for name , glyph in font [ " glyf " ] . glyphs . items ( ) :
all_pgms = [
m [ " glyf " ] [ name ] . program
for m in master_ttfs
if hasattr ( m [ " glyf " ] [ name ] , " program " )
]
if not any ( all_pgms ) :
continue
glyph . expand ( font [ " glyf " ] )
if hasattr ( glyph , " program " ) :
font_pgm = glyph . program
else :
font_pgm = Program ( )
if any ( pgm != font_pgm for pgm in all_pgms if pgm ) :
log . warning ( " Masters have incompatible glyph programs in glyph ' %s ' , hinting is discarded. " % name )
_remove_TTHinting ( font )
return
# cvt table
all_cvs = [ Vector ( m [ " cvt " ] . values ) for m in master_ttfs if " cvt " in m ]
if len ( all_cvs ) == 0 :
# There is no cvt table to make a cvar table from, we're done here.
return
if len ( all_cvs ) != len ( master_ttfs ) :
log . warning ( " Some masters have no cvt table, hinting is discarded. " )
_remove_TTHinting ( font )
return
num_cvt0 = len ( all_cvs [ 0 ] )
if ( any ( len ( c ) != num_cvt0 for c in all_cvs ) ) :
log . warning ( " Masters have incompatible cvt tables, hinting is discarded. " )
_remove_TTHinting ( font )
return
# We can build the cvar table now.
2017-10-09 13:08:55 +02:00
cvar = font [ " cvar " ] = newTable ( ' cvar ' )
cvar . version = 1
cvar . variations = [ ]
2017-10-12 10:40:40 +02:00
deltas = model . getDeltas ( all_cvs )
2017-10-09 13:08:55 +02:00
supports = model . supports
for i , ( delta , support ) in enumerate ( zip ( deltas [ 1 : ] , supports [ 1 : ] ) ) :
2017-10-22 12:03:52 +01:00
delta = [ round ( d ) for d in delta ]
2017-10-09 17:30:27 +02:00
if all ( abs ( v ) < = tolerance for v in delta ) :
continue
2017-10-09 13:08:55 +02:00
var = TupleVariation ( support , delta )
cvar . variations . append ( var )
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
2018-02-18 21:45:27 -08:00
# Direct mapping
2016-07-01 15:31:00 -07:00
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 = [ ]
for glyphName in font . getGlyphOrder ( ) :
2018-02-18 22:57:03 -08:00
items . append ( hAdvanceDeltas [ glyphName ] )
2016-08-11 01:02:52 -07:00
2018-02-21 01:22:59 -08:00
# Build indirect mapping to save on duplicates, compare both sizes
uniq = list ( set ( items ) )
mapper = { v : i for i , v in enumerate ( uniq ) }
mapping = [ mapper [ item ] for item in items ]
advanceMapping = builder . buildVarIdxMap ( mapping , font . getGlyphOrder ( ) )
2016-08-11 01:02:52 -07:00
2018-02-21 01:22:59 -08:00
# Direct
2016-08-10 03:15:38 -07:00
varData = builder . buildVarData ( varTupleIndexes , items )
2018-02-21 01:22:59 -08:00
directStore = builder . buildVarStore ( varTupleList , [ varData ] )
2016-07-01 15:31:00 -07:00
2018-02-21 01:22:59 -08:00
# Indirect
varData = builder . buildVarData ( varTupleIndexes , uniq )
indirectStore = builder . buildVarStore ( varTupleList , [ varData ] )
mapping = indirectStore . optimize ( )
advanceMapping . mapping = { k : mapping [ v ] for k , v in advanceMapping . mapping . items ( ) }
# Compile both, see which is more compact
writer = OTTableWriter ( )
directStore . compile ( writer , font )
directSize = len ( writer . getAllData ( ) )
writer = OTTableWriter ( )
indirectStore . compile ( writer , font )
advanceMapping . compile ( writer , font )
indirectSize = len ( writer . getAllData ( ) )
use_direct = directSize < indirectSize
# Done; put it all together.
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-08-11 01:35:56 -07:00
hvar . LsbMap = hvar . RsbMap = None
2018-02-21 01:22:59 -08:00
if use_direct :
hvar . VarStore = directStore
hvar . AdvWidthMap = None
else :
hvar . VarStore = indirectStore
hvar . AdvWidthMap = advanceMapping
2016-08-10 01:17:45 -07:00
2017-04-10 21:01:00 +02:00
def _add_MVAR ( font , model , master_ttfs , axisTags ) :
log . info ( " Generating MVAR " )
2017-10-20 11:32:15 -04:00
store_builder = varStore . OnlineVarStoreBuilder ( axisTags )
2017-04-10 21:01:00 +02:00
store_builder . setModel ( model )
records = [ ]
lastTableTag = None
fontTable = None
tables = None
2017-10-22 12:19:24 +01:00
for tag , ( tableTag , itemName ) in sorted ( MVAR_ENTRIES . items ( ) , key = lambda kv : kv [ 1 ] ) :
2017-04-10 21:01:00 +02:00
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 :
2018-02-21 00:55:39 -08:00
store = store_builder . finish ( )
# Optimize
mapping = store . optimize ( )
for rec in records :
rec . VarIdx = mapping [ rec . VarIdx ]
2017-06-20 15:01:23 -07:00
MVAR = font [ " MVAR " ] = newTable ( ' MVAR ' )
mvar = MVAR . table = ot . MVAR ( )
mvar . Version = 0x00010000
mvar . Reserved = 0
2018-02-21 00:55:39 -08:00
mvar . VarStore = store
2017-08-03 17:57:16 +01:00
# XXX these should not be hard-coded but computed automatically
mvar . ValueRecordSize = 8
mvar . ValueRecordCount = len ( records )
2017-06-20 15:01:23 -07:00
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 ' ] )
2018-02-21 01:08:29 -08:00
# TODO Merge GSUB
# TODO Merge GDEF itself!
2016-09-07 17:11:21 -07:00
store = merger . store_builder . finish ( )
2018-02-21 01:08:29 -08:00
if not store . VarData :
return
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
2018-02-21 00:22:14 -08:00
# Optimize
varidx_map = store . optimize ( )
GDEF . remap_device_varidxes ( varidx_map )
2018-02-21 01:08:29 -08:00
if ' GPOS ' in font :
font [ ' GPOS ' ] . table . remap_device_varidxes ( varidx_map )
2018-02-21 00:22:14 -08:00
2016-08-13 03:09:11 -07:00
2017-10-19 13:59:43 -07:00
# Pretty much all of this file should be redesigned and moved inot submodules...
# Such a mess right now, but kludging along...
class _DesignspaceAxis ( object ) :
def __repr__ ( self ) :
return repr ( self . __dict__ )
@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-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
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 ( )
2017-10-19 13:59:43 -07:00
axis = _DesignspaceAxis ( )
2017-09-15 16:49:16 -04:00
for item in [ ' name ' , ' tag ' , ' minimum ' , ' default ' , ' maximum ' , ' map ' ] :
2017-04-12 17:14:22 -07:00
assert item in axis_dict , ' Axis does not have " %s " ' % item
2017-09-15 16:49:16 -04:00
if ' labelname ' not in axis_dict :
axis_dict [ ' labelname ' ] = { ' en ' : axis_name }
2017-04-12 17:14:22 -07:00
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-10-19 13:59:43 -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-08-03 12:38:23 +01: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
2018-03-05 15:32:17 -08:00
def build ( designspace_filename , master_finder = lambda s : s , exclude = [ ] , optimize = True ) :
2017-05-22 19:40:20 -07:00
"""
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-08-03 12:38:23 +01:00
fvar = _add_fvar ( vf , axes , instances )
2018-02-19 17:12:28 -08:00
if ' STAT ' not in exclude :
_add_stat ( vf , axes )
if ' avar ' not in exclude :
_add_avar ( vf , axes )
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-10-25 15:40:14 -06:00
model = models . VariationModel ( normalized_master_locs , axisOrder = axisTags )
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 " )
2018-02-19 17:12:28 -08:00
if ' MVAR ' not in exclude :
_add_MVAR ( vf , model , master_fonts , axisTags )
if ' HVAR ' not in exclude :
_add_HVAR ( vf , model , master_fonts , axisTags )
if ' GDEF ' not in exclude or ' GPOS ' not in exclude :
_merge_OTL ( vf , model , master_fonts , axisTags )
if ' gvar ' not in exclude and ' glyf ' in vf :
2018-03-05 15:32:17 -08:00
_add_gvar ( vf , model , master_fonts , optimize = optimize )
2018-02-19 17:12:28 -08:00
if ' cvar ' not in exclude :
2017-10-12 10:40:40 +02:00
_merge_TTHinting ( vf , model , master_fonts )
2016-04-14 18:27:44 -07:00
2018-02-19 19:16:35 -08:00
for tag in exclude :
if tag in vf :
del vf [ tag ]
2017-04-12 17:14:22 -07:00
return vf , model , master_ttfs
2016-09-02 17:10:16 -07:00
2018-04-25 11:16:54 +01:00
class MasterFinder ( object ) :
def __init__ ( self , template ) :
self . template = template
def __call__ ( self , src_path ) :
fullname = os . path . abspath ( src_path )
dirname , basename = os . path . split ( fullname )
stem , ext = os . path . splitext ( basename )
path = self . template . format (
fullname = fullname ,
dirname = dirname ,
basename = basename ,
stem = stem ,
ext = ext ,
)
return os . path . normpath ( path )
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 ' )
2018-04-25 11:16:54 +01:00
parser . add_argument (
' -o ' ,
metavar = ' OUTPUTFILE ' ,
dest = ' outfile ' ,
default = None ,
help = ' output file '
)
parser . add_argument (
' -x ' ,
metavar = ' TAG ' ,
dest = ' exclude ' ,
action = ' append ' ,
default = [ ] ,
help = ' exclude table '
)
parser . add_argument (
' --disable-iup ' ,
dest = ' optimize ' ,
action = ' store_false ' ,
help = ' do not perform IUP optimization '
)
parser . add_argument (
' --master-finder ' ,
default = ' master_ttf_interpolatable/ {stem} .ttf ' ,
help = (
' templated string used for finding binary font '
' files given the source file names defined in the '
' designspace document. The following special strings '
' are defined: {fullname} is the absolute source file '
' name; {basename} is the file name without its '
' directory; {stem} is the basename without the file '
' extension; {ext} is the source file extension; '
' {dirname} is the directory of the absolute file '
' name. The default value is " %(default)s " . '
)
)
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
2018-04-25 11:16:54 +01:00
finder = MasterFinder ( options . master_finder )
2018-02-19 17:12:28 -08:00
outfile = options . outfile
if outfile is None :
outfile = os . path . splitext ( designspace_filename ) [ 0 ] + ' -VF.ttf '
2016-09-02 17:10:16 -07:00
2018-04-25 11:16:54 +01:00
vf , model , master_ttfs = build (
designspace_filename ,
finder ,
exclude = options . exclude ,
optimize = options . optimize
)
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 )