fonttools/Lib/fontTools/varLib/avarPlanner.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1005 lines
27 KiB
Python
Raw Normal View History

from fontTools.ttLib import newTable
2023-07-24 13:00:03 -06:00
from fontTools.ttLib.tables._f_v_a_r import Axis as fvarAxis
2023-07-22 21:18:52 -06:00
from fontTools.pens.areaPen import AreaPen
from fontTools.pens.basePen import NullPen
from fontTools.pens.statisticsPen import StatisticsPen
2023-07-23 12:09:02 -06:00
from fontTools.varLib.models import piecewiseLinearMap, normalizeValue
2023-07-22 21:18:52 -06:00
from fontTools.misc.cliTools import makeOutputFileName
import math
import logging
from pprint import pformat
__all__ = [
"planWeightAxis",
"planWidthAxis",
"planSlantAxis",
2023-07-24 13:11:14 -06:00
"planOpticalSizeAxis",
"planAxis",
"sanitizeWeight",
"sanitizeWidth",
"sanitizeSlant",
"measureWeight",
"measureWidth",
"measureSlant",
2023-07-24 13:59:17 -06:00
"normalizeLinear",
"normalizeLog",
2023-07-24 14:18:22 -06:00
"normalizeDegrees",
"interpolateLinear",
"interpolateLog",
2023-07-24 14:53:14 -06:00
"processAxis",
"makeDesignspaceSnippet",
"addEmptyAvar",
"main",
]
log = logging.getLogger("fontTools.varLib.avarPlanner")
2023-07-22 21:18:52 -06:00
WEIGHTS = [
50,
100,
150,
200,
250,
300,
350,
400,
450,
500,
550,
600,
650,
700,
750,
800,
850,
900,
950,
]
2023-07-22 21:18:52 -06:00
WIDTHS = [
25.0,
37.5,
50.0,
62.5,
75.0,
87.5,
100.0,
112.5,
125.0,
137.5,
150.0,
162.5,
175.0,
187.5,
200.0,
]
2023-07-22 21:18:52 -06:00
SLANTS = list(math.degrees(math.atan(d / 20.0)) for d in range(-20, 21))
2023-07-24 13:11:14 -06:00
SIZES = [
2023-07-26 12:49:23 -06:00
5,
6,
7,
2023-07-24 13:11:14 -06:00
8,
9,
10,
11,
12,
14,
18,
24,
30,
36,
48,
60,
72,
96,
2023-07-24 13:59:17 -06:00
120,
144,
2023-07-26 12:49:23 -06:00
192,
240,
288,
2023-07-24 13:11:14 -06:00
]
SAMPLES = 8
2023-07-23 10:23:39 -06:00
2023-07-24 13:59:17 -06:00
def normalizeLinear(value, rangeMin, rangeMax):
"""Linearly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
return (value - rangeMin) / (rangeMax - rangeMin)
def interpolateLinear(t, a, b):
"""Linear interpolation between a and b, with t typically in [0, 1]."""
return a + t * (b - a)
2023-07-24 13:59:17 -06:00
def normalizeLog(value, rangeMin, rangeMax):
"""Logarithmically normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
logMin = math.log(rangeMin)
logMax = math.log(rangeMax)
return (math.log(value) - logMin) / (logMax - logMin)
def interpolateLog(t, a, b):
"""Logarithmic interpolation between a and b, with t typically in [0, 1]."""
logA = math.log(a)
logB = math.log(b)
return math.exp(logA + t * (logB - logA))
2023-07-24 14:18:22 -06:00
def normalizeDegrees(value, rangeMin, rangeMax):
"""Angularly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
tanMin = math.tan(math.radians(rangeMin))
tanMax = math.tan(math.radians(rangeMax))
return (math.tan(math.radians(value)) - tanMin) / (tanMax - tanMin)
def measureWeight(glyphset, glyphs=None):
"""Measure the perceptual average weight of the given glyphs."""
2023-07-23 10:38:33 -06:00
if isinstance(glyphs, dict):
frequencies = glyphs
else:
frequencies = {g: 1 for g in glyphs}
2023-07-22 21:18:52 -06:00
wght_sum = wdth_sum = 0
2023-07-23 10:38:33 -06:00
for glyph_name in glyphs:
if frequencies is not None:
frequency = frequencies.get(glyph_name, 0)
if frequency == 0:
continue
else:
frequency = 1
2023-07-22 21:18:52 -06:00
glyph = glyphset[glyph_name]
pen = AreaPen(glyphset=glyphset)
glyph.draw(pen)
mult = glyph.width * frequency
wght_sum += mult * abs(pen.value)
wdth_sum += mult
2023-07-22 21:18:52 -06:00
return wght_sum / wdth_sum
def measureWidth(glyphset, glyphs=None):
"""Measure the average width of the given glyphs."""
if isinstance(glyphs, dict):
frequencies = glyphs
else:
frequencies = {g: 1 for g in glyphs}
wdth_sum = 0
freq_sum = 0
for glyph_name in glyphs:
if frequencies is not None:
frequency = frequencies.get(glyph_name, 0)
if frequency == 0:
continue
else:
frequency = 1
glyph = glyphset[glyph_name]
pen = NullPen()
glyph.draw(pen)
wdth_sum += glyph.width * frequency
freq_sum += frequency
return wdth_sum / freq_sum
def measureSlant(glyphset, glyphs=None):
"""Measure the perceptual average slant angle of the given glyphs."""
if isinstance(glyphs, dict):
frequencies = glyphs
else:
frequencies = {g: 1 for g in glyphs}
slnt_sum = 0
freq_sum = 0
for glyph_name in glyphs:
if frequencies is not None:
frequency = frequencies.get(glyph_name, 0)
if frequency == 0:
continue
else:
frequency = 1
glyph = glyphset[glyph_name]
pen = StatisticsPen(glyphset=glyphset)
glyph.draw(pen)
mult = glyph.width * frequency
slnt_sum += mult * pen.slant
freq_sum += mult
return -math.degrees(math.atan(slnt_sum / freq_sum))
def sanitizeWidth(userTriple, designTriple, pins, measurements):
"""Sanitize the width axis limits."""
minVal, defaultVal, maxVal = (
measurements[designTriple[0]],
measurements[designTriple[1]],
measurements[designTriple[2]],
)
calculatedMinVal = userTriple[1] * (minVal / defaultVal)
calculatedMaxVal = userTriple[1] * (maxVal / defaultVal)
log.info("Original width axis limits: %g:%g:%g", *userTriple)
log.info(
"Calculated width axis limits: %g:%g:%g",
calculatedMinVal,
userTriple[1],
calculatedMaxVal,
)
if (
abs(calculatedMinVal - userTriple[0]) / userTriple[1] > 0.05
or abs(calculatedMaxVal - userTriple[2]) / userTriple[1] > 0.05
):
log.warning("Calculated width axis min/max do not match user input.")
log.warning(
" Current width axis limits: %g:%g:%g",
*userTriple,
)
log.warning(
2023-07-23 19:12:08 -06:00
" Suggested width axis limits: %g:%g:%g",
calculatedMinVal,
userTriple[1],
calculatedMaxVal,
)
return False
return True
def sanitizeWeight(userTriple, designTriple, pins, measurements):
"""Sanitize the weight axis limits."""
if len(set(userTriple)) < 3:
return True
minVal, defaultVal, maxVal = (
measurements[designTriple[0]],
measurements[designTriple[1]],
measurements[designTriple[2]],
)
logMin = math.log(minVal)
logDefault = math.log(defaultVal)
logMax = math.log(maxVal)
t = (userTriple[1] - userTriple[0]) / (userTriple[2] - userTriple[0])
y = math.exp(logMin + t * (logMax - logMin))
t = (y - minVal) / (maxVal - minVal)
calculatedDefaultVal = userTriple[0] + t * (userTriple[2] - userTriple[0])
log.info("Original weight axis limits: %g:%g:%g", *userTriple)
log.info(
"Calculated weight axis limits: %g:%g:%g",
userTriple[0],
calculatedDefaultVal,
userTriple[2],
)
if abs(calculatedDefaultVal - userTriple[1]) / userTriple[1] > 0.05:
log.warning("Calculated weight axis default does not match user input.")
log.warning(
" Current weight axis limits: %g:%g:%g",
*userTriple,
)
log.warning(
2023-07-23 19:12:08 -06:00
" Suggested weight axis limits, changing default: %g:%g:%g",
userTriple[0],
calculatedDefaultVal,
userTriple[2],
)
t = (userTriple[2] - userTriple[0]) / (userTriple[1] - userTriple[0])
y = math.exp(logMin + t * (logDefault - logMin))
t = (y - minVal) / (defaultVal - minVal)
calculatedMaxVal = userTriple[0] + t * (userTriple[1] - userTriple[0])
log.warning(
2023-07-23 19:12:08 -06:00
" Suggested weight axis limits, changing maximum: %g:%g:%g",
userTriple[0],
userTriple[1],
calculatedMaxVal,
)
t = (userTriple[0] - userTriple[2]) / (userTriple[1] - userTriple[2])
y = math.exp(logMax + t * (logDefault - logMax))
t = (y - maxVal) / (defaultVal - maxVal)
calculatedMinVal = userTriple[2] + t * (userTriple[1] - userTriple[2])
log.warning(
2023-07-23 19:12:08 -06:00
" Suggested weight axis limits, changing minimum: %g:%g:%g",
calculatedMinVal,
userTriple[1],
userTriple[2],
)
return False
return True
def sanitizeSlant(userTriple, designTriple, pins, measurements):
"""Sanitize the slant axis limits."""
log.info("Original slant axis limits: %g:%g:%g", *userTriple)
log.info(
"Calculated slant axis limits: %g:%g:%g",
measurements[designTriple[0]],
measurements[designTriple[1]],
measurements[designTriple[2]],
)
if (
abs(measurements[designTriple[0]] - userTriple[0]) > 1
or abs(measurements[designTriple[1]] - userTriple[1]) > 1
or abs(measurements[designTriple[2]] - userTriple[2]) > 1
):
log.warning("Calculated slant axis min/default/max do not match user input.")
log.warning(
" Current slant axis limits: %g:%g:%g",
*userTriple,
)
log.warning(
2023-07-23 19:12:08 -06:00
" Suggested slant axis limits: %g:%g:%g",
measurements[designTriple[0]],
measurements[designTriple[1]],
measurements[designTriple[2]],
)
return False
return True
def planAxis(
measureFunc,
2023-07-24 13:59:17 -06:00
normalizeFunc,
interpolateFunc,
glyphSetFunc,
axisTag,
2023-07-24 13:00:03 -06:00
axisLimits,
2023-07-24 16:51:44 -06:00
values,
samples=None,
glyphs=None,
designLimits=None,
2023-07-23 12:09:02 -06:00
pins=None,
sanitizeFunc=None,
):
2023-07-24 16:51:44 -06:00
"""Plan an axis.
measureFunc: callable that takes a glyphset and an optional
list of glyphnames, and returns the glyphset-wide measurement
to be used for the axis.
normalizeFunc: callable that takes a measurement and a minimum
and maximum, and normalizes the measurement into the range 0..1,
possibly extrapolating too.
interpolateFunc: callable that takes a normalized t value, and a
minimum and maximum, and returns the interpolated value,
possibly extrapolating too.
glyphSetFunc: callable that takes a variations "location" dictionary,
and returns a glyphset.
axisTag: the axis tag string.
axisLimits: a triple of minimum, default, and maximum values for
the axis. Or an `fvar` Axis object.
values: a list of output values to map for this axis.
samples: the number of samples to use when sampling. Default 8.
glyphs: a list of glyph names to use when sampling. Defaults to None,
which will process all glyphs.
designLimits: an optional triple of minimum, default, and maximum values
represenging the "design" limits for the axis. If not provided, the
axisLimits will be used.
pins: an optional dictionary of before/after mapping entries to pin in
the output.
sanitizeFunc: an optional callable to call to sanitize the axis limits.
"""
2023-07-24 13:00:03 -06:00
if isinstance(axisLimits, fvarAxis):
axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue)
minValue, defaultValue, maxValue = axisLimits
2023-07-23 10:27:19 -06:00
if samples is None:
samples = SAMPLES
2023-07-23 10:38:33 -06:00
if glyphs is None:
glyphs = glyphSetFunc({}).keys()
2023-07-23 12:09:02 -06:00
if pins is None:
pins = {}
else:
pins = pins.copy()
2023-07-23 10:27:19 -06:00
2023-07-24 15:13:34 -06:00
log.info(
"Axis limits min %g / default %g / max %g", minValue, defaultValue, maxValue
)
triple = (minValue, defaultValue, maxValue)
if designLimits is not None:
2023-07-24 15:00:00 -06:00
log.info("Axis design-limits min %g / default %g / max %g", *designLimits)
else:
designLimits = triple
2023-07-22 21:18:52 -06:00
2023-07-23 12:09:02 -06:00
if pins:
log.info("Pins %s", sorted(pins.items()))
pins.update(
{
minValue: designLimits[0],
defaultValue: designLimits[1],
maxValue: designLimits[2],
}
)
2023-07-23 12:09:02 -06:00
2023-07-22 21:18:52 -06:00
out = {}
outNormalized = {}
axisMeasurements = {}
2023-07-23 19:16:44 -06:00
for value in sorted({minValue, defaultValue, maxValue} | set(pins.keys())):
glyphset = glyphSetFunc(location={axisTag: value})
2023-07-23 19:16:44 -06:00
designValue = pins[value]
axisMeasurements[designValue] = measureFunc(glyphset, glyphs)
2023-07-22 21:18:52 -06:00
if sanitizeFunc is not None:
log.info("Sanitizing axis limit values for the `%s` axis.", axisTag)
sanitizeFunc(triple, designLimits, pins, axisMeasurements)
log.debug("Calculated average value:\n%s", pformat(axisMeasurements))
2023-07-22 21:18:52 -06:00
2023-07-23 12:09:02 -06:00
for (rangeMin, targetMin), (rangeMax, targetMax) in zip(
list(sorted(pins.items()))[:-1],
list(sorted(pins.items()))[1:],
):
targetValues = {w for w in values if rangeMin < w < rangeMax}
if not targetValues:
2023-07-22 21:18:52 -06:00
continue
2023-07-23 12:09:02 -06:00
normalizedMin = normalizeValue(rangeMin, triple)
normalizedMax = normalizeValue(rangeMax, triple)
normalizedTargetMin = normalizeValue(targetMin, designLimits)
normalizedTargetMax = normalizeValue(targetMax, designLimits)
2023-07-22 21:18:52 -06:00
log.info("Planning target values %s.", sorted(targetValues))
2023-07-23 10:27:19 -06:00
log.info("Sampling %u points in range %g,%g.", samples, rangeMin, rangeMax)
valueMeasurements = axisMeasurements.copy()
2023-07-23 10:27:19 -06:00
for sample in range(1, samples + 1):
value = rangeMin + (rangeMax - rangeMin) * sample / (samples + 1)
log.debug("Sampling value %g.", value)
glyphset = glyphSetFunc(location={axisTag: value})
designValue = piecewiseLinearMap(value, pins)
valueMeasurements[designValue] = measureFunc(glyphset, glyphs)
log.debug("Sampled average value:\n%s", pformat(valueMeasurements))
measurementValue = {}
for value in sorted(valueMeasurements):
measurementValue[valueMeasurements[value]] = value
2023-07-23 12:09:02 -06:00
out[rangeMin] = targetMin
outNormalized[normalizedMin] = normalizedTargetMin
for value in sorted(targetValues):
2023-07-24 13:59:17 -06:00
t = normalizeFunc(value, rangeMin, rangeMax)
targetMeasurement = interpolateFunc(
t, valueMeasurements[targetMin], valueMeasurements[targetMax]
)
targetValue = piecewiseLinearMap(targetMeasurement, measurementValue)
log.debug("Planned mapping value %g to %g." % (value, targetValue))
out[value] = targetValue
2023-07-24 13:59:17 -06:00
valueNormalized = normalizedMin + (value - rangeMin) / (
rangeMax - rangeMin
) * (normalizedMax - normalizedMin)
outNormalized[valueNormalized] = normalizedTargetMin + (
targetValue - targetMin
) / (targetMax - targetMin) * (normalizedTargetMax - normalizedTargetMin)
2023-07-23 12:09:02 -06:00
out[rangeMax] = targetMax
outNormalized[normalizedMax] = normalizedTargetMax
2023-07-22 21:18:52 -06:00
log.info("Planned mapping for the `%s` axis:\n%s", axisTag, pformat(out))
log.info(
"Planned normalized mapping for the `%s` axis:\n%s",
axisTag,
pformat(outNormalized),
)
2023-07-24 14:18:22 -06:00
if all(abs(k - v) < 0.01 for k, v in outNormalized.items()):
log.info("Detected identity mapping for the `%s` axis. Dropping.", axisTag)
out = {}
outNormalized = {}
2023-07-22 21:18:52 -06:00
return out, outNormalized
def planWeightAxis(
glyphSetFunc,
2023-07-24 13:00:03 -06:00
axisLimits,
weights=None,
samples=None,
glyphs=None,
designLimits=None,
pins=None,
sanitize=False,
):
2023-07-24 16:59:27 -06:00
"""Plan a weight (`wght`) axis.
weights: A list of weight values to plan for. If None, the default
values are used.
This function simply calls planAxis with values=weights, and the appropriate
arguments. See documenation for planAxis for more information.
"""
if weights is None:
weights = WEIGHTS
return planAxis(
measureWeight,
2023-07-24 13:59:17 -06:00
normalizeLinear,
interpolateLog,
glyphSetFunc,
"wght",
2023-07-24 13:00:03 -06:00
axisLimits,
values=weights,
samples=samples,
glyphs=glyphs,
designLimits=designLimits,
pins=pins,
sanitizeFunc=sanitizeWeight if sanitize else None,
)
def planWidthAxis(
glyphSetFunc,
2023-07-24 13:00:03 -06:00
axisLimits,
widths=None,
samples=None,
glyphs=None,
designLimits=None,
pins=None,
sanitize=False,
):
2023-07-24 16:59:27 -06:00
"""Plan a width (`wdth`) axis.
widths: A list of width values (percentages) to plan for. If None, the default
values are used.
This function simply calls planAxis with values=widths, and the appropriate
arguments. See documenation for planAxis for more information.
"""
if widths is None:
widths = WIDTHS
return planAxis(
measureWidth,
2023-07-24 13:59:17 -06:00
normalizeLinear,
interpolateLinear,
glyphSetFunc,
"wdth",
2023-07-24 13:00:03 -06:00
axisLimits,
values=widths,
samples=samples,
glyphs=glyphs,
designLimits=designLimits,
pins=pins,
sanitizeFunc=sanitizeWidth if sanitize else None,
)
def planSlantAxis(
glyphSetFunc,
2023-07-24 13:00:03 -06:00
axisLimits,
slants=None,
samples=None,
glyphs=None,
designLimits=None,
pins=None,
sanitize=False,
):
2023-07-24 16:59:27 -06:00
"""Plan a slant (`slnt`) axis.
slants: A list slant angles to plan for. If None, the default
values are used.
This function simply calls planAxis with values=slants, and the appropriate
arguments. See documenation for planAxis for more information.
"""
if slants is None:
slants = SLANTS
return planAxis(
measureSlant,
2023-07-24 14:18:22 -06:00
normalizeDegrees,
interpolateLinear,
glyphSetFunc,
"slnt",
2023-07-24 13:00:03 -06:00
axisLimits,
values=slants,
samples=samples,
glyphs=glyphs,
designLimits=designLimits,
pins=pins,
sanitizeFunc=sanitizeSlant if sanitize else None,
)
2023-07-24 13:11:14 -06:00
def planOpticalSizeAxis(
glyphSetFunc,
axisLimits,
sizes=None,
samples=None,
glyphs=None,
designLimits=None,
pins=None,
sanitize=False,
):
2023-07-24 16:59:27 -06:00
"""Plan a optical-size (`opsz`) axis.
sizes: A list of optical size values to plan for. If None, the default
values are used.
This function simply calls planAxis with values=sizes, and the appropriate
arguments. See documenation for planAxis for more information.
"""
2023-07-24 13:11:14 -06:00
if sizes is None:
sizes = SIZES
return planAxis(
measureWeight,
2023-07-24 13:59:17 -06:00
normalizeLog,
interpolateLog,
2023-07-24 13:11:14 -06:00
glyphSetFunc,
"opsz",
2023-07-24 13:11:14 -06:00
axisLimits,
values=sizes,
samples=samples,
glyphs=glyphs,
designLimits=designLimits,
pins=pins,
)
def makeDesignspaceSnippet(axisTag, axisName, axisLimit, mapping):
"""Make a designspace snippet for a single axis."""
designspaceSnippet = (
' <axis tag="%s" name="%s" minimum="%g" default="%g" maximum="%g"'
% ((axisTag, axisName) + axisLimit)
)
if mapping:
designspaceSnippet += ">\n"
else:
designspaceSnippet += "/>"
for key, value in mapping.items():
designspaceSnippet += ' <map input="%g" output="%g"/>\n' % (key, value)
if mapping:
designspaceSnippet += " </axis>"
return designspaceSnippet
2023-07-23 10:19:35 -06:00
def addEmptyAvar(font):
"""Add an empty `avar` table to the font."""
font["avar"] = avar = newTable("avar")
2023-07-23 10:19:35 -06:00
for axis in fvar.axes:
avar.segments[axis.axisTag] = {}
2023-07-23 10:19:35 -06:00
2023-07-24 14:53:14 -06:00
def processAxis(
font,
planFunc,
axisTag,
axisName,
values,
samples=None,
glyphs=None,
designLimits=None,
pins=None,
sanitize=False,
plot=False,
):
2023-07-24 14:58:14 -06:00
"""Process a single axis."""
2023-07-24 15:56:44 -06:00
axisLimits = None
for axis in font["fvar"].axes:
if axis.axisTag == axisTag:
axisLimits = axis
break
2023-07-24 14:53:14 -06:00
if axisLimits is None:
return ""
2023-07-24 15:56:44 -06:00
axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue)
2023-07-24 14:53:14 -06:00
log.info("Planning %s axis.", axisName)
if "avar" in font:
existingMapping = font["avar"].segments[axisTag]
font["avar"].segments[axisTag] = {}
else:
existingMapping = None
if values is not None and isinstance(values, str):
values = [float(w) for w in values.split()]
if designLimits is not None and isinstance(designLimits, str):
designLimits = [float(d) for d in options.designLimits.split(":")]
assert (
len(designLimits) == 3
and designLimits[0] <= designLimits[1] <= designLimits[2]
)
else:
designLimits = None
if pins is not None and isinstance(pins, str):
newPins = {}
for pin in pins.split():
before, after = pin.split(":")
newPins[float(before)] = float(after)
pins = newPins
del newPins
mapping, mappingNormalized = planFunc(
font.getGlyphSet,
axisLimits,
values,
samples=samples,
glyphs=glyphs,
designLimits=designLimits,
pins=pins,
sanitize=sanitize,
)
if plot:
from matplotlib import pyplot
pyplot.plot(
sorted(mappingNormalized),
[mappingNormalized[k] for k in sorted(mappingNormalized)],
)
pyplot.show()
if existingMapping is not None:
log.info("Existing %s mapping:\n%s", axisName, pformat(existingMapping))
if mapping:
if "avar" not in font:
addEmptyAvar(font)
font["avar"].segments[axisTag] = mappingNormalized
else:
if "avar" in font:
font["avar"].segments[axisTag] = {}
designspaceSnippet = makeDesignspaceSnippet(
axisTag,
axisName,
axisLimits,
mapping,
)
return designspaceSnippet
2023-07-22 21:18:52 -06:00
def main(args=None):
2023-07-24 13:11:14 -06:00
"""Plan the standard axis mappings for a variable font"""
2023-07-22 21:18:52 -06:00
if args is None:
import sys
args = sys.argv[1:]
from fontTools import configLogger
2023-07-22 21:18:52 -06:00
from fontTools.ttLib import TTFont
import argparse
parser = argparse.ArgumentParser(
"fonttools varLib.avarPlanner",
description="Plan `avar` table for variable font",
)
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
parser.add_argument(
"-o",
"--output-file",
type=str,
help="Output font file name.",
)
parser.add_argument(
"--weights", type=str, help="Space-separate list of weights to generate."
)
parser.add_argument(
"--widths", type=str, help="Space-separate list of widths to generate."
)
parser.add_argument(
"--slants", type=str, help="Space-separate list of slants to generate."
)
2023-07-24 13:11:14 -06:00
parser.add_argument(
"--sizes", type=str, help="Space-separate list of optical-sizes to generate."
)
parser.add_argument("--samples", type=int, help="Number of samples.")
parser.add_argument(
"-s", "--sanitize", action="store_true", help="Sanitize axis limits"
)
parser.add_argument(
"-g",
"--glyphs",
type=str,
help="Space-separate list of glyphs to use for sampling.",
)
parser.add_argument(
"--weight-design-limits",
type=str,
2023-07-23 15:47:03 -06:00
help="min:default:max in design units for the `wght` axis.",
)
2023-07-23 12:09:02 -06:00
parser.add_argument(
"--width-design-limits",
2023-07-23 12:09:02 -06:00
type=str,
2023-07-23 15:47:03 -06:00
help="min:default:max in design units for the `wdth` axis.",
)
parser.add_argument(
"--slant-design-limits",
type=str,
help="min:default:max in design units for the `slnt` axis.",
)
2023-07-24 13:11:14 -06:00
parser.add_argument(
"--optical-size-design-limits",
type=str,
help="min:default:max in design units for the `opsz` axis.",
)
2023-07-23 15:47:03 -06:00
parser.add_argument(
"--weight-pins",
type=str,
2023-07-24 09:52:52 -06:00
help="Space-separate list of before:after pins for the `wght` axis.",
2023-07-23 12:09:02 -06:00
)
parser.add_argument(
"--width-pins",
type=str,
2023-07-24 09:52:52 -06:00
help="Space-separate list of before:after pins for the `wdth` axis.",
)
parser.add_argument(
"--slant-pins",
type=str,
2023-07-24 09:52:52 -06:00
help="Space-separate list of before:after pins for the `slnt` axis.",
)
2023-07-24 13:11:14 -06:00
parser.add_argument(
"--optical-size-pins",
type=str,
help="Space-separate list of before:after pins for the `opsz` axis.",
)
2023-07-23 09:41:29 -06:00
parser.add_argument(
"-p", "--plot", action="store_true", help="Plot the resulting mapping."
)
2023-07-22 21:18:52 -06:00
logging_group = parser.add_mutually_exclusive_group(required=False)
logging_group.add_argument(
"-v", "--verbose", action="store_true", help="Run more verbosely."
)
logging_group.add_argument(
"-q", "--quiet", action="store_true", help="Turn verbosity off."
)
2023-07-22 21:18:52 -06:00
options = parser.parse_args(args)
configLogger(
level=("DEBUG" if options.verbose else "WARNING" if options.quiet else "INFO")
)
2023-07-22 21:18:52 -06:00
font = TTFont(options.font)
if not "fvar" in font:
log.error("Not a variable font.")
return 1
2023-07-22 21:18:52 -06:00
if options.glyphs is not None:
glyphs = options.glyphs.split()
if ":" in options.glyphs:
glyphs = {}
for g in options.glyphs.split():
if ":" in g:
glyph, frequency = g.split(":")
glyphs[glyph] = float(frequency)
else:
glyphs[g] = 1.0
else:
glyphs = None
2023-07-24 14:53:14 -06:00
designspaceSnippets = []
2023-07-24 14:53:14 -06:00
designspaceSnippets.append(
processAxis(
font,
planWeightAxis,
"wght",
"Weight",
values=options.weights,
samples=options.samples,
glyphs=glyphs,
2023-07-24 14:53:14 -06:00
designLimits=options.weight_design_limits,
pins=options.weight_pins,
sanitize=options.sanitize,
2023-07-24 14:53:14 -06:00
plot=options.plot,
)
2023-07-24 14:53:14 -06:00
)
designspaceSnippets.append(
processAxis(
font,
planWidthAxis,
"wdth",
"Width",
values=options.widths,
2023-07-23 10:27:19 -06:00
samples=options.samples,
2023-07-23 10:38:33 -06:00
glyphs=glyphs,
2023-07-24 14:53:14 -06:00
designLimits=options.width_design_limits,
pins=options.width_pins,
sanitize=options.sanitize,
2023-07-24 14:53:14 -06:00
plot=options.plot,
)
2023-07-24 14:53:14 -06:00
)
designspaceSnippets.append(
processAxis(
font,
planSlantAxis,
"slnt",
"Slant",
values=options.slants,
samples=options.samples,
glyphs=glyphs,
2023-07-24 14:53:14 -06:00
designLimits=options.slant_design_limits,
pins=options.slant_pins,
sanitize=options.sanitize,
2023-07-24 14:53:14 -06:00
plot=options.plot,
)
2023-07-24 14:53:14 -06:00
)
designspaceSnippets.append(
processAxis(
font,
planOpticalSizeAxis,
"opsz",
"OpticalSize",
values=options.sizes,
2023-07-24 13:11:14 -06:00
samples=options.samples,
glyphs=glyphs,
2023-07-24 14:53:14 -06:00
designLimits=options.optical_size_design_limits,
pins=options.optical_size_pins,
2023-07-24 13:11:14 -06:00
sanitize=options.sanitize,
2023-07-24 14:53:14 -06:00
plot=options.plot,
2023-07-24 13:11:14 -06:00
)
2023-07-24 14:53:14 -06:00
)
2023-07-22 21:18:52 -06:00
log.info("Designspace snippet:")
2023-07-24 14:53:14 -06:00
for snippet in designspaceSnippets:
if snippet:
print(snippet)
2023-07-24 13:11:14 -06:00
if options.output_file is None:
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar")
else:
outfile = options.output_file
if outfile:
log.info("Saving %s", outfile)
font.save(outfile)
2023-07-22 21:18:52 -06:00
if __name__ == "__main__":
import sys
sys.exit(main())