fonttools 4.60.2__cp311-cp311-win32.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.
- fontTools/__init__.py +8 -0
- fontTools/__main__.py +35 -0
- fontTools/afmLib.py +439 -0
- fontTools/agl.py +5233 -0
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +258 -0
- fontTools/cffLib/CFFToCFF2.py +305 -0
- fontTools/cffLib/__init__.py +3694 -0
- fontTools/cffLib/specializer.py +927 -0
- fontTools/cffLib/transforms.py +495 -0
- fontTools/cffLib/width.py +210 -0
- fontTools/colorLib/__init__.py +0 -0
- fontTools/colorLib/builder.py +664 -0
- fontTools/colorLib/errors.py +2 -0
- fontTools/colorLib/geometry.py +143 -0
- fontTools/colorLib/table_builder.py +223 -0
- fontTools/colorLib/unbuilder.py +81 -0
- fontTools/config/__init__.py +90 -0
- fontTools/cu2qu/__init__.py +15 -0
- fontTools/cu2qu/__main__.py +6 -0
- fontTools/cu2qu/benchmark.py +54 -0
- fontTools/cu2qu/cli.py +198 -0
- fontTools/cu2qu/cu2qu.c +15817 -0
- fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
- fontTools/cu2qu/cu2qu.py +563 -0
- fontTools/cu2qu/errors.py +77 -0
- fontTools/cu2qu/ufo.py +363 -0
- fontTools/designspaceLib/__init__.py +3343 -0
- fontTools/designspaceLib/__main__.py +6 -0
- fontTools/designspaceLib/split.py +475 -0
- fontTools/designspaceLib/statNames.py +260 -0
- fontTools/designspaceLib/types.py +147 -0
- fontTools/encodings/MacRoman.py +258 -0
- fontTools/encodings/StandardEncoding.py +258 -0
- fontTools/encodings/__init__.py +1 -0
- fontTools/encodings/codecs.py +135 -0
- fontTools/feaLib/__init__.py +4 -0
- fontTools/feaLib/__main__.py +78 -0
- fontTools/feaLib/ast.py +2143 -0
- fontTools/feaLib/builder.py +1814 -0
- fontTools/feaLib/error.py +22 -0
- fontTools/feaLib/lexer.c +17029 -0
- fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
- fontTools/feaLib/lexer.py +287 -0
- fontTools/feaLib/location.py +12 -0
- fontTools/feaLib/lookupDebugInfo.py +12 -0
- fontTools/feaLib/parser.py +2394 -0
- fontTools/feaLib/variableScalar.py +118 -0
- fontTools/fontBuilder.py +1014 -0
- fontTools/help.py +36 -0
- fontTools/merge/__init__.py +248 -0
- fontTools/merge/__main__.py +6 -0
- fontTools/merge/base.py +81 -0
- fontTools/merge/cmap.py +173 -0
- fontTools/merge/layout.py +526 -0
- fontTools/merge/options.py +85 -0
- fontTools/merge/tables.py +352 -0
- fontTools/merge/unicode.py +78 -0
- fontTools/merge/util.py +143 -0
- fontTools/misc/__init__.py +1 -0
- fontTools/misc/arrayTools.py +424 -0
- fontTools/misc/bezierTools.c +39731 -0
- fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
- fontTools/misc/bezierTools.py +1500 -0
- fontTools/misc/classifyTools.py +170 -0
- fontTools/misc/cliTools.py +53 -0
- fontTools/misc/configTools.py +349 -0
- fontTools/misc/cython.py +27 -0
- fontTools/misc/dictTools.py +83 -0
- fontTools/misc/eexec.py +119 -0
- fontTools/misc/encodingTools.py +72 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +456 -0
- fontTools/misc/filenames.py +245 -0
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +253 -0
- fontTools/misc/intTools.py +25 -0
- fontTools/misc/iterTools.py +12 -0
- fontTools/misc/lazyTools.py +42 -0
- fontTools/misc/loggingTools.py +543 -0
- fontTools/misc/macCreatorType.py +56 -0
- fontTools/misc/macRes.py +261 -0
- fontTools/misc/plistlib/__init__.py +681 -0
- fontTools/misc/plistlib/py.typed +0 -0
- fontTools/misc/psCharStrings.py +1511 -0
- fontTools/misc/psLib.py +398 -0
- fontTools/misc/psOperators.py +572 -0
- fontTools/misc/py23.py +96 -0
- fontTools/misc/roundTools.py +110 -0
- fontTools/misc/sstruct.py +227 -0
- fontTools/misc/symfont.py +242 -0
- fontTools/misc/testTools.py +233 -0
- fontTools/misc/textTools.py +156 -0
- fontTools/misc/timeTools.py +88 -0
- fontTools/misc/transform.py +516 -0
- fontTools/misc/treeTools.py +45 -0
- fontTools/misc/vector.py +147 -0
- fontTools/misc/visitor.py +158 -0
- fontTools/misc/xmlReader.py +188 -0
- fontTools/misc/xmlWriter.py +231 -0
- fontTools/mtiLib/__init__.py +1400 -0
- fontTools/mtiLib/__main__.py +5 -0
- fontTools/otlLib/__init__.py +1 -0
- fontTools/otlLib/builder.py +3465 -0
- fontTools/otlLib/error.py +11 -0
- fontTools/otlLib/maxContextCalc.py +96 -0
- fontTools/otlLib/optimize/__init__.py +53 -0
- fontTools/otlLib/optimize/__main__.py +6 -0
- fontTools/otlLib/optimize/gpos.py +439 -0
- fontTools/pens/__init__.py +1 -0
- fontTools/pens/areaPen.py +52 -0
- fontTools/pens/basePen.py +475 -0
- fontTools/pens/boundsPen.py +98 -0
- fontTools/pens/cairoPen.py +26 -0
- fontTools/pens/cocoaPen.py +26 -0
- fontTools/pens/cu2quPen.py +325 -0
- fontTools/pens/explicitClosingLinePen.py +101 -0
- fontTools/pens/filterPen.py +433 -0
- fontTools/pens/freetypePen.py +462 -0
- fontTools/pens/hashPointPen.py +89 -0
- fontTools/pens/momentsPen.c +13378 -0
- fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
- fontTools/pens/momentsPen.py +879 -0
- fontTools/pens/perimeterPen.py +69 -0
- fontTools/pens/pointInsidePen.py +192 -0
- fontTools/pens/pointPen.py +643 -0
- fontTools/pens/qtPen.py +29 -0
- fontTools/pens/qu2cuPen.py +105 -0
- fontTools/pens/quartzPen.py +43 -0
- fontTools/pens/recordingPen.py +335 -0
- fontTools/pens/reportLabPen.py +79 -0
- fontTools/pens/reverseContourPen.py +96 -0
- fontTools/pens/roundingPen.py +130 -0
- fontTools/pens/statisticsPen.py +312 -0
- fontTools/pens/svgPathPen.py +310 -0
- fontTools/pens/t2CharStringPen.py +88 -0
- fontTools/pens/teePen.py +55 -0
- fontTools/pens/transformPen.py +115 -0
- fontTools/pens/ttGlyphPen.py +335 -0
- fontTools/pens/wxPen.py +29 -0
- fontTools/qu2cu/__init__.py +15 -0
- fontTools/qu2cu/__main__.py +7 -0
- fontTools/qu2cu/benchmark.py +56 -0
- fontTools/qu2cu/cli.py +125 -0
- fontTools/qu2cu/qu2cu.c +16682 -0
- fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
- fontTools/qu2cu/qu2cu.py +405 -0
- fontTools/subset/__init__.py +4096 -0
- fontTools/subset/__main__.py +6 -0
- fontTools/subset/cff.py +184 -0
- fontTools/subset/svg.py +253 -0
- fontTools/subset/util.py +25 -0
- fontTools/svgLib/__init__.py +3 -0
- fontTools/svgLib/path/__init__.py +65 -0
- fontTools/svgLib/path/arc.py +154 -0
- fontTools/svgLib/path/parser.py +322 -0
- fontTools/svgLib/path/shapes.py +183 -0
- fontTools/t1Lib/__init__.py +648 -0
- fontTools/tfmLib.py +460 -0
- fontTools/ttLib/__init__.py +30 -0
- fontTools/ttLib/__main__.py +148 -0
- fontTools/ttLib/macUtils.py +54 -0
- fontTools/ttLib/removeOverlaps.py +395 -0
- fontTools/ttLib/reorderGlyphs.py +285 -0
- fontTools/ttLib/scaleUpem.py +436 -0
- fontTools/ttLib/sfnt.py +661 -0
- fontTools/ttLib/standardGlyphOrder.py +271 -0
- fontTools/ttLib/tables/B_A_S_E_.py +14 -0
- fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
- fontTools/ttLib/tables/C_B_D_T_.py +113 -0
- fontTools/ttLib/tables/C_B_L_C_.py +19 -0
- fontTools/ttLib/tables/C_F_F_.py +61 -0
- fontTools/ttLib/tables/C_F_F__2.py +26 -0
- fontTools/ttLib/tables/C_O_L_R_.py +165 -0
- fontTools/ttLib/tables/C_P_A_L_.py +305 -0
- fontTools/ttLib/tables/D_S_I_G_.py +158 -0
- fontTools/ttLib/tables/D__e_b_g.py +35 -0
- fontTools/ttLib/tables/DefaultTable.py +49 -0
- fontTools/ttLib/tables/E_B_D_T_.py +835 -0
- fontTools/ttLib/tables/E_B_L_C_.py +718 -0
- fontTools/ttLib/tables/F_F_T_M_.py +52 -0
- fontTools/ttLib/tables/F__e_a_t.py +149 -0
- fontTools/ttLib/tables/G_D_E_F_.py +13 -0
- fontTools/ttLib/tables/G_M_A_P_.py +148 -0
- fontTools/ttLib/tables/G_P_K_G_.py +133 -0
- fontTools/ttLib/tables/G_P_O_S_.py +14 -0
- fontTools/ttLib/tables/G_S_U_B_.py +13 -0
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/G__l_a_t.py +235 -0
- fontTools/ttLib/tables/G__l_o_c.py +85 -0
- fontTools/ttLib/tables/H_V_A_R_.py +13 -0
- fontTools/ttLib/tables/J_S_T_F_.py +13 -0
- fontTools/ttLib/tables/L_T_S_H_.py +58 -0
- fontTools/ttLib/tables/M_A_T_H_.py +13 -0
- fontTools/ttLib/tables/M_E_T_A_.py +352 -0
- fontTools/ttLib/tables/M_V_A_R_.py +13 -0
- fontTools/ttLib/tables/O_S_2f_2.py +752 -0
- fontTools/ttLib/tables/S_I_N_G_.py +99 -0
- fontTools/ttLib/tables/S_T_A_T_.py +15 -0
- fontTools/ttLib/tables/S_V_G_.py +223 -0
- fontTools/ttLib/tables/S__i_l_f.py +1040 -0
- fontTools/ttLib/tables/S__i_l_l.py +92 -0
- fontTools/ttLib/tables/T_S_I_B_.py +13 -0
- fontTools/ttLib/tables/T_S_I_C_.py +14 -0
- fontTools/ttLib/tables/T_S_I_D_.py +13 -0
- fontTools/ttLib/tables/T_S_I_J_.py +13 -0
- fontTools/ttLib/tables/T_S_I_P_.py +13 -0
- fontTools/ttLib/tables/T_S_I_S_.py +13 -0
- fontTools/ttLib/tables/T_S_I_V_.py +26 -0
- fontTools/ttLib/tables/T_S_I__0.py +70 -0
- fontTools/ttLib/tables/T_S_I__1.py +163 -0
- fontTools/ttLib/tables/T_S_I__2.py +17 -0
- fontTools/ttLib/tables/T_S_I__3.py +22 -0
- fontTools/ttLib/tables/T_S_I__5.py +60 -0
- fontTools/ttLib/tables/T_T_F_A_.py +14 -0
- fontTools/ttLib/tables/TupleVariation.py +884 -0
- fontTools/ttLib/tables/V_A_R_C_.py +12 -0
- fontTools/ttLib/tables/V_D_M_X_.py +249 -0
- fontTools/ttLib/tables/V_O_R_G_.py +165 -0
- fontTools/ttLib/tables/V_V_A_R_.py +13 -0
- fontTools/ttLib/tables/__init__.py +98 -0
- fontTools/ttLib/tables/_a_n_k_r.py +15 -0
- fontTools/ttLib/tables/_a_v_a_r.py +193 -0
- fontTools/ttLib/tables/_b_s_l_n.py +15 -0
- fontTools/ttLib/tables/_c_i_d_g.py +24 -0
- fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
- fontTools/ttLib/tables/_c_v_a_r.py +94 -0
- fontTools/ttLib/tables/_c_v_t.py +56 -0
- fontTools/ttLib/tables/_f_e_a_t.py +15 -0
- fontTools/ttLib/tables/_f_p_g_m.py +62 -0
- fontTools/ttLib/tables/_f_v_a_r.py +261 -0
- fontTools/ttLib/tables/_g_a_s_p.py +63 -0
- fontTools/ttLib/tables/_g_c_i_d.py +13 -0
- fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
- fontTools/ttLib/tables/_g_v_a_r.py +340 -0
- fontTools/ttLib/tables/_h_d_m_x.py +127 -0
- fontTools/ttLib/tables/_h_e_a_d.py +130 -0
- fontTools/ttLib/tables/_h_h_e_a.py +147 -0
- fontTools/ttLib/tables/_h_m_t_x.py +164 -0
- fontTools/ttLib/tables/_k_e_r_n.py +289 -0
- fontTools/ttLib/tables/_l_c_a_r.py +13 -0
- fontTools/ttLib/tables/_l_o_c_a.py +70 -0
- fontTools/ttLib/tables/_l_t_a_g.py +72 -0
- fontTools/ttLib/tables/_m_a_x_p.py +147 -0
- fontTools/ttLib/tables/_m_e_t_a.py +112 -0
- fontTools/ttLib/tables/_m_o_r_t.py +14 -0
- fontTools/ttLib/tables/_m_o_r_x.py +15 -0
- fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
- fontTools/ttLib/tables/_o_p_b_d.py +14 -0
- fontTools/ttLib/tables/_p_o_s_t.py +319 -0
- fontTools/ttLib/tables/_p_r_e_p.py +16 -0
- fontTools/ttLib/tables/_p_r_o_p.py +12 -0
- fontTools/ttLib/tables/_s_b_i_x.py +129 -0
- fontTools/ttLib/tables/_t_r_a_k.py +332 -0
- fontTools/ttLib/tables/_v_h_e_a.py +139 -0
- fontTools/ttLib/tables/_v_m_t_x.py +19 -0
- fontTools/ttLib/tables/asciiTable.py +20 -0
- fontTools/ttLib/tables/grUtils.py +92 -0
- fontTools/ttLib/tables/otBase.py +1458 -0
- fontTools/ttLib/tables/otConverters.py +2068 -0
- fontTools/ttLib/tables/otData.py +6400 -0
- fontTools/ttLib/tables/otTables.py +2703 -0
- fontTools/ttLib/tables/otTraverse.py +163 -0
- fontTools/ttLib/tables/sbixGlyph.py +149 -0
- fontTools/ttLib/tables/sbixStrike.py +177 -0
- fontTools/ttLib/tables/table_API_readme.txt +91 -0
- fontTools/ttLib/tables/ttProgram.py +594 -0
- fontTools/ttLib/ttCollection.py +125 -0
- fontTools/ttLib/ttFont.py +1148 -0
- fontTools/ttLib/ttGlyphSet.py +490 -0
- fontTools/ttLib/ttVisitor.py +32 -0
- fontTools/ttLib/woff2.py +1680 -0
- fontTools/ttx.py +479 -0
- fontTools/ufoLib/__init__.py +2575 -0
- fontTools/ufoLib/converters.py +407 -0
- fontTools/ufoLib/errors.py +30 -0
- fontTools/ufoLib/etree.py +6 -0
- fontTools/ufoLib/filenames.py +356 -0
- fontTools/ufoLib/glifLib.py +2120 -0
- fontTools/ufoLib/kerning.py +141 -0
- fontTools/ufoLib/plistlib.py +47 -0
- fontTools/ufoLib/pointPen.py +6 -0
- fontTools/ufoLib/utils.py +107 -0
- fontTools/ufoLib/validators.py +1208 -0
- fontTools/unicode.py +50 -0
- fontTools/unicodedata/Blocks.py +817 -0
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/OTTags.py +50 -0
- fontTools/unicodedata/ScriptExtensions.py +832 -0
- fontTools/unicodedata/Scripts.py +3639 -0
- fontTools/unicodedata/__init__.py +306 -0
- fontTools/varLib/__init__.py +1600 -0
- fontTools/varLib/__main__.py +6 -0
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/avar/unbuild.py +271 -0
- fontTools/varLib/avarPlanner.py +8 -0
- fontTools/varLib/builder.py +215 -0
- fontTools/varLib/cff.py +631 -0
- fontTools/varLib/errors.py +219 -0
- fontTools/varLib/featureVars.py +703 -0
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +2052 -0
- fontTools/varLib/instancer/__main__.py +5 -0
- fontTools/varLib/instancer/featureVars.py +190 -0
- fontTools/varLib/instancer/names.py +388 -0
- fontTools/varLib/instancer/solver.py +309 -0
- fontTools/varLib/interpolatable.py +1209 -0
- fontTools/varLib/interpolatableHelpers.py +399 -0
- fontTools/varLib/interpolatablePlot.py +1269 -0
- fontTools/varLib/interpolatableTestContourOrder.py +82 -0
- fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
- fontTools/varLib/interpolate_layout.py +124 -0
- fontTools/varLib/iup.c +19815 -0
- fontTools/varLib/iup.cp311-win32.pyd +0 -0
- fontTools/varLib/iup.py +490 -0
- fontTools/varLib/merger.py +1717 -0
- fontTools/varLib/models.py +642 -0
- fontTools/varLib/multiVarStore.py +253 -0
- fontTools/varLib/mutator.py +529 -0
- fontTools/varLib/mvar.py +40 -0
- fontTools/varLib/plot.py +238 -0
- fontTools/varLib/stat.py +149 -0
- fontTools/varLib/varStore.py +739 -0
- fontTools/voltLib/__init__.py +5 -0
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +452 -0
- fontTools/voltLib/error.py +12 -0
- fontTools/voltLib/lexer.py +99 -0
- fontTools/voltLib/parser.py +664 -0
- fontTools/voltLib/voltToFea.py +911 -0
- fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
- fonttools-4.60.2.dist-info/METADATA +2250 -0
- fonttools-4.60.2.dist-info/RECORD +353 -0
- fonttools-4.60.2.dist-info/WHEEL +5 -0
- fonttools-4.60.2.dist-info/entry_points.txt +5 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
- fonttools-4.60.2.dist-info/top_level.txt +1 -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())
|