12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004 |
- 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())
|