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
fontTools/subset/cff.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from fontTools.misc import psCharStrings
|
|
2
|
+
from fontTools import ttLib
|
|
3
|
+
from fontTools.pens.basePen import NullPen
|
|
4
|
+
from fontTools.misc.roundTools import otRound
|
|
5
|
+
from fontTools.misc.loggingTools import deprecateFunction
|
|
6
|
+
from fontTools.subset.util import _add_method, _uniq_sort
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
|
|
10
|
+
def __init__(self, components, localSubrs, globalSubrs):
|
|
11
|
+
psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs)
|
|
12
|
+
self.components = components
|
|
13
|
+
|
|
14
|
+
def op_endchar(self, index):
|
|
15
|
+
args = self.popall()
|
|
16
|
+
if len(args) >= 4:
|
|
17
|
+
from fontTools.encodings.StandardEncoding import StandardEncoding
|
|
18
|
+
|
|
19
|
+
# endchar can do seac accent bulding; The T2 spec says it's deprecated,
|
|
20
|
+
# but recent software that shall remain nameless does output it.
|
|
21
|
+
adx, ady, bchar, achar = args[-4:]
|
|
22
|
+
baseGlyph = StandardEncoding[bchar]
|
|
23
|
+
accentGlyph = StandardEncoding[achar]
|
|
24
|
+
self.components.add(baseGlyph)
|
|
25
|
+
self.components.add(accentGlyph)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
29
|
+
def closure_glyphs(self, s):
|
|
30
|
+
cff = self.cff
|
|
31
|
+
assert len(cff) == 1
|
|
32
|
+
font = cff[cff.keys()[0]]
|
|
33
|
+
glyphSet = font.CharStrings
|
|
34
|
+
|
|
35
|
+
decompose = s.glyphs
|
|
36
|
+
while decompose:
|
|
37
|
+
components = set()
|
|
38
|
+
for g in decompose:
|
|
39
|
+
if g not in glyphSet:
|
|
40
|
+
continue
|
|
41
|
+
gl = glyphSet[g]
|
|
42
|
+
|
|
43
|
+
subrs = getattr(gl.private, "Subrs", [])
|
|
44
|
+
decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
|
|
45
|
+
decompiler.execute(gl)
|
|
46
|
+
components -= s.glyphs
|
|
47
|
+
s.glyphs.update(components)
|
|
48
|
+
decompose = components
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False):
|
|
52
|
+
c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName)
|
|
53
|
+
if isCFF2 or ignoreWidth:
|
|
54
|
+
# CFF2 charstrings have no widths nor 'endchar' operators
|
|
55
|
+
c.setProgram([] if isCFF2 else ["endchar"])
|
|
56
|
+
else:
|
|
57
|
+
if hasattr(font, "FDArray") and font.FDArray is not None:
|
|
58
|
+
private = font.FDArray[fdSelectIndex].Private
|
|
59
|
+
else:
|
|
60
|
+
private = font.Private
|
|
61
|
+
dfltWdX = private.defaultWidthX
|
|
62
|
+
nmnlWdX = private.nominalWidthX
|
|
63
|
+
pen = NullPen()
|
|
64
|
+
c.draw(pen) # this will set the charstring's width
|
|
65
|
+
if c.width != dfltWdX:
|
|
66
|
+
c.program = [c.width - nmnlWdX, "endchar"]
|
|
67
|
+
else:
|
|
68
|
+
c.program = ["endchar"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
72
|
+
def prune_pre_subset(self, font, options):
|
|
73
|
+
cff = self.cff
|
|
74
|
+
# CFF table must have one font only
|
|
75
|
+
cff.fontNames = cff.fontNames[:1]
|
|
76
|
+
|
|
77
|
+
if options.notdef_glyph and not options.notdef_outline:
|
|
78
|
+
isCFF2 = cff.major > 1
|
|
79
|
+
for fontname in cff.keys():
|
|
80
|
+
font = cff[fontname]
|
|
81
|
+
_empty_charstring(font, ".notdef", isCFF2=isCFF2)
|
|
82
|
+
|
|
83
|
+
# Clear useless Encoding
|
|
84
|
+
for fontname in cff.keys():
|
|
85
|
+
font = cff[fontname]
|
|
86
|
+
# https://github.com/fonttools/fonttools/issues/620
|
|
87
|
+
font.Encoding = "StandardEncoding"
|
|
88
|
+
|
|
89
|
+
return True # bool(cff.fontNames)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
93
|
+
def subset_glyphs(self, s):
|
|
94
|
+
cff = self.cff
|
|
95
|
+
for fontname in cff.keys():
|
|
96
|
+
font = cff[fontname]
|
|
97
|
+
cs = font.CharStrings
|
|
98
|
+
|
|
99
|
+
glyphs = s.glyphs.union(s.glyphs_emptied)
|
|
100
|
+
|
|
101
|
+
# Load all glyphs
|
|
102
|
+
for g in font.charset:
|
|
103
|
+
if g not in glyphs:
|
|
104
|
+
continue
|
|
105
|
+
c, _ = cs.getItemAndSelector(g)
|
|
106
|
+
|
|
107
|
+
if cs.charStringsAreIndexed:
|
|
108
|
+
indices = [i for i, g in enumerate(font.charset) if g in glyphs]
|
|
109
|
+
csi = cs.charStringsIndex
|
|
110
|
+
csi.items = [csi.items[i] for i in indices]
|
|
111
|
+
del csi.file, csi.offsets
|
|
112
|
+
if hasattr(font, "FDSelect"):
|
|
113
|
+
sel = font.FDSelect
|
|
114
|
+
sel.format = None
|
|
115
|
+
sel.gidArray = [sel.gidArray[i] for i in indices]
|
|
116
|
+
newCharStrings = {}
|
|
117
|
+
for indicesIdx, charsetIdx in enumerate(indices):
|
|
118
|
+
g = font.charset[charsetIdx]
|
|
119
|
+
if g in cs.charStrings:
|
|
120
|
+
newCharStrings[g] = indicesIdx
|
|
121
|
+
cs.charStrings = newCharStrings
|
|
122
|
+
else:
|
|
123
|
+
cs.charStrings = {g: v for g, v in cs.charStrings.items() if g in glyphs}
|
|
124
|
+
font.charset = [g for g in font.charset if g in glyphs]
|
|
125
|
+
font.numGlyphs = len(font.charset)
|
|
126
|
+
|
|
127
|
+
if s.options.retain_gids:
|
|
128
|
+
isCFF2 = cff.major > 1
|
|
129
|
+
for g in s.glyphs_emptied:
|
|
130
|
+
_empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True)
|
|
131
|
+
|
|
132
|
+
return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
136
|
+
def prune_post_subset(self, ttfFont, options):
|
|
137
|
+
cff = self.cff
|
|
138
|
+
for fontname in cff.keys():
|
|
139
|
+
font = cff[fontname]
|
|
140
|
+
cs = font.CharStrings
|
|
141
|
+
|
|
142
|
+
# Drop unused FontDictionaries
|
|
143
|
+
if hasattr(font, "FDSelect"):
|
|
144
|
+
sel = font.FDSelect
|
|
145
|
+
indices = _uniq_sort(sel.gidArray)
|
|
146
|
+
sel.gidArray = [indices.index(ss) for ss in sel.gidArray]
|
|
147
|
+
arr = font.FDArray
|
|
148
|
+
arr.items = [arr[i] for i in indices]
|
|
149
|
+
del arr.file, arr.offsets
|
|
150
|
+
|
|
151
|
+
# Desubroutinize if asked for
|
|
152
|
+
if options.desubroutinize:
|
|
153
|
+
cff.desubroutinize()
|
|
154
|
+
|
|
155
|
+
# Drop hints if not needed
|
|
156
|
+
if not options.hinting:
|
|
157
|
+
self.remove_hints()
|
|
158
|
+
elif not options.desubroutinize:
|
|
159
|
+
self.remove_unused_subroutines()
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@deprecateFunction(
|
|
164
|
+
"use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning
|
|
165
|
+
)
|
|
166
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
167
|
+
def desubroutinize(self):
|
|
168
|
+
self.cff.desubroutinize()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@deprecateFunction(
|
|
172
|
+
"use 'CFFFontSet.remove_hints()' instead", category=DeprecationWarning
|
|
173
|
+
)
|
|
174
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
175
|
+
def remove_hints(self):
|
|
176
|
+
self.cff.remove_hints()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@deprecateFunction(
|
|
180
|
+
"use 'CFFFontSet.remove_unused_subroutines' instead", category=DeprecationWarning
|
|
181
|
+
)
|
|
182
|
+
@_add_method(ttLib.getTableClass("CFF "))
|
|
183
|
+
def remove_unused_subroutines(self):
|
|
184
|
+
self.cff.remove_unused_subroutines()
|
fontTools/subset/svg.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from itertools import chain, count
|
|
6
|
+
from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from lxml import etree
|
|
10
|
+
except ImportError:
|
|
11
|
+
# lxml is required for subsetting SVG, but we prefer to delay the import error
|
|
12
|
+
# until subset_glyphs() is called (i.e. if font to subset has an 'SVG ' table)
|
|
13
|
+
etree = None
|
|
14
|
+
|
|
15
|
+
from fontTools import ttLib
|
|
16
|
+
from fontTools.subset.util import _add_method
|
|
17
|
+
from fontTools.ttLib.tables.S_V_G_ import SVGDocument
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["subset_glyphs"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
GID_RE = re.compile(r"^glyph(\d+)$")
|
|
24
|
+
|
|
25
|
+
NAMESPACES = {
|
|
26
|
+
"svg": "http://www.w3.org/2000/svg",
|
|
27
|
+
"xlink": "http://www.w3.org/1999/xlink",
|
|
28
|
+
}
|
|
29
|
+
XLINK_HREF = f'{{{NAMESPACES["xlink"]}}}href'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# TODO(antrotype): Replace with functools.cache once we are 3.9+
|
|
33
|
+
@lru_cache(maxsize=None)
|
|
34
|
+
def xpath(path):
|
|
35
|
+
# compile XPath upfront, caching result to reuse on multiple elements
|
|
36
|
+
return etree.XPath(path, namespaces=NAMESPACES)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def group_elements_by_id(tree: etree.Element) -> Dict[str, etree.Element]:
|
|
40
|
+
# select all svg elements with 'id' attribute no matter where they are
|
|
41
|
+
# including the root element itself:
|
|
42
|
+
# https://github.com/fonttools/fonttools/issues/2548
|
|
43
|
+
return {el.attrib["id"]: el for el in xpath("//svg:*[@id]")(tree)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_css_declarations(style_attr: str) -> Dict[str, str]:
|
|
47
|
+
# https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/style
|
|
48
|
+
# https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax#css_declarations
|
|
49
|
+
result = {}
|
|
50
|
+
for declaration in style_attr.split(";"):
|
|
51
|
+
if declaration.count(":") == 1:
|
|
52
|
+
property_name, value = declaration.split(":")
|
|
53
|
+
property_name = property_name.strip()
|
|
54
|
+
result[property_name] = value.strip()
|
|
55
|
+
elif declaration.strip():
|
|
56
|
+
raise ValueError(f"Invalid CSS declaration syntax: {declaration}")
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def iter_referenced_ids(tree: etree.Element) -> Iterator[str]:
|
|
61
|
+
# Yield all the ids that can be reached via references from this element tree.
|
|
62
|
+
# We currently support xlink:href (as used by <use> and gradient templates),
|
|
63
|
+
# and local url(#...) links found in fill or clip-path attributes
|
|
64
|
+
# TODO(anthrotype): Check we aren't missing other supported kinds of reference
|
|
65
|
+
find_svg_elements_with_references = xpath(
|
|
66
|
+
".//svg:*[ "
|
|
67
|
+
"starts-with(@xlink:href, '#') "
|
|
68
|
+
"or starts-with(@fill, 'url(#') "
|
|
69
|
+
"or starts-with(@clip-path, 'url(#') "
|
|
70
|
+
"or contains(@style, ':url(#') "
|
|
71
|
+
"]",
|
|
72
|
+
)
|
|
73
|
+
for el in chain([tree], find_svg_elements_with_references(tree)):
|
|
74
|
+
ref_id = href_local_target(el)
|
|
75
|
+
if ref_id is not None:
|
|
76
|
+
yield ref_id
|
|
77
|
+
|
|
78
|
+
attrs = el.attrib
|
|
79
|
+
if "style" in attrs:
|
|
80
|
+
attrs = {**dict(attrs), **parse_css_declarations(el.attrib["style"])}
|
|
81
|
+
for attr in ("fill", "clip-path"):
|
|
82
|
+
if attr in attrs:
|
|
83
|
+
value = attrs[attr]
|
|
84
|
+
if value.startswith("url(#") and value.endswith(")"):
|
|
85
|
+
ref_id = value[5:-1]
|
|
86
|
+
assert ref_id
|
|
87
|
+
yield ref_id
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def closure_element_ids(
|
|
91
|
+
elements: Dict[str, etree.Element], element_ids: Set[str]
|
|
92
|
+
) -> None:
|
|
93
|
+
# Expand the initial subset of element ids to include ids that can be reached
|
|
94
|
+
# via references from the initial set.
|
|
95
|
+
unvisited = element_ids
|
|
96
|
+
while unvisited:
|
|
97
|
+
referenced: Set[str] = set()
|
|
98
|
+
for el_id in unvisited:
|
|
99
|
+
if el_id not in elements:
|
|
100
|
+
# ignore dangling reference; not our job to validate svg
|
|
101
|
+
continue
|
|
102
|
+
referenced.update(iter_referenced_ids(elements[el_id]))
|
|
103
|
+
referenced -= element_ids
|
|
104
|
+
element_ids.update(referenced)
|
|
105
|
+
unvisited = referenced
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def subset_elements(el: etree.Element, retained_ids: Set[str]) -> bool:
|
|
109
|
+
# Keep elements if their id is in the subset, or any of their children's id is.
|
|
110
|
+
# Drop elements whose id is not in the subset, and either have no children,
|
|
111
|
+
# or all their children are being dropped.
|
|
112
|
+
if el.attrib.get("id") in retained_ids:
|
|
113
|
+
# if id is in the set, don't recurse; keep whole subtree
|
|
114
|
+
return True
|
|
115
|
+
# recursively subset all the children; we use a list comprehension instead
|
|
116
|
+
# of a parentheses-less generator expression because we don't want any() to
|
|
117
|
+
# short-circuit, as our function has a side effect of dropping empty elements.
|
|
118
|
+
if any([subset_elements(e, retained_ids) for e in el]):
|
|
119
|
+
return True
|
|
120
|
+
assert len(el) == 0
|
|
121
|
+
parent = el.getparent()
|
|
122
|
+
if parent is not None:
|
|
123
|
+
parent.remove(el)
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def remap_glyph_ids(
|
|
128
|
+
svg: etree.Element, glyph_index_map: Dict[int, int]
|
|
129
|
+
) -> Dict[str, str]:
|
|
130
|
+
# Given {old_gid: new_gid} map, rename all elements containing id="glyph{gid}"
|
|
131
|
+
# special attributes
|
|
132
|
+
elements = group_elements_by_id(svg)
|
|
133
|
+
id_map = {}
|
|
134
|
+
for el_id, el in elements.items():
|
|
135
|
+
m = GID_RE.match(el_id)
|
|
136
|
+
if not m:
|
|
137
|
+
continue
|
|
138
|
+
old_index = int(m.group(1))
|
|
139
|
+
new_index = glyph_index_map.get(old_index)
|
|
140
|
+
if new_index is not None:
|
|
141
|
+
if old_index == new_index:
|
|
142
|
+
continue
|
|
143
|
+
new_id = f"glyph{new_index}"
|
|
144
|
+
else:
|
|
145
|
+
# If the old index is missing, the element correspond to a glyph that was
|
|
146
|
+
# excluded from the font's subset.
|
|
147
|
+
# We rename it to avoid clashes with the new GIDs or other element ids.
|
|
148
|
+
new_id = f".{el_id}"
|
|
149
|
+
n = count(1)
|
|
150
|
+
while new_id in elements:
|
|
151
|
+
new_id = f"{new_id}.{next(n)}"
|
|
152
|
+
|
|
153
|
+
id_map[el_id] = new_id
|
|
154
|
+
el.attrib["id"] = new_id
|
|
155
|
+
|
|
156
|
+
return id_map
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def href_local_target(el: etree.Element) -> Optional[str]:
|
|
160
|
+
if XLINK_HREF in el.attrib:
|
|
161
|
+
href = el.attrib[XLINK_HREF]
|
|
162
|
+
if href.startswith("#") and len(href) > 1:
|
|
163
|
+
return href[1:] # drop the leading #
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def update_glyph_href_links(svg: etree.Element, id_map: Dict[str, str]) -> None:
|
|
168
|
+
# update all xlink:href="#glyph..." attributes to point to the new glyph ids
|
|
169
|
+
for el in xpath(".//svg:*[starts-with(@xlink:href, '#glyph')]")(svg):
|
|
170
|
+
old_id = href_local_target(el)
|
|
171
|
+
assert old_id is not None
|
|
172
|
+
if old_id in id_map:
|
|
173
|
+
new_id = id_map[old_id]
|
|
174
|
+
el.attrib[XLINK_HREF] = f"#{new_id}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def ranges(ints: Iterable[int]) -> Iterator[Tuple[int, int]]:
|
|
178
|
+
# Yield sorted, non-overlapping (min, max) ranges of consecutive integers
|
|
179
|
+
sorted_ints = iter(sorted(set(ints)))
|
|
180
|
+
try:
|
|
181
|
+
start = end = next(sorted_ints)
|
|
182
|
+
except StopIteration:
|
|
183
|
+
return
|
|
184
|
+
for v in sorted_ints:
|
|
185
|
+
if v - 1 == end:
|
|
186
|
+
end = v
|
|
187
|
+
else:
|
|
188
|
+
yield (start, end)
|
|
189
|
+
start = end = v
|
|
190
|
+
yield (start, end)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@_add_method(ttLib.getTableClass("SVG "))
|
|
194
|
+
def subset_glyphs(self, s) -> bool:
|
|
195
|
+
if etree is None:
|
|
196
|
+
raise ImportError("No module named 'lxml', required to subset SVG")
|
|
197
|
+
|
|
198
|
+
# glyph names (before subsetting)
|
|
199
|
+
glyph_order: List[str] = s.orig_glyph_order
|
|
200
|
+
# map from glyph names to original glyph indices
|
|
201
|
+
rev_orig_glyph_map: Dict[str, int] = s.reverseOrigGlyphMap
|
|
202
|
+
# map from original to new glyph indices (after subsetting)
|
|
203
|
+
glyph_index_map: Dict[int, int] = s.glyph_index_map
|
|
204
|
+
|
|
205
|
+
new_docs: List[SVGDocument] = []
|
|
206
|
+
for doc in self.docList:
|
|
207
|
+
glyphs = {
|
|
208
|
+
glyph_order[i] for i in range(doc.startGlyphID, doc.endGlyphID + 1)
|
|
209
|
+
}.intersection(s.glyphs)
|
|
210
|
+
if not glyphs:
|
|
211
|
+
# no intersection: we can drop the whole record
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
svg = etree.fromstring(
|
|
215
|
+
# encode because fromstring dislikes xml encoding decl if input is str.
|
|
216
|
+
# SVG xml encoding must be utf-8 as per OT spec.
|
|
217
|
+
doc.data.encode("utf-8"),
|
|
218
|
+
parser=etree.XMLParser(
|
|
219
|
+
# Disable libxml2 security restrictions to support very deep trees.
|
|
220
|
+
# Without this we would get an error like this:
|
|
221
|
+
# `lxml.etree.XMLSyntaxError: internal error: Huge input lookup`
|
|
222
|
+
# when parsing big fonts e.g. noto-emoji-picosvg.ttf.
|
|
223
|
+
huge_tree=True,
|
|
224
|
+
# ignore blank text as it's not meaningful in OT-SVG; it also prevents
|
|
225
|
+
# dangling tail text after removing an element when pretty_print=True
|
|
226
|
+
remove_blank_text=True,
|
|
227
|
+
# don't replace entities; we don't expect any in OT-SVG and they may
|
|
228
|
+
# be abused for XXE attacks
|
|
229
|
+
resolve_entities=False,
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
elements = group_elements_by_id(svg)
|
|
234
|
+
gids = {rev_orig_glyph_map[g] for g in glyphs}
|
|
235
|
+
element_ids = {f"glyph{i}" for i in gids}
|
|
236
|
+
closure_element_ids(elements, element_ids)
|
|
237
|
+
|
|
238
|
+
if not subset_elements(svg, element_ids):
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if not s.options.retain_gids:
|
|
242
|
+
id_map = remap_glyph_ids(svg, glyph_index_map)
|
|
243
|
+
update_glyph_href_links(svg, id_map)
|
|
244
|
+
|
|
245
|
+
new_doc = etree.tostring(svg, pretty_print=s.options.pretty_svg).decode("utf-8")
|
|
246
|
+
|
|
247
|
+
new_gids = (glyph_index_map[i] for i in gids)
|
|
248
|
+
for start, end in ranges(new_gids):
|
|
249
|
+
new_docs.append(SVGDocument(new_doc, start, end, doc.compressed))
|
|
250
|
+
|
|
251
|
+
self.docList = new_docs
|
|
252
|
+
|
|
253
|
+
return bool(self.docList)
|
fontTools/subset/util.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Private utility methods used by the subset modules"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _add_method(*clazzes):
|
|
5
|
+
"""Returns a decorator function that adds a new method to one or
|
|
6
|
+
more classes."""
|
|
7
|
+
|
|
8
|
+
def wrapper(method):
|
|
9
|
+
done = []
|
|
10
|
+
for clazz in clazzes:
|
|
11
|
+
if clazz in done:
|
|
12
|
+
continue # Support multiple names of a clazz
|
|
13
|
+
done.append(clazz)
|
|
14
|
+
assert clazz.__name__ != "DefaultTable", "Oops, table class not found."
|
|
15
|
+
assert not hasattr(
|
|
16
|
+
clazz, method.__name__
|
|
17
|
+
), "Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__)
|
|
18
|
+
setattr(clazz, method.__name__, method)
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return wrapper
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _uniq_sort(l):
|
|
25
|
+
return sorted(set(l))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from fontTools.pens.transformPen import TransformPen
|
|
2
|
+
from fontTools.misc import etree
|
|
3
|
+
from fontTools.misc.textTools import tostr
|
|
4
|
+
from .parser import parse_path
|
|
5
|
+
from .shapes import PathBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = [tostr(s) for s in ("SVGPath", "parse_path")]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SVGPath(object):
|
|
12
|
+
"""Parse SVG ``path`` elements from a file or string, and draw them
|
|
13
|
+
onto a glyph object that supports the FontTools Pen protocol.
|
|
14
|
+
|
|
15
|
+
For example, reading from an SVG file and drawing to a Defcon Glyph:
|
|
16
|
+
|
|
17
|
+
.. code-block::
|
|
18
|
+
|
|
19
|
+
import defcon
|
|
20
|
+
glyph = defcon.Glyph()
|
|
21
|
+
pen = glyph.getPen()
|
|
22
|
+
svg = SVGPath("path/to/a.svg")
|
|
23
|
+
svg.draw(pen)
|
|
24
|
+
|
|
25
|
+
Or reading from a string containing SVG data, using the alternative
|
|
26
|
+
'fromstring' (a class method):
|
|
27
|
+
|
|
28
|
+
.. code-block::
|
|
29
|
+
|
|
30
|
+
data = '<?xml version="1.0" ...'
|
|
31
|
+
svg = SVGPath.fromstring(data)
|
|
32
|
+
svg.draw(pen)
|
|
33
|
+
|
|
34
|
+
Both constructors can optionally take a 'transform' matrix (6-float
|
|
35
|
+
tuple, or a FontTools Transform object) to modify the draw output.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, filename=None, transform=None):
|
|
39
|
+
if filename is None:
|
|
40
|
+
self.root = etree.ElementTree()
|
|
41
|
+
else:
|
|
42
|
+
tree = etree.parse(filename)
|
|
43
|
+
self.root = tree.getroot()
|
|
44
|
+
self.transform = transform
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def fromstring(cls, data, transform=None):
|
|
48
|
+
self = cls(transform=transform)
|
|
49
|
+
self.root = etree.fromstring(data)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def draw(self, pen):
|
|
53
|
+
if self.transform:
|
|
54
|
+
pen = TransformPen(pen, self.transform)
|
|
55
|
+
pb = PathBuilder()
|
|
56
|
+
# xpath | doesn't seem to reliable work so just walk it
|
|
57
|
+
for el in self.root.iter():
|
|
58
|
+
pb.add_path_from_element(el)
|
|
59
|
+
original_pen = pen
|
|
60
|
+
for path, transform in zip(pb.paths, pb.transforms):
|
|
61
|
+
if transform:
|
|
62
|
+
pen = TransformPen(original_pen, transform)
|
|
63
|
+
else:
|
|
64
|
+
pen = original_pen
|
|
65
|
+
parse_path(path, pen)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Convert SVG Path's elliptical arcs to Bezier curves.
|
|
2
|
+
|
|
3
|
+
The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic
|
|
4
|
+
https://github.com/chromium/chromium/blob/93831f2/third_party/
|
|
5
|
+
blink/renderer/core/svg/svg_path_parser.cc#L169-L278
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fontTools.misc.transform import Identity, Scale
|
|
9
|
+
from math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TWO_PI = 2 * pi
|
|
13
|
+
PI_OVER_TWO = 0.5 * pi
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _map_point(matrix, pt):
|
|
17
|
+
# apply Transform matrix to a point represented as a complex number
|
|
18
|
+
r = matrix.transformPoint((pt.real, pt.imag))
|
|
19
|
+
return r[0] + r[1] * 1j
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EllipticalArc(object):
|
|
23
|
+
def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point):
|
|
24
|
+
self.current_point = current_point
|
|
25
|
+
self.rx = rx
|
|
26
|
+
self.ry = ry
|
|
27
|
+
self.rotation = rotation
|
|
28
|
+
self.large = large
|
|
29
|
+
self.sweep = sweep
|
|
30
|
+
self.target_point = target_point
|
|
31
|
+
|
|
32
|
+
# SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate
|
|
33
|
+
# uses radians
|
|
34
|
+
self.angle = radians(rotation)
|
|
35
|
+
|
|
36
|
+
# these derived attributes are computed by the _parametrize method
|
|
37
|
+
self.center_point = self.theta1 = self.theta2 = self.theta_arc = None
|
|
38
|
+
|
|
39
|
+
def _parametrize(self):
|
|
40
|
+
# convert from endopoint to center parametrization:
|
|
41
|
+
# https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
|
|
42
|
+
|
|
43
|
+
# If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
|
|
44
|
+
# "lineto") joining the endpoints.
|
|
45
|
+
# http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
|
|
46
|
+
rx = fabs(self.rx)
|
|
47
|
+
ry = fabs(self.ry)
|
|
48
|
+
if not (rx and ry):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# If the current point and target point for the arc are identical, it should
|
|
52
|
+
# be treated as a zero length path. This ensures continuity in animations.
|
|
53
|
+
if self.target_point == self.current_point:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
mid_point_distance = (self.current_point - self.target_point) * 0.5
|
|
57
|
+
|
|
58
|
+
point_transform = Identity.rotate(-self.angle)
|
|
59
|
+
|
|
60
|
+
transformed_mid_point = _map_point(point_transform, mid_point_distance)
|
|
61
|
+
square_rx = rx * rx
|
|
62
|
+
square_ry = ry * ry
|
|
63
|
+
square_x = transformed_mid_point.real * transformed_mid_point.real
|
|
64
|
+
square_y = transformed_mid_point.imag * transformed_mid_point.imag
|
|
65
|
+
|
|
66
|
+
# Check if the radii are big enough to draw the arc, scale radii if not.
|
|
67
|
+
# http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
|
|
68
|
+
radii_scale = square_x / square_rx + square_y / square_ry
|
|
69
|
+
if radii_scale > 1:
|
|
70
|
+
rx *= sqrt(radii_scale)
|
|
71
|
+
ry *= sqrt(radii_scale)
|
|
72
|
+
self.rx, self.ry = rx, ry
|
|
73
|
+
|
|
74
|
+
point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle)
|
|
75
|
+
|
|
76
|
+
point1 = _map_point(point_transform, self.current_point)
|
|
77
|
+
point2 = _map_point(point_transform, self.target_point)
|
|
78
|
+
delta = point2 - point1
|
|
79
|
+
|
|
80
|
+
d = delta.real * delta.real + delta.imag * delta.imag
|
|
81
|
+
scale_factor_squared = max(1 / d - 0.25, 0.0)
|
|
82
|
+
|
|
83
|
+
scale_factor = sqrt(scale_factor_squared)
|
|
84
|
+
if self.sweep == self.large:
|
|
85
|
+
scale_factor = -scale_factor
|
|
86
|
+
|
|
87
|
+
delta *= scale_factor
|
|
88
|
+
center_point = (point1 + point2) * 0.5
|
|
89
|
+
center_point += complex(-delta.imag, delta.real)
|
|
90
|
+
point1 -= center_point
|
|
91
|
+
point2 -= center_point
|
|
92
|
+
|
|
93
|
+
theta1 = atan2(point1.imag, point1.real)
|
|
94
|
+
theta2 = atan2(point2.imag, point2.real)
|
|
95
|
+
|
|
96
|
+
theta_arc = theta2 - theta1
|
|
97
|
+
if theta_arc < 0 and self.sweep:
|
|
98
|
+
theta_arc += TWO_PI
|
|
99
|
+
elif theta_arc > 0 and not self.sweep:
|
|
100
|
+
theta_arc -= TWO_PI
|
|
101
|
+
|
|
102
|
+
self.theta1 = theta1
|
|
103
|
+
self.theta2 = theta1 + theta_arc
|
|
104
|
+
self.theta_arc = theta_arc
|
|
105
|
+
self.center_point = center_point
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def _decompose_to_cubic_curves(self):
|
|
110
|
+
if self.center_point is None and not self._parametrize():
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry)
|
|
114
|
+
|
|
115
|
+
# Some results of atan2 on some platform implementations are not exact
|
|
116
|
+
# enough. So that we get more cubic curves than expected here. Adding 0.001f
|
|
117
|
+
# reduces the count of sgements to the correct count.
|
|
118
|
+
num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001))))
|
|
119
|
+
for i in range(num_segments):
|
|
120
|
+
start_theta = self.theta1 + i * self.theta_arc / num_segments
|
|
121
|
+
end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments
|
|
122
|
+
|
|
123
|
+
t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
|
|
124
|
+
if not isfinite(t):
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
sin_start_theta = sin(start_theta)
|
|
128
|
+
cos_start_theta = cos(start_theta)
|
|
129
|
+
sin_end_theta = sin(end_theta)
|
|
130
|
+
cos_end_theta = cos(end_theta)
|
|
131
|
+
|
|
132
|
+
point1 = complex(
|
|
133
|
+
cos_start_theta - t * sin_start_theta,
|
|
134
|
+
sin_start_theta + t * cos_start_theta,
|
|
135
|
+
)
|
|
136
|
+
point1 += self.center_point
|
|
137
|
+
target_point = complex(cos_end_theta, sin_end_theta)
|
|
138
|
+
target_point += self.center_point
|
|
139
|
+
point2 = target_point
|
|
140
|
+
point2 += complex(t * sin_end_theta, -t * cos_end_theta)
|
|
141
|
+
|
|
142
|
+
point1 = _map_point(point_transform, point1)
|
|
143
|
+
point2 = _map_point(point_transform, point2)
|
|
144
|
+
target_point = _map_point(point_transform, target_point)
|
|
145
|
+
|
|
146
|
+
yield point1, point2, target_point
|
|
147
|
+
|
|
148
|
+
def draw(self, pen):
|
|
149
|
+
for point1, point2, target_point in self._decompose_to_cubic_curves():
|
|
150
|
+
pen.curveTo(
|
|
151
|
+
(point1.real, point1.imag),
|
|
152
|
+
(point2.real, point2.imag),
|
|
153
|
+
(target_point.real, target_point.imag),
|
|
154
|
+
)
|