xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/avarPlanner.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import newTable
2*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._f_v_a_r import Axis as fvarAxis
3*e1fe3e4aSElliott Hughesfrom fontTools.pens.areaPen import AreaPen
4*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import NullPen
5*e1fe3e4aSElliott Hughesfrom fontTools.pens.statisticsPen import StatisticsPen
6*e1fe3e4aSElliott Hughesfrom fontTools.varLib.models import piecewiseLinearMap, normalizeValue
7*e1fe3e4aSElliott Hughesfrom fontTools.misc.cliTools import makeOutputFileName
8*e1fe3e4aSElliott Hughesimport math
9*e1fe3e4aSElliott Hughesimport logging
10*e1fe3e4aSElliott Hughesfrom pprint import pformat
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott Hughes__all__ = [
13*e1fe3e4aSElliott Hughes    "planWeightAxis",
14*e1fe3e4aSElliott Hughes    "planWidthAxis",
15*e1fe3e4aSElliott Hughes    "planSlantAxis",
16*e1fe3e4aSElliott Hughes    "planOpticalSizeAxis",
17*e1fe3e4aSElliott Hughes    "planAxis",
18*e1fe3e4aSElliott Hughes    "sanitizeWeight",
19*e1fe3e4aSElliott Hughes    "sanitizeWidth",
20*e1fe3e4aSElliott Hughes    "sanitizeSlant",
21*e1fe3e4aSElliott Hughes    "measureWeight",
22*e1fe3e4aSElliott Hughes    "measureWidth",
23*e1fe3e4aSElliott Hughes    "measureSlant",
24*e1fe3e4aSElliott Hughes    "normalizeLinear",
25*e1fe3e4aSElliott Hughes    "normalizeLog",
26*e1fe3e4aSElliott Hughes    "normalizeDegrees",
27*e1fe3e4aSElliott Hughes    "interpolateLinear",
28*e1fe3e4aSElliott Hughes    "interpolateLog",
29*e1fe3e4aSElliott Hughes    "processAxis",
30*e1fe3e4aSElliott Hughes    "makeDesignspaceSnippet",
31*e1fe3e4aSElliott Hughes    "addEmptyAvar",
32*e1fe3e4aSElliott Hughes    "main",
33*e1fe3e4aSElliott Hughes]
34*e1fe3e4aSElliott Hughes
35*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.varLib.avarPlanner")
36*e1fe3e4aSElliott Hughes
37*e1fe3e4aSElliott HughesWEIGHTS = [
38*e1fe3e4aSElliott Hughes    50,
39*e1fe3e4aSElliott Hughes    100,
40*e1fe3e4aSElliott Hughes    150,
41*e1fe3e4aSElliott Hughes    200,
42*e1fe3e4aSElliott Hughes    250,
43*e1fe3e4aSElliott Hughes    300,
44*e1fe3e4aSElliott Hughes    350,
45*e1fe3e4aSElliott Hughes    400,
46*e1fe3e4aSElliott Hughes    450,
47*e1fe3e4aSElliott Hughes    500,
48*e1fe3e4aSElliott Hughes    550,
49*e1fe3e4aSElliott Hughes    600,
50*e1fe3e4aSElliott Hughes    650,
51*e1fe3e4aSElliott Hughes    700,
52*e1fe3e4aSElliott Hughes    750,
53*e1fe3e4aSElliott Hughes    800,
54*e1fe3e4aSElliott Hughes    850,
55*e1fe3e4aSElliott Hughes    900,
56*e1fe3e4aSElliott Hughes    950,
57*e1fe3e4aSElliott Hughes]
58*e1fe3e4aSElliott Hughes
59*e1fe3e4aSElliott HughesWIDTHS = [
60*e1fe3e4aSElliott Hughes    25.0,
61*e1fe3e4aSElliott Hughes    37.5,
62*e1fe3e4aSElliott Hughes    50.0,
63*e1fe3e4aSElliott Hughes    62.5,
64*e1fe3e4aSElliott Hughes    75.0,
65*e1fe3e4aSElliott Hughes    87.5,
66*e1fe3e4aSElliott Hughes    100.0,
67*e1fe3e4aSElliott Hughes    112.5,
68*e1fe3e4aSElliott Hughes    125.0,
69*e1fe3e4aSElliott Hughes    137.5,
70*e1fe3e4aSElliott Hughes    150.0,
71*e1fe3e4aSElliott Hughes    162.5,
72*e1fe3e4aSElliott Hughes    175.0,
73*e1fe3e4aSElliott Hughes    187.5,
74*e1fe3e4aSElliott Hughes    200.0,
75*e1fe3e4aSElliott Hughes]
76*e1fe3e4aSElliott Hughes
77*e1fe3e4aSElliott HughesSLANTS = list(math.degrees(math.atan(d / 20.0)) for d in range(-20, 21))
78*e1fe3e4aSElliott Hughes
79*e1fe3e4aSElliott HughesSIZES = [
80*e1fe3e4aSElliott Hughes    5,
81*e1fe3e4aSElliott Hughes    6,
82*e1fe3e4aSElliott Hughes    7,
83*e1fe3e4aSElliott Hughes    8,
84*e1fe3e4aSElliott Hughes    9,
85*e1fe3e4aSElliott Hughes    10,
86*e1fe3e4aSElliott Hughes    11,
87*e1fe3e4aSElliott Hughes    12,
88*e1fe3e4aSElliott Hughes    14,
89*e1fe3e4aSElliott Hughes    18,
90*e1fe3e4aSElliott Hughes    24,
91*e1fe3e4aSElliott Hughes    30,
92*e1fe3e4aSElliott Hughes    36,
93*e1fe3e4aSElliott Hughes    48,
94*e1fe3e4aSElliott Hughes    60,
95*e1fe3e4aSElliott Hughes    72,
96*e1fe3e4aSElliott Hughes    96,
97*e1fe3e4aSElliott Hughes    120,
98*e1fe3e4aSElliott Hughes    144,
99*e1fe3e4aSElliott Hughes    192,
100*e1fe3e4aSElliott Hughes    240,
101*e1fe3e4aSElliott Hughes    288,
102*e1fe3e4aSElliott Hughes]
103*e1fe3e4aSElliott Hughes
104*e1fe3e4aSElliott Hughes
105*e1fe3e4aSElliott HughesSAMPLES = 8
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes
108*e1fe3e4aSElliott Hughesdef normalizeLinear(value, rangeMin, rangeMax):
109*e1fe3e4aSElliott Hughes    """Linearly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
110*e1fe3e4aSElliott Hughes    return (value - rangeMin) / (rangeMax - rangeMin)
111*e1fe3e4aSElliott Hughes
112*e1fe3e4aSElliott Hughes
113*e1fe3e4aSElliott Hughesdef interpolateLinear(t, a, b):
114*e1fe3e4aSElliott Hughes    """Linear interpolation between a and b, with t typically in [0, 1]."""
115*e1fe3e4aSElliott Hughes    return a + t * (b - a)
116*e1fe3e4aSElliott Hughes
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughesdef normalizeLog(value, rangeMin, rangeMax):
119*e1fe3e4aSElliott Hughes    """Logarithmically normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
120*e1fe3e4aSElliott Hughes    logMin = math.log(rangeMin)
121*e1fe3e4aSElliott Hughes    logMax = math.log(rangeMax)
122*e1fe3e4aSElliott Hughes    return (math.log(value) - logMin) / (logMax - logMin)
123*e1fe3e4aSElliott Hughes
124*e1fe3e4aSElliott Hughes
125*e1fe3e4aSElliott Hughesdef interpolateLog(t, a, b):
126*e1fe3e4aSElliott Hughes    """Logarithmic interpolation between a and b, with t typically in [0, 1]."""
127*e1fe3e4aSElliott Hughes    logA = math.log(a)
128*e1fe3e4aSElliott Hughes    logB = math.log(b)
129*e1fe3e4aSElliott Hughes    return math.exp(logA + t * (logB - logA))
130*e1fe3e4aSElliott Hughes
131*e1fe3e4aSElliott Hughes
132*e1fe3e4aSElliott Hughesdef normalizeDegrees(value, rangeMin, rangeMax):
133*e1fe3e4aSElliott Hughes    """Angularly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation."""
134*e1fe3e4aSElliott Hughes    tanMin = math.tan(math.radians(rangeMin))
135*e1fe3e4aSElliott Hughes    tanMax = math.tan(math.radians(rangeMax))
136*e1fe3e4aSElliott Hughes    return (math.tan(math.radians(value)) - tanMin) / (tanMax - tanMin)
137*e1fe3e4aSElliott Hughes
138*e1fe3e4aSElliott Hughes
139*e1fe3e4aSElliott Hughesdef measureWeight(glyphset, glyphs=None):
140*e1fe3e4aSElliott Hughes    """Measure the perceptual average weight of the given glyphs."""
141*e1fe3e4aSElliott Hughes    if isinstance(glyphs, dict):
142*e1fe3e4aSElliott Hughes        frequencies = glyphs
143*e1fe3e4aSElliott Hughes    else:
144*e1fe3e4aSElliott Hughes        frequencies = {g: 1 for g in glyphs}
145*e1fe3e4aSElliott Hughes
146*e1fe3e4aSElliott Hughes    wght_sum = wdth_sum = 0
147*e1fe3e4aSElliott Hughes    for glyph_name in glyphs:
148*e1fe3e4aSElliott Hughes        if frequencies is not None:
149*e1fe3e4aSElliott Hughes            frequency = frequencies.get(glyph_name, 0)
150*e1fe3e4aSElliott Hughes            if frequency == 0:
151*e1fe3e4aSElliott Hughes                continue
152*e1fe3e4aSElliott Hughes        else:
153*e1fe3e4aSElliott Hughes            frequency = 1
154*e1fe3e4aSElliott Hughes
155*e1fe3e4aSElliott Hughes        glyph = glyphset[glyph_name]
156*e1fe3e4aSElliott Hughes
157*e1fe3e4aSElliott Hughes        pen = AreaPen(glyphset=glyphset)
158*e1fe3e4aSElliott Hughes        glyph.draw(pen)
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughes        mult = glyph.width * frequency
161*e1fe3e4aSElliott Hughes        wght_sum += mult * abs(pen.value)
162*e1fe3e4aSElliott Hughes        wdth_sum += mult
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes    return wght_sum / wdth_sum
165*e1fe3e4aSElliott Hughes
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughesdef measureWidth(glyphset, glyphs=None):
168*e1fe3e4aSElliott Hughes    """Measure the average width of the given glyphs."""
169*e1fe3e4aSElliott Hughes    if isinstance(glyphs, dict):
170*e1fe3e4aSElliott Hughes        frequencies = glyphs
171*e1fe3e4aSElliott Hughes    else:
172*e1fe3e4aSElliott Hughes        frequencies = {g: 1 for g in glyphs}
173*e1fe3e4aSElliott Hughes
174*e1fe3e4aSElliott Hughes    wdth_sum = 0
175*e1fe3e4aSElliott Hughes    freq_sum = 0
176*e1fe3e4aSElliott Hughes    for glyph_name in glyphs:
177*e1fe3e4aSElliott Hughes        if frequencies is not None:
178*e1fe3e4aSElliott Hughes            frequency = frequencies.get(glyph_name, 0)
179*e1fe3e4aSElliott Hughes            if frequency == 0:
180*e1fe3e4aSElliott Hughes                continue
181*e1fe3e4aSElliott Hughes        else:
182*e1fe3e4aSElliott Hughes            frequency = 1
183*e1fe3e4aSElliott Hughes
184*e1fe3e4aSElliott Hughes        glyph = glyphset[glyph_name]
185*e1fe3e4aSElliott Hughes
186*e1fe3e4aSElliott Hughes        pen = NullPen()
187*e1fe3e4aSElliott Hughes        glyph.draw(pen)
188*e1fe3e4aSElliott Hughes
189*e1fe3e4aSElliott Hughes        wdth_sum += glyph.width * frequency
190*e1fe3e4aSElliott Hughes        freq_sum += frequency
191*e1fe3e4aSElliott Hughes
192*e1fe3e4aSElliott Hughes    return wdth_sum / freq_sum
193*e1fe3e4aSElliott Hughes
194*e1fe3e4aSElliott Hughes
195*e1fe3e4aSElliott Hughesdef measureSlant(glyphset, glyphs=None):
196*e1fe3e4aSElliott Hughes    """Measure the perceptual average slant angle of the given glyphs."""
197*e1fe3e4aSElliott Hughes    if isinstance(glyphs, dict):
198*e1fe3e4aSElliott Hughes        frequencies = glyphs
199*e1fe3e4aSElliott Hughes    else:
200*e1fe3e4aSElliott Hughes        frequencies = {g: 1 for g in glyphs}
201*e1fe3e4aSElliott Hughes
202*e1fe3e4aSElliott Hughes    slnt_sum = 0
203*e1fe3e4aSElliott Hughes    freq_sum = 0
204*e1fe3e4aSElliott Hughes    for glyph_name in glyphs:
205*e1fe3e4aSElliott Hughes        if frequencies is not None:
206*e1fe3e4aSElliott Hughes            frequency = frequencies.get(glyph_name, 0)
207*e1fe3e4aSElliott Hughes            if frequency == 0:
208*e1fe3e4aSElliott Hughes                continue
209*e1fe3e4aSElliott Hughes        else:
210*e1fe3e4aSElliott Hughes            frequency = 1
211*e1fe3e4aSElliott Hughes
212*e1fe3e4aSElliott Hughes        glyph = glyphset[glyph_name]
213*e1fe3e4aSElliott Hughes
214*e1fe3e4aSElliott Hughes        pen = StatisticsPen(glyphset=glyphset)
215*e1fe3e4aSElliott Hughes        glyph.draw(pen)
216*e1fe3e4aSElliott Hughes
217*e1fe3e4aSElliott Hughes        mult = glyph.width * frequency
218*e1fe3e4aSElliott Hughes        slnt_sum += mult * pen.slant
219*e1fe3e4aSElliott Hughes        freq_sum += mult
220*e1fe3e4aSElliott Hughes
221*e1fe3e4aSElliott Hughes    return -math.degrees(math.atan(slnt_sum / freq_sum))
222*e1fe3e4aSElliott Hughes
223*e1fe3e4aSElliott Hughes
224*e1fe3e4aSElliott Hughesdef sanitizeWidth(userTriple, designTriple, pins, measurements):
225*e1fe3e4aSElliott Hughes    """Sanitize the width axis limits."""
226*e1fe3e4aSElliott Hughes
227*e1fe3e4aSElliott Hughes    minVal, defaultVal, maxVal = (
228*e1fe3e4aSElliott Hughes        measurements[designTriple[0]],
229*e1fe3e4aSElliott Hughes        measurements[designTriple[1]],
230*e1fe3e4aSElliott Hughes        measurements[designTriple[2]],
231*e1fe3e4aSElliott Hughes    )
232*e1fe3e4aSElliott Hughes
233*e1fe3e4aSElliott Hughes    calculatedMinVal = userTriple[1] * (minVal / defaultVal)
234*e1fe3e4aSElliott Hughes    calculatedMaxVal = userTriple[1] * (maxVal / defaultVal)
235*e1fe3e4aSElliott Hughes
236*e1fe3e4aSElliott Hughes    log.info("Original width axis limits: %g:%g:%g", *userTriple)
237*e1fe3e4aSElliott Hughes    log.info(
238*e1fe3e4aSElliott Hughes        "Calculated width axis limits: %g:%g:%g",
239*e1fe3e4aSElliott Hughes        calculatedMinVal,
240*e1fe3e4aSElliott Hughes        userTriple[1],
241*e1fe3e4aSElliott Hughes        calculatedMaxVal,
242*e1fe3e4aSElliott Hughes    )
243*e1fe3e4aSElliott Hughes
244*e1fe3e4aSElliott Hughes    if (
245*e1fe3e4aSElliott Hughes        abs(calculatedMinVal - userTriple[0]) / userTriple[1] > 0.05
246*e1fe3e4aSElliott Hughes        or abs(calculatedMaxVal - userTriple[2]) / userTriple[1] > 0.05
247*e1fe3e4aSElliott Hughes    ):
248*e1fe3e4aSElliott Hughes        log.warning("Calculated width axis min/max do not match user input.")
249*e1fe3e4aSElliott Hughes        log.warning(
250*e1fe3e4aSElliott Hughes            "  Current width axis limits: %g:%g:%g",
251*e1fe3e4aSElliott Hughes            *userTriple,
252*e1fe3e4aSElliott Hughes        )
253*e1fe3e4aSElliott Hughes        log.warning(
254*e1fe3e4aSElliott Hughes            "  Suggested width axis limits: %g:%g:%g",
255*e1fe3e4aSElliott Hughes            calculatedMinVal,
256*e1fe3e4aSElliott Hughes            userTriple[1],
257*e1fe3e4aSElliott Hughes            calculatedMaxVal,
258*e1fe3e4aSElliott Hughes        )
259*e1fe3e4aSElliott Hughes
260*e1fe3e4aSElliott Hughes        return False
261*e1fe3e4aSElliott Hughes
262*e1fe3e4aSElliott Hughes    return True
263*e1fe3e4aSElliott Hughes
264*e1fe3e4aSElliott Hughes
265*e1fe3e4aSElliott Hughesdef sanitizeWeight(userTriple, designTriple, pins, measurements):
266*e1fe3e4aSElliott Hughes    """Sanitize the weight axis limits."""
267*e1fe3e4aSElliott Hughes
268*e1fe3e4aSElliott Hughes    if len(set(userTriple)) < 3:
269*e1fe3e4aSElliott Hughes        return True
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    minVal, defaultVal, maxVal = (
272*e1fe3e4aSElliott Hughes        measurements[designTriple[0]],
273*e1fe3e4aSElliott Hughes        measurements[designTriple[1]],
274*e1fe3e4aSElliott Hughes        measurements[designTriple[2]],
275*e1fe3e4aSElliott Hughes    )
276*e1fe3e4aSElliott Hughes
277*e1fe3e4aSElliott Hughes    logMin = math.log(minVal)
278*e1fe3e4aSElliott Hughes    logDefault = math.log(defaultVal)
279*e1fe3e4aSElliott Hughes    logMax = math.log(maxVal)
280*e1fe3e4aSElliott Hughes
281*e1fe3e4aSElliott Hughes    t = (userTriple[1] - userTriple[0]) / (userTriple[2] - userTriple[0])
282*e1fe3e4aSElliott Hughes    y = math.exp(logMin + t * (logMax - logMin))
283*e1fe3e4aSElliott Hughes    t = (y - minVal) / (maxVal - minVal)
284*e1fe3e4aSElliott Hughes    calculatedDefaultVal = userTriple[0] + t * (userTriple[2] - userTriple[0])
285*e1fe3e4aSElliott Hughes
286*e1fe3e4aSElliott Hughes    log.info("Original weight axis limits: %g:%g:%g", *userTriple)
287*e1fe3e4aSElliott Hughes    log.info(
288*e1fe3e4aSElliott Hughes        "Calculated weight axis limits: %g:%g:%g",
289*e1fe3e4aSElliott Hughes        userTriple[0],
290*e1fe3e4aSElliott Hughes        calculatedDefaultVal,
291*e1fe3e4aSElliott Hughes        userTriple[2],
292*e1fe3e4aSElliott Hughes    )
293*e1fe3e4aSElliott Hughes
294*e1fe3e4aSElliott Hughes    if abs(calculatedDefaultVal - userTriple[1]) / userTriple[1] > 0.05:
295*e1fe3e4aSElliott Hughes        log.warning("Calculated weight axis default does not match user input.")
296*e1fe3e4aSElliott Hughes
297*e1fe3e4aSElliott Hughes        log.warning(
298*e1fe3e4aSElliott Hughes            "  Current weight axis limits: %g:%g:%g",
299*e1fe3e4aSElliott Hughes            *userTriple,
300*e1fe3e4aSElliott Hughes        )
301*e1fe3e4aSElliott Hughes
302*e1fe3e4aSElliott Hughes        log.warning(
303*e1fe3e4aSElliott Hughes            "  Suggested weight axis limits, changing default: %g:%g:%g",
304*e1fe3e4aSElliott Hughes            userTriple[0],
305*e1fe3e4aSElliott Hughes            calculatedDefaultVal,
306*e1fe3e4aSElliott Hughes            userTriple[2],
307*e1fe3e4aSElliott Hughes        )
308*e1fe3e4aSElliott Hughes
309*e1fe3e4aSElliott Hughes        t = (userTriple[2] - userTriple[0]) / (userTriple[1] - userTriple[0])
310*e1fe3e4aSElliott Hughes        y = math.exp(logMin + t * (logDefault - logMin))
311*e1fe3e4aSElliott Hughes        t = (y - minVal) / (defaultVal - minVal)
312*e1fe3e4aSElliott Hughes        calculatedMaxVal = userTriple[0] + t * (userTriple[1] - userTriple[0])
313*e1fe3e4aSElliott Hughes        log.warning(
314*e1fe3e4aSElliott Hughes            "  Suggested weight axis limits, changing maximum: %g:%g:%g",
315*e1fe3e4aSElliott Hughes            userTriple[0],
316*e1fe3e4aSElliott Hughes            userTriple[1],
317*e1fe3e4aSElliott Hughes            calculatedMaxVal,
318*e1fe3e4aSElliott Hughes        )
319*e1fe3e4aSElliott Hughes
320*e1fe3e4aSElliott Hughes        t = (userTriple[0] - userTriple[2]) / (userTriple[1] - userTriple[2])
321*e1fe3e4aSElliott Hughes        y = math.exp(logMax + t * (logDefault - logMax))
322*e1fe3e4aSElliott Hughes        t = (y - maxVal) / (defaultVal - maxVal)
323*e1fe3e4aSElliott Hughes        calculatedMinVal = userTriple[2] + t * (userTriple[1] - userTriple[2])
324*e1fe3e4aSElliott Hughes        log.warning(
325*e1fe3e4aSElliott Hughes            "  Suggested weight axis limits, changing minimum: %g:%g:%g",
326*e1fe3e4aSElliott Hughes            calculatedMinVal,
327*e1fe3e4aSElliott Hughes            userTriple[1],
328*e1fe3e4aSElliott Hughes            userTriple[2],
329*e1fe3e4aSElliott Hughes        )
330*e1fe3e4aSElliott Hughes
331*e1fe3e4aSElliott Hughes        return False
332*e1fe3e4aSElliott Hughes
333*e1fe3e4aSElliott Hughes    return True
334*e1fe3e4aSElliott Hughes
335*e1fe3e4aSElliott Hughes
336*e1fe3e4aSElliott Hughesdef sanitizeSlant(userTriple, designTriple, pins, measurements):
337*e1fe3e4aSElliott Hughes    """Sanitize the slant axis limits."""
338*e1fe3e4aSElliott Hughes
339*e1fe3e4aSElliott Hughes    log.info("Original slant axis limits: %g:%g:%g", *userTriple)
340*e1fe3e4aSElliott Hughes    log.info(
341*e1fe3e4aSElliott Hughes        "Calculated slant axis limits: %g:%g:%g",
342*e1fe3e4aSElliott Hughes        measurements[designTriple[0]],
343*e1fe3e4aSElliott Hughes        measurements[designTriple[1]],
344*e1fe3e4aSElliott Hughes        measurements[designTriple[2]],
345*e1fe3e4aSElliott Hughes    )
346*e1fe3e4aSElliott Hughes
347*e1fe3e4aSElliott Hughes    if (
348*e1fe3e4aSElliott Hughes        abs(measurements[designTriple[0]] - userTriple[0]) > 1
349*e1fe3e4aSElliott Hughes        or abs(measurements[designTriple[1]] - userTriple[1]) > 1
350*e1fe3e4aSElliott Hughes        or abs(measurements[designTriple[2]] - userTriple[2]) > 1
351*e1fe3e4aSElliott Hughes    ):
352*e1fe3e4aSElliott Hughes        log.warning("Calculated slant axis min/default/max do not match user input.")
353*e1fe3e4aSElliott Hughes        log.warning(
354*e1fe3e4aSElliott Hughes            "  Current slant axis limits: %g:%g:%g",
355*e1fe3e4aSElliott Hughes            *userTriple,
356*e1fe3e4aSElliott Hughes        )
357*e1fe3e4aSElliott Hughes        log.warning(
358*e1fe3e4aSElliott Hughes            "  Suggested slant axis limits: %g:%g:%g",
359*e1fe3e4aSElliott Hughes            measurements[designTriple[0]],
360*e1fe3e4aSElliott Hughes            measurements[designTriple[1]],
361*e1fe3e4aSElliott Hughes            measurements[designTriple[2]],
362*e1fe3e4aSElliott Hughes        )
363*e1fe3e4aSElliott Hughes
364*e1fe3e4aSElliott Hughes        return False
365*e1fe3e4aSElliott Hughes
366*e1fe3e4aSElliott Hughes    return True
367*e1fe3e4aSElliott Hughes
368*e1fe3e4aSElliott Hughes
369*e1fe3e4aSElliott Hughesdef planAxis(
370*e1fe3e4aSElliott Hughes    measureFunc,
371*e1fe3e4aSElliott Hughes    normalizeFunc,
372*e1fe3e4aSElliott Hughes    interpolateFunc,
373*e1fe3e4aSElliott Hughes    glyphSetFunc,
374*e1fe3e4aSElliott Hughes    axisTag,
375*e1fe3e4aSElliott Hughes    axisLimits,
376*e1fe3e4aSElliott Hughes    values,
377*e1fe3e4aSElliott Hughes    samples=None,
378*e1fe3e4aSElliott Hughes    glyphs=None,
379*e1fe3e4aSElliott Hughes    designLimits=None,
380*e1fe3e4aSElliott Hughes    pins=None,
381*e1fe3e4aSElliott Hughes    sanitizeFunc=None,
382*e1fe3e4aSElliott Hughes):
383*e1fe3e4aSElliott Hughes    """Plan an axis.
384*e1fe3e4aSElliott Hughes
385*e1fe3e4aSElliott Hughes    measureFunc: callable that takes a glyphset and an optional
386*e1fe3e4aSElliott Hughes    list of glyphnames, and returns the glyphset-wide measurement
387*e1fe3e4aSElliott Hughes    to be used for the axis.
388*e1fe3e4aSElliott Hughes
389*e1fe3e4aSElliott Hughes    normalizeFunc: callable that takes a measurement and a minimum
390*e1fe3e4aSElliott Hughes    and maximum, and normalizes the measurement into the range 0..1,
391*e1fe3e4aSElliott Hughes    possibly extrapolating too.
392*e1fe3e4aSElliott Hughes
393*e1fe3e4aSElliott Hughes    interpolateFunc: callable that takes a normalized t value, and a
394*e1fe3e4aSElliott Hughes    minimum and maximum, and returns the interpolated value,
395*e1fe3e4aSElliott Hughes    possibly extrapolating too.
396*e1fe3e4aSElliott Hughes
397*e1fe3e4aSElliott Hughes    glyphSetFunc: callable that takes a variations "location" dictionary,
398*e1fe3e4aSElliott Hughes    and returns a glyphset.
399*e1fe3e4aSElliott Hughes
400*e1fe3e4aSElliott Hughes    axisTag: the axis tag string.
401*e1fe3e4aSElliott Hughes
402*e1fe3e4aSElliott Hughes    axisLimits: a triple of minimum, default, and maximum values for
403*e1fe3e4aSElliott Hughes    the axis. Or an `fvar` Axis object.
404*e1fe3e4aSElliott Hughes
405*e1fe3e4aSElliott Hughes    values: a list of output values to map for this axis.
406*e1fe3e4aSElliott Hughes
407*e1fe3e4aSElliott Hughes    samples: the number of samples to use when sampling. Default 8.
408*e1fe3e4aSElliott Hughes
409*e1fe3e4aSElliott Hughes    glyphs: a list of glyph names to use when sampling. Defaults to None,
410*e1fe3e4aSElliott Hughes    which will process all glyphs.
411*e1fe3e4aSElliott Hughes
412*e1fe3e4aSElliott Hughes    designLimits: an optional triple of minimum, default, and maximum values
413*e1fe3e4aSElliott Hughes    represenging the "design" limits for the axis. If not provided, the
414*e1fe3e4aSElliott Hughes    axisLimits will be used.
415*e1fe3e4aSElliott Hughes
416*e1fe3e4aSElliott Hughes    pins: an optional dictionary of before/after mapping entries to pin in
417*e1fe3e4aSElliott Hughes    the output.
418*e1fe3e4aSElliott Hughes
419*e1fe3e4aSElliott Hughes    sanitizeFunc: an optional callable to call to sanitize the axis limits.
420*e1fe3e4aSElliott Hughes    """
421*e1fe3e4aSElliott Hughes
422*e1fe3e4aSElliott Hughes    if isinstance(axisLimits, fvarAxis):
423*e1fe3e4aSElliott Hughes        axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue)
424*e1fe3e4aSElliott Hughes    minValue, defaultValue, maxValue = axisLimits
425*e1fe3e4aSElliott Hughes
426*e1fe3e4aSElliott Hughes    if samples is None:
427*e1fe3e4aSElliott Hughes        samples = SAMPLES
428*e1fe3e4aSElliott Hughes    if glyphs is None:
429*e1fe3e4aSElliott Hughes        glyphs = glyphSetFunc({}).keys()
430*e1fe3e4aSElliott Hughes    if pins is None:
431*e1fe3e4aSElliott Hughes        pins = {}
432*e1fe3e4aSElliott Hughes    else:
433*e1fe3e4aSElliott Hughes        pins = pins.copy()
434*e1fe3e4aSElliott Hughes
435*e1fe3e4aSElliott Hughes    log.info(
436*e1fe3e4aSElliott Hughes        "Axis limits min %g / default %g / max %g", minValue, defaultValue, maxValue
437*e1fe3e4aSElliott Hughes    )
438*e1fe3e4aSElliott Hughes    triple = (minValue, defaultValue, maxValue)
439*e1fe3e4aSElliott Hughes
440*e1fe3e4aSElliott Hughes    if designLimits is not None:
441*e1fe3e4aSElliott Hughes        log.info("Axis design-limits min %g / default %g / max %g", *designLimits)
442*e1fe3e4aSElliott Hughes    else:
443*e1fe3e4aSElliott Hughes        designLimits = triple
444*e1fe3e4aSElliott Hughes
445*e1fe3e4aSElliott Hughes    if pins:
446*e1fe3e4aSElliott Hughes        log.info("Pins %s", sorted(pins.items()))
447*e1fe3e4aSElliott Hughes    pins.update(
448*e1fe3e4aSElliott Hughes        {
449*e1fe3e4aSElliott Hughes            minValue: designLimits[0],
450*e1fe3e4aSElliott Hughes            defaultValue: designLimits[1],
451*e1fe3e4aSElliott Hughes            maxValue: designLimits[2],
452*e1fe3e4aSElliott Hughes        }
453*e1fe3e4aSElliott Hughes    )
454*e1fe3e4aSElliott Hughes
455*e1fe3e4aSElliott Hughes    out = {}
456*e1fe3e4aSElliott Hughes    outNormalized = {}
457*e1fe3e4aSElliott Hughes
458*e1fe3e4aSElliott Hughes    axisMeasurements = {}
459*e1fe3e4aSElliott Hughes    for value in sorted({minValue, defaultValue, maxValue} | set(pins.keys())):
460*e1fe3e4aSElliott Hughes        glyphset = glyphSetFunc(location={axisTag: value})
461*e1fe3e4aSElliott Hughes        designValue = pins[value]
462*e1fe3e4aSElliott Hughes        axisMeasurements[designValue] = measureFunc(glyphset, glyphs)
463*e1fe3e4aSElliott Hughes
464*e1fe3e4aSElliott Hughes    if sanitizeFunc is not None:
465*e1fe3e4aSElliott Hughes        log.info("Sanitizing axis limit values for the `%s` axis.", axisTag)
466*e1fe3e4aSElliott Hughes        sanitizeFunc(triple, designLimits, pins, axisMeasurements)
467*e1fe3e4aSElliott Hughes
468*e1fe3e4aSElliott Hughes    log.debug("Calculated average value:\n%s", pformat(axisMeasurements))
469*e1fe3e4aSElliott Hughes
470*e1fe3e4aSElliott Hughes    for (rangeMin, targetMin), (rangeMax, targetMax) in zip(
471*e1fe3e4aSElliott Hughes        list(sorted(pins.items()))[:-1],
472*e1fe3e4aSElliott Hughes        list(sorted(pins.items()))[1:],
473*e1fe3e4aSElliott Hughes    ):
474*e1fe3e4aSElliott Hughes        targetValues = {w for w in values if rangeMin < w < rangeMax}
475*e1fe3e4aSElliott Hughes        if not targetValues:
476*e1fe3e4aSElliott Hughes            continue
477*e1fe3e4aSElliott Hughes
478*e1fe3e4aSElliott Hughes        normalizedMin = normalizeValue(rangeMin, triple)
479*e1fe3e4aSElliott Hughes        normalizedMax = normalizeValue(rangeMax, triple)
480*e1fe3e4aSElliott Hughes        normalizedTargetMin = normalizeValue(targetMin, designLimits)
481*e1fe3e4aSElliott Hughes        normalizedTargetMax = normalizeValue(targetMax, designLimits)
482*e1fe3e4aSElliott Hughes
483*e1fe3e4aSElliott Hughes        log.info("Planning target values %s.", sorted(targetValues))
484*e1fe3e4aSElliott Hughes        log.info("Sampling %u points in range %g,%g.", samples, rangeMin, rangeMax)
485*e1fe3e4aSElliott Hughes        valueMeasurements = axisMeasurements.copy()
486*e1fe3e4aSElliott Hughes        for sample in range(1, samples + 1):
487*e1fe3e4aSElliott Hughes            value = rangeMin + (rangeMax - rangeMin) * sample / (samples + 1)
488*e1fe3e4aSElliott Hughes            log.debug("Sampling value %g.", value)
489*e1fe3e4aSElliott Hughes            glyphset = glyphSetFunc(location={axisTag: value})
490*e1fe3e4aSElliott Hughes            designValue = piecewiseLinearMap(value, pins)
491*e1fe3e4aSElliott Hughes            valueMeasurements[designValue] = measureFunc(glyphset, glyphs)
492*e1fe3e4aSElliott Hughes        log.debug("Sampled average value:\n%s", pformat(valueMeasurements))
493*e1fe3e4aSElliott Hughes
494*e1fe3e4aSElliott Hughes        measurementValue = {}
495*e1fe3e4aSElliott Hughes        for value in sorted(valueMeasurements):
496*e1fe3e4aSElliott Hughes            measurementValue[valueMeasurements[value]] = value
497*e1fe3e4aSElliott Hughes
498*e1fe3e4aSElliott Hughes        out[rangeMin] = targetMin
499*e1fe3e4aSElliott Hughes        outNormalized[normalizedMin] = normalizedTargetMin
500*e1fe3e4aSElliott Hughes        for value in sorted(targetValues):
501*e1fe3e4aSElliott Hughes            t = normalizeFunc(value, rangeMin, rangeMax)
502*e1fe3e4aSElliott Hughes            targetMeasurement = interpolateFunc(
503*e1fe3e4aSElliott Hughes                t, valueMeasurements[targetMin], valueMeasurements[targetMax]
504*e1fe3e4aSElliott Hughes            )
505*e1fe3e4aSElliott Hughes            targetValue = piecewiseLinearMap(targetMeasurement, measurementValue)
506*e1fe3e4aSElliott Hughes            log.debug("Planned mapping value %g to %g." % (value, targetValue))
507*e1fe3e4aSElliott Hughes            out[value] = targetValue
508*e1fe3e4aSElliott Hughes            valueNormalized = normalizedMin + (value - rangeMin) / (
509*e1fe3e4aSElliott Hughes                rangeMax - rangeMin
510*e1fe3e4aSElliott Hughes            ) * (normalizedMax - normalizedMin)
511*e1fe3e4aSElliott Hughes            outNormalized[valueNormalized] = normalizedTargetMin + (
512*e1fe3e4aSElliott Hughes                targetValue - targetMin
513*e1fe3e4aSElliott Hughes            ) / (targetMax - targetMin) * (normalizedTargetMax - normalizedTargetMin)
514*e1fe3e4aSElliott Hughes        out[rangeMax] = targetMax
515*e1fe3e4aSElliott Hughes        outNormalized[normalizedMax] = normalizedTargetMax
516*e1fe3e4aSElliott Hughes
517*e1fe3e4aSElliott Hughes    log.info("Planned mapping for the `%s` axis:\n%s", axisTag, pformat(out))
518*e1fe3e4aSElliott Hughes    log.info(
519*e1fe3e4aSElliott Hughes        "Planned normalized mapping for the `%s` axis:\n%s",
520*e1fe3e4aSElliott Hughes        axisTag,
521*e1fe3e4aSElliott Hughes        pformat(outNormalized),
522*e1fe3e4aSElliott Hughes    )
523*e1fe3e4aSElliott Hughes
524*e1fe3e4aSElliott Hughes    if all(abs(k - v) < 0.01 for k, v in outNormalized.items()):
525*e1fe3e4aSElliott Hughes        log.info("Detected identity mapping for the `%s` axis. Dropping.", axisTag)
526*e1fe3e4aSElliott Hughes        out = {}
527*e1fe3e4aSElliott Hughes        outNormalized = {}
528*e1fe3e4aSElliott Hughes
529*e1fe3e4aSElliott Hughes    return out, outNormalized
530*e1fe3e4aSElliott Hughes
531*e1fe3e4aSElliott Hughes
532*e1fe3e4aSElliott Hughesdef planWeightAxis(
533*e1fe3e4aSElliott Hughes    glyphSetFunc,
534*e1fe3e4aSElliott Hughes    axisLimits,
535*e1fe3e4aSElliott Hughes    weights=None,
536*e1fe3e4aSElliott Hughes    samples=None,
537*e1fe3e4aSElliott Hughes    glyphs=None,
538*e1fe3e4aSElliott Hughes    designLimits=None,
539*e1fe3e4aSElliott Hughes    pins=None,
540*e1fe3e4aSElliott Hughes    sanitize=False,
541*e1fe3e4aSElliott Hughes):
542*e1fe3e4aSElliott Hughes    """Plan a weight (`wght`) axis.
543*e1fe3e4aSElliott Hughes
544*e1fe3e4aSElliott Hughes    weights: A list of weight values to plan for. If None, the default
545*e1fe3e4aSElliott Hughes    values are used.
546*e1fe3e4aSElliott Hughes
547*e1fe3e4aSElliott Hughes    This function simply calls planAxis with values=weights, and the appropriate
548*e1fe3e4aSElliott Hughes    arguments. See documenation for planAxis for more information.
549*e1fe3e4aSElliott Hughes    """
550*e1fe3e4aSElliott Hughes
551*e1fe3e4aSElliott Hughes    if weights is None:
552*e1fe3e4aSElliott Hughes        weights = WEIGHTS
553*e1fe3e4aSElliott Hughes
554*e1fe3e4aSElliott Hughes    return planAxis(
555*e1fe3e4aSElliott Hughes        measureWeight,
556*e1fe3e4aSElliott Hughes        normalizeLinear,
557*e1fe3e4aSElliott Hughes        interpolateLog,
558*e1fe3e4aSElliott Hughes        glyphSetFunc,
559*e1fe3e4aSElliott Hughes        "wght",
560*e1fe3e4aSElliott Hughes        axisLimits,
561*e1fe3e4aSElliott Hughes        values=weights,
562*e1fe3e4aSElliott Hughes        samples=samples,
563*e1fe3e4aSElliott Hughes        glyphs=glyphs,
564*e1fe3e4aSElliott Hughes        designLimits=designLimits,
565*e1fe3e4aSElliott Hughes        pins=pins,
566*e1fe3e4aSElliott Hughes        sanitizeFunc=sanitizeWeight if sanitize else None,
567*e1fe3e4aSElliott Hughes    )
568*e1fe3e4aSElliott Hughes
569*e1fe3e4aSElliott Hughes
570*e1fe3e4aSElliott Hughesdef planWidthAxis(
571*e1fe3e4aSElliott Hughes    glyphSetFunc,
572*e1fe3e4aSElliott Hughes    axisLimits,
573*e1fe3e4aSElliott Hughes    widths=None,
574*e1fe3e4aSElliott Hughes    samples=None,
575*e1fe3e4aSElliott Hughes    glyphs=None,
576*e1fe3e4aSElliott Hughes    designLimits=None,
577*e1fe3e4aSElliott Hughes    pins=None,
578*e1fe3e4aSElliott Hughes    sanitize=False,
579*e1fe3e4aSElliott Hughes):
580*e1fe3e4aSElliott Hughes    """Plan a width (`wdth`) axis.
581*e1fe3e4aSElliott Hughes
582*e1fe3e4aSElliott Hughes    widths: A list of width values (percentages) to plan for. If None, the default
583*e1fe3e4aSElliott Hughes    values are used.
584*e1fe3e4aSElliott Hughes
585*e1fe3e4aSElliott Hughes    This function simply calls planAxis with values=widths, and the appropriate
586*e1fe3e4aSElliott Hughes    arguments. See documenation for planAxis for more information.
587*e1fe3e4aSElliott Hughes    """
588*e1fe3e4aSElliott Hughes
589*e1fe3e4aSElliott Hughes    if widths is None:
590*e1fe3e4aSElliott Hughes        widths = WIDTHS
591*e1fe3e4aSElliott Hughes
592*e1fe3e4aSElliott Hughes    return planAxis(
593*e1fe3e4aSElliott Hughes        measureWidth,
594*e1fe3e4aSElliott Hughes        normalizeLinear,
595*e1fe3e4aSElliott Hughes        interpolateLinear,
596*e1fe3e4aSElliott Hughes        glyphSetFunc,
597*e1fe3e4aSElliott Hughes        "wdth",
598*e1fe3e4aSElliott Hughes        axisLimits,
599*e1fe3e4aSElliott Hughes        values=widths,
600*e1fe3e4aSElliott Hughes        samples=samples,
601*e1fe3e4aSElliott Hughes        glyphs=glyphs,
602*e1fe3e4aSElliott Hughes        designLimits=designLimits,
603*e1fe3e4aSElliott Hughes        pins=pins,
604*e1fe3e4aSElliott Hughes        sanitizeFunc=sanitizeWidth if sanitize else None,
605*e1fe3e4aSElliott Hughes    )
606*e1fe3e4aSElliott Hughes
607*e1fe3e4aSElliott Hughes
608*e1fe3e4aSElliott Hughesdef planSlantAxis(
609*e1fe3e4aSElliott Hughes    glyphSetFunc,
610*e1fe3e4aSElliott Hughes    axisLimits,
611*e1fe3e4aSElliott Hughes    slants=None,
612*e1fe3e4aSElliott Hughes    samples=None,
613*e1fe3e4aSElliott Hughes    glyphs=None,
614*e1fe3e4aSElliott Hughes    designLimits=None,
615*e1fe3e4aSElliott Hughes    pins=None,
616*e1fe3e4aSElliott Hughes    sanitize=False,
617*e1fe3e4aSElliott Hughes):
618*e1fe3e4aSElliott Hughes    """Plan a slant (`slnt`) axis.
619*e1fe3e4aSElliott Hughes
620*e1fe3e4aSElliott Hughes    slants: A list slant angles to plan for. If None, the default
621*e1fe3e4aSElliott Hughes    values are used.
622*e1fe3e4aSElliott Hughes
623*e1fe3e4aSElliott Hughes    This function simply calls planAxis with values=slants, and the appropriate
624*e1fe3e4aSElliott Hughes    arguments. See documenation for planAxis for more information.
625*e1fe3e4aSElliott Hughes    """
626*e1fe3e4aSElliott Hughes
627*e1fe3e4aSElliott Hughes    if slants is None:
628*e1fe3e4aSElliott Hughes        slants = SLANTS
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hughes    return planAxis(
631*e1fe3e4aSElliott Hughes        measureSlant,
632*e1fe3e4aSElliott Hughes        normalizeDegrees,
633*e1fe3e4aSElliott Hughes        interpolateLinear,
634*e1fe3e4aSElliott Hughes        glyphSetFunc,
635*e1fe3e4aSElliott Hughes        "slnt",
636*e1fe3e4aSElliott Hughes        axisLimits,
637*e1fe3e4aSElliott Hughes        values=slants,
638*e1fe3e4aSElliott Hughes        samples=samples,
639*e1fe3e4aSElliott Hughes        glyphs=glyphs,
640*e1fe3e4aSElliott Hughes        designLimits=designLimits,
641*e1fe3e4aSElliott Hughes        pins=pins,
642*e1fe3e4aSElliott Hughes        sanitizeFunc=sanitizeSlant if sanitize else None,
643*e1fe3e4aSElliott Hughes    )
644*e1fe3e4aSElliott Hughes
645*e1fe3e4aSElliott Hughes
646*e1fe3e4aSElliott Hughesdef planOpticalSizeAxis(
647*e1fe3e4aSElliott Hughes    glyphSetFunc,
648*e1fe3e4aSElliott Hughes    axisLimits,
649*e1fe3e4aSElliott Hughes    sizes=None,
650*e1fe3e4aSElliott Hughes    samples=None,
651*e1fe3e4aSElliott Hughes    glyphs=None,
652*e1fe3e4aSElliott Hughes    designLimits=None,
653*e1fe3e4aSElliott Hughes    pins=None,
654*e1fe3e4aSElliott Hughes    sanitize=False,
655*e1fe3e4aSElliott Hughes):
656*e1fe3e4aSElliott Hughes    """Plan a optical-size (`opsz`) axis.
657*e1fe3e4aSElliott Hughes
658*e1fe3e4aSElliott Hughes    sizes: A list of optical size values to plan for. If None, the default
659*e1fe3e4aSElliott Hughes    values are used.
660*e1fe3e4aSElliott Hughes
661*e1fe3e4aSElliott Hughes    This function simply calls planAxis with values=sizes, and the appropriate
662*e1fe3e4aSElliott Hughes    arguments. See documenation for planAxis for more information.
663*e1fe3e4aSElliott Hughes    """
664*e1fe3e4aSElliott Hughes
665*e1fe3e4aSElliott Hughes    if sizes is None:
666*e1fe3e4aSElliott Hughes        sizes = SIZES
667*e1fe3e4aSElliott Hughes
668*e1fe3e4aSElliott Hughes    return planAxis(
669*e1fe3e4aSElliott Hughes        measureWeight,
670*e1fe3e4aSElliott Hughes        normalizeLog,
671*e1fe3e4aSElliott Hughes        interpolateLog,
672*e1fe3e4aSElliott Hughes        glyphSetFunc,
673*e1fe3e4aSElliott Hughes        "opsz",
674*e1fe3e4aSElliott Hughes        axisLimits,
675*e1fe3e4aSElliott Hughes        values=sizes,
676*e1fe3e4aSElliott Hughes        samples=samples,
677*e1fe3e4aSElliott Hughes        glyphs=glyphs,
678*e1fe3e4aSElliott Hughes        designLimits=designLimits,
679*e1fe3e4aSElliott Hughes        pins=pins,
680*e1fe3e4aSElliott Hughes    )
681*e1fe3e4aSElliott Hughes
682*e1fe3e4aSElliott Hughes
683*e1fe3e4aSElliott Hughesdef makeDesignspaceSnippet(axisTag, axisName, axisLimit, mapping):
684*e1fe3e4aSElliott Hughes    """Make a designspace snippet for a single axis."""
685*e1fe3e4aSElliott Hughes
686*e1fe3e4aSElliott Hughes    designspaceSnippet = (
687*e1fe3e4aSElliott Hughes        '    <axis tag="%s" name="%s" minimum="%g" default="%g" maximum="%g"'
688*e1fe3e4aSElliott Hughes        % ((axisTag, axisName) + axisLimit)
689*e1fe3e4aSElliott Hughes    )
690*e1fe3e4aSElliott Hughes    if mapping:
691*e1fe3e4aSElliott Hughes        designspaceSnippet += ">\n"
692*e1fe3e4aSElliott Hughes    else:
693*e1fe3e4aSElliott Hughes        designspaceSnippet += "/>"
694*e1fe3e4aSElliott Hughes
695*e1fe3e4aSElliott Hughes    for key, value in mapping.items():
696*e1fe3e4aSElliott Hughes        designspaceSnippet += '      <map input="%g" output="%g"/>\n' % (key, value)
697*e1fe3e4aSElliott Hughes
698*e1fe3e4aSElliott Hughes    if mapping:
699*e1fe3e4aSElliott Hughes        designspaceSnippet += "    </axis>"
700*e1fe3e4aSElliott Hughes
701*e1fe3e4aSElliott Hughes    return designspaceSnippet
702*e1fe3e4aSElliott Hughes
703*e1fe3e4aSElliott Hughes
704*e1fe3e4aSElliott Hughesdef addEmptyAvar(font):
705*e1fe3e4aSElliott Hughes    """Add an empty `avar` table to the font."""
706*e1fe3e4aSElliott Hughes    font["avar"] = avar = newTable("avar")
707*e1fe3e4aSElliott Hughes    for axis in fvar.axes:
708*e1fe3e4aSElliott Hughes        avar.segments[axis.axisTag] = {}
709*e1fe3e4aSElliott Hughes
710*e1fe3e4aSElliott Hughes
711*e1fe3e4aSElliott Hughesdef processAxis(
712*e1fe3e4aSElliott Hughes    font,
713*e1fe3e4aSElliott Hughes    planFunc,
714*e1fe3e4aSElliott Hughes    axisTag,
715*e1fe3e4aSElliott Hughes    axisName,
716*e1fe3e4aSElliott Hughes    values,
717*e1fe3e4aSElliott Hughes    samples=None,
718*e1fe3e4aSElliott Hughes    glyphs=None,
719*e1fe3e4aSElliott Hughes    designLimits=None,
720*e1fe3e4aSElliott Hughes    pins=None,
721*e1fe3e4aSElliott Hughes    sanitize=False,
722*e1fe3e4aSElliott Hughes    plot=False,
723*e1fe3e4aSElliott Hughes):
724*e1fe3e4aSElliott Hughes    """Process a single axis."""
725*e1fe3e4aSElliott Hughes
726*e1fe3e4aSElliott Hughes    axisLimits = None
727*e1fe3e4aSElliott Hughes    for axis in font["fvar"].axes:
728*e1fe3e4aSElliott Hughes        if axis.axisTag == axisTag:
729*e1fe3e4aSElliott Hughes            axisLimits = axis
730*e1fe3e4aSElliott Hughes            break
731*e1fe3e4aSElliott Hughes    if axisLimits is None:
732*e1fe3e4aSElliott Hughes        return ""
733*e1fe3e4aSElliott Hughes    axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue)
734*e1fe3e4aSElliott Hughes
735*e1fe3e4aSElliott Hughes    log.info("Planning %s axis.", axisName)
736*e1fe3e4aSElliott Hughes
737*e1fe3e4aSElliott Hughes    if "avar" in font:
738*e1fe3e4aSElliott Hughes        existingMapping = font["avar"].segments[axisTag]
739*e1fe3e4aSElliott Hughes        font["avar"].segments[axisTag] = {}
740*e1fe3e4aSElliott Hughes    else:
741*e1fe3e4aSElliott Hughes        existingMapping = None
742*e1fe3e4aSElliott Hughes
743*e1fe3e4aSElliott Hughes    if values is not None and isinstance(values, str):
744*e1fe3e4aSElliott Hughes        values = [float(w) for w in values.split()]
745*e1fe3e4aSElliott Hughes
746*e1fe3e4aSElliott Hughes    if designLimits is not None and isinstance(designLimits, str):
747*e1fe3e4aSElliott Hughes        designLimits = [float(d) for d in options.designLimits.split(":")]
748*e1fe3e4aSElliott Hughes        assert (
749*e1fe3e4aSElliott Hughes            len(designLimits) == 3
750*e1fe3e4aSElliott Hughes            and designLimits[0] <= designLimits[1] <= designLimits[2]
751*e1fe3e4aSElliott Hughes        )
752*e1fe3e4aSElliott Hughes    else:
753*e1fe3e4aSElliott Hughes        designLimits = None
754*e1fe3e4aSElliott Hughes
755*e1fe3e4aSElliott Hughes    if pins is not None and isinstance(pins, str):
756*e1fe3e4aSElliott Hughes        newPins = {}
757*e1fe3e4aSElliott Hughes        for pin in pins.split():
758*e1fe3e4aSElliott Hughes            before, after = pin.split(":")
759*e1fe3e4aSElliott Hughes            newPins[float(before)] = float(after)
760*e1fe3e4aSElliott Hughes        pins = newPins
761*e1fe3e4aSElliott Hughes        del newPins
762*e1fe3e4aSElliott Hughes
763*e1fe3e4aSElliott Hughes    mapping, mappingNormalized = planFunc(
764*e1fe3e4aSElliott Hughes        font.getGlyphSet,
765*e1fe3e4aSElliott Hughes        axisLimits,
766*e1fe3e4aSElliott Hughes        values,
767*e1fe3e4aSElliott Hughes        samples=samples,
768*e1fe3e4aSElliott Hughes        glyphs=glyphs,
769*e1fe3e4aSElliott Hughes        designLimits=designLimits,
770*e1fe3e4aSElliott Hughes        pins=pins,
771*e1fe3e4aSElliott Hughes        sanitize=sanitize,
772*e1fe3e4aSElliott Hughes    )
773*e1fe3e4aSElliott Hughes
774*e1fe3e4aSElliott Hughes    if plot:
775*e1fe3e4aSElliott Hughes        from matplotlib import pyplot
776*e1fe3e4aSElliott Hughes
777*e1fe3e4aSElliott Hughes        pyplot.plot(
778*e1fe3e4aSElliott Hughes            sorted(mappingNormalized),
779*e1fe3e4aSElliott Hughes            [mappingNormalized[k] for k in sorted(mappingNormalized)],
780*e1fe3e4aSElliott Hughes        )
781*e1fe3e4aSElliott Hughes        pyplot.show()
782*e1fe3e4aSElliott Hughes
783*e1fe3e4aSElliott Hughes    if existingMapping is not None:
784*e1fe3e4aSElliott Hughes        log.info("Existing %s mapping:\n%s", axisName, pformat(existingMapping))
785*e1fe3e4aSElliott Hughes
786*e1fe3e4aSElliott Hughes    if mapping:
787*e1fe3e4aSElliott Hughes        if "avar" not in font:
788*e1fe3e4aSElliott Hughes            addEmptyAvar(font)
789*e1fe3e4aSElliott Hughes        font["avar"].segments[axisTag] = mappingNormalized
790*e1fe3e4aSElliott Hughes    else:
791*e1fe3e4aSElliott Hughes        if "avar" in font:
792*e1fe3e4aSElliott Hughes            font["avar"].segments[axisTag] = {}
793*e1fe3e4aSElliott Hughes
794*e1fe3e4aSElliott Hughes    designspaceSnippet = makeDesignspaceSnippet(
795*e1fe3e4aSElliott Hughes        axisTag,
796*e1fe3e4aSElliott Hughes        axisName,
797*e1fe3e4aSElliott Hughes        axisLimits,
798*e1fe3e4aSElliott Hughes        mapping,
799*e1fe3e4aSElliott Hughes    )
800*e1fe3e4aSElliott Hughes    return designspaceSnippet
801*e1fe3e4aSElliott Hughes
802*e1fe3e4aSElliott Hughes
803*e1fe3e4aSElliott Hughesdef main(args=None):
804*e1fe3e4aSElliott Hughes    """Plan the standard axis mappings for a variable font"""
805*e1fe3e4aSElliott Hughes
806*e1fe3e4aSElliott Hughes    if args is None:
807*e1fe3e4aSElliott Hughes        import sys
808*e1fe3e4aSElliott Hughes
809*e1fe3e4aSElliott Hughes        args = sys.argv[1:]
810*e1fe3e4aSElliott Hughes
811*e1fe3e4aSElliott Hughes    from fontTools import configLogger
812*e1fe3e4aSElliott Hughes    from fontTools.ttLib import TTFont
813*e1fe3e4aSElliott Hughes    import argparse
814*e1fe3e4aSElliott Hughes
815*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
816*e1fe3e4aSElliott Hughes        "fonttools varLib.avarPlanner",
817*e1fe3e4aSElliott Hughes        description="Plan `avar` table for variable font",
818*e1fe3e4aSElliott Hughes    )
819*e1fe3e4aSElliott Hughes    parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
820*e1fe3e4aSElliott Hughes    parser.add_argument(
821*e1fe3e4aSElliott Hughes        "-o",
822*e1fe3e4aSElliott Hughes        "--output-file",
823*e1fe3e4aSElliott Hughes        type=str,
824*e1fe3e4aSElliott Hughes        help="Output font file name.",
825*e1fe3e4aSElliott Hughes    )
826*e1fe3e4aSElliott Hughes    parser.add_argument(
827*e1fe3e4aSElliott Hughes        "--weights", type=str, help="Space-separate list of weights to generate."
828*e1fe3e4aSElliott Hughes    )
829*e1fe3e4aSElliott Hughes    parser.add_argument(
830*e1fe3e4aSElliott Hughes        "--widths", type=str, help="Space-separate list of widths to generate."
831*e1fe3e4aSElliott Hughes    )
832*e1fe3e4aSElliott Hughes    parser.add_argument(
833*e1fe3e4aSElliott Hughes        "--slants", type=str, help="Space-separate list of slants to generate."
834*e1fe3e4aSElliott Hughes    )
835*e1fe3e4aSElliott Hughes    parser.add_argument(
836*e1fe3e4aSElliott Hughes        "--sizes", type=str, help="Space-separate list of optical-sizes to generate."
837*e1fe3e4aSElliott Hughes    )
838*e1fe3e4aSElliott Hughes    parser.add_argument("--samples", type=int, help="Number of samples.")
839*e1fe3e4aSElliott Hughes    parser.add_argument(
840*e1fe3e4aSElliott Hughes        "-s", "--sanitize", action="store_true", help="Sanitize axis limits"
841*e1fe3e4aSElliott Hughes    )
842*e1fe3e4aSElliott Hughes    parser.add_argument(
843*e1fe3e4aSElliott Hughes        "-g",
844*e1fe3e4aSElliott Hughes        "--glyphs",
845*e1fe3e4aSElliott Hughes        type=str,
846*e1fe3e4aSElliott Hughes        help="Space-separate list of glyphs to use for sampling.",
847*e1fe3e4aSElliott Hughes    )
848*e1fe3e4aSElliott Hughes    parser.add_argument(
849*e1fe3e4aSElliott Hughes        "--weight-design-limits",
850*e1fe3e4aSElliott Hughes        type=str,
851*e1fe3e4aSElliott Hughes        help="min:default:max in design units for the `wght` axis.",
852*e1fe3e4aSElliott Hughes    )
853*e1fe3e4aSElliott Hughes    parser.add_argument(
854*e1fe3e4aSElliott Hughes        "--width-design-limits",
855*e1fe3e4aSElliott Hughes        type=str,
856*e1fe3e4aSElliott Hughes        help="min:default:max in design units for the `wdth` axis.",
857*e1fe3e4aSElliott Hughes    )
858*e1fe3e4aSElliott Hughes    parser.add_argument(
859*e1fe3e4aSElliott Hughes        "--slant-design-limits",
860*e1fe3e4aSElliott Hughes        type=str,
861*e1fe3e4aSElliott Hughes        help="min:default:max in design units for the `slnt` axis.",
862*e1fe3e4aSElliott Hughes    )
863*e1fe3e4aSElliott Hughes    parser.add_argument(
864*e1fe3e4aSElliott Hughes        "--optical-size-design-limits",
865*e1fe3e4aSElliott Hughes        type=str,
866*e1fe3e4aSElliott Hughes        help="min:default:max in design units for the `opsz` axis.",
867*e1fe3e4aSElliott Hughes    )
868*e1fe3e4aSElliott Hughes    parser.add_argument(
869*e1fe3e4aSElliott Hughes        "--weight-pins",
870*e1fe3e4aSElliott Hughes        type=str,
871*e1fe3e4aSElliott Hughes        help="Space-separate list of before:after pins for the `wght` axis.",
872*e1fe3e4aSElliott Hughes    )
873*e1fe3e4aSElliott Hughes    parser.add_argument(
874*e1fe3e4aSElliott Hughes        "--width-pins",
875*e1fe3e4aSElliott Hughes        type=str,
876*e1fe3e4aSElliott Hughes        help="Space-separate list of before:after pins for the `wdth` axis.",
877*e1fe3e4aSElliott Hughes    )
878*e1fe3e4aSElliott Hughes    parser.add_argument(
879*e1fe3e4aSElliott Hughes        "--slant-pins",
880*e1fe3e4aSElliott Hughes        type=str,
881*e1fe3e4aSElliott Hughes        help="Space-separate list of before:after pins for the `slnt` axis.",
882*e1fe3e4aSElliott Hughes    )
883*e1fe3e4aSElliott Hughes    parser.add_argument(
884*e1fe3e4aSElliott Hughes        "--optical-size-pins",
885*e1fe3e4aSElliott Hughes        type=str,
886*e1fe3e4aSElliott Hughes        help="Space-separate list of before:after pins for the `opsz` axis.",
887*e1fe3e4aSElliott Hughes    )
888*e1fe3e4aSElliott Hughes    parser.add_argument(
889*e1fe3e4aSElliott Hughes        "-p", "--plot", action="store_true", help="Plot the resulting mapping."
890*e1fe3e4aSElliott Hughes    )
891*e1fe3e4aSElliott Hughes
892*e1fe3e4aSElliott Hughes    logging_group = parser.add_mutually_exclusive_group(required=False)
893*e1fe3e4aSElliott Hughes    logging_group.add_argument(
894*e1fe3e4aSElliott Hughes        "-v", "--verbose", action="store_true", help="Run more verbosely."
895*e1fe3e4aSElliott Hughes    )
896*e1fe3e4aSElliott Hughes    logging_group.add_argument(
897*e1fe3e4aSElliott Hughes        "-q", "--quiet", action="store_true", help="Turn verbosity off."
898*e1fe3e4aSElliott Hughes    )
899*e1fe3e4aSElliott Hughes
900*e1fe3e4aSElliott Hughes    options = parser.parse_args(args)
901*e1fe3e4aSElliott Hughes
902*e1fe3e4aSElliott Hughes    configLogger(
903*e1fe3e4aSElliott Hughes        level=("DEBUG" if options.verbose else "WARNING" if options.quiet else "INFO")
904*e1fe3e4aSElliott Hughes    )
905*e1fe3e4aSElliott Hughes
906*e1fe3e4aSElliott Hughes    font = TTFont(options.font)
907*e1fe3e4aSElliott Hughes    if not "fvar" in font:
908*e1fe3e4aSElliott Hughes        log.error("Not a variable font.")
909*e1fe3e4aSElliott Hughes        return 1
910*e1fe3e4aSElliott Hughes
911*e1fe3e4aSElliott Hughes    if options.glyphs is not None:
912*e1fe3e4aSElliott Hughes        glyphs = options.glyphs.split()
913*e1fe3e4aSElliott Hughes        if ":" in options.glyphs:
914*e1fe3e4aSElliott Hughes            glyphs = {}
915*e1fe3e4aSElliott Hughes            for g in options.glyphs.split():
916*e1fe3e4aSElliott Hughes                if ":" in g:
917*e1fe3e4aSElliott Hughes                    glyph, frequency = g.split(":")
918*e1fe3e4aSElliott Hughes                    glyphs[glyph] = float(frequency)
919*e1fe3e4aSElliott Hughes                else:
920*e1fe3e4aSElliott Hughes                    glyphs[g] = 1.0
921*e1fe3e4aSElliott Hughes    else:
922*e1fe3e4aSElliott Hughes        glyphs = None
923*e1fe3e4aSElliott Hughes
924*e1fe3e4aSElliott Hughes    designspaceSnippets = []
925*e1fe3e4aSElliott Hughes
926*e1fe3e4aSElliott Hughes    designspaceSnippets.append(
927*e1fe3e4aSElliott Hughes        processAxis(
928*e1fe3e4aSElliott Hughes            font,
929*e1fe3e4aSElliott Hughes            planWeightAxis,
930*e1fe3e4aSElliott Hughes            "wght",
931*e1fe3e4aSElliott Hughes            "Weight",
932*e1fe3e4aSElliott Hughes            values=options.weights,
933*e1fe3e4aSElliott Hughes            samples=options.samples,
934*e1fe3e4aSElliott Hughes            glyphs=glyphs,
935*e1fe3e4aSElliott Hughes            designLimits=options.weight_design_limits,
936*e1fe3e4aSElliott Hughes            pins=options.weight_pins,
937*e1fe3e4aSElliott Hughes            sanitize=options.sanitize,
938*e1fe3e4aSElliott Hughes            plot=options.plot,
939*e1fe3e4aSElliott Hughes        )
940*e1fe3e4aSElliott Hughes    )
941*e1fe3e4aSElliott Hughes    designspaceSnippets.append(
942*e1fe3e4aSElliott Hughes        processAxis(
943*e1fe3e4aSElliott Hughes            font,
944*e1fe3e4aSElliott Hughes            planWidthAxis,
945*e1fe3e4aSElliott Hughes            "wdth",
946*e1fe3e4aSElliott Hughes            "Width",
947*e1fe3e4aSElliott Hughes            values=options.widths,
948*e1fe3e4aSElliott Hughes            samples=options.samples,
949*e1fe3e4aSElliott Hughes            glyphs=glyphs,
950*e1fe3e4aSElliott Hughes            designLimits=options.width_design_limits,
951*e1fe3e4aSElliott Hughes            pins=options.width_pins,
952*e1fe3e4aSElliott Hughes            sanitize=options.sanitize,
953*e1fe3e4aSElliott Hughes            plot=options.plot,
954*e1fe3e4aSElliott Hughes        )
955*e1fe3e4aSElliott Hughes    )
956*e1fe3e4aSElliott Hughes    designspaceSnippets.append(
957*e1fe3e4aSElliott Hughes        processAxis(
958*e1fe3e4aSElliott Hughes            font,
959*e1fe3e4aSElliott Hughes            planSlantAxis,
960*e1fe3e4aSElliott Hughes            "slnt",
961*e1fe3e4aSElliott Hughes            "Slant",
962*e1fe3e4aSElliott Hughes            values=options.slants,
963*e1fe3e4aSElliott Hughes            samples=options.samples,
964*e1fe3e4aSElliott Hughes            glyphs=glyphs,
965*e1fe3e4aSElliott Hughes            designLimits=options.slant_design_limits,
966*e1fe3e4aSElliott Hughes            pins=options.slant_pins,
967*e1fe3e4aSElliott Hughes            sanitize=options.sanitize,
968*e1fe3e4aSElliott Hughes            plot=options.plot,
969*e1fe3e4aSElliott Hughes        )
970*e1fe3e4aSElliott Hughes    )
971*e1fe3e4aSElliott Hughes    designspaceSnippets.append(
972*e1fe3e4aSElliott Hughes        processAxis(
973*e1fe3e4aSElliott Hughes            font,
974*e1fe3e4aSElliott Hughes            planOpticalSizeAxis,
975*e1fe3e4aSElliott Hughes            "opsz",
976*e1fe3e4aSElliott Hughes            "OpticalSize",
977*e1fe3e4aSElliott Hughes            values=options.sizes,
978*e1fe3e4aSElliott Hughes            samples=options.samples,
979*e1fe3e4aSElliott Hughes            glyphs=glyphs,
980*e1fe3e4aSElliott Hughes            designLimits=options.optical_size_design_limits,
981*e1fe3e4aSElliott Hughes            pins=options.optical_size_pins,
982*e1fe3e4aSElliott Hughes            sanitize=options.sanitize,
983*e1fe3e4aSElliott Hughes            plot=options.plot,
984*e1fe3e4aSElliott Hughes        )
985*e1fe3e4aSElliott Hughes    )
986*e1fe3e4aSElliott Hughes
987*e1fe3e4aSElliott Hughes    log.info("Designspace snippet:")
988*e1fe3e4aSElliott Hughes    for snippet in designspaceSnippets:
989*e1fe3e4aSElliott Hughes        if snippet:
990*e1fe3e4aSElliott Hughes            print(snippet)
991*e1fe3e4aSElliott Hughes
992*e1fe3e4aSElliott Hughes    if options.output_file is None:
993*e1fe3e4aSElliott Hughes        outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar")
994*e1fe3e4aSElliott Hughes    else:
995*e1fe3e4aSElliott Hughes        outfile = options.output_file
996*e1fe3e4aSElliott Hughes    if outfile:
997*e1fe3e4aSElliott Hughes        log.info("Saving %s", outfile)
998*e1fe3e4aSElliott Hughes        font.save(outfile)
999*e1fe3e4aSElliott Hughes
1000*e1fe3e4aSElliott Hughes
1001*e1fe3e4aSElliott Hughesif __name__ == "__main__":
1002*e1fe3e4aSElliott Hughes    import sys
1003*e1fe3e4aSElliott Hughes
1004*e1fe3e4aSElliott Hughes    sys.exit(main())
1005