from fontTools.ttLib import newTable from fontTools.pens.areaPen import AreaPen from fontTools.varLib.models import piecewiseLinearMap from fontTools.misc.cliTools import makeOutputFileName import math import logging from pprint import pformat 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, ] SAMPLES = 8 def getGlyphsetBlackness(glyphset, frequencies=None): wght_sum = wdth_sum = 0 for glyph_name in glyphset: 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) wght_sum += abs(pen.value) * glyph.width * frequency wdth_sum += glyph.width * frequency return wght_sum / wdth_sum def planWeightAxis( font, minValue, defaultValue, maxValue, weights=WEIGHTS, frequencies=None ): log.info("Weight min %g / default %g / max %g", minValue, defaultValue, maxValue) if "avar" in font: log.debug("Checking that font doesn't have weight mapping already.") existingMapping = font["avar"].segments["wght"] if existingMapping and existingMapping != {-1: -1, 0: 0, +1: +1}: log.error("Font already has a `avar` weight mapping. Remove it.") out = {} outNormalized = {} upem = font["head"].unitsPerEm axisWeightAverage = {} for weight in sorted({minValue, defaultValue, maxValue}): glyphset = font.getGlyphSet(location={"wght": weight}) axisWeightAverage[weight] = getGlyphsetBlackness(glyphset, frequencies) / ( upem * upem ) log.info("Calculated average glyph black ratio:\n%s", pformat(axisWeightAverage)) outNormalized[-1] = -1 for extremeValue in sorted({minValue, maxValue} - {defaultValue}): rangeMin = min(defaultValue, extremeValue) rangeMax = max(defaultValue, extremeValue) targetWeights = {w for w in weights if rangeMin < w < rangeMax} if not targetWeights: continue bias = -1 if extremeValue < defaultValue else 0 log.info("Planning target weights %s.", sorted(targetWeights)) log.info("Sampling %u points in range %g,%g.", SAMPLES, rangeMin, rangeMax) weightBlackness = axisWeightAverage.copy() for sample in range(1, SAMPLES + 1): weight = rangeMin + (rangeMax - rangeMin) * sample / (SAMPLES + 1) log.info("Sampling weight %g.", weight) glyphset = font.getGlyphSet(location={"wght": weight}) weightBlackness[weight] = getGlyphsetBlackness(glyphset, frequencies) / ( upem * upem ) log.info("Sampled average glyph black ratio:\n%s", pformat(weightBlackness)) blacknessWeight = {} for weight in sorted(weightBlackness): blacknessWeight[weightBlackness[weight]] = weight logMin = math.log(weightBlackness[rangeMin]) logMax = math.log(weightBlackness[rangeMax]) out[rangeMin] = rangeMin outNormalized[bias] = bias for weight in sorted(targetWeights): t = (weight - 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 outNormalized[t + bias] = (targetWeight - rangeMin) / ( rangeMax - rangeMin ) + bias out[rangeMax] = rangeMax outNormalized[bias + 1] = bias + 1 outNormalized[+1] = +1 log.info("Planned mapping:\n%s", pformat(out)) log.info("Planned normalized mapping:\n%s", pformat(outNormalized)) return out, outNormalized def main(args=None): from fontTools import configLogger if args is None: import sys args = sys.argv[1:] 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="font.ttf", help="Font file.") 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 "ERROR" if options.quiet else "INFO") ) font = TTFont(options.font) if not "fvar" in font: log.error("Not a variable font.") sys.exit(1) fvar = font["fvar"] wghtAxis = None for axis in fvar.axes: if axis.axisTag == "wght": wghtAxis = axis break if "avar" in font: existingMapping = font["avar"].segments["wght"] if wghtAxis: font["avar"].segments["wght"] = {} else: existingMapping = None if wghtAxis: out, outNormalized = planWeightAxis( font, wghtAxis.minValue, wghtAxis.defaultValue, wghtAxis.maxValue ) if options.plot: from matplotlib import pyplot pyplot.plot( sorted(outNormalized), [outNormalized[k] for k in sorted(outNormalized)] ) pyplot.show() if existingMapping is not None: log.info("Existing weight mapping:\n%s", pformat(existingMapping)) if "avar" not in font: font["avar"] = newTable("avar") for axis in fvar.axes: font["avar"].segments[axis.axisTag] = {} avar = font["avar"] if wghtAxis: avar.segments["wght"] = outNormalized designspaceSnippet = ( ' \n' % (wghtAxis.minValue, wghtAxis.maxValue, wghtAxis.defaultValue) ) for key, value in out.items(): designspaceSnippet += ' \n' % (key, value) designspaceSnippet += " " log.info("Designspace snippet:") print(designspaceSnippet) outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar") log.info("Saving %s", outfile) font.save(outfile) if __name__ == "__main__": import sys sys.exit(main())