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,495 @@
|
|
|
1
|
+
from fontTools.misc.psCharStrings import (
|
|
2
|
+
SimpleT2Decompiler,
|
|
3
|
+
T2WidthExtractor,
|
|
4
|
+
calcSubrBias,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _uniq_sort(l):
|
|
9
|
+
return sorted(set(l))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StopHintCountEvent(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
|
|
17
|
+
stop_hintcount_ops = (
|
|
18
|
+
"op_hintmask",
|
|
19
|
+
"op_cntrmask",
|
|
20
|
+
"op_rmoveto",
|
|
21
|
+
"op_hmoveto",
|
|
22
|
+
"op_vmoveto",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __init__(self, localSubrs, globalSubrs, private=None):
|
|
26
|
+
SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
|
|
27
|
+
|
|
28
|
+
def execute(self, charString):
|
|
29
|
+
self.need_hintcount = True # until proven otherwise
|
|
30
|
+
for op_name in self.stop_hintcount_ops:
|
|
31
|
+
setattr(self, op_name, self.stop_hint_count)
|
|
32
|
+
|
|
33
|
+
if hasattr(charString, "_desubroutinized"):
|
|
34
|
+
# If a charstring has already been desubroutinized, we will still
|
|
35
|
+
# need to execute it if we need to count hints in order to
|
|
36
|
+
# compute the byte length for mask arguments, and haven't finished
|
|
37
|
+
# counting hints pairs.
|
|
38
|
+
if self.need_hintcount and self.callingStack:
|
|
39
|
+
try:
|
|
40
|
+
SimpleT2Decompiler.execute(self, charString)
|
|
41
|
+
except StopHintCountEvent:
|
|
42
|
+
del self.callingStack[-1]
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
charString._patches = []
|
|
46
|
+
SimpleT2Decompiler.execute(self, charString)
|
|
47
|
+
desubroutinized = charString.program[:]
|
|
48
|
+
for idx, expansion in reversed(charString._patches):
|
|
49
|
+
assert idx >= 2
|
|
50
|
+
assert desubroutinized[idx - 1] in [
|
|
51
|
+
"callsubr",
|
|
52
|
+
"callgsubr",
|
|
53
|
+
], desubroutinized[idx - 1]
|
|
54
|
+
assert type(desubroutinized[idx - 2]) == int
|
|
55
|
+
if expansion[-1] == "return":
|
|
56
|
+
expansion = expansion[:-1]
|
|
57
|
+
desubroutinized[idx - 2 : idx] = expansion
|
|
58
|
+
if not self.private.in_cff2:
|
|
59
|
+
if "endchar" in desubroutinized:
|
|
60
|
+
# Cut off after first endchar
|
|
61
|
+
desubroutinized = desubroutinized[
|
|
62
|
+
: desubroutinized.index("endchar") + 1
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
charString._desubroutinized = desubroutinized
|
|
66
|
+
del charString._patches
|
|
67
|
+
|
|
68
|
+
def op_callsubr(self, index):
|
|
69
|
+
subr = self.localSubrs[self.operandStack[-1] + self.localBias]
|
|
70
|
+
SimpleT2Decompiler.op_callsubr(self, index)
|
|
71
|
+
self.processSubr(index, subr)
|
|
72
|
+
|
|
73
|
+
def op_callgsubr(self, index):
|
|
74
|
+
subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
|
|
75
|
+
SimpleT2Decompiler.op_callgsubr(self, index)
|
|
76
|
+
self.processSubr(index, subr)
|
|
77
|
+
|
|
78
|
+
def stop_hint_count(self, *args):
|
|
79
|
+
self.need_hintcount = False
|
|
80
|
+
for op_name in self.stop_hintcount_ops:
|
|
81
|
+
setattr(self, op_name, None)
|
|
82
|
+
cs = self.callingStack[-1]
|
|
83
|
+
if hasattr(cs, "_desubroutinized"):
|
|
84
|
+
raise StopHintCountEvent()
|
|
85
|
+
|
|
86
|
+
def op_hintmask(self, index):
|
|
87
|
+
SimpleT2Decompiler.op_hintmask(self, index)
|
|
88
|
+
if self.need_hintcount:
|
|
89
|
+
self.stop_hint_count()
|
|
90
|
+
|
|
91
|
+
def processSubr(self, index, subr):
|
|
92
|
+
cs = self.callingStack[-1]
|
|
93
|
+
if not hasattr(cs, "_desubroutinized"):
|
|
94
|
+
cs._patches.append((index, subr._desubroutinized))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def desubroutinizeCharString(cs):
|
|
98
|
+
"""Desubroutinize a charstring in-place."""
|
|
99
|
+
cs.decompile()
|
|
100
|
+
subrs = getattr(cs.private, "Subrs", [])
|
|
101
|
+
decompiler = _DesubroutinizingT2Decompiler(subrs, cs.globalSubrs, cs.private)
|
|
102
|
+
decompiler.execute(cs)
|
|
103
|
+
cs.program = cs._desubroutinized
|
|
104
|
+
del cs._desubroutinized
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def desubroutinize(cff):
|
|
108
|
+
for fontName in cff.fontNames:
|
|
109
|
+
font = cff[fontName]
|
|
110
|
+
cs = font.CharStrings
|
|
111
|
+
for c in cs.values():
|
|
112
|
+
desubroutinizeCharString(c)
|
|
113
|
+
# Delete all the local subrs
|
|
114
|
+
if hasattr(font, "FDArray"):
|
|
115
|
+
for fd in font.FDArray:
|
|
116
|
+
pd = fd.Private
|
|
117
|
+
if hasattr(pd, "Subrs"):
|
|
118
|
+
del pd.Subrs
|
|
119
|
+
if "Subrs" in pd.rawDict:
|
|
120
|
+
del pd.rawDict["Subrs"]
|
|
121
|
+
else:
|
|
122
|
+
pd = font.Private
|
|
123
|
+
if hasattr(pd, "Subrs"):
|
|
124
|
+
del pd.Subrs
|
|
125
|
+
if "Subrs" in pd.rawDict:
|
|
126
|
+
del pd.rawDict["Subrs"]
|
|
127
|
+
# as well as the global subrs
|
|
128
|
+
cff.GlobalSubrs.clear()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _MarkingT2Decompiler(SimpleT2Decompiler):
|
|
132
|
+
def __init__(self, localSubrs, globalSubrs, private):
|
|
133
|
+
SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
|
|
134
|
+
for subrs in [localSubrs, globalSubrs]:
|
|
135
|
+
if subrs and not hasattr(subrs, "_used"):
|
|
136
|
+
subrs._used = set()
|
|
137
|
+
|
|
138
|
+
def op_callsubr(self, index):
|
|
139
|
+
self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
|
|
140
|
+
SimpleT2Decompiler.op_callsubr(self, index)
|
|
141
|
+
|
|
142
|
+
def op_callgsubr(self, index):
|
|
143
|
+
self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
|
|
144
|
+
SimpleT2Decompiler.op_callgsubr(self, index)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _DehintingT2Decompiler(T2WidthExtractor):
|
|
148
|
+
class Hints(object):
|
|
149
|
+
def __init__(self):
|
|
150
|
+
# Whether calling this charstring produces any hint stems
|
|
151
|
+
# Note that if a charstring starts with hintmask, it will
|
|
152
|
+
# have has_hint set to True, because it *might* produce an
|
|
153
|
+
# implicit vstem if called under certain conditions.
|
|
154
|
+
self.has_hint = False
|
|
155
|
+
# Index to start at to drop all hints
|
|
156
|
+
self.last_hint = 0
|
|
157
|
+
# Index up to which we know more hints are possible.
|
|
158
|
+
# Only relevant if status is 0 or 1.
|
|
159
|
+
self.last_checked = 0
|
|
160
|
+
# The status means:
|
|
161
|
+
# 0: after dropping hints, this charstring is empty
|
|
162
|
+
# 1: after dropping hints, there may be more hints
|
|
163
|
+
# continuing after this, or there might be
|
|
164
|
+
# other things. Not clear yet.
|
|
165
|
+
# 2: no more hints possible after this charstring
|
|
166
|
+
self.status = 0
|
|
167
|
+
# Has hintmask instructions; not recursive
|
|
168
|
+
self.has_hintmask = False
|
|
169
|
+
# List of indices of calls to empty subroutines to remove.
|
|
170
|
+
self.deletions = []
|
|
171
|
+
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
|
|
176
|
+
):
|
|
177
|
+
self._css = css
|
|
178
|
+
T2WidthExtractor.__init__(
|
|
179
|
+
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
|
|
180
|
+
)
|
|
181
|
+
self.private = private
|
|
182
|
+
|
|
183
|
+
def execute(self, charString):
|
|
184
|
+
old_hints = charString._hints if hasattr(charString, "_hints") else None
|
|
185
|
+
charString._hints = self.Hints()
|
|
186
|
+
|
|
187
|
+
T2WidthExtractor.execute(self, charString)
|
|
188
|
+
|
|
189
|
+
hints = charString._hints
|
|
190
|
+
|
|
191
|
+
if hints.has_hint or hints.has_hintmask:
|
|
192
|
+
self._css.add(charString)
|
|
193
|
+
|
|
194
|
+
if hints.status != 2:
|
|
195
|
+
# Check from last_check, make sure we didn't have any operators.
|
|
196
|
+
for i in range(hints.last_checked, len(charString.program) - 1):
|
|
197
|
+
if isinstance(charString.program[i], str):
|
|
198
|
+
hints.status = 2
|
|
199
|
+
break
|
|
200
|
+
else:
|
|
201
|
+
hints.status = 1 # There's *something* here
|
|
202
|
+
hints.last_checked = len(charString.program)
|
|
203
|
+
|
|
204
|
+
if old_hints:
|
|
205
|
+
assert hints.__dict__ == old_hints.__dict__
|
|
206
|
+
|
|
207
|
+
def op_callsubr(self, index):
|
|
208
|
+
subr = self.localSubrs[self.operandStack[-1] + self.localBias]
|
|
209
|
+
T2WidthExtractor.op_callsubr(self, index)
|
|
210
|
+
self.processSubr(index, subr)
|
|
211
|
+
|
|
212
|
+
def op_callgsubr(self, index):
|
|
213
|
+
subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
|
|
214
|
+
T2WidthExtractor.op_callgsubr(self, index)
|
|
215
|
+
self.processSubr(index, subr)
|
|
216
|
+
|
|
217
|
+
def op_hstem(self, index):
|
|
218
|
+
T2WidthExtractor.op_hstem(self, index)
|
|
219
|
+
self.processHint(index)
|
|
220
|
+
|
|
221
|
+
def op_vstem(self, index):
|
|
222
|
+
T2WidthExtractor.op_vstem(self, index)
|
|
223
|
+
self.processHint(index)
|
|
224
|
+
|
|
225
|
+
def op_hstemhm(self, index):
|
|
226
|
+
T2WidthExtractor.op_hstemhm(self, index)
|
|
227
|
+
self.processHint(index)
|
|
228
|
+
|
|
229
|
+
def op_vstemhm(self, index):
|
|
230
|
+
T2WidthExtractor.op_vstemhm(self, index)
|
|
231
|
+
self.processHint(index)
|
|
232
|
+
|
|
233
|
+
def op_hintmask(self, index):
|
|
234
|
+
rv = T2WidthExtractor.op_hintmask(self, index)
|
|
235
|
+
self.processHintmask(index)
|
|
236
|
+
return rv
|
|
237
|
+
|
|
238
|
+
def op_cntrmask(self, index):
|
|
239
|
+
rv = T2WidthExtractor.op_cntrmask(self, index)
|
|
240
|
+
self.processHintmask(index)
|
|
241
|
+
return rv
|
|
242
|
+
|
|
243
|
+
def processHintmask(self, index):
|
|
244
|
+
cs = self.callingStack[-1]
|
|
245
|
+
hints = cs._hints
|
|
246
|
+
hints.has_hintmask = True
|
|
247
|
+
if hints.status != 2:
|
|
248
|
+
# Check from last_check, see if we may be an implicit vstem
|
|
249
|
+
for i in range(hints.last_checked, index - 1):
|
|
250
|
+
if isinstance(cs.program[i], str):
|
|
251
|
+
hints.status = 2
|
|
252
|
+
break
|
|
253
|
+
else:
|
|
254
|
+
# We are an implicit vstem
|
|
255
|
+
hints.has_hint = True
|
|
256
|
+
hints.last_hint = index + 1
|
|
257
|
+
hints.status = 0
|
|
258
|
+
hints.last_checked = index + 1
|
|
259
|
+
|
|
260
|
+
def processHint(self, index):
|
|
261
|
+
cs = self.callingStack[-1]
|
|
262
|
+
hints = cs._hints
|
|
263
|
+
hints.has_hint = True
|
|
264
|
+
hints.last_hint = index
|
|
265
|
+
hints.last_checked = index
|
|
266
|
+
|
|
267
|
+
def processSubr(self, index, subr):
|
|
268
|
+
cs = self.callingStack[-1]
|
|
269
|
+
hints = cs._hints
|
|
270
|
+
subr_hints = subr._hints
|
|
271
|
+
|
|
272
|
+
# Check from last_check, make sure we didn't have
|
|
273
|
+
# any operators.
|
|
274
|
+
if hints.status != 2:
|
|
275
|
+
for i in range(hints.last_checked, index - 1):
|
|
276
|
+
if isinstance(cs.program[i], str):
|
|
277
|
+
hints.status = 2
|
|
278
|
+
break
|
|
279
|
+
hints.last_checked = index
|
|
280
|
+
|
|
281
|
+
if hints.status != 2:
|
|
282
|
+
if subr_hints.has_hint:
|
|
283
|
+
hints.has_hint = True
|
|
284
|
+
|
|
285
|
+
# Decide where to chop off from
|
|
286
|
+
if subr_hints.status == 0:
|
|
287
|
+
hints.last_hint = index
|
|
288
|
+
else:
|
|
289
|
+
hints.last_hint = index - 2 # Leave the subr call in
|
|
290
|
+
|
|
291
|
+
elif subr_hints.status == 0:
|
|
292
|
+
hints.deletions.append(index)
|
|
293
|
+
|
|
294
|
+
hints.status = max(hints.status, subr_hints.status)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _cs_subset_subroutines(charstring, subrs, gsubrs):
|
|
298
|
+
p = charstring.program
|
|
299
|
+
for i in range(1, len(p)):
|
|
300
|
+
if p[i] == "callsubr":
|
|
301
|
+
assert isinstance(p[i - 1], int)
|
|
302
|
+
p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
|
|
303
|
+
elif p[i] == "callgsubr":
|
|
304
|
+
assert isinstance(p[i - 1], int)
|
|
305
|
+
p[i - 1] = (
|
|
306
|
+
gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _cs_drop_hints(charstring):
|
|
311
|
+
hints = charstring._hints
|
|
312
|
+
|
|
313
|
+
if hints.deletions:
|
|
314
|
+
p = charstring.program
|
|
315
|
+
for idx in reversed(hints.deletions):
|
|
316
|
+
del p[idx - 2 : idx]
|
|
317
|
+
|
|
318
|
+
if hints.has_hint:
|
|
319
|
+
assert not hints.deletions or hints.last_hint <= hints.deletions[0]
|
|
320
|
+
charstring.program = charstring.program[hints.last_hint :]
|
|
321
|
+
if not charstring.program:
|
|
322
|
+
# TODO CFF2 no need for endchar.
|
|
323
|
+
charstring.program.append("endchar")
|
|
324
|
+
if hasattr(charstring, "width"):
|
|
325
|
+
# Insert width back if needed
|
|
326
|
+
if charstring.width != charstring.private.defaultWidthX:
|
|
327
|
+
# For CFF2 charstrings, this should never happen
|
|
328
|
+
assert (
|
|
329
|
+
charstring.private.defaultWidthX is not None
|
|
330
|
+
), "CFF2 CharStrings must not have an initial width value"
|
|
331
|
+
charstring.program.insert(
|
|
332
|
+
0, charstring.width - charstring.private.nominalWidthX
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if hints.has_hintmask:
|
|
336
|
+
i = 0
|
|
337
|
+
p = charstring.program
|
|
338
|
+
while i < len(p):
|
|
339
|
+
if p[i] in ["hintmask", "cntrmask"]:
|
|
340
|
+
assert i + 1 <= len(p)
|
|
341
|
+
del p[i : i + 2]
|
|
342
|
+
continue
|
|
343
|
+
i += 1
|
|
344
|
+
|
|
345
|
+
assert len(charstring.program)
|
|
346
|
+
|
|
347
|
+
del charstring._hints
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def remove_hints(cff, *, removeUnusedSubrs: bool = True):
|
|
351
|
+
for fontname in cff.keys():
|
|
352
|
+
font = cff[fontname]
|
|
353
|
+
cs = font.CharStrings
|
|
354
|
+
# This can be tricky, but doesn't have to. What we do is:
|
|
355
|
+
#
|
|
356
|
+
# - Run all used glyph charstrings and recurse into subroutines,
|
|
357
|
+
# - For each charstring (including subroutines), if it has any
|
|
358
|
+
# of the hint stem operators, we mark it as such.
|
|
359
|
+
# Upon returning, for each charstring we note all the
|
|
360
|
+
# subroutine calls it makes that (recursively) contain a stem,
|
|
361
|
+
# - Dropping hinting then consists of the following two ops:
|
|
362
|
+
# * Drop the piece of the program in each charstring before the
|
|
363
|
+
# last call to a stem op or a stem-calling subroutine,
|
|
364
|
+
# * Drop all hintmask operations.
|
|
365
|
+
# - It's trickier... A hintmask right after hints and a few numbers
|
|
366
|
+
# will act as an implicit vstemhm. As such, we track whether
|
|
367
|
+
# we have seen any non-hint operators so far and do the right
|
|
368
|
+
# thing, recursively... Good luck understanding that :(
|
|
369
|
+
css = set()
|
|
370
|
+
for c in cs.values():
|
|
371
|
+
c.decompile()
|
|
372
|
+
subrs = getattr(c.private, "Subrs", [])
|
|
373
|
+
decompiler = _DehintingT2Decompiler(
|
|
374
|
+
css,
|
|
375
|
+
subrs,
|
|
376
|
+
c.globalSubrs,
|
|
377
|
+
c.private.nominalWidthX,
|
|
378
|
+
c.private.defaultWidthX,
|
|
379
|
+
c.private,
|
|
380
|
+
)
|
|
381
|
+
decompiler.execute(c)
|
|
382
|
+
c.width = decompiler.width
|
|
383
|
+
for charstring in css:
|
|
384
|
+
_cs_drop_hints(charstring)
|
|
385
|
+
del css
|
|
386
|
+
|
|
387
|
+
# Drop font-wide hinting values
|
|
388
|
+
all_privs = []
|
|
389
|
+
if hasattr(font, "FDArray"):
|
|
390
|
+
all_privs.extend(fd.Private for fd in font.FDArray)
|
|
391
|
+
else:
|
|
392
|
+
all_privs.append(font.Private)
|
|
393
|
+
for priv in all_privs:
|
|
394
|
+
for k in [
|
|
395
|
+
"BlueValues",
|
|
396
|
+
"OtherBlues",
|
|
397
|
+
"FamilyBlues",
|
|
398
|
+
"FamilyOtherBlues",
|
|
399
|
+
"BlueScale",
|
|
400
|
+
"BlueShift",
|
|
401
|
+
"BlueFuzz",
|
|
402
|
+
"StemSnapH",
|
|
403
|
+
"StemSnapV",
|
|
404
|
+
"StdHW",
|
|
405
|
+
"StdVW",
|
|
406
|
+
"ForceBold",
|
|
407
|
+
"LanguageGroup",
|
|
408
|
+
"ExpansionFactor",
|
|
409
|
+
]:
|
|
410
|
+
if hasattr(priv, k):
|
|
411
|
+
setattr(priv, k, None)
|
|
412
|
+
if removeUnusedSubrs:
|
|
413
|
+
remove_unused_subroutines(cff)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _pd_delete_empty_subrs(private_dict):
|
|
417
|
+
if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
|
|
418
|
+
if "Subrs" in private_dict.rawDict:
|
|
419
|
+
del private_dict.rawDict["Subrs"]
|
|
420
|
+
del private_dict.Subrs
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def remove_unused_subroutines(cff):
|
|
424
|
+
for fontname in cff.keys():
|
|
425
|
+
font = cff[fontname]
|
|
426
|
+
cs = font.CharStrings
|
|
427
|
+
# Renumber subroutines to remove unused ones
|
|
428
|
+
|
|
429
|
+
# Mark all used subroutines
|
|
430
|
+
for c in cs.values():
|
|
431
|
+
subrs = getattr(c.private, "Subrs", [])
|
|
432
|
+
decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
|
|
433
|
+
decompiler.execute(c)
|
|
434
|
+
|
|
435
|
+
all_subrs = [font.GlobalSubrs]
|
|
436
|
+
if hasattr(font, "FDArray"):
|
|
437
|
+
all_subrs.extend(
|
|
438
|
+
fd.Private.Subrs
|
|
439
|
+
for fd in font.FDArray
|
|
440
|
+
if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
|
|
441
|
+
)
|
|
442
|
+
elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
|
|
443
|
+
all_subrs.append(font.Private.Subrs)
|
|
444
|
+
|
|
445
|
+
subrs = set(subrs) # Remove duplicates
|
|
446
|
+
|
|
447
|
+
# Prepare
|
|
448
|
+
for subrs in all_subrs:
|
|
449
|
+
if not hasattr(subrs, "_used"):
|
|
450
|
+
subrs._used = set()
|
|
451
|
+
subrs._used = _uniq_sort(subrs._used)
|
|
452
|
+
subrs._old_bias = calcSubrBias(subrs)
|
|
453
|
+
subrs._new_bias = calcSubrBias(subrs._used)
|
|
454
|
+
|
|
455
|
+
# Renumber glyph charstrings
|
|
456
|
+
for c in cs.values():
|
|
457
|
+
subrs = getattr(c.private, "Subrs", None)
|
|
458
|
+
_cs_subset_subroutines(c, subrs, font.GlobalSubrs)
|
|
459
|
+
|
|
460
|
+
# Renumber subroutines themselves
|
|
461
|
+
for subrs in all_subrs:
|
|
462
|
+
if subrs == font.GlobalSubrs:
|
|
463
|
+
if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
|
|
464
|
+
local_subrs = font.Private.Subrs
|
|
465
|
+
elif (
|
|
466
|
+
hasattr(font, "FDArray")
|
|
467
|
+
and len(font.FDArray) == 1
|
|
468
|
+
and hasattr(font.FDArray[0].Private, "Subrs")
|
|
469
|
+
):
|
|
470
|
+
# Technically we shouldn't do this. But I've run into fonts that do it.
|
|
471
|
+
local_subrs = font.FDArray[0].Private.Subrs
|
|
472
|
+
else:
|
|
473
|
+
local_subrs = None
|
|
474
|
+
else:
|
|
475
|
+
local_subrs = subrs
|
|
476
|
+
|
|
477
|
+
subrs.items = [subrs.items[i] for i in subrs._used]
|
|
478
|
+
if hasattr(subrs, "file"):
|
|
479
|
+
del subrs.file
|
|
480
|
+
if hasattr(subrs, "offsets"):
|
|
481
|
+
del subrs.offsets
|
|
482
|
+
|
|
483
|
+
for subr in subrs.items:
|
|
484
|
+
_cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)
|
|
485
|
+
|
|
486
|
+
# Delete local SubrsIndex if empty
|
|
487
|
+
if hasattr(font, "FDArray"):
|
|
488
|
+
for fd in font.FDArray:
|
|
489
|
+
_pd_delete_empty_subrs(fd.Private)
|
|
490
|
+
else:
|
|
491
|
+
_pd_delete_empty_subrs(font.Private)
|
|
492
|
+
|
|
493
|
+
# Cleanup
|
|
494
|
+
for subrs in all_subrs:
|
|
495
|
+
del subrs._used, subrs._old_bias, subrs._new_bias
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""T2CharString glyph width optimizer.
|
|
4
|
+
|
|
5
|
+
CFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX``
|
|
6
|
+
value do not need to specify their width in their charstring, saving bytes.
|
|
7
|
+
This module determines the optimum ``defaultWidthX`` and ``nominalWidthX``
|
|
8
|
+
values for a font, when provided with a list of glyph widths."""
|
|
9
|
+
|
|
10
|
+
from fontTools.ttLib import TTFont
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from operator import add
|
|
13
|
+
from functools import reduce
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["optimizeWidths", "main"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class missingdict(dict):
|
|
20
|
+
def __init__(self, missing_func):
|
|
21
|
+
self.missing_func = missing_func
|
|
22
|
+
|
|
23
|
+
def __missing__(self, v):
|
|
24
|
+
return self.missing_func(v)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cumSum(f, op=add, start=0, decreasing=False):
|
|
28
|
+
keys = sorted(f.keys())
|
|
29
|
+
minx, maxx = keys[0], keys[-1]
|
|
30
|
+
|
|
31
|
+
total = reduce(op, f.values(), start)
|
|
32
|
+
|
|
33
|
+
if decreasing:
|
|
34
|
+
missing = lambda x: start if x > maxx else total
|
|
35
|
+
domain = range(maxx, minx - 1, -1)
|
|
36
|
+
else:
|
|
37
|
+
missing = lambda x: start if x < minx else total
|
|
38
|
+
domain = range(minx, maxx + 1)
|
|
39
|
+
|
|
40
|
+
out = missingdict(missing)
|
|
41
|
+
|
|
42
|
+
v = start
|
|
43
|
+
for x in domain:
|
|
44
|
+
v = op(v, f[x])
|
|
45
|
+
out[x] = v
|
|
46
|
+
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def byteCost(widths, default, nominal):
|
|
51
|
+
if not hasattr(widths, "items"):
|
|
52
|
+
d = defaultdict(int)
|
|
53
|
+
for w in widths:
|
|
54
|
+
d[w] += 1
|
|
55
|
+
widths = d
|
|
56
|
+
|
|
57
|
+
cost = 0
|
|
58
|
+
for w, freq in widths.items():
|
|
59
|
+
if w == default:
|
|
60
|
+
continue
|
|
61
|
+
diff = abs(w - nominal)
|
|
62
|
+
if diff <= 107:
|
|
63
|
+
cost += freq
|
|
64
|
+
elif diff <= 1131:
|
|
65
|
+
cost += freq * 2
|
|
66
|
+
else:
|
|
67
|
+
cost += freq * 5
|
|
68
|
+
return cost
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def optimizeWidthsBruteforce(widths):
|
|
72
|
+
"""Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts."""
|
|
73
|
+
|
|
74
|
+
d = defaultdict(int)
|
|
75
|
+
for w in widths:
|
|
76
|
+
d[w] += 1
|
|
77
|
+
|
|
78
|
+
# Maximum number of bytes using default can possibly save
|
|
79
|
+
maxDefaultAdvantage = 5 * max(d.values())
|
|
80
|
+
|
|
81
|
+
minw, maxw = min(widths), max(widths)
|
|
82
|
+
domain = list(range(minw, maxw + 1))
|
|
83
|
+
|
|
84
|
+
bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain)
|
|
85
|
+
|
|
86
|
+
bestCost = len(widths) * 5 + 1
|
|
87
|
+
for nominal in domain:
|
|
88
|
+
if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage:
|
|
89
|
+
continue
|
|
90
|
+
for default in domain:
|
|
91
|
+
cost = byteCost(widths, default, nominal)
|
|
92
|
+
if cost < bestCost:
|
|
93
|
+
bestCost = cost
|
|
94
|
+
bestDefault = default
|
|
95
|
+
bestNominal = nominal
|
|
96
|
+
|
|
97
|
+
return bestDefault, bestNominal
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def optimizeWidths(widths):
|
|
101
|
+
"""Given a list of glyph widths, or dictionary mapping glyph width to number of
|
|
102
|
+
glyphs having that, returns a tuple of best CFF default and nominal glyph widths.
|
|
103
|
+
|
|
104
|
+
This algorithm is linear in UPEM+numGlyphs."""
|
|
105
|
+
|
|
106
|
+
if not hasattr(widths, "items"):
|
|
107
|
+
d = defaultdict(int)
|
|
108
|
+
for w in widths:
|
|
109
|
+
d[w] += 1
|
|
110
|
+
widths = d
|
|
111
|
+
|
|
112
|
+
keys = sorted(widths.keys())
|
|
113
|
+
minw, maxw = keys[0], keys[-1]
|
|
114
|
+
domain = list(range(minw, maxw + 1))
|
|
115
|
+
|
|
116
|
+
# Cumulative sum/max forward/backward.
|
|
117
|
+
cumFrqU = cumSum(widths, op=add)
|
|
118
|
+
cumMaxU = cumSum(widths, op=max)
|
|
119
|
+
cumFrqD = cumSum(widths, op=add, decreasing=True)
|
|
120
|
+
cumMaxD = cumSum(widths, op=max, decreasing=True)
|
|
121
|
+
|
|
122
|
+
# Cost per nominal choice, without default consideration.
|
|
123
|
+
nomnCostU = missingdict(
|
|
124
|
+
lambda x: cumFrqU[x] + cumFrqU[x - 108] + cumFrqU[x - 1132] * 3
|
|
125
|
+
)
|
|
126
|
+
nomnCostD = missingdict(
|
|
127
|
+
lambda x: cumFrqD[x] + cumFrqD[x + 108] + cumFrqD[x + 1132] * 3
|
|
128
|
+
)
|
|
129
|
+
nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x])
|
|
130
|
+
|
|
131
|
+
# Cost-saving per nominal choice, by best default choice.
|
|
132
|
+
dfltCostU = missingdict(
|
|
133
|
+
lambda x: max(cumMaxU[x], cumMaxU[x - 108] * 2, cumMaxU[x - 1132] * 5)
|
|
134
|
+
)
|
|
135
|
+
dfltCostD = missingdict(
|
|
136
|
+
lambda x: max(cumMaxD[x], cumMaxD[x + 108] * 2, cumMaxD[x + 1132] * 5)
|
|
137
|
+
)
|
|
138
|
+
dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x]))
|
|
139
|
+
|
|
140
|
+
# Combined cost per nominal choice.
|
|
141
|
+
bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x])
|
|
142
|
+
|
|
143
|
+
# Best nominal.
|
|
144
|
+
nominal = min(domain, key=lambda x: bestCost[x])
|
|
145
|
+
|
|
146
|
+
# Work back the best default.
|
|
147
|
+
bestC = bestCost[nominal]
|
|
148
|
+
dfltC = nomnCost[nominal] - bestCost[nominal]
|
|
149
|
+
ends = []
|
|
150
|
+
if dfltC == dfltCostU[nominal]:
|
|
151
|
+
starts = [nominal, nominal - 108, nominal - 1132]
|
|
152
|
+
for start in starts:
|
|
153
|
+
while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]:
|
|
154
|
+
start -= 1
|
|
155
|
+
ends.append(start)
|
|
156
|
+
else:
|
|
157
|
+
starts = [nominal, nominal + 108, nominal + 1132]
|
|
158
|
+
for start in starts:
|
|
159
|
+
while cumMaxD[start] and cumMaxD[start] == cumMaxD[start + 1]:
|
|
160
|
+
start += 1
|
|
161
|
+
ends.append(start)
|
|
162
|
+
default = min(ends, key=lambda default: byteCost(widths, default, nominal))
|
|
163
|
+
|
|
164
|
+
return default, nominal
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main(args=None):
|
|
168
|
+
"""Calculate optimum defaultWidthX/nominalWidthX values"""
|
|
169
|
+
|
|
170
|
+
import argparse
|
|
171
|
+
|
|
172
|
+
parser = argparse.ArgumentParser(
|
|
173
|
+
"fonttools cffLib.width",
|
|
174
|
+
description=main.__doc__,
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files"
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"-b",
|
|
181
|
+
"--brute-force",
|
|
182
|
+
dest="brute",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="Use brute-force approach (VERY slow)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
args = parser.parse_args(args)
|
|
188
|
+
|
|
189
|
+
for fontfile in args.inputs:
|
|
190
|
+
font = TTFont(fontfile)
|
|
191
|
+
hmtx = font["hmtx"]
|
|
192
|
+
widths = [m[0] for m in hmtx.metrics.values()]
|
|
193
|
+
if args.brute:
|
|
194
|
+
default, nominal = optimizeWidthsBruteforce(widths)
|
|
195
|
+
else:
|
|
196
|
+
default, nominal = optimizeWidths(widths)
|
|
197
|
+
print(
|
|
198
|
+
"glyphs=%d default=%d nominal=%d byteCost=%d"
|
|
199
|
+
% (len(widths), default, nominal, byteCost(widths, default, nominal))
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
import sys
|
|
205
|
+
|
|
206
|
+
if len(sys.argv) == 1:
|
|
207
|
+
import doctest
|
|
208
|
+
|
|
209
|
+
sys.exit(doctest.testmod().failed)
|
|
210
|
+
main()
|
|
File without changes
|