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
2016-04-14 18:27:44 -07:00
import os . path
2017-01-06 20:22:45 +01:00
import logging
2018-12-20 14:18:59 +00:00
from copy import deepcopy
2017-01-06 20:22:45 +01:00
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
2019-01-13 13:39:15 +00:00
axis . axisNameID = nameTable . addMultilingualName ( a . labelNames , font )
2019-01-27 10:57:08 +00:00
axis . flags = int ( a . hidden )
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
2019-01-13 14:57:10 +00:00
if " en " not in instance . localisedStyleName :
assert instance . styleName
localisedStyleName = dict ( instance . localisedStyleName )
localisedStyleName [ " en " ] = tounicode ( instance . styleName )
else :
localisedStyleName = instance . localisedStyleName
2018-09-11 18:16:52 +02:00
psname = instance . postScriptFontName
2016-09-02 17:29:22 -07:00
2016-04-14 18:27:44 -07:00
inst = NamedInstance ( )
2019-01-13 14:57:10 +00:00
inst . subfamilyNameID = nameTable . addMultilingualName ( localisedStyleName )
2016-10-04 14:56:26 +01:00
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
2019-03-05 10:02:35 -08:00
def _get_phantom_points ( font , glyphName , defaultVerticalOrigin = None ) :
glyf = font [ " glyf " ]
glyph = glyf [ glyphName ]
horizontalAdvanceWidth , leftSideBearing = font [ " hmtx " ] . metrics [ glyphName ]
if not hasattr ( glyph , ' xMin ' ) :
glyph . recalcBounds ( glyf )
leftSideX = glyph . xMin - leftSideBearing
rightSideX = leftSideX + horizontalAdvanceWidth
if " vmtx " in font :
verticalAdvanceWidth , topSideBearing = font [ " vmtx " ] . metrics [ glyphName ]
topSideY = topSideBearing + glyph . yMax
else :
# without vmtx, use ascent as vertical origin and UPEM as vertical advance
# like HarfBuzz does
verticalAdvanceWidth = font [ " head " ] . unitsPerEm
try :
topSideY = font [ " hhea " ] . ascent
except KeyError :
# sparse masters may not contain an hhea table; use the ascent
# of the default master as the vertical origin
assert defaultVerticalOrigin is not None
topSideY = defaultVerticalOrigin
bottomSideY = topSideY - verticalAdvanceWidth
return [
( leftSideX , 0 ) ,
( rightSideX , 0 ) ,
( 0 , topSideY ) ,
( 0 , bottomSideY ) ,
]
2016-04-14 23:55:11 -07:00
# TODO Move to glyf or gvar table proper
2019-03-05 10:02:35 -08:00
def _GetCoordinates ( font , glyphName , defaultVerticalOrigin = None ) :
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.
2019-03-05 10:02:35 -08:00
phantomPoints = _get_phantom_points ( font , glyphName , defaultVerticalOrigin )
2016-04-27 00:25:31 -07:00
coord = coord . copy ( )
2019-03-05 10:02:35 -08:00
coord . extend ( phantomPoints )
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 ' ]
2019-03-05 10:02:35 -08:00
# use hhea.ascent of base master as default vertical origin when vmtx is missing
defaultVerticalOrigin = font [ ' hhea ' ] . ascent
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 ( )
2019-03-05 10:02:35 -08:00
allData = [
_GetCoordinates ( m , glyph , defaultVerticalOrigin = defaultVerticalOrigin )
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 )
2019-03-21 10:06:47 -07:00
MetricsFields = namedtuple ( ' MetricsFields ' ,
2019-03-26 13:24:35 -07:00
[ ' tableTag ' , ' metricsTag ' , ' sb1 ' , ' sb2 ' , ' advMapping ' , ' vOrigMapping ' ] )
2019-03-21 10:06:47 -07:00
2019-03-26 13:24:35 -07:00
hvarFields = MetricsFields ( tableTag = ' HVAR ' , metricsTag = ' hmtx ' , sb1 = ' LsbMap ' ,
sb2 = ' RsbMap ' , advMapping = ' AdvWidthMap ' , vOrigMapping = None )
vvarFields = MetricsFields ( tableTag = ' VVAR ' , metricsTag = ' vmtx ' , sb1 = ' TsbMap ' ,
sb2 = ' BsbMap ' , advMapping = ' AdvHeightMap ' , vOrigMapping = ' VOrgMap ' )
2019-03-21 10:06:47 -07:00
2018-11-08 14:48:24 -05:00
def _add_HVAR ( font , masterModel , master_ttfs , axisTags ) :
2019-03-26 13:24:35 -07:00
_add_VHVAR ( font , masterModel , master_ttfs , axisTags , hvarFields )
2019-03-21 10:06:47 -07:00
def _add_VVAR ( font , masterModel , master_ttfs , axisTags ) :
2019-03-26 13:24:35 -07:00
_add_VHVAR ( font , masterModel , master_ttfs , axisTags , vvarFields )
2016-07-01 15:31:00 -07:00
2019-03-26 13:24:35 -07:00
def _add_VHVAR ( font , masterModel , master_ttfs , axisTags , tableFields ) :
2019-03-21 10:06:47 -07:00
2019-03-26 13:24:35 -07:00
tableTag = tableFields . tableTag
assert tableTag not in font
log . info ( " Generating " + tableTag )
VHVAR = newTable ( tableTag )
2019-04-02 09:12:14 -07:00
tableClass = getattr ( ot , tableTag )
vhvar = VHVAR . table = tableClass ( )
2019-03-21 10:06:47 -07:00
vhvar . Version = 0x00010000
2016-08-09 20:53:19 -07:00
2018-11-08 14:48:24 -05:00
glyphOrder = font . getGlyphOrder ( )
2019-03-21 10:06:47 -07:00
# Build list of source font advance widths for each glyph
2019-03-26 13:24:35 -07:00
metricsTag = tableFields . metricsTag
advMetricses = [ m [ metricsTag ] . metrics for m in master_ttfs ]
2019-03-21 10:06:47 -07:00
# Build list of source font vertical origin coords for each glyph
2019-03-26 13:24:35 -07:00
if tableTag == ' VVAR ' and ' VORG ' in master_ttfs [ 0 ] :
vOrigMetricses = [ m [ ' VORG ' ] . VOriginRecords for m in master_ttfs ]
defaultYOrigs = [ m [ ' VORG ' ] . defaultVertOriginY for m in master_ttfs ]
vOrigMetricses = list ( zip ( vOrigMetricses , defaultYOrigs ) )
2019-03-21 10:06:47 -07:00
else :
2019-03-26 13:24:35 -07:00
vOrigMetricses = None
2019-03-21 10:06:47 -07:00
metricsStore , advanceMapping , vOrigMapping = _get_advance_metrics ( font ,
2019-03-26 13:24:35 -07:00
masterModel , master_ttfs , axisTags , glyphOrder , advMetricses ,
vOrigMetricses )
2019-03-21 10:06:47 -07:00
vhvar . VarStore = metricsStore
if advanceMapping is None :
2019-03-26 13:24:35 -07:00
setattr ( vhvar , tableFields . advMapping , None )
2019-03-21 10:06:47 -07:00
else :
2019-03-26 13:24:35 -07:00
setattr ( vhvar , tableFields . advMapping , advanceMapping )
2019-03-21 10:06:47 -07:00
if vOrigMapping is not None :
2019-03-26 13:24:35 -07:00
setattr ( vhvar , tableFields . vOrigMapping , vOrigMapping )
setattr ( vhvar , tableFields . sb1 , None )
setattr ( vhvar , tableFields . sb2 , None )
2018-11-08 14:48:24 -05:00
2019-03-26 13:24:35 -07:00
font [ tableTag ] = VHVAR
2019-03-21 10:06:47 -07:00
return
def _get_advance_metrics ( font , masterModel , master_ttfs ,
2019-03-26 13:24:35 -07:00
axisTags , glyphOrder , advMetricses , vOrigMetricses = None ) :
2019-03-21 10:06:47 -07:00
vhAdvanceDeltasAndSupports = { }
vOrigDeltasAndSupports = { }
for glyph in glyphOrder :
2019-03-26 13:24:35 -07:00
vhAdvances = [ metrics [ glyph ] [ 0 ] if glyph in metrics else None for metrics in advMetricses ]
2019-03-21 10:06:47 -07:00
vhAdvanceDeltasAndSupports [ glyph ] = masterModel . getDeltasAndSupports ( vhAdvances )
singleModel = models . allEqual ( id ( v [ 1 ] ) for v in vhAdvanceDeltasAndSupports . values ( ) )
2019-03-26 13:24:35 -07:00
if vOrigMetricses :
2019-03-21 10:06:47 -07:00
singleModel = False
for glyph in glyphOrder :
# We need to supply a vOrigs tuple with non-None default values
2019-03-26 13:24:35 -07:00
# for each glyph. vOrigMetricses contains values only for those
2019-03-21 10:06:47 -07:00
# glyphs which have a non-default vOrig.
vOrigs = [ metrics [ glyph ] if glyph in metrics else defaultVOrig
2019-03-26 13:24:35 -07:00
for metrics , defaultVOrig in vOrigMetricses ]
2019-03-21 10:06:47 -07:00
vOrigDeltasAndSupports [ glyph ] = masterModel . getDeltasAndSupports ( vOrigs )
2018-11-08 14:48:24 -05:00
directStore = None
if singleModel :
# Build direct mapping
2019-03-21 10:06:47 -07:00
supports = next ( iter ( vhAdvanceDeltasAndSupports . 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 :
2019-03-21 10:06:47 -07:00
varData . addItem ( vhAdvanceDeltasAndSupports [ glyphName ] [ 0 ] )
2018-11-08 16:29:29 -05:00
varData . optimize ( )
2018-11-08 14:48:24 -05:00
directStore = builder . buildVarStore ( varTupleList , [ varData ] )
# Build optimized indirect mapping
storeBuilder = varStore . OnlineVarStoreBuilder ( axisTags )
2019-03-26 13:24:35 -07:00
advMapping = { }
2018-11-08 14:48:24 -05:00
for glyphName in glyphOrder :
2019-03-21 10:06:47 -07:00
deltas , supports = vhAdvanceDeltasAndSupports [ glyphName ]
2018-11-08 14:48:24 -05:00
storeBuilder . setSupports ( supports )
2019-03-26 13:24:35 -07:00
advMapping [ glyphName ] = storeBuilder . storeDeltas ( deltas )
2019-03-21 10:06:47 -07:00
2019-03-26 13:24:35 -07:00
if vOrigMetricses :
vOrigMap = { }
2019-03-21 10:06:47 -07:00
for glyphName in glyphOrder :
deltas , supports = vOrigDeltasAndSupports [ glyphName ]
storeBuilder . setSupports ( supports )
2019-03-26 13:24:35 -07:00
vOrigMap [ glyphName ] = storeBuilder . storeDeltas ( deltas )
2019-03-21 10:06:47 -07:00
2018-11-08 14:48:24 -05:00
indirectStore = storeBuilder . finish ( )
mapping2 = indirectStore . optimize ( )
2019-03-26 13:24:35 -07:00
advMapping = [ mapping2 [ advMapping [ g ] ] for g in glyphOrder ]
advanceMapping = builder . buildVarIdxMap ( advMapping , glyphOrder )
2019-03-21 10:06:47 -07:00
2019-03-26 13:24:35 -07:00
if vOrigMetricses :
vOrigMap = [ mapping2 [ vOrigMap [ g ] ] for g in glyphOrder ]
2018-11-08 14:48:24 -05:00
2019-03-26 13:24:35 -07:00
useDirect = False
2019-03-21 10:06:47 -07:00
vOrigMapping = None
2018-11-08 14:48:24 -05:00
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 ( ) )
2019-03-26 13:24:35 -07:00
useDirect = directSize < indirectSize
2018-02-21 01:22:59 -08:00
2019-03-26 13:24:35 -07:00
if useDirect :
2019-03-21 10:06:47 -07:00
metricsStore = directStore
advanceMapping = None
2018-02-21 01:22:59 -08:00
else :
2019-03-21 10:06:47 -07:00
metricsStore = indirectStore
2019-03-26 13:24:35 -07:00
if vOrigMetricses :
vOrigMapping = builder . buildVarIdxMap ( vOrigMap , glyphOrder )
2019-03-21 10:06:47 -07:00
return metricsStore , advanceMapping , vOrigMapping
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
2019-01-15 16:16:40 +00:00
# HACK: we need to special-case post.underlineThickness and .underlinePosition
# and unilaterally/arbitrarily define a sentinel value to distinguish the case
# when a post table is present in a given master simply because that's where
# the glyph names in TrueType must be stored, but the underline values are not
2019-01-15 18:19:19 +00:00
# meant to be used for building MVAR's deltas. The value of -0x8000 (-36768)
2019-01-15 16:16:40 +00:00
# the minimum FWord (int16) value, was chosen for its unlikelyhood to appear
# in real-world underline position/thickness values.
2019-01-15 18:19:19 +00:00
specialTags = { " unds " : - 0x8000 , " undo " : - 0x8000 }
2019-02-04 16:03:47 +00:00
2017-10-22 12:19:24 +01:00
for tag , ( tableTag , itemName ) in sorted ( MVAR_ENTRIES . items ( ) , key = lambda kv : kv [ 1 ] ) :
2019-02-04 16:03:47 +00:00
# For each tag, fetch the associated table from all fonts (or not when we are
# still looking at a tag from the same tables) and set up the variation model
# for them.
2017-04-10 21:01:00 +02:00
if tableTag != lastTableTag :
tables = fontTable = None
if tableTag in font :
fontTable = font [ tableTag ]
2019-01-15 16:16:40 +00:00
tables = [ ]
for master in master_ttfs :
2019-01-15 18:19:19 +00:00
if tableTag not in master or (
tag in specialTags
and getattr ( master [ tableTag ] , itemName ) == specialTags [ tag ]
) :
2019-01-15 16:16:40 +00:00
tables . append ( None )
2019-01-15 18:19:19 +00:00
else :
tables . append ( master [ tableTag ] )
2019-02-04 16:03:47 +00:00
model , tables = masterModel . getSubModel ( tables )
store_builder . setModel ( model )
2017-04-10 21:01:00 +02:00
lastTableTag = tableTag
2019-02-04 16:03:47 +00:00
if tables is None : # Tag not applicable to the master font.
2017-04-10 21:01:00 +02:00
continue
# TODO support gasp entries
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 ) :
2018-12-20 11:58:58 +00:00
# TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
# never a file path, as that's already handled by caller
2018-12-19 13:40:11 +00:00
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 ( [
2019-01-13 13:39:15 +00:00
( ' weight ' , ( ' wght ' , { ' en ' : u ' Weight ' } ) ) ,
( ' width ' , ( ' wdth ' , { ' en ' : u ' Width ' } ) ) ,
( ' slant ' , ( ' slnt ' , { ' en ' : u ' Slant ' } ) ) ,
( ' optical ' , ( ' opsz ' , { ' en ' : u ' Optical Size ' } ) ) ,
2019-02-22 11:29:33 +00:00
( ' italic ' , ( ' ital ' , { ' en ' : u ' Italic ' } ) ) ,
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 :
2019-01-13 13:39:15 +00:00
axis . labelNames [ " en " ] = tounicode ( 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-20 11:58:58 +00:00
if hasattr ( designspace , " sources " ) : # Assume a DesignspaceDocument
pass
else : # Assume a file path
designspace = DesignSpaceDocument . fromfile ( designspace )
2017-05-22 19:40:20 -07:00
2018-12-19 13:40:11 +00:00
ds = load_designspace ( designspace )
2017-04-12 17:14:22 -07:00
log . info ( " Building variable font " )
2018-12-19 13:40:11 +00:00
2018-12-20 11:58:58 +00:00
log . info ( " Loading master fonts " )
master_fonts = load_masters ( designspace , master_finder )
# TODO: 'master_ttfs' is unused except for return value, remove later
master_ttfs = [ ]
for master in master_fonts :
try :
master_ttfs . append ( master . reader . file . name )
except AttributeError :
master_ttfs . append ( None ) # in-memory fonts have no path
# Copy the base master to work from it
2018-12-20 14:18:59 +00:00
vf = deepcopy ( master_fonts [ ds . 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.
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 )
2019-03-21 10:06:47 -07:00
if ' VVAR ' not in exclude and ' vmtx ' in vf :
_add_VVAR ( vf , model , master_fonts , axisTags )
2018-02-19 17:12:28 -08:00
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
2019-01-14 16:30:11 +00:00
def _open_font ( path , master_finder ) :
# load TTFont masters from given 'path': this can be either a .TTX or an
# OpenType binary font; or if neither of these, try use the 'master_finder'
# callable to resolve the path to a valid .TTX or OpenType font binary.
from fontTools . ttx import guessFileType
master_path = os . path . normpath ( path )
tp = guessFileType ( master_path )
if tp is None :
# not an OpenType binary/ttx, fall back to the master finder.
master_path = master_finder ( master_path )
tp = guessFileType ( master_path )
if tp in ( " TTX " , " OTX " ) :
font = TTFont ( )
font . importXML ( master_path )
elif tp in ( " TTF " , " OTF " , " WOFF " , " WOFF2 " ) :
font = TTFont ( master_path )
else :
raise VarLibError ( " Invalid master path: %r " % master_path )
return font
2018-12-20 11:58:58 +00:00
def load_masters ( designspace , master_finder = lambda s : s ) :
2018-12-20 08:53:28 +00:00
""" Ensure that all SourceDescriptor.font attributes have an appropriate TTFont
2019-01-03 15:41:15 +00:00
object loaded , or else open TTFont objects from the SourceDescriptor . path
attributes .
2019-01-14 16:30:11 +00:00
The paths can point to either an OpenType font , a TTX file , or a UFO . In the
latter case , use the provided master_finder callable to map from UFO paths to
the respective master font binaries ( e . g . . ttf , . otf or . ttx ) .
2018-12-20 11:58:58 +00:00
Return list of master TTFont objects in the same order they are listed in the
DesignSpaceDocument .
"""
2018-12-20 08:53:28 +00:00
master_fonts = [ ]
2018-12-20 11:58:58 +00:00
for master in designspace . sources :
2018-12-20 08:53:28 +00:00
# 1. If the caller already supplies a TTFont for a source, just take it.
if master . font :
font = master . font
master_fonts . append ( font )
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 "
2018-12-20 11:58:58 +00:00
" required TTFont object in the ' font ' attribute. "
% ( master . name or " <Unknown> " )
)
2018-12-20 08:53:28 +00:00
else :
2019-01-03 14:21:09 +00:00
if master . path is None :
raise AttributeError (
" Designspace source ' %s ' has neither ' font ' nor ' path ' "
" attributes " % ( master . name or " <Unknown> " )
)
2019-01-14 16:30:11 +00:00
# 2. A SourceDescriptor's path might point an OpenType binary, a
# TTX file, or another source file (e.g. UFO), in which case we
# resolve the path using 'master_finder' function
2019-02-05 13:02:53 +00:00
master . font = font = _open_font ( master . path , master_finder )
2018-12-20 11:58:58 +00:00
master_fonts . append ( font )
return master_fonts
2018-12-20 08:53:28 +00: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-12-20 11:58:58 +00:00
vf , _ , _ = build (
2018-04-25 11:16:54 +01:00
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 )