2016-04-27 01:30:19 -07:00
|
|
|
"""
|
2016-04-27 01:41:48 -07:00
|
|
|
Instantiate a variation font. Run, eg:
|
|
|
|
|
2024-09-03 17:53:56 +01:00
|
|
|
.. code-block:: sh
|
|
|
|
|
|
|
|
$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
|
2016-04-27 01:30:19 -07:00
|
|
|
"""
|
2024-02-06 15:47:35 +02:00
|
|
|
|
2021-03-03 18:48:08 -07:00
|
|
|
from fontTools.misc.fixedTools import floatToFixedToFloat, floatToFixed
|
|
|
|
from fontTools.misc.roundTools import otRound
|
2018-11-29 17:07:51 -05:00
|
|
|
from fontTools.pens.boundsPen import BoundsPen
|
2018-11-15 15:18:03 -05:00
|
|
|
from fontTools.ttLib import TTFont, newTable
|
|
|
|
from fontTools.ttLib.tables import ttProgram
|
2019-02-28 12:34:43 -05:00
|
|
|
from fontTools.ttLib.tables._g_l_y_f import (
|
|
|
|
GlyphCoordinates,
|
|
|
|
flagOverlapSimple,
|
|
|
|
OVERLAP_COMPOUND,
|
2022-12-13 11:26:36 +00:00
|
|
|
)
|
2018-09-11 18:07:15 +02:00
|
|
|
from fontTools.varLib.models import (
|
2018-11-29 14:08:53 -05:00
|
|
|
supportScalar,
|
|
|
|
normalizeLocation,
|
|
|
|
piecewiseLinearMap,
|
2018-09-11 18:07:15 +02:00
|
|
|
)
|
2017-10-20 16:09:40 -04:00
|
|
|
from fontTools.varLib.merger import MutatorMerger
|
2017-10-20 13:50:08 -04:00
|
|
|
from fontTools.varLib.varStore import VarStoreInstancer
|
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
|
2018-11-29 17:07:51 -05:00
|
|
|
import fontTools.subset.cff
|
2016-04-27 01:30:19 -07:00
|
|
|
import os.path
|
2017-10-15 17:43:06 +02:00
|
|
|
import logging
|
2019-08-10 15:39:02 +01:00
|
|
|
from io import BytesIO
|
2017-10-15 17:43:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("fontTools.varlib.mutator")
|
2016-04-27 01:30:19 -07:00
|
|
|
|
mutator: round to closest width class
this is the mapping between the half of each percentage intervals and
the associated usWidthClass:
{56.25: 1,
68.75: 2,
81.25: 3,
93.75: 4,
106.25: 5,
118.75: 6,
137.5: 7,
175.0: 8}
Notice how wdth=80 (in the adjusted test case) will fall in width class
3, instead of 4, because it is < 81.25, thus closer to the nominal 75
than to 87.5.
2018-06-19 14:16:17 +01:00
|
|
|
# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest
|
|
|
|
OS2_WIDTH_CLASS_VALUES = {}
|
|
|
|
percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0]
|
|
|
|
for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
|
|
|
|
half = (prev + curr) / 2
|
|
|
|
OS2_WIDTH_CLASS_VALUES[half] = i
|
2018-06-18 19:40:30 +01:00
|
|
|
|
|
|
|
|
2018-11-29 17:07:51 -05:00
|
|
|
def interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):
|
|
|
|
pd_blend_lists = (
|
|
|
|
"BlueValues",
|
|
|
|
"OtherBlues",
|
|
|
|
"FamilyBlues",
|
|
|
|
"FamilyOtherBlues",
|
|
|
|
"StemSnapH",
|
|
|
|
"StemSnapV",
|
|
|
|
)
|
|
|
|
pd_blend_values = ("BlueScale", "BlueShift", "BlueFuzz", "StdHW", "StdVW")
|
|
|
|
for fontDict in topDict.FDArray:
|
|
|
|
pd = fontDict.Private
|
|
|
|
vsindex = pd.vsindex if (hasattr(pd, "vsindex")) else 0
|
|
|
|
for key, value in pd.rawDict.items():
|
|
|
|
if (key in pd_blend_values) and isinstance(value, list):
|
|
|
|
delta = interpolateFromDeltas(vsindex, value[1:])
|
|
|
|
pd.rawDict[key] = otRound(value[0] + delta)
|
|
|
|
elif (key in pd_blend_lists) and isinstance(value[0], list):
|
|
|
|
"""If any argument in a BlueValues list is a blend list,
|
|
|
|
then they all are. The first value of each list is an
|
|
|
|
absolute value. The delta tuples are calculated from
|
|
|
|
relative master values, hence we need to append all the
|
|
|
|
deltas to date to each successive absolute value."""
|
|
|
|
delta = 0
|
|
|
|
for i, val_list in enumerate(value):
|
|
|
|
delta += otRound(interpolateFromDeltas(vsindex, val_list[1:]))
|
|
|
|
value[i] = val_list[0] + delta
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
|
|
|
|
charstrings = topDict.CharStrings
|
|
|
|
for gname in glyphOrder:
|
|
|
|
# Interpolate charstring
|
Sparse cff2vf support (#1591)
* Added getter (in the form of a property decorator) for T2Charstring.vsindex. Fixes endless compile loop in some circumstances.
Fixed bug in mutator: need to remove vsindex from snapshotted charstrings, plus formatting clean up
* Fix for subsetting HVAR tables that have an AdvanceWidthMap when the --retain-gid option is used. Needed to make subset_test.py::test_retain_gids_cff2 tests pass.
* in varLib/cffLib.py, add support for sparse sources, and sources with more than one model, and hence more than one VarData element in the VarStore.
CFF2 source fonts with multiple FontDicts in the FDArray need some extra work. With sparse fonts, some of the source fonts may have a fewer FontDicts than the default font. The getfd_map function() builds a map from the FontDict indices in the default font to those in each region font. This is needed when building up the blend value lists in the master font FontDict PrivateDicts, in order to fetch PrivateDict values from the correct FontDict in each region font.
In specializer.py, add support for CFF2 CharStrings with blend operators. 1) In generalizeCommands, convert a blend op to a list of args that are blend lists for the following regular operator. A blend list as a default font value, followed by the delta tuple. 2) In specializeCommands(), convert these back to blend ops, combining as many successive blend lists as allowed by the stack limit.
Add test case for sparse CFF2 sources.
The test font has 55 glyphs. 2 glyphs use only 2 sources (weight = 0 and 100). The rest use 4 source fonts: the two end points of the weight axis, and two intermediate masters. The intermediate masters are only 1 design space unit apart, and are used to change glyph design at the point in design space. For the rest, at most 2 glyphs use the same set of source fonts. There are 12 source fonts.
Add test case for specializer programToCommands() and commandsToProgram by converting each CharString.program in the font to a command list, and back again, and comparing original and final versions.
2019-04-26 09:33:52 -07:00
|
|
|
# e.g replace blend op args with regular args,
|
|
|
|
# and use and discard vsindex op.
|
2018-11-29 17:07:51 -05:00
|
|
|
charstring = charstrings[gname]
|
|
|
|
new_program = []
|
Sparse cff2vf support (#1591)
* Added getter (in the form of a property decorator) for T2Charstring.vsindex. Fixes endless compile loop in some circumstances.
Fixed bug in mutator: need to remove vsindex from snapshotted charstrings, plus formatting clean up
* Fix for subsetting HVAR tables that have an AdvanceWidthMap when the --retain-gid option is used. Needed to make subset_test.py::test_retain_gids_cff2 tests pass.
* in varLib/cffLib.py, add support for sparse sources, and sources with more than one model, and hence more than one VarData element in the VarStore.
CFF2 source fonts with multiple FontDicts in the FDArray need some extra work. With sparse fonts, some of the source fonts may have a fewer FontDicts than the default font. The getfd_map function() builds a map from the FontDict indices in the default font to those in each region font. This is needed when building up the blend value lists in the master font FontDict PrivateDicts, in order to fetch PrivateDict values from the correct FontDict in each region font.
In specializer.py, add support for CFF2 CharStrings with blend operators. 1) In generalizeCommands, convert a blend op to a list of args that are blend lists for the following regular operator. A blend list as a default font value, followed by the delta tuple. 2) In specializeCommands(), convert these back to blend ops, combining as many successive blend lists as allowed by the stack limit.
Add test case for sparse CFF2 sources.
The test font has 55 glyphs. 2 glyphs use only 2 sources (weight = 0 and 100). The rest use 4 source fonts: the two end points of the weight axis, and two intermediate masters. The intermediate masters are only 1 design space unit apart, and are used to change glyph design at the point in design space. For the rest, at most 2 glyphs use the same set of source fonts. There are 12 source fonts.
Add test case for specializer programToCommands() and commandsToProgram by converting each CharString.program in the font to a command list, and back again, and comparing original and final versions.
2019-04-26 09:33:52 -07:00
|
|
|
vsindex = 0
|
2018-11-29 17:07:51 -05:00
|
|
|
last_i = 0
|
|
|
|
for i, token in enumerate(charstring.program):
|
Sparse cff2vf support (#1591)
* Added getter (in the form of a property decorator) for T2Charstring.vsindex. Fixes endless compile loop in some circumstances.
Fixed bug in mutator: need to remove vsindex from snapshotted charstrings, plus formatting clean up
* Fix for subsetting HVAR tables that have an AdvanceWidthMap when the --retain-gid option is used. Needed to make subset_test.py::test_retain_gids_cff2 tests pass.
* in varLib/cffLib.py, add support for sparse sources, and sources with more than one model, and hence more than one VarData element in the VarStore.
CFF2 source fonts with multiple FontDicts in the FDArray need some extra work. With sparse fonts, some of the source fonts may have a fewer FontDicts than the default font. The getfd_map function() builds a map from the FontDict indices in the default font to those in each region font. This is needed when building up the blend value lists in the master font FontDict PrivateDicts, in order to fetch PrivateDict values from the correct FontDict in each region font.
In specializer.py, add support for CFF2 CharStrings with blend operators. 1) In generalizeCommands, convert a blend op to a list of args that are blend lists for the following regular operator. A blend list as a default font value, followed by the delta tuple. 2) In specializeCommands(), convert these back to blend ops, combining as many successive blend lists as allowed by the stack limit.
Add test case for sparse CFF2 sources.
The test font has 55 glyphs. 2 glyphs use only 2 sources (weight = 0 and 100). The rest use 4 source fonts: the two end points of the weight axis, and two intermediate masters. The intermediate masters are only 1 design space unit apart, and are used to change glyph design at the point in design space. For the rest, at most 2 glyphs use the same set of source fonts. There are 12 source fonts.
Add test case for specializer programToCommands() and commandsToProgram by converting each CharString.program in the font to a command list, and back again, and comparing original and final versions.
2019-04-26 09:33:52 -07:00
|
|
|
if token == "vsindex":
|
|
|
|
vsindex = charstring.program[i - 1]
|
|
|
|
if last_i != 0:
|
|
|
|
new_program.extend(charstring.program[last_i : i - 1])
|
|
|
|
last_i = i + 1
|
|
|
|
elif token == "blend":
|
|
|
|
num_regions = charstring.getNumRegions(vsindex)
|
|
|
|
numMasters = 1 + num_regions
|
2018-11-29 17:07:51 -05:00
|
|
|
num_args = charstring.program[i - 1]
|
Sparse cff2vf support (#1591)
* Added getter (in the form of a property decorator) for T2Charstring.vsindex. Fixes endless compile loop in some circumstances.
Fixed bug in mutator: need to remove vsindex from snapshotted charstrings, plus formatting clean up
* Fix for subsetting HVAR tables that have an AdvanceWidthMap when the --retain-gid option is used. Needed to make subset_test.py::test_retain_gids_cff2 tests pass.
* in varLib/cffLib.py, add support for sparse sources, and sources with more than one model, and hence more than one VarData element in the VarStore.
CFF2 source fonts with multiple FontDicts in the FDArray need some extra work. With sparse fonts, some of the source fonts may have a fewer FontDicts than the default font. The getfd_map function() builds a map from the FontDict indices in the default font to those in each region font. This is needed when building up the blend value lists in the master font FontDict PrivateDicts, in order to fetch PrivateDict values from the correct FontDict in each region font.
In specializer.py, add support for CFF2 CharStrings with blend operators. 1) In generalizeCommands, convert a blend op to a list of args that are blend lists for the following regular operator. A blend list as a default font value, followed by the delta tuple. 2) In specializeCommands(), convert these back to blend ops, combining as many successive blend lists as allowed by the stack limit.
Add test case for sparse CFF2 sources.
The test font has 55 glyphs. 2 glyphs use only 2 sources (weight = 0 and 100). The rest use 4 source fonts: the two end points of the weight axis, and two intermediate masters. The intermediate masters are only 1 design space unit apart, and are used to change glyph design at the point in design space. For the rest, at most 2 glyphs use the same set of source fonts. There are 12 source fonts.
Add test case for specializer programToCommands() and commandsToProgram by converting each CharString.program in the font to a command list, and back again, and comparing original and final versions.
2019-04-26 09:33:52 -07:00
|
|
|
# The program list starting at program[i] is now:
|
|
|
|
# ..args for following operations
|
|
|
|
# num_args values from the default font
|
|
|
|
# num_args tuples, each with numMasters-1 delta values
|
|
|
|
# num_blend_args
|
|
|
|
# 'blend'
|
|
|
|
argi = i - (num_args * numMasters + 1)
|
2018-11-29 17:07:51 -05:00
|
|
|
end_args = tuplei = argi + num_args
|
|
|
|
while argi < end_args:
|
|
|
|
next_ti = tuplei + num_regions
|
|
|
|
deltas = charstring.program[tuplei:next_ti]
|
|
|
|
delta = interpolateFromDeltas(vsindex, deltas)
|
|
|
|
charstring.program[argi] += otRound(delta)
|
|
|
|
tuplei = next_ti
|
|
|
|
argi += 1
|
|
|
|
new_program.extend(charstring.program[last_i:end_args])
|
|
|
|
last_i = i + 1
|
|
|
|
if last_i != 0:
|
|
|
|
new_program.extend(charstring.program[last_i:])
|
|
|
|
charstring.program = new_program
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
|
|
|
|
"""Unlike TrueType glyphs, neither advance width nor bounding box
|
|
|
|
info is stored in a CFF2 charstring. The width data exists only in
|
|
|
|
the hmtx and HVAR tables. Since LSB data cannot be interpolated
|
|
|
|
reliably from the master LSB values in the hmtx table, we traverse
|
|
|
|
the charstring to determine the actual bound box."""
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-29 17:07:51 -05:00
|
|
|
charstrings = topDict.CharStrings
|
|
|
|
boundsPen = BoundsPen(glyphOrder)
|
|
|
|
hmtx = varfont["hmtx"]
|
|
|
|
hvar_table = None
|
|
|
|
if "HVAR" in varfont:
|
|
|
|
hvar_table = varfont["HVAR"].table
|
|
|
|
fvar = varfont["fvar"]
|
|
|
|
varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-29 17:07:51 -05:00
|
|
|
for gid, gname in enumerate(glyphOrder):
|
|
|
|
entry = list(hmtx[gname])
|
|
|
|
# get width delta.
|
|
|
|
if hvar_table:
|
|
|
|
if hvar_table.AdvWidthMap:
|
|
|
|
width_idx = hvar_table.AdvWidthMap.mapping[gname]
|
|
|
|
else:
|
|
|
|
width_idx = gid
|
|
|
|
width_delta = otRound(varStoreInstancer[width_idx])
|
|
|
|
else:
|
|
|
|
width_delta = 0
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-29 17:07:51 -05:00
|
|
|
# get LSB.
|
|
|
|
boundsPen.init()
|
|
|
|
charstring = charstrings[gname]
|
|
|
|
charstring.draw(boundsPen)
|
|
|
|
if boundsPen.bounds is None:
|
|
|
|
# Happens with non-marking glyphs
|
|
|
|
lsb_delta = 0
|
|
|
|
else:
|
2021-05-02 23:59:49 +02:00
|
|
|
lsb = otRound(boundsPen.bounds[0])
|
2020-12-07 17:16:15 +00:00
|
|
|
lsb_delta = entry[1] - lsb
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-29 17:07:51 -05:00
|
|
|
if lsb_delta or width_delta:
|
|
|
|
if width_delta:
|
2022-09-20 16:27:54 +02:00
|
|
|
entry[0] = max(0, entry[0] + width_delta)
|
2018-11-29 17:07:51 -05:00
|
|
|
if lsb_delta:
|
|
|
|
entry[1] = lsb
|
|
|
|
hmtx[gname] = tuple(entry)
|
|
|
|
|
|
|
|
|
2019-02-28 12:34:43 -05:00
|
|
|
def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
|
2017-10-15 17:43:06 +02:00
|
|
|
"""Generate a static instance from a variable TTFont and a dictionary
|
|
|
|
defining the desired location along the variable font's axes.
|
|
|
|
The location values must be specified as user-space coordinates, e.g.:
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2024-09-03 17:53:56 +01:00
|
|
|
.. code-block::
|
|
|
|
|
|
|
|
{'wght': 400, 'wdth': 100}
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
By default, a new TTFont object is returned. If ``inplace`` is True, the
|
|
|
|
input varfont is modified and reduced to a static font.
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2019-02-28 12:34:43 -05:00
|
|
|
When the overlap parameter is defined as True,
|
|
|
|
OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1. See
|
|
|
|
https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
|
2017-10-15 17:43:06 +02:00
|
|
|
"""
|
|
|
|
if not inplace:
|
|
|
|
# make a copy to leave input varfont unmodified
|
|
|
|
stream = BytesIO()
|
|
|
|
varfont.save(stream)
|
|
|
|
stream.seek(0)
|
|
|
|
varfont = TTFont(stream)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2016-04-27 01:30:19 -07:00
|
|
|
fvar = varfont["fvar"]
|
2016-08-15 16:13:58 -07:00
|
|
|
axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes}
|
2017-10-15 17:43:06 +02:00
|
|
|
loc = normalizeLocation(location, axes)
|
2017-10-19 13:59:43 -07:00
|
|
|
if "avar" in varfont:
|
|
|
|
maps = varfont["avar"].segments
|
2018-09-11 18:07:15 +02:00
|
|
|
loc = {k: piecewiseLinearMap(v, maps[k]) for k, v in loc.items()}
|
2017-10-19 15:33:19 -07:00
|
|
|
# Quantize to F2Dot14, to avoid surprise interpolations.
|
|
|
|
loc = {k: floatToFixedToFloat(v, 14) for k, v in loc.items()}
|
2016-04-27 01:30:19 -07:00
|
|
|
# Location is normalized now
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Normalized location: %s", loc)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-13 12:48:09 -08:00
|
|
|
if "gvar" in varfont:
|
|
|
|
log.info("Mutating glyf/gvar tables")
|
|
|
|
gvar = varfont["gvar"]
|
|
|
|
glyf = varfont["glyf"]
|
2021-04-13 13:49:12 -06:00
|
|
|
hMetrics = varfont["hmtx"].metrics
|
2021-04-14 13:47:45 -06:00
|
|
|
vMetrics = getattr(varfont.get("vmtx"), "metrics", None)
|
2018-11-13 12:48:09 -08:00
|
|
|
# get list of glyph names in gvar sorted by component depth
|
|
|
|
glyphnames = sorted(
|
|
|
|
gvar.variations.keys(),
|
|
|
|
key=lambda name: (
|
2024-02-06 15:42:21 -07:00
|
|
|
(
|
|
|
|
glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
|
|
|
|
if glyf[name].isComposite()
|
|
|
|
else 0
|
|
|
|
),
|
2018-11-13 12:48:09 -08:00
|
|
|
name,
|
2022-12-13 11:26:36 +00:00
|
|
|
),
|
2018-11-13 12:48:09 -08:00
|
|
|
)
|
|
|
|
for glyphname in glyphnames:
|
|
|
|
variations = gvar.variations[glyphname]
|
2021-04-14 14:09:40 -06:00
|
|
|
coordinates, _ = glyf._getCoordinatesAndControls(
|
|
|
|
glyphname, hMetrics, vMetrics
|
|
|
|
)
|
2018-11-13 12:48:09 -08:00
|
|
|
origCoords, endPts = None, None
|
|
|
|
for var in variations:
|
|
|
|
scalar = supportScalar(loc, var.axes)
|
|
|
|
if not scalar:
|
|
|
|
continue
|
|
|
|
delta = var.coordinates
|
|
|
|
if None in delta:
|
|
|
|
if origCoords is None:
|
2021-04-14 14:09:40 -06:00
|
|
|
origCoords, g = glyf._getCoordinatesAndControls(
|
|
|
|
glyphname, hMetrics, vMetrics
|
|
|
|
)
|
2019-03-11 15:50:16 +00:00
|
|
|
delta = iup_delta(delta, origCoords, g.endPts)
|
2018-11-13 12:48:09 -08:00
|
|
|
coordinates += GlyphCoordinates(delta) * scalar
|
2021-04-14 14:09:40 -06:00
|
|
|
glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics)
|
2018-11-19 20:12:45 -08:00
|
|
|
else:
|
|
|
|
glyf = None
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2022-10-26 14:48:32 -06:00
|
|
|
if "DSIG" in varfont:
|
|
|
|
del varfont["DSIG"]
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-05 13:32:06 +02:00
|
|
|
if "cvar" in varfont:
|
2017-10-20 16:09:40 -04:00
|
|
|
log.info("Mutating cvt/cvar tables")
|
2017-10-05 13:32:06 +02:00
|
|
|
cvar = varfont["cvar"]
|
|
|
|
cvt = varfont["cvt "]
|
|
|
|
deltas = {}
|
|
|
|
for var in cvar.variations:
|
|
|
|
scalar = supportScalar(loc, var.axes)
|
|
|
|
if not scalar:
|
|
|
|
continue
|
|
|
|
for i, c in enumerate(var.coordinates):
|
|
|
|
if c is not None:
|
|
|
|
deltas[i] = deltas.get(i, 0) + scalar * c
|
|
|
|
for i, delta in deltas.items():
|
2018-06-14 17:40:11 +01:00
|
|
|
cvt[i] += otRound(delta)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-13 12:48:09 -08:00
|
|
|
if "CFF2" in varfont:
|
|
|
|
log.info("Mutating CFF2 table")
|
|
|
|
glyphOrder = varfont.getGlyphOrder()
|
2018-11-20 12:44:21 -08:00
|
|
|
CFF2 = varfont["CFF2"]
|
2018-11-19 20:35:34 -08:00
|
|
|
topDict = CFF2.cff.topDictIndex[0]
|
2018-11-29 14:12:50 -05:00
|
|
|
vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
|
2018-11-14 10:20:48 -08:00
|
|
|
interpolateFromDeltas = vsInstancer.interpolateFromDeltas
|
|
|
|
interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
|
2018-12-04 18:34:22 -08:00
|
|
|
CFF2.desubroutinize()
|
2018-11-29 14:12:50 -05:00
|
|
|
interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
|
2018-11-20 12:44:21 -08:00
|
|
|
interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
|
|
|
|
del topDict.rawDict["VarStore"]
|
|
|
|
del topDict.VarStore
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-19 11:12:03 -07:00
|
|
|
if "MVAR" in varfont:
|
2017-10-20 16:09:40 -04:00
|
|
|
log.info("Mutating MVAR table")
|
2017-10-19 11:12:03 -07:00
|
|
|
mvar = varfont["MVAR"].table
|
2017-10-20 13:50:08 -04:00
|
|
|
varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
|
2017-10-19 11:12:03 -07:00
|
|
|
records = mvar.ValueRecord
|
|
|
|
for rec in records:
|
|
|
|
mvarTag = rec.ValueTag
|
2017-10-22 12:19:24 +01:00
|
|
|
if mvarTag not in MVAR_ENTRIES:
|
2017-10-19 11:12:03 -07:00
|
|
|
continue
|
2017-10-22 12:19:24 +01:00
|
|
|
tableTag, itemName = MVAR_ENTRIES[mvarTag]
|
2018-06-14 17:40:11 +01:00
|
|
|
delta = otRound(varStoreInstancer[rec.VarIdx])
|
2017-10-19 11:12:03 -07:00
|
|
|
if not delta:
|
|
|
|
continue
|
2017-10-20 13:50:08 -04:00
|
|
|
setattr(
|
|
|
|
varfont[tableTag],
|
|
|
|
itemName,
|
2018-11-16 18:11:25 -05:00
|
|
|
getattr(varfont[tableTag], itemName) + delta,
|
|
|
|
)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-19 16:22:49 -05:00
|
|
|
log.info("Mutating FeatureVariations")
|
|
|
|
for tableTag in "GSUB", "GPOS":
|
|
|
|
if not tableTag in varfont:
|
|
|
|
continue
|
|
|
|
table = varfont[tableTag].table
|
2020-10-05 18:17:14 +01:00
|
|
|
if not getattr(table, "FeatureVariations", None):
|
2018-11-19 16:22:49 -05:00
|
|
|
continue
|
|
|
|
variations = table.FeatureVariations
|
|
|
|
for record in variations.FeatureVariationRecord:
|
|
|
|
applies = True
|
|
|
|
for condition in record.ConditionSet.ConditionTable:
|
|
|
|
if condition.Format == 1:
|
|
|
|
axisIdx = condition.AxisIndex
|
|
|
|
axisTag = fvar.axes[axisIdx].axisTag
|
|
|
|
Min = condition.FilterRangeMinValue
|
|
|
|
Max = condition.FilterRangeMaxValue
|
|
|
|
v = loc[axisTag]
|
|
|
|
if not (Min <= v <= Max):
|
|
|
|
applies = False
|
2022-12-13 11:26:36 +00:00
|
|
|
else:
|
2018-11-19 16:22:49 -05:00
|
|
|
applies = False
|
|
|
|
if not applies:
|
|
|
|
break
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-19 16:22:49 -05:00
|
|
|
if applies:
|
|
|
|
assert record.FeatureTableSubstitution.Version == 0x00010000
|
|
|
|
for rec in record.FeatureTableSubstitution.SubstitutionRecord:
|
2024-02-06 15:42:21 -07:00
|
|
|
table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = (
|
|
|
|
rec.Feature
|
|
|
|
)
|
2018-11-19 16:22:49 -05:00
|
|
|
break
|
|
|
|
del table.FeatureVariations
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-19 16:22:49 -05:00
|
|
|
if "GDEF" in varfont and varfont["GDEF"].table.Version >= 0x00010003:
|
2017-10-20 16:09:40 -04:00
|
|
|
log.info("Mutating GDEF/GPOS/GSUB tables")
|
2018-11-19 16:22:49 -05:00
|
|
|
gdef = varfont["GDEF"].table
|
|
|
|
instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2019-04-17 19:18:55 +01:00
|
|
|
merger = MutatorMerger(varfont, instancer)
|
2018-11-19 16:42:53 -05:00
|
|
|
merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-19 16:22:49 -05:00
|
|
|
# Downgrade GDEF.
|
|
|
|
del gdef.VarStore
|
|
|
|
gdef.Version = 0x00010002
|
|
|
|
if gdef.MarkGlyphSetsDef is None:
|
|
|
|
del gdef.MarkGlyphSetsDef
|
|
|
|
gdef.Version = 0x00010000
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-19 16:22:49 -05:00
|
|
|
if not (
|
|
|
|
gdef.LigCaretList
|
|
|
|
or gdef.MarkAttachClassDef
|
|
|
|
or gdef.GlyphClassDef
|
|
|
|
or gdef.AttachList
|
|
|
|
or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)
|
|
|
|
):
|
|
|
|
del varfont["GDEF"]
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-16 14:12:26 -05:00
|
|
|
addidef = False
|
2018-11-19 20:12:45 -08:00
|
|
|
if glyf:
|
|
|
|
for glyph in glyf.glyphs.values():
|
|
|
|
if hasattr(glyph, "program"):
|
|
|
|
instructions = glyph.program.getAssembly()
|
|
|
|
# If GETVARIATION opcode is used in bytecode of any glyph add IDEF
|
|
|
|
addidef = any(op.startswith("GETVARIATION") for op in instructions)
|
|
|
|
if addidef:
|
|
|
|
break
|
2019-02-28 12:34:43 -05:00
|
|
|
if overlap:
|
|
|
|
for glyph_name in glyf.keys():
|
|
|
|
glyph = glyf[glyph_name]
|
|
|
|
# Set OVERLAP_COMPOUND bit for compound glyphs
|
|
|
|
if glyph.isComposite():
|
|
|
|
glyph.components[0].flags |= OVERLAP_COMPOUND
|
|
|
|
# Set OVERLAP_SIMPLE bit for simple glyphs
|
|
|
|
elif glyph.numberOfContours > 0:
|
|
|
|
glyph.flags[0] |= flagOverlapSimple
|
2018-11-16 14:12:26 -05:00
|
|
|
if addidef:
|
|
|
|
log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
|
|
|
|
asm = []
|
2018-11-16 17:49:17 -05:00
|
|
|
if "fpgm" in varfont:
|
2018-11-16 14:12:26 -05:00
|
|
|
fpgm = varfont["fpgm"]
|
|
|
|
asm = fpgm.program.getAssembly()
|
|
|
|
else:
|
|
|
|
fpgm = newTable("fpgm")
|
|
|
|
fpgm.program = ttProgram.Program()
|
|
|
|
varfont["fpgm"] = fpgm
|
|
|
|
asm.append("PUSHB[000] 145")
|
|
|
|
asm.append("IDEF[ ]")
|
|
|
|
args = [str(len(loc))]
|
2018-11-16 15:33:33 -05:00
|
|
|
for a in fvar.axes:
|
|
|
|
args.append(str(floatToFixed(loc[a.axisTag], 14)))
|
2018-11-16 14:12:26 -05:00
|
|
|
asm.append("NPUSHW[ ] " + " ".join(args))
|
|
|
|
asm.append("ENDF[ ]")
|
|
|
|
fpgm.program.fromAssembly(asm)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-11-16 14:12:26 -05:00
|
|
|
# Change maxp attributes as IDEF is added
|
2018-11-16 17:49:17 -05:00
|
|
|
if "maxp" in varfont:
|
2018-11-16 14:12:26 -05:00
|
|
|
maxp = varfont["maxp"]
|
2021-02-28 18:19:24 -07:00
|
|
|
setattr(
|
|
|
|
maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0)
|
|
|
|
)
|
|
|
|
setattr(
|
|
|
|
maxp,
|
|
|
|
"maxStackElements",
|
|
|
|
max(len(loc), getattr(maxp, "maxStackElements", 0)),
|
2022-12-13 11:26:36 +00:00
|
|
|
)
|
|
|
|
|
2018-04-18 12:35:54 +01:00
|
|
|
if "name" in varfont:
|
|
|
|
log.info("Pruning name table")
|
|
|
|
exclude = {a.axisNameID for a in fvar.axes}
|
|
|
|
for i in fvar.instances:
|
|
|
|
exclude.add(i.subfamilyNameID)
|
|
|
|
exclude.add(i.postscriptNameID)
|
2019-01-13 14:42:36 +00:00
|
|
|
if "ltag" in varfont:
|
|
|
|
# Drop the whole 'ltag' table if all its language tags are referenced by
|
|
|
|
# name records to be pruned.
|
|
|
|
# TODO: prune unused ltag tags and re-enumerate langIDs accordingly
|
|
|
|
excludedUnicodeLangIDs = [
|
|
|
|
n.langID
|
|
|
|
for n in varfont["name"].names
|
|
|
|
if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
|
|
|
|
]
|
|
|
|
if set(excludedUnicodeLangIDs) == set(range(len((varfont["ltag"].tags)))):
|
|
|
|
del varfont["ltag"]
|
2018-04-18 12:35:54 +01:00
|
|
|
varfont["name"].names[:] = [
|
|
|
|
n for n in varfont["name"].names if n.nameID not in exclude
|
|
|
|
]
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-06-18 19:40:30 +01:00
|
|
|
if "wght" in location and "OS/2" in varfont:
|
2018-06-18 19:44:40 +01:00
|
|
|
varfont["OS/2"].usWeightClass = otRound(max(1, min(location["wght"], 1000)))
|
2018-06-18 19:40:30 +01:00
|
|
|
if "wdth" in location:
|
|
|
|
wdth = location["wdth"]
|
|
|
|
for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
|
mutator: round to closest width class
this is the mapping between the half of each percentage intervals and
the associated usWidthClass:
{56.25: 1,
68.75: 2,
81.25: 3,
93.75: 4,
106.25: 5,
118.75: 6,
137.5: 7,
175.0: 8}
Notice how wdth=80 (in the adjusted test case) will fall in width class
3, instead of 4, because it is < 81.25, thus closer to the nominal 75
than to 87.5.
2018-06-19 14:16:17 +01:00
|
|
|
if wdth < percent:
|
2018-06-18 19:40:30 +01:00
|
|
|
varfont["OS/2"].usWidthClass = widthClass
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
varfont["OS/2"].usWidthClass = 9
|
|
|
|
if "slnt" in location and "post" in varfont:
|
|
|
|
varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Removing variable tables")
|
2017-03-04 23:30:37 -08:00
|
|
|
for tag in ("avar", "cvar", "fvar", "gvar", "HVAR", "MVAR", "VVAR", "STAT"):
|
2016-04-27 01:30:19 -07:00
|
|
|
if tag in varfont:
|
|
|
|
del varfont[tag]
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
return varfont
|
|
|
|
|
|
|
|
|
|
|
|
def main(args=None):
|
2020-05-12 06:31:13 +01:00
|
|
|
"""Instantiate a variation font"""
|
2017-10-15 17:43:06 +02:00
|
|
|
from fontTools import configLogger
|
2018-03-27 14:06:37 +01:00
|
|
|
import argparse
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-03-27 14:06:37 +01:00
|
|
|
parser = argparse.ArgumentParser(
|
2018-11-16 18:11:25 -05:00
|
|
|
"fonttools varLib.mutator", description="Instantiate a variable font"
|
|
|
|
)
|
|
|
|
parser.add_argument("input", metavar="INPUT.ttf", help="Input variable TTF file.")
|
2018-03-27 14:06:37 +01:00
|
|
|
parser.add_argument(
|
2018-11-16 18:11:25 -05:00
|
|
|
"locargs",
|
|
|
|
metavar="AXIS=LOC",
|
|
|
|
nargs="*",
|
|
|
|
help="List of space separated locations. A location consist in "
|
|
|
|
"the name of a variation axis, followed by '=' and a number. E.g.: "
|
|
|
|
" wght=700 wdth=80. The default is the location of the base master.",
|
|
|
|
)
|
2018-03-27 14:06:37 +01:00
|
|
|
parser.add_argument(
|
2018-11-16 18:11:25 -05:00
|
|
|
"-o",
|
|
|
|
"--output",
|
|
|
|
metavar="OUTPUT.ttf",
|
|
|
|
default=None,
|
|
|
|
help="Output instance TTF file (default: INPUT-instance.ttf).",
|
|
|
|
)
|
2022-06-13 22:03:32 +00:00
|
|
|
parser.add_argument(
|
2022-06-14 15:11:47 +01:00
|
|
|
"--no-recalc-timestamp",
|
|
|
|
dest="recalc_timestamp",
|
|
|
|
action="store_false",
|
|
|
|
help="Don't set the output font's timestamp to the current time.",
|
|
|
|
)
|
2018-03-27 14:06:37 +01:00
|
|
|
logging_group = parser.add_mutually_exclusive_group(required=False)
|
|
|
|
logging_group.add_argument(
|
2018-11-16 18:11:25 -05:00
|
|
|
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
|
|
|
)
|
2018-03-27 14:06:37 +01:00
|
|
|
logging_group.add_argument(
|
2018-11-16 18:11:25 -05:00
|
|
|
"-q", "--quiet", action="store_true", help="Turn verbosity off."
|
|
|
|
)
|
2019-02-28 09:50:58 -08:00
|
|
|
parser.add_argument(
|
2019-02-28 09:46:16 -08:00
|
|
|
"--no-overlap",
|
|
|
|
dest="overlap",
|
|
|
|
action="store_false",
|
|
|
|
help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags.",
|
|
|
|
)
|
2018-03-27 14:06:37 +01:00
|
|
|
options = parser.parse_args(args)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2018-03-27 14:06:37 +01:00
|
|
|
varfilename = options.input
|
|
|
|
outfile = (
|
|
|
|
os.path.splitext(varfilename)[0] + "-instance.ttf"
|
|
|
|
if not options.output
|
|
|
|
else options.output
|
|
|
|
)
|
|
|
|
configLogger(
|
|
|
|
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
|
|
|
|
)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
loc = {}
|
2018-03-27 14:06:37 +01:00
|
|
|
for arg in options.locargs:
|
|
|
|
try:
|
|
|
|
tag, val = arg.split("=")
|
|
|
|
assert len(tag) <= 4
|
|
|
|
loc[tag.ljust(4)] = float(val)
|
|
|
|
except (ValueError, AssertionError):
|
|
|
|
parser.error("invalid location argument format: %r" % arg)
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Location: %s", loc)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Loading variable font")
|
2022-06-14 15:11:47 +01:00
|
|
|
varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2019-02-28 09:46:16 -08:00
|
|
|
instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2017-10-15 17:43:06 +02:00
|
|
|
log.info("Saving instance font %s", outfile)
|
2016-04-27 01:30:19 -07:00
|
|
|
varfont.save(outfile)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import sys
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2016-04-27 01:30:19 -07:00
|
|
|
if len(sys.argv) > 1:
|
2017-01-11 12:10:58 +00:00
|
|
|
sys.exit(main())
|
2017-01-11 12:24:04 +00:00
|
|
|
import doctest
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2016-04-27 01:30:19 -07:00
|
|
|
sys.exit(doctest.testmod().failed)
|