fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-musllinux_1_2_aarch64.whl

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