From cdf33a67fc9f8e1c9aba382bd6af810c09159cfe Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 23 Jul 2023 16:24:01 -0600 Subject: [PATCH] [varLib.avarPlanner] Add width axis planning --- Lib/fontTools/varLib/avarPlanner.py | 340 ++++++++++++++++++++++------ 1 file changed, 270 insertions(+), 70 deletions(-) diff --git a/Lib/fontTools/varLib/avarPlanner.py b/Lib/fontTools/varLib/avarPlanner.py index 180736883..1df5bca13 100644 --- a/Lib/fontTools/varLib/avarPlanner.py +++ b/Lib/fontTools/varLib/avarPlanner.py @@ -6,6 +6,17 @@ import math import logging from pprint import pformat +__all__ = [ + "planWeightAxis", + "planWidthAxis", + "planAxis", + "measureBlackness", + "measureWidth", + "makeDesignspaceSnippet", + "addEmptyAvar", + "main", +] + log = logging.getLogger("fontTools.varLib.avarPlanner") WEIGHTS = [ @@ -30,13 +41,26 @@ WEIGHTS = [ 950, ] +WIDTHS = [ + 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, +] + SAMPLES = 8 -__all__ = ["planWeightAxis", "addEmptyAvar", "getGlyphsetBlackness", "main"] - - -def getGlyphsetBlackness(glyphset, glyphs=None): +def measureBlackness(glyphset, glyphs=None): if isinstance(glyphs, dict): frequencies = glyphs else: @@ -62,19 +86,46 @@ def getGlyphsetBlackness(glyphset, glyphs=None): return wght_sum / wdth_sum -def planWeightAxis( +def measureWidth(glyphset, glyphs=None): + 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 = AreaPen(glyphset=glyphset) + glyph.draw(pen) + + wdth_sum += glyph.width * frequency + freq_sum += frequency + + return wdth_sum / freq_sum + + +def planAxis( + axisTag, + measureFunc, glyphSetFunc, minValue, defaultValue, maxValue, - weights=None, + values=None, samples=None, glyphs=None, designUnits=None, pins=None, ): - if weights is None: - weights = WEIGHTS if samples is None: samples = SAMPLES if glyphs is None: @@ -84,19 +135,19 @@ def planWeightAxis( else: pins = pins.copy() - log.info("Weight min %g / default %g / max %g", minValue, defaultValue, maxValue) + log.info("Value min %g / default %g / max %g", minValue, defaultValue, maxValue) triple = (minValue, defaultValue, maxValue) if designUnits is not None: - log.info("Weight design-units min %g / default %g / max %g", *designUnits) + log.info("Value design-units min %g / default %g / max %g", *designUnits) else: designUnits = triple # if "avar" in font: - # log.debug("Checking that font doesn't have weight mapping already.") - # existingMapping = font["avar"].segments["wght"] + # log.debug("Checking that font doesn't have axis mapping already.") + # existingMapping = font["avar"].segments[axisTag] # if existingMapping and existingMapping != {-1: -1, 0: 0, +1: +1}: - # log.error("Font already has a `avar` weight mapping. Remove it.") + # log.error("Font already has a `avar` value mapping. Remove it.") if pins: log.info("Pins %s", sorted(pins.items())) @@ -113,23 +164,21 @@ def planWeightAxis( upem = 1 # font["head"].unitsPerEm axisBlackness = {} - for weight in sorted({minValue, defaultValue, maxValue} | set(pins.values())): - glyphset = glyphSetFunc(location={"wght": weight}) + for value in sorted({minValue, defaultValue, maxValue} | set(pins.values())): + glyphset = glyphSetFunc(location={axisTag: value}) - designWeight = piecewiseLinearMap(weight, pins) + designValue = piecewiseLinearMap(value, pins) - axisBlackness[designWeight] = getGlyphsetBlackness(glyphset, glyphs) / ( - upem * upem - ) + axisBlackness[designValue] = measureFunc(glyphset, glyphs) / (upem * upem) - log.debug("Calculated average glyph black ratio:\n%s", pformat(axisBlackness)) + log.debug("Calculated average value:\n%s", pformat(axisBlackness)) for (rangeMin, targetMin), (rangeMax, targetMax) in zip( list(sorted(pins.items()))[:-1], list(sorted(pins.items()))[1:], ): - targetWeights = {w for w in weights if rangeMin < w < rangeMax} - if not targetWeights: + targetValues = {w for w in values if rangeMin < w < rangeMax} + if not targetValues: continue normalizedMin = normalizeValue(rangeMin, triple) @@ -137,36 +186,34 @@ def planWeightAxis( normalizedTargetMin = normalizeValue(targetMin, designUnits) normalizedTargetMax = normalizeValue(targetMax, designUnits) - log.info("Planning target weights %s.", sorted(targetWeights)) + log.info("Planning target values %s.", sorted(targetValues)) log.info("Sampling %u points in range %g,%g.", samples, rangeMin, rangeMax) - weightBlackness = axisBlackness.copy() + valueBlackness = axisBlackness.copy() for sample in range(1, samples + 1): - weight = rangeMin + (rangeMax - rangeMin) * sample / (samples + 1) - log.info("Sampling weight %g.", weight) - glyphset = glyphSetFunc(location={"wght": weight}) - designWeight = piecewiseLinearMap(weight, pins) - weightBlackness[designWeight] = getGlyphsetBlackness(glyphset, glyphs) / ( - upem * upem - ) - log.debug("Sampled average glyph black ratio:\n%s", pformat(weightBlackness)) + value = rangeMin + (rangeMax - rangeMin) * sample / (samples + 1) + log.info("Sampling value %g.", value) + glyphset = glyphSetFunc(location={axisTag: value}) + designValue = piecewiseLinearMap(value, pins) + valueBlackness[designValue] = measureFunc(glyphset, glyphs) / (upem * upem) + log.debug("Sampled average value:\n%s", pformat(valueBlackness)) - blacknessWeight = {} - for weight in sorted(weightBlackness): - blacknessWeight[weightBlackness[weight]] = weight + blacknessValue = {} + for value in sorted(valueBlackness): + blacknessValue[valueBlackness[value]] = value - logMin = math.log(weightBlackness[targetMin]) - logMax = math.log(weightBlackness[targetMax]) + logMin = math.log(valueBlackness[targetMin]) + logMax = math.log(valueBlackness[targetMax]) out[rangeMin] = targetMin outNormalized[normalizedMin] = normalizedTargetMin - for weight in sorted(targetWeights): - t = (weight - rangeMin) / (rangeMax - rangeMin) + for value in sorted(targetValues): + t = (value - rangeMin) / (rangeMax - rangeMin) targetBlackness = math.exp(logMin + t * (logMax - logMin)) - targetWeight = piecewiseLinearMap(targetBlackness, blacknessWeight) - log.info("Planned mapping weight %g to %g." % (weight, targetWeight)) - out[weight] = targetWeight + targetValue = piecewiseLinearMap(targetBlackness, blacknessValue) + log.info("Planned mapping value %g to %g." % (value, targetValue)) + out[value] = targetValue outNormalized[ normalizedMin + t * (normalizedMax - normalizedMin) - ] = normalizedTargetMin + (targetWeight - targetMin) / ( + ] = normalizedTargetMin + (targetValue - targetMin) / ( targetMax - targetMin ) * ( normalizedTargetMax - normalizedTargetMin @@ -174,11 +221,98 @@ def planWeightAxis( out[rangeMax] = targetMax outNormalized[normalizedMax] = normalizedTargetMax - log.info("Planned mapping:\n%s", pformat(out)) - log.info("Planned normalized mapping:\n%s", pformat(outNormalized)) + 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.02 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, + minValue, + defaultValue, + maxValue, + weights=None, + samples=None, + glyphs=None, + designUnits=None, + pins=None, +): + if weights is None: + weights = WEIGHTS + + return planAxis( + "wght", + measureBlackness, + glyphSetFunc, + minValue, + defaultValue, + maxValue, + values=weights, + samples=samples, + glyphs=glyphs, + designUnits=designUnits, + pins=pins, + ) + + +def planWidthAxis( + glyphSetFunc, + minValue, + defaultValue, + maxValue, + widths=None, + samples=None, + glyphs=None, + designUnits=None, + pins=None, +): + if widths is None: + widths = WIDTHS + + return planAxis( + "wdth", + measureWidth, + glyphSetFunc, + minValue, + defaultValue, + maxValue, + values=widths, + samples=samples, + glyphs=glyphs, + designUnits=designUnits, + pins=pins, + ) + + +def makeDesignspaceSnippet(axisTag, axisName, axisLimit, mapping): + designspaceSnippet = ( + ' \n' - % (wghtAxis.minValue, wghtAxis.maxValue, wghtAxis.defaultValue) + if wdthAxis: + avar.segments["wdth"] = widthMappingNormalized + designspaceSnippet = makeDesignspaceSnippet( + "wdth", + "Width", + (wdthAxis.minValue, wdthAxis.defaultValue, wdthAxis.maxValue), + widthMapping, + ) + log.info("Width axis designspace snippet:") + print(designspaceSnippet) + + if wghtAxis: + avar.segments["wght"] = weightMappingNormalized + designspaceSnippet = makeDesignspaceSnippet( + "wght", + "Weight", + (wghtAxis.minValue, wghtAxis.defaultValue, wghtAxis.maxValue), + weightMapping, ) - for key, value in out.items(): - designspaceSnippet += ' \n' % (key, value) - designspaceSnippet += " " log.info("Weight axis designspace snippet:") print(designspaceSnippet)