Spaces:
Running
Running
from fontTools.ttLib import newTable | |
from fontTools.ttLib.tables._f_v_a_r import Axis as fvarAxis | |
from fontTools.pens.areaPen import AreaPen | |
from fontTools.pens.basePen import NullPen | |
from fontTools.pens.statisticsPen import StatisticsPen | |
from fontTools.varLib.models import piecewiseLinearMap, normalizeValue | |
from fontTools.misc.cliTools import makeOutputFileName | |
import math | |
import logging | |
from pprint import pformat | |
__all__ = [ | |
"planWeightAxis", | |
"planWidthAxis", | |
"planSlantAxis", | |
"planOpticalSizeAxis", | |
"planAxis", | |
"sanitizeWeight", | |
"sanitizeWidth", | |
"sanitizeSlant", | |
"measureWeight", | |
"measureWidth", | |
"measureSlant", | |
"normalizeLinear", | |
"normalizeLog", | |
"normalizeDegrees", | |
"interpolateLinear", | |
"interpolateLog", | |
"processAxis", | |
"makeDesignspaceSnippet", | |
"addEmptyAvar", | |
"main", | |
] | |
log = logging.getLogger("fontTools.varLib.avarPlanner") | |
WEIGHTS = [ | |
50, | |
100, | |
150, | |
200, | |
250, | |
300, | |
350, | |
400, | |
450, | |
500, | |
550, | |
600, | |
650, | |
700, | |
750, | |
800, | |
850, | |
900, | |
950, | |
] | |
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, | |
] | |
SLANTS = list(math.degrees(math.atan(d / 20.0)) for d in range(-20, 21)) | |
SIZES = [ | |
5, | |
6, | |
7, | |
8, | |
9, | |
10, | |
11, | |
12, | |
14, | |
18, | |
24, | |
30, | |
36, | |
48, | |
60, | |
72, | |
96, | |
120, | |
144, | |
192, | |
240, | |
288, | |
] | |
SAMPLES = 8 | |
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) | |
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)) | |
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.""" | |
if isinstance(glyphs, dict): | |
frequencies = glyphs | |
else: | |
frequencies = {g: 1 for g in glyphs} | |
wght_sum = wdth_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 = AreaPen(glyphset=glyphset) | |
glyph.draw(pen) | |
mult = glyph.width * frequency | |
wght_sum += mult * abs(pen.value) | |
wdth_sum += mult | |
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( | |
" 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( | |
" 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( | |
" 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( | |
" 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( | |
" Suggested slant axis limits: %g:%g:%g", | |
measurements[designTriple[0]], | |
measurements[designTriple[1]], | |
measurements[designTriple[2]], | |
) | |
return False | |
return True | |
def planAxis( | |
measureFunc, | |
normalizeFunc, | |
interpolateFunc, | |
glyphSetFunc, | |
axisTag, | |
axisLimits, | |
values, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitizeFunc=None, | |
): | |
"""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. | |
""" | |
if isinstance(axisLimits, fvarAxis): | |
axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue) | |
minValue, defaultValue, maxValue = axisLimits | |
if samples is None: | |
samples = SAMPLES | |
if glyphs is None: | |
glyphs = glyphSetFunc({}).keys() | |
if pins is None: | |
pins = {} | |
else: | |
pins = pins.copy() | |
log.info( | |
"Axis limits min %g / default %g / max %g", minValue, defaultValue, maxValue | |
) | |
triple = (minValue, defaultValue, maxValue) | |
if designLimits is not None: | |
log.info("Axis design-limits min %g / default %g / max %g", *designLimits) | |
else: | |
designLimits = triple | |
if pins: | |
log.info("Pins %s", sorted(pins.items())) | |
pins.update( | |
{ | |
minValue: designLimits[0], | |
defaultValue: designLimits[1], | |
maxValue: designLimits[2], | |
} | |
) | |
out = {} | |
outNormalized = {} | |
axisMeasurements = {} | |
for value in sorted({minValue, defaultValue, maxValue} | set(pins.keys())): | |
glyphset = glyphSetFunc(location={axisTag: value}) | |
designValue = pins[value] | |
axisMeasurements[designValue] = measureFunc(glyphset, glyphs) | |
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)) | |
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: | |
continue | |
normalizedMin = normalizeValue(rangeMin, triple) | |
normalizedMax = normalizeValue(rangeMax, triple) | |
normalizedTargetMin = normalizeValue(targetMin, designLimits) | |
normalizedTargetMax = normalizeValue(targetMax, designLimits) | |
log.info("Planning target values %s.", sorted(targetValues)) | |
log.info("Sampling %u points in range %g,%g.", samples, rangeMin, rangeMax) | |
valueMeasurements = axisMeasurements.copy() | |
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 | |
out[rangeMin] = targetMin | |
outNormalized[normalizedMin] = normalizedTargetMin | |
for value in sorted(targetValues): | |
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 | |
valueNormalized = normalizedMin + (value - rangeMin) / ( | |
rangeMax - rangeMin | |
) * (normalizedMax - normalizedMin) | |
outNormalized[valueNormalized] = normalizedTargetMin + ( | |
targetValue - targetMin | |
) / (targetMax - targetMin) * (normalizedTargetMax - normalizedTargetMin) | |
out[rangeMax] = targetMax | |
outNormalized[normalizedMax] = normalizedTargetMax | |
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), | |
) | |
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 = {} | |
return out, outNormalized | |
def planWeightAxis( | |
glyphSetFunc, | |
axisLimits, | |
weights=None, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitize=False, | |
): | |
"""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, | |
normalizeLinear, | |
interpolateLog, | |
glyphSetFunc, | |
"wght", | |
axisLimits, | |
values=weights, | |
samples=samples, | |
glyphs=glyphs, | |
designLimits=designLimits, | |
pins=pins, | |
sanitizeFunc=sanitizeWeight if sanitize else None, | |
) | |
def planWidthAxis( | |
glyphSetFunc, | |
axisLimits, | |
widths=None, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitize=False, | |
): | |
"""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, | |
normalizeLinear, | |
interpolateLinear, | |
glyphSetFunc, | |
"wdth", | |
axisLimits, | |
values=widths, | |
samples=samples, | |
glyphs=glyphs, | |
designLimits=designLimits, | |
pins=pins, | |
sanitizeFunc=sanitizeWidth if sanitize else None, | |
) | |
def planSlantAxis( | |
glyphSetFunc, | |
axisLimits, | |
slants=None, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitize=False, | |
): | |
"""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, | |
normalizeDegrees, | |
interpolateLinear, | |
glyphSetFunc, | |
"slnt", | |
axisLimits, | |
values=slants, | |
samples=samples, | |
glyphs=glyphs, | |
designLimits=designLimits, | |
pins=pins, | |
sanitizeFunc=sanitizeSlant if sanitize else None, | |
) | |
def planOpticalSizeAxis( | |
glyphSetFunc, | |
axisLimits, | |
sizes=None, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitize=False, | |
): | |
"""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. | |
""" | |
if sizes is None: | |
sizes = SIZES | |
return planAxis( | |
measureWeight, | |
normalizeLog, | |
interpolateLog, | |
glyphSetFunc, | |
"opsz", | |
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 | |
def addEmptyAvar(font): | |
"""Add an empty `avar` table to the font.""" | |
font["avar"] = avar = newTable("avar") | |
for axis in fvar.axes: | |
avar.segments[axis.axisTag] = {} | |
def processAxis( | |
font, | |
planFunc, | |
axisTag, | |
axisName, | |
values, | |
samples=None, | |
glyphs=None, | |
designLimits=None, | |
pins=None, | |
sanitize=False, | |
plot=False, | |
): | |
"""Process a single axis.""" | |
axisLimits = None | |
for axis in font["fvar"].axes: | |
if axis.axisTag == axisTag: | |
axisLimits = axis | |
break | |
if axisLimits is None: | |
return "" | |
axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue) | |
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 | |
def main(args=None): | |
"""Plan the standard axis mappings for a variable font""" | |
if args is None: | |
import sys | |
args = sys.argv[1:] | |
from fontTools import configLogger | |
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." | |
) | |
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, | |
help="min:default:max in design units for the `wght` axis.", | |
) | |
parser.add_argument( | |
"--width-design-limits", | |
type=str, | |
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.", | |
) | |
parser.add_argument( | |
"--optical-size-design-limits", | |
type=str, | |
help="min:default:max in design units for the `opsz` axis.", | |
) | |
parser.add_argument( | |
"--weight-pins", | |
type=str, | |
help="Space-separate list of before:after pins for the `wght` axis.", | |
) | |
parser.add_argument( | |
"--width-pins", | |
type=str, | |
help="Space-separate list of before:after pins for the `wdth` axis.", | |
) | |
parser.add_argument( | |
"--slant-pins", | |
type=str, | |
help="Space-separate list of before:after pins for the `slnt` axis.", | |
) | |
parser.add_argument( | |
"--optical-size-pins", | |
type=str, | |
help="Space-separate list of before:after pins for the `opsz` axis.", | |
) | |
parser.add_argument( | |
"-p", "--plot", action="store_true", help="Plot the resulting mapping." | |
) | |
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." | |
) | |
options = parser.parse_args(args) | |
configLogger( | |
level=("DEBUG" if options.verbose else "WARNING" if options.quiet else "INFO") | |
) | |
font = TTFont(options.font) | |
if not "fvar" in font: | |
log.error("Not a variable font.") | |
return 1 | |
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 | |
designspaceSnippets = [] | |
designspaceSnippets.append( | |
processAxis( | |
font, | |
planWeightAxis, | |
"wght", | |
"Weight", | |
values=options.weights, | |
samples=options.samples, | |
glyphs=glyphs, | |
designLimits=options.weight_design_limits, | |
pins=options.weight_pins, | |
sanitize=options.sanitize, | |
plot=options.plot, | |
) | |
) | |
designspaceSnippets.append( | |
processAxis( | |
font, | |
planWidthAxis, | |
"wdth", | |
"Width", | |
values=options.widths, | |
samples=options.samples, | |
glyphs=glyphs, | |
designLimits=options.width_design_limits, | |
pins=options.width_pins, | |
sanitize=options.sanitize, | |
plot=options.plot, | |
) | |
) | |
designspaceSnippets.append( | |
processAxis( | |
font, | |
planSlantAxis, | |
"slnt", | |
"Slant", | |
values=options.slants, | |
samples=options.samples, | |
glyphs=glyphs, | |
designLimits=options.slant_design_limits, | |
pins=options.slant_pins, | |
sanitize=options.sanitize, | |
plot=options.plot, | |
) | |
) | |
designspaceSnippets.append( | |
processAxis( | |
font, | |
planOpticalSizeAxis, | |
"opsz", | |
"OpticalSize", | |
values=options.sizes, | |
samples=options.samples, | |
glyphs=glyphs, | |
designLimits=options.optical_size_design_limits, | |
pins=options.optical_size_pins, | |
sanitize=options.sanitize, | |
plot=options.plot, | |
) | |
) | |
log.info("Designspace snippet:") | |
for snippet in designspaceSnippets: | |
if snippet: | |
print(snippet) | |
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) | |
if __name__ == "__main__": | |
import sys | |
sys.exit(main()) | |