diff options
Diffstat (limited to 'Lib/fontTools/varLib/avarPlanner.py')
-rw-r--r-- | Lib/fontTools/varLib/avarPlanner.py | 1004 |
1 files changed, 1004 insertions, 0 deletions
diff --git a/Lib/fontTools/varLib/avarPlanner.py b/Lib/fontTools/varLib/avarPlanner.py new file mode 100644 index 00000000..2e173443 --- /dev/null +++ b/Lib/fontTools/varLib/avarPlanner.py @@ -0,0 +1,1004 @@ +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()) |