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 :
2018-04-17 14:10:50 +02:00
$ fontmake - o ttf - interpolatable - g NotoSansArabic - MM . glyphs
2016-04-15 13:56:37 -07:00
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
2018-04-17 14:10:50 +02: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 *
2018-06-14 17:40:11 +01:00
from fontTools . misc . fixedTools import otRound
2017-10-10 12:43:15 +02:00
from fontTools . misc . arrayTools import Vector
2018-12-19 18:29:34 +00:00
from fontTools . ttLib import TTFont , newTable , TTLibError
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
2018-09-11 18:16:52 +02:00
from fontTools . varLib import builder , models , varStore
2018-11-08 17:57:51 -05:00
from fontTools . varLib . merger import VariationMerger
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
2018-09-12 19:59:29 +02:00
from fontTools . varLib . featureVars import addFeatureVariations
2018-09-11 18:16:52 +02:00
from fontTools . designspaceLib import DesignSpaceDocument , AxisDescriptor
2018-09-12 19:16:13 +02:00
from collections import OrderedDict , namedtuple
2018-12-19 13:40:11 +00:00
import io
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
2018-09-11 18:16:52 +02:00
axis . axisNameID = nameTable . addName ( tounicode ( a . labelNames [ ' 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 :
2018-09-11 18:16:52 +02:00
coordinates = instance . location
name = tounicode ( instance . styleName )
psname = instance . 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 .
2018-09-11 18:16:52 +02:00
axes is an ordered dictionary of AxisDescriptor objects .
2017-08-03 12:38:23 +01:00
"""
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
2018-09-11 18:16:52 +02:00
items = sorted ( axis . map )
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 :
2018-04-17 14:10:50 +02:00
return
2018-02-05 12:51:01 +00:00
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-12-14 15:04:32 +00:00
stat . Version = 0x00010001
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 )
2018-06-14 17:40:11 +01:00
horizontalAdvanceWidth = otRound ( 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
2018-06-14 17:40:11 +01:00
leftSideBearing = otRound ( 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
2018-11-07 22:38:15 -05:00
def _add_gvar ( font , masterModel , 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 = { }
2018-12-17 08:03:40 -05:00
glyf = font [ ' glyf ' ]
2016-04-14 23:55:11 -07:00
for glyph in font . getGlyphOrder ( ) :
2016-04-14 18:27:44 -07:00
2018-12-17 08:03:40 -05:00
isComposite = glyf [ glyph ] . isComposite ( )
2016-04-14 23:55:11 -07:00
allData = [ _GetCoordinates ( m , glyph ) for m in master_ttfs ]
2018-11-08 10:01:47 -05:00
model , allData = masterModel . getSubModel ( allData )
2018-11-07 22:38:15 -05:00
2016-04-14 23:55:11 -07:00
allCoords = [ d [ 0 ] for d in allData ]
allControls = [ d [ 1 ] for d in allData ]
control = allControls [ 0 ]
2018-11-10 15:05:26 -05:00
if not models . allEqual ( 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 : ] ) ) :
2018-12-17 08:03:40 -05:00
if all ( abs ( v ) < = tolerance for v in delta . array ) and not isComposite :
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 :
2018-12-17 08:03:40 -05:00
""" In composite glyphs, there should be one 0 entry
to make sure the gvar entry is written to the font .
This is to work around an issue with macOS 10.14 and can be
removed once the behaviour of macOS is changed .
https : / / github . com / fonttools / fonttools / issues / 1381
"""
if all ( d is None for d in delta_opt ) :
delta_opt = [ ( 0 , 0 ) ] + [ None ] * ( len ( delta_opt ) - 1 )
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
2018-11-07 22:49:15 -05:00
def _merge_TTHinting ( font , masterModel , master_ttfs , tolerance = 0.5 ) :
2017-10-12 10:40:40 +02:00
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
2018-11-07 22:41:31 -05:00
if name in m [ ' glyf ' ] and hasattr ( m [ " glyf " ] [ name ] , " program " )
2017-10-12 10:40:40 +02:00
]
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 )
2018-11-07 22:41:31 -05:00
# TODO Only drop hinting from this glyph.
2017-10-12 10:40:40 +02:00
_remove_TTHinting ( font )
return
# cvt table
2018-11-07 22:49:15 -05:00
all_cvs = [ Vector ( m [ " cvt " ] . values ) if ' cvt ' in m else None
for m in master_ttfs ]
2018-10-24 18:46:45 +02:00
2018-11-07 22:49:15 -05:00
nonNone_cvs = models . nonNone ( all_cvs )
if not nonNone_cvs :
2017-10-12 10:40:40 +02:00
# There is no cvt table to make a cvar table from, we're done here.
return
2018-11-10 15:05:26 -05:00
if not models . allEqual ( len ( c ) for c in nonNone_cvs ) :
2017-10-12 10:40:40 +02:00
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 = [ ]
2018-11-08 10:05:18 -05:00
deltas , supports = masterModel . getDeltasAndSupports ( all_cvs )
2017-10-09 13:08:55 +02:00
for i , ( delta , support ) in enumerate ( zip ( deltas [ 1 : ] , supports [ 1 : ] ) ) :
2018-06-14 17:40:11 +01:00
delta = [ otRound ( 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 )
2018-11-08 14:48:24 -05:00
def _add_HVAR ( font , masterModel , 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
2018-11-08 14:48:24 -05:00
glyphOrder = font . getGlyphOrder ( )
hAdvanceDeltasAndSupports = { }
2016-08-09 20:53:19 -07:00
metricses = [ m [ " hmtx " ] . metrics for m in master_ttfs ]
2018-11-08 14:48:24 -05:00
for glyph in glyphOrder :
hAdvances = [ metrics [ glyph ] [ 0 ] if glyph in metrics else None for metrics in metricses ]
hAdvanceDeltasAndSupports [ glyph ] = masterModel . getDeltasAndSupports ( hAdvances )
2018-11-10 15:05:26 -05:00
singleModel = models . allEqual ( id ( v [ 1 ] ) for v in hAdvanceDeltasAndSupports . values ( ) )
2018-11-08 14:48:24 -05:00
directStore = None
if singleModel :
# Build direct mapping
2018-11-08 16:29:29 -05:00
supports = next ( iter ( hAdvanceDeltasAndSupports . values ( ) ) ) [ 1 ] [ 1 : ]
2018-11-08 14:48:24 -05:00
varTupleList = builder . buildVarRegionList ( supports , axisTags )
varTupleIndexes = list ( range ( len ( supports ) ) )
2018-11-08 16:29:29 -05:00
varData = builder . buildVarData ( varTupleIndexes , [ ] , optimize = False )
2018-11-08 14:48:24 -05:00
for glyphName in glyphOrder :
2018-11-08 16:29:29 -05:00
varData . addItem ( hAdvanceDeltasAndSupports [ glyphName ] [ 0 ] )
varData . optimize ( )
2018-11-08 14:48:24 -05:00
directStore = builder . buildVarStore ( varTupleList , [ varData ] )
# Build optimized indirect mapping
storeBuilder = varStore . OnlineVarStoreBuilder ( axisTags )
mapping = { }
for glyphName in glyphOrder :
deltas , supports = hAdvanceDeltasAndSupports [ glyphName ]
storeBuilder . setSupports ( supports )
mapping [ glyphName ] = storeBuilder . storeDeltas ( deltas )
indirectStore = storeBuilder . finish ( )
mapping2 = indirectStore . optimize ( )
mapping = [ mapping2 [ mapping [ g ] ] for g in glyphOrder ]
advanceMapping = builder . buildVarIdxMap ( mapping , glyphOrder )
use_direct = False
if directStore :
# 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
2018-02-21 01:22:59 -08:00
# 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
2018-11-08 18:02:28 -05:00
def _add_MVAR ( font , masterModel , master_ttfs , axisTags ) :
2017-04-10 21:01:00 +02:00
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
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 :
fontTable = font [ tableTag ]
2018-11-08 18:02:28 -05:00
tables = [ master [ tableTag ] if tableTag in master else None
for master in master_ttfs ]
2017-04-10 21:01:00 +02:00
lastTableTag = tableTag
if tables is None :
continue
# TODO support gasp entries
2018-11-08 18:02:28 -05:00
model , tables = masterModel . getSubModel ( tables )
store_builder . setModel ( model )
2017-04-10 21:01:00 +02:00
master_values = [ getattr ( table , itemName ) for table in tables ]
2018-11-10 15:05:26 -05:00
if models . allEqual ( master_values ) :
2017-04-10 21:01:00 +02:00
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
2018-11-08 23:02:40 -05:00
merger . mergeTables ( font , master_fonts , [ ' GSUB ' , ' GDEF ' , ' GPOS ' ] )
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
2018-09-12 19:59:29 +02:00
def _add_GSUB_feature_variations ( font , axes , internal_axis_supports , rules ) :
def normalize ( name , value ) :
return models . normalizeLocation (
{ name : value } , internal_axis_supports
) [ name ]
log . info ( " Generating GSUB FeatureVariations " )
axis_tags = { name : axis . tag for name , axis in axes . items ( ) }
conditional_subs = [ ]
for rule in rules :
region = [ ]
for conditions in rule . conditionSets :
space = { }
for condition in conditions :
axis_name = condition [ " name " ]
2018-10-24 19:08:11 +02:00
if condition [ " minimum " ] is not None :
2018-10-24 18:46:45 +02:00
minimum = normalize ( axis_name , condition [ " minimum " ] )
else :
minimum = - 1.0
2018-10-24 19:08:11 +02:00
if condition [ " maximum " ] is not None :
2018-10-24 18:46:45 +02:00
maximum = normalize ( axis_name , condition [ " maximum " ] )
else :
maximum = 1.0
2018-09-12 19:59:29 +02:00
tag = axis_tags [ axis_name ]
space [ tag ] = ( minimum , maximum )
region . append ( space )
subs = { k : v for k , v in rule . subs }
conditional_subs . append ( ( region , subs ) )
addFeatureVariations ( font , conditional_subs )
2018-09-12 19:16:13 +02:00
_DesignSpaceData = namedtuple (
" _DesignSpaceData " ,
[
" axes " ,
" internal_axis_supports " ,
" base_idx " ,
" normalized_master_locs " ,
" masters " ,
" instances " ,
" rules " ,
] ,
)
2018-10-23 10:20:24 -07:00
def _add_CFF2 ( varFont , model , master_fonts ) :
2018-12-04 19:22:02 -08:00
from . cff import ( convertCFFtoCFF2 , addCFFVarStore , merge_region_fonts )
2018-10-23 10:20:24 -07:00
glyphOrder = varFont . getGlyphOrder ( )
convertCFFtoCFF2 ( varFont )
2018-11-30 21:46:16 -05:00
ordered_fonts_list = model . reorderMasters ( master_fonts , model . reverseMapping )
2018-11-30 09:12:27 -08:00
# re-ordering the master list simplifies building the CFF2 data item lists.
2018-10-23 10:20:24 -07:00
addCFFVarStore ( varFont , model ) # Add VarStore to the CFF2 font.
merge_region_fonts ( varFont , model , ordered_fonts_list , glyphOrder )
2018-12-19 13:40:11 +00:00
def load_designspace ( designspace ) :
if hasattr ( designspace , " sources " ) : # Assume a DesignspaceDocument
ds = designspace
else : # Assume a file path
ds = DesignSpaceDocument . fromfile ( designspace )
2016-04-14 18:27:44 -07:00
2018-09-11 18:16:52 +02:00
masters = ds . sources
2017-04-12 15:39:05 -07:00
if not masters :
raise VarLibError ( " no sources found in .designspace " )
2018-09-11 18:16:52 +02:00
instances = ds . 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
2018-09-11 18:25:43 +02:00
axes = OrderedDict ( )
for axis in ds . axes :
axis_name = axis . name
if not axis_name :
assert axis . tag is not None
axis_name = axis . name = axis . tag
if axis_name in standard_axis_map :
if axis . tag is None :
axis . tag = standard_axis_map [ axis_name ] [ 0 ]
if not axis . labelNames :
axis . labelNames . update ( standard_axis_map [ axis_name ] [ 1 ] )
else :
assert axis . tag is not None
if not axis . labelNames :
axis . labelNames [ " en " ] = axis_name
2017-02-25 20:53:48 -08:00
2018-09-11 18:25:43 +02:00
axes [ axis_name ] = axis
2018-09-11 18:16:52 +02:00
log . info ( " Axes: \n %s " , pformat ( [ axis . asdict ( ) for axis in axes . values ( ) ] ) )
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 :
2018-09-11 18:16:52 +02:00
obj_name = obj . name or obj . styleName or ' '
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
2018-09-11 18:16:52 +02:00
internal_master_locs = [ o . location for o in masters ]
2018-04-17 14:10:50 +02:00
log . info ( " Internal master locations: \n %s " , pformat ( internal_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
2018-04-17 14:10:50 +02:00
normalized_master_locs = [ models . normalizeLocation ( m , internal_axis_supports ) for m in internal_master_locs ]
2017-05-22 19:40:20 -07:00
log . info ( " Normalized master locations: \n %s " , pformat ( normalized_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-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 )
2018-09-12 19:16:13 +02:00
return _DesignSpaceData (
axes ,
internal_axis_supports ,
base_idx ,
normalized_master_locs ,
masters ,
instances ,
ds . rules ,
)
2017-05-22 19:40:20 -07:00
2018-12-19 13:40:11 +00:00
def build ( designspace , 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 ) .
"""
2018-12-19 13:40:11 +00:00
ds = load_designspace ( designspace )
2018-12-20 08:53:28 +00:00
master_fonts , master_ttfs = _ensure_sources ( designspace , ds , master_finder )
2017-04-12 17:14:22 -07:00
log . info ( " Building variable font " )
2018-12-19 13:40:11 +00:00
2018-12-20 08:53:28 +00:00
# Copy the master source TTFont object to work on it.
buffer = io . BytesIO ( )
master_fonts [ ds . base_idx ] . save ( buffer )
buffer . seek ( 0 )
vf = TTFont ( buffer )
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.
2018-09-12 19:16:13 +02:00
fvar = _add_fvar ( vf , ds . axes , ds . instances )
2018-02-19 17:12:28 -08:00
if ' STAT ' not in exclude :
2018-09-12 19:16:13 +02:00
_add_stat ( vf , ds . axes )
2018-02-19 17:12:28 -08:00
if ' avar ' not in exclude :
2018-09-12 19:16:13 +02:00
_add_avar ( vf , ds . axes )
2017-04-12 17:14:22 -07:00
# Map from axis names to axis tags...
2018-09-12 19:16:13 +02:00
normalized_master_locs = [
{ ds . axes [ k ] . tag : v for k , v in loc . items ( ) } for loc in ds . normalized_master_locs
]
2017-04-12 17:14:22 -07:00
# 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 )
2018-09-12 19:16:13 +02:00
assert 0 == model . mapping [ ds . 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-03-06 17:36:57 -08:00
if ' cvar ' not in exclude and ' glyf ' in vf :
2017-10-12 10:40:40 +02:00
_merge_TTHinting ( vf , model , master_fonts )
2018-09-12 19:59:29 +02:00
if ' GSUB ' not in exclude and ds . rules :
_add_GSUB_feature_variations ( vf , ds . axes , ds . internal_axis_supports , ds . rules )
2018-10-23 10:20:24 -07:00
if ' CFF2 ' not in exclude and ' CFF ' in vf :
_add_CFF2 ( 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 ]
2018-12-19 18:39:47 +00:00
# TODO: Only return vf for 4.0+, the rest is unused.
2017-04-12 17:14:22 -07:00
return vf , model , master_ttfs
2016-09-02 17:10:16 -07:00
2018-12-20 08:53:28 +00:00
def _ensure_sources ( designspace , designspace_data , master_finder ) :
""" Ensure that all SourceDescriptor.font attributes have an appropriate TTFont
object loaded . """
master_fonts = [ ]
master_ttfs = [ ]
if hasattr ( designspace , " sources " ) : # Assume a DesignspaceDocument
basedir = getattr ( designspace , " path " , None )
else : # Assume a path
basedir = os . path . dirname ( designspace )
for master in designspace_data . masters :
# 1. If the caller already supplies a TTFont for a source, just take it.
if master . font :
font = master . font
master_fonts . append ( font )
master_ttfs . append ( None ) # No file path for in-memory object.
else :
# If a SourceDescriptor has a layer name, demand that the compiled TTFont
# be supplied by the caller. This spares us from modifying MasterFinder.
if master . layerName :
raise AttributeError (
" Designspace source ' %s ' specified a layer name but lacks the "
" then required TTFont object in the ' font ' attribute. "
% getattr ( master , " name " , " <Unknown> " )
)
else :
# 2. A SourceDescriptor's filename might point to a UFO or an OpenType
# binary. Find out the hard way.
master_path = os . path . join ( basedir , master . filename )
try :
font = TTFont ( master_path )
master_fonts . append ( font )
master_ttfs . append ( master_path )
except ( IOError , TTLibError ) :
# 3. Probably no OpenType binary, fall back to the master finder.
master_path = master_finder ( master_path )
font = TTFont ( master_path )
master_fonts . append ( font )
master_ttfs . append ( master_path )
# TODO: Drop return values for 4.0, master_fonts is just a list comprehension away.
return master_fonts , master_ttfs
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 )