fonttools/Lib/fontTools/varLib/avarPlanner.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

232 lines
6.8 KiB
Python
Raw Normal View History

from fontTools.ttLib import newTable
2023-07-22 21:18:52 -06:00
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")
2023-07-22 21:18:52 -06:00
WEIGHTS = [
50,
100,
150,
200,
250,
300,
350,
400,
450,
500,
550,
600,
650,
700,
750,
800,
850,
900,
950,
]
2023-07-22 21:18:52 -06:00
SAMPLES = 8
2023-07-23 10:23:39 -06:00
__all__ = ["planWeightAxis", "addEmptyAvar", "getGlyphsetBlackness", "main"]
def getGlyphsetBlackness(glyphset, frequencies=None):
2023-07-22 21:18:52 -06:00
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
2023-07-22 21:18:52 -06:00
glyph = glyphset[glyph_name]
pen = AreaPen(glyphset=glyphset)
glyph.draw(pen)
wght_sum += abs(pen.value) * glyph.width * frequency
wdth_sum += glyph.width * frequency
2023-07-22 21:18:52 -06:00
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)
2023-07-22 21:18:52 -06:00
2023-07-23 09:45:58 -06:00
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.")
2023-07-23 09:45:58 -06:00
2023-07-22 21:18:52 -06:00
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
)
2023-07-22 21:18:52 -06:00
log.debug("Calculated average glyph black ratio:\n%s", pformat(axisWeightAverage))
2023-07-22 21:18:52 -06:00
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
2023-07-23 09:45:58 -06:00
log.info("Planning target weights %s.", sorted(targetWeights))
log.info("Sampling %u points in range %g,%g.", SAMPLES, rangeMin, rangeMax)
2023-07-22 21:18:52 -06:00
weightBlackness = axisWeightAverage.copy()
for sample in range(1, SAMPLES + 1):
weight = rangeMin + (rangeMax - rangeMin) * sample / (SAMPLES + 1)
2023-07-23 09:45:58 -06:00
log.info("Sampling weight %g.", weight)
2023-07-22 21:18:52 -06:00
glyphset = font.getGlyphSet(location={"wght": weight})
weightBlackness[weight] = getGlyphsetBlackness(glyphset, frequencies) / (
upem * upem
)
log.debug("Sampled average glyph black ratio:\n%s", pformat(weightBlackness))
2023-07-22 21:18:52 -06:00
blacknessWeight = {}
for weight in sorted(weightBlackness):
blacknessWeight[weightBlackness[weight]] = weight
logMin = math.log(weightBlackness[rangeMin])
logMax = math.log(weightBlackness[rangeMax])
2023-07-22 21:18:52 -06:00
out[rangeMin] = rangeMin
outNormalized[bias] = bias
for weight in sorted(targetWeights):
t = (weight - rangeMin) / (rangeMax - rangeMin)
targetBlackness = math.exp(logMin + t * (logMax - logMin))
2023-07-22 21:18:52 -06:00
targetWeight = piecewiseLinearMap(targetBlackness, blacknessWeight)
2023-07-23 09:45:58 -06:00
log.info("Planned mapping weight %g to %g." % (weight, targetWeight))
2023-07-22 21:18:52 -06:00
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))
2023-07-22 21:18:52 -06:00
return out, outNormalized
2023-07-23 10:19:35 -06:00
def addEmptyAvar(font):
font["avar"] = newTable("avar")
for axis in fvar.axes:
font["avar"].segments[axis.axisTag] = {}
2023-07-22 21:18:52 -06:00
def main(args=None):
from fontTools import configLogger
2023-07-22 21:18:52 -06:00
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.")
2023-07-23 09:41:29 -06:00
parser.add_argument(
"-p", "--plot", action="store_true", help="Plot the resulting mapping."
)
2023-07-22 21:18:52 -06:00
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."
)
2023-07-22 21:18:52 -06:00
options = parser.parse_args(args)
configLogger(
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
)
2023-07-22 21:18:52 -06:00
font = TTFont(options.font)
if not "fvar" in font:
log.error("Not a variable font.")
sys.exit(1)
2023-07-22 21:18:52 -06:00
fvar = font["fvar"]
wghtAxis = None
2023-07-22 21:18:52 -06:00
for axis in fvar.axes:
if axis.axisTag == "wght":
wghtAxis = axis
break
2023-07-22 21:18:52 -06:00
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
2023-07-23 09:41:29 -06:00
pyplot.plot(
sorted(outNormalized), [outNormalized[k] for k in sorted(outNormalized)]
)
pyplot.show()
2023-07-23 09:41:29 -06:00
if existingMapping is not None:
log.info("Existing weight mapping:\n%s", pformat(existingMapping))
if "avar" not in font:
2023-07-23 10:19:35 -06:00
addEmptyAvar(font)
2023-07-22 21:18:52 -06:00
avar = font["avar"]
if wghtAxis:
avar.segments["wght"] = outNormalized
2023-07-22 21:18:52 -06:00
designspaceSnippet = (
' <axis tag="wght" name="Weight" minimum="%g" maximum="%g" default="%g">\n'
% (wghtAxis.minValue, wghtAxis.maxValue, wghtAxis.defaultValue)
)
for key, value in out.items():
designspaceSnippet += ' <map input="%g" output="%g"/>\n' % (key, value)
designspaceSnippet += " </axis>"
log.info("Designspace snippet:")
print(designspaceSnippet)
2023-07-22 21:18:52 -06:00
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar")
log.info("Saving %s", outfile)
2023-07-22 21:18:52 -06:00
font.save(outfile)
if __name__ == "__main__":
import sys
sys.exit(main())