fonttools 4.55.6__cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.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.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

Files changed (329) hide show
  1. fontTools/__init__.py +8 -0
  2. fontTools/__main__.py +35 -0
  3. fontTools/afmLib.py +439 -0
  4. fontTools/agl.py +5233 -0
  5. fontTools/cffLib/CFF2ToCFF.py +203 -0
  6. fontTools/cffLib/CFFToCFF2.py +305 -0
  7. fontTools/cffLib/__init__.py +3659 -0
  8. fontTools/cffLib/specializer.py +924 -0
  9. fontTools/cffLib/transforms.py +490 -0
  10. fontTools/cffLib/width.py +210 -0
  11. fontTools/colorLib/__init__.py +0 -0
  12. fontTools/colorLib/builder.py +664 -0
  13. fontTools/colorLib/errors.py +2 -0
  14. fontTools/colorLib/geometry.py +143 -0
  15. fontTools/colorLib/table_builder.py +223 -0
  16. fontTools/colorLib/unbuilder.py +81 -0
  17. fontTools/config/__init__.py +75 -0
  18. fontTools/cu2qu/__init__.py +15 -0
  19. fontTools/cu2qu/__main__.py +6 -0
  20. fontTools/cu2qu/benchmark.py +54 -0
  21. fontTools/cu2qu/cli.py +198 -0
  22. fontTools/cu2qu/cu2qu.c +14829 -0
  23. fontTools/cu2qu/cu2qu.cpython-39-x86_64-linux-gnu.so +0 -0
  24. fontTools/cu2qu/cu2qu.py +531 -0
  25. fontTools/cu2qu/errors.py +77 -0
  26. fontTools/cu2qu/ufo.py +349 -0
  27. fontTools/designspaceLib/__init__.py +3338 -0
  28. fontTools/designspaceLib/__main__.py +6 -0
  29. fontTools/designspaceLib/split.py +475 -0
  30. fontTools/designspaceLib/statNames.py +253 -0
  31. fontTools/designspaceLib/types.py +147 -0
  32. fontTools/encodings/MacRoman.py +258 -0
  33. fontTools/encodings/StandardEncoding.py +258 -0
  34. fontTools/encodings/__init__.py +1 -0
  35. fontTools/encodings/codecs.py +135 -0
  36. fontTools/feaLib/__init__.py +4 -0
  37. fontTools/feaLib/__main__.py +78 -0
  38. fontTools/feaLib/ast.py +2134 -0
  39. fontTools/feaLib/builder.py +1747 -0
  40. fontTools/feaLib/error.py +22 -0
  41. fontTools/feaLib/lexer.c +17986 -0
  42. fontTools/feaLib/lexer.cpython-39-x86_64-linux-gnu.so +0 -0
  43. fontTools/feaLib/lexer.py +287 -0
  44. fontTools/feaLib/location.py +12 -0
  45. fontTools/feaLib/lookupDebugInfo.py +12 -0
  46. fontTools/feaLib/parser.py +2359 -0
  47. fontTools/feaLib/variableScalar.py +113 -0
  48. fontTools/fontBuilder.py +1008 -0
  49. fontTools/help.py +36 -0
  50. fontTools/merge/__init__.py +248 -0
  51. fontTools/merge/__main__.py +6 -0
  52. fontTools/merge/base.py +81 -0
  53. fontTools/merge/cmap.py +141 -0
  54. fontTools/merge/layout.py +526 -0
  55. fontTools/merge/options.py +85 -0
  56. fontTools/merge/tables.py +341 -0
  57. fontTools/merge/unicode.py +78 -0
  58. fontTools/merge/util.py +143 -0
  59. fontTools/misc/__init__.py +1 -0
  60. fontTools/misc/arrayTools.py +424 -0
  61. fontTools/misc/bezierTools.c +41831 -0
  62. fontTools/misc/bezierTools.cpython-39-x86_64-linux-gnu.so +0 -0
  63. fontTools/misc/bezierTools.py +1497 -0
  64. fontTools/misc/classifyTools.py +170 -0
  65. fontTools/misc/cliTools.py +53 -0
  66. fontTools/misc/configTools.py +349 -0
  67. fontTools/misc/cython.py +27 -0
  68. fontTools/misc/dictTools.py +83 -0
  69. fontTools/misc/eexec.py +119 -0
  70. fontTools/misc/encodingTools.py +72 -0
  71. fontTools/misc/etree.py +479 -0
  72. fontTools/misc/filenames.py +245 -0
  73. fontTools/misc/fixedTools.py +253 -0
  74. fontTools/misc/intTools.py +25 -0
  75. fontTools/misc/iterTools.py +12 -0
  76. fontTools/misc/lazyTools.py +42 -0
  77. fontTools/misc/loggingTools.py +543 -0
  78. fontTools/misc/macCreatorType.py +56 -0
  79. fontTools/misc/macRes.py +261 -0
  80. fontTools/misc/plistlib/__init__.py +681 -0
  81. fontTools/misc/plistlib/py.typed +0 -0
  82. fontTools/misc/psCharStrings.py +1496 -0
  83. fontTools/misc/psLib.py +398 -0
  84. fontTools/misc/psOperators.py +572 -0
  85. fontTools/misc/py23.py +96 -0
  86. fontTools/misc/roundTools.py +110 -0
  87. fontTools/misc/sstruct.py +231 -0
  88. fontTools/misc/symfont.py +244 -0
  89. fontTools/misc/testTools.py +229 -0
  90. fontTools/misc/textTools.py +154 -0
  91. fontTools/misc/timeTools.py +88 -0
  92. fontTools/misc/transform.py +516 -0
  93. fontTools/misc/treeTools.py +45 -0
  94. fontTools/misc/vector.py +147 -0
  95. fontTools/misc/visitor.py +142 -0
  96. fontTools/misc/xmlReader.py +188 -0
  97. fontTools/misc/xmlWriter.py +204 -0
  98. fontTools/mtiLib/__init__.py +1402 -0
  99. fontTools/mtiLib/__main__.py +5 -0
  100. fontTools/otlLib/__init__.py +1 -0
  101. fontTools/otlLib/builder.py +3221 -0
  102. fontTools/otlLib/error.py +11 -0
  103. fontTools/otlLib/maxContextCalc.py +96 -0
  104. fontTools/otlLib/optimize/__init__.py +53 -0
  105. fontTools/otlLib/optimize/__main__.py +6 -0
  106. fontTools/otlLib/optimize/gpos.py +453 -0
  107. fontTools/pens/__init__.py +1 -0
  108. fontTools/pens/areaPen.py +52 -0
  109. fontTools/pens/basePen.py +475 -0
  110. fontTools/pens/boundsPen.py +98 -0
  111. fontTools/pens/cairoPen.py +26 -0
  112. fontTools/pens/cocoaPen.py +26 -0
  113. fontTools/pens/cu2quPen.py +325 -0
  114. fontTools/pens/explicitClosingLinePen.py +101 -0
  115. fontTools/pens/filterPen.py +241 -0
  116. fontTools/pens/freetypePen.py +462 -0
  117. fontTools/pens/hashPointPen.py +89 -0
  118. fontTools/pens/momentsPen.c +13448 -0
  119. fontTools/pens/momentsPen.cpython-39-x86_64-linux-gnu.so +0 -0
  120. fontTools/pens/momentsPen.py +879 -0
  121. fontTools/pens/perimeterPen.py +69 -0
  122. fontTools/pens/pointInsidePen.py +192 -0
  123. fontTools/pens/pointPen.py +600 -0
  124. fontTools/pens/qtPen.py +29 -0
  125. fontTools/pens/qu2cuPen.py +105 -0
  126. fontTools/pens/quartzPen.py +43 -0
  127. fontTools/pens/recordingPen.py +335 -0
  128. fontTools/pens/reportLabPen.py +79 -0
  129. fontTools/pens/reverseContourPen.py +96 -0
  130. fontTools/pens/roundingPen.py +130 -0
  131. fontTools/pens/statisticsPen.py +312 -0
  132. fontTools/pens/svgPathPen.py +310 -0
  133. fontTools/pens/t2CharStringPen.py +68 -0
  134. fontTools/pens/teePen.py +55 -0
  135. fontTools/pens/transformPen.py +115 -0
  136. fontTools/pens/ttGlyphPen.py +335 -0
  137. fontTools/pens/wxPen.py +29 -0
  138. fontTools/qu2cu/__init__.py +15 -0
  139. fontTools/qu2cu/__main__.py +7 -0
  140. fontTools/qu2cu/benchmark.py +56 -0
  141. fontTools/qu2cu/cli.py +125 -0
  142. fontTools/qu2cu/qu2cu.c +16269 -0
  143. fontTools/qu2cu/qu2cu.cpython-39-x86_64-linux-gnu.so +0 -0
  144. fontTools/qu2cu/qu2cu.py +405 -0
  145. fontTools/subset/__init__.py +3838 -0
  146. fontTools/subset/__main__.py +6 -0
  147. fontTools/subset/cff.py +184 -0
  148. fontTools/subset/svg.py +253 -0
  149. fontTools/subset/util.py +25 -0
  150. fontTools/svgLib/__init__.py +3 -0
  151. fontTools/svgLib/path/__init__.py +65 -0
  152. fontTools/svgLib/path/arc.py +154 -0
  153. fontTools/svgLib/path/parser.py +322 -0
  154. fontTools/svgLib/path/shapes.py +183 -0
  155. fontTools/t1Lib/__init__.py +648 -0
  156. fontTools/tfmLib.py +460 -0
  157. fontTools/ttLib/__init__.py +26 -0
  158. fontTools/ttLib/__main__.py +109 -0
  159. fontTools/ttLib/macUtils.py +54 -0
  160. fontTools/ttLib/removeOverlaps.py +393 -0
  161. fontTools/ttLib/reorderGlyphs.py +284 -0
  162. fontTools/ttLib/scaleUpem.py +436 -0
  163. fontTools/ttLib/sfnt.py +662 -0
  164. fontTools/ttLib/standardGlyphOrder.py +271 -0
  165. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  166. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  167. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  168. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  169. fontTools/ttLib/tables/C_F_F_.py +61 -0
  170. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  171. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  172. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  173. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  174. fontTools/ttLib/tables/D__e_b_g.py +17 -0
  175. fontTools/ttLib/tables/DefaultTable.py +49 -0
  176. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  177. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  178. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  179. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  180. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  181. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  182. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  183. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  184. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  185. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  186. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  187. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  188. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  189. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  190. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  191. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  192. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  193. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  194. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  195. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  196. fontTools/ttLib/tables/S_V_G_.py +223 -0
  197. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  198. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  199. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  200. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  201. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  202. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  203. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  204. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  205. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  206. fontTools/ttLib/tables/T_S_I__0.py +59 -0
  207. fontTools/ttLib/tables/T_S_I__1.py +166 -0
  208. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  209. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  210. fontTools/ttLib/tables/T_S_I__5.py +49 -0
  211. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  212. fontTools/ttLib/tables/TupleVariation.py +884 -0
  213. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  214. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  215. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  216. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  217. fontTools/ttLib/tables/__init__.py +97 -0
  218. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  219. fontTools/ttLib/tables/_a_v_a_r.py +191 -0
  220. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  221. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  222. fontTools/ttLib/tables/_c_m_a_p.py +1578 -0
  223. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  224. fontTools/ttLib/tables/_c_v_t.py +55 -0
  225. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  226. fontTools/ttLib/tables/_f_p_g_m.py +60 -0
  227. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  228. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  229. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  230. fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
  231. fontTools/ttLib/tables/_g_v_a_r.py +292 -0
  232. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  233. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  234. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  235. fontTools/ttLib/tables/_h_m_t_x.py +160 -0
  236. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  237. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  238. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  239. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  240. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  241. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  242. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  243. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  244. fontTools/ttLib/tables/_n_a_m_e.py +1237 -0
  245. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  246. fontTools/ttLib/tables/_p_o_s_t.py +317 -0
  247. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  248. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  249. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  250. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  251. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  252. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  253. fontTools/ttLib/tables/asciiTable.py +20 -0
  254. fontTools/ttLib/tables/grUtils.py +92 -0
  255. fontTools/ttLib/tables/otBase.py +1465 -0
  256. fontTools/ttLib/tables/otConverters.py +2065 -0
  257. fontTools/ttLib/tables/otData.py +6400 -0
  258. fontTools/ttLib/tables/otTables.py +2700 -0
  259. fontTools/ttLib/tables/otTraverse.py +162 -0
  260. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  261. fontTools/ttLib/tables/sbixStrike.py +177 -0
  262. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  263. fontTools/ttLib/tables/ttProgram.py +594 -0
  264. fontTools/ttLib/ttCollection.py +125 -0
  265. fontTools/ttLib/ttFont.py +1155 -0
  266. fontTools/ttLib/ttGlyphSet.py +500 -0
  267. fontTools/ttLib/ttVisitor.py +32 -0
  268. fontTools/ttLib/woff2.py +1683 -0
  269. fontTools/ttx.py +467 -0
  270. fontTools/ufoLib/__init__.py +2477 -0
  271. fontTools/ufoLib/converters.py +334 -0
  272. fontTools/ufoLib/errors.py +22 -0
  273. fontTools/ufoLib/etree.py +6 -0
  274. fontTools/ufoLib/filenames.py +291 -0
  275. fontTools/ufoLib/glifLib.py +2022 -0
  276. fontTools/ufoLib/kerning.py +91 -0
  277. fontTools/ufoLib/plistlib.py +47 -0
  278. fontTools/ufoLib/pointPen.py +6 -0
  279. fontTools/ufoLib/utils.py +76 -0
  280. fontTools/ufoLib/validators.py +1186 -0
  281. fontTools/unicode.py +50 -0
  282. fontTools/unicodedata/Blocks.py +802 -0
  283. fontTools/unicodedata/OTTags.py +50 -0
  284. fontTools/unicodedata/ScriptExtensions.py +806 -0
  285. fontTools/unicodedata/Scripts.py +3618 -0
  286. fontTools/unicodedata/__init__.py +298 -0
  287. fontTools/varLib/__init__.py +1511 -0
  288. fontTools/varLib/__main__.py +6 -0
  289. fontTools/varLib/avar.py +260 -0
  290. fontTools/varLib/avarPlanner.py +1004 -0
  291. fontTools/varLib/builder.py +215 -0
  292. fontTools/varLib/cff.py +631 -0
  293. fontTools/varLib/errors.py +219 -0
  294. fontTools/varLib/featureVars.py +689 -0
  295. fontTools/varLib/instancer/__init__.py +1937 -0
  296. fontTools/varLib/instancer/__main__.py +5 -0
  297. fontTools/varLib/instancer/featureVars.py +190 -0
  298. fontTools/varLib/instancer/names.py +388 -0
  299. fontTools/varLib/instancer/solver.py +309 -0
  300. fontTools/varLib/interpolatable.py +1209 -0
  301. fontTools/varLib/interpolatableHelpers.py +396 -0
  302. fontTools/varLib/interpolatablePlot.py +1269 -0
  303. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  304. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  305. fontTools/varLib/interpolate_layout.py +124 -0
  306. fontTools/varLib/iup.c +19154 -0
  307. fontTools/varLib/iup.cpython-39-x86_64-linux-gnu.so +0 -0
  308. fontTools/varLib/iup.py +490 -0
  309. fontTools/varLib/merger.py +1717 -0
  310. fontTools/varLib/models.py +642 -0
  311. fontTools/varLib/multiVarStore.py +253 -0
  312. fontTools/varLib/mutator.py +518 -0
  313. fontTools/varLib/mvar.py +40 -0
  314. fontTools/varLib/plot.py +238 -0
  315. fontTools/varLib/stat.py +149 -0
  316. fontTools/varLib/varStore.py +767 -0
  317. fontTools/voltLib/__init__.py +5 -0
  318. fontTools/voltLib/ast.py +448 -0
  319. fontTools/voltLib/error.py +12 -0
  320. fontTools/voltLib/lexer.py +99 -0
  321. fontTools/voltLib/parser.py +656 -0
  322. fontTools/voltLib/voltToFea.py +730 -0
  323. fonttools-4.55.6.data/data/share/man/man1/ttx.1 +225 -0
  324. fonttools-4.55.6.dist-info/LICENSE +21 -0
  325. fonttools-4.55.6.dist-info/METADATA +3413 -0
  326. fonttools-4.55.6.dist-info/RECORD +329 -0
  327. fonttools-4.55.6.dist-info/WHEEL +6 -0
  328. fonttools-4.55.6.dist-info/entry_points.txt +5 -0
  329. fonttools-4.55.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3838 @@
1
+ # Copyright 2013 Google, Inc. All Rights Reserved.
2
+ #
3
+ # Google Author(s): Behdad Esfahbod
4
+
5
+ from fontTools import config
6
+ from fontTools.misc.roundTools import otRound
7
+ from fontTools import ttLib
8
+ from fontTools.ttLib.tables import otTables
9
+ from fontTools.ttLib.tables.otBase import USE_HARFBUZZ_REPACKER
10
+ from fontTools.otlLib.maxContextCalc import maxCtxFont
11
+ from fontTools.pens.basePen import NullPen
12
+ from fontTools.misc.loggingTools import Timer
13
+ from fontTools.misc.cliTools import makeOutputFileName
14
+ from fontTools.subset.util import _add_method, _uniq_sort
15
+ from fontTools.subset.cff import *
16
+ from fontTools.subset.svg import *
17
+ from fontTools.varLib import varStore, multiVarStore # For monkey-patching
18
+ from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor
19
+ import sys
20
+ import struct
21
+ import array
22
+ import logging
23
+ from collections import Counter, defaultdict
24
+ from functools import reduce
25
+ from types import MethodType
26
+
27
+ __usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
28
+
29
+ __doc__ = (
30
+ """\
31
+ pyftsubset -- OpenType font subsetter and optimizer
32
+
33
+ pyftsubset is an OpenType font subsetter and optimizer, based on fontTools.
34
+ It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff)
35
+ font file. The subsetted glyph set is based on the specified glyphs
36
+ or characters, and specified OpenType layout features.
37
+
38
+ The tool also performs some size-reducing optimizations, aimed for using
39
+ subset fonts as webfonts. Individual optimizations can be enabled or
40
+ disabled, and are enabled by default when they are safe.
41
+
42
+ Usage: """
43
+ + __usage__
44
+ + """
45
+
46
+ At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file,
47
+ --text, --text-file, --unicodes, or --unicodes-file, must be specified.
48
+
49
+ Args:
50
+
51
+ font-file
52
+ The input font file.
53
+ glyph
54
+ Specify one or more glyph identifiers to include in the subset. Must be
55
+ PS glyph names, or the special string '*' to keep the entire glyph set.
56
+
57
+ Initial glyph set specification
58
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59
+
60
+ These options populate the initial glyph set. Same option can appear
61
+ multiple times, and the results are accummulated.
62
+
63
+ --gids=<NNN>[,<NNN>...]
64
+ Specify comma/whitespace-separated list of glyph IDs or ranges as decimal
65
+ numbers. For example, --gids=10-12,14 adds glyphs with numbers 10, 11,
66
+ 12, and 14.
67
+
68
+ --gids-file=<path>
69
+ Like --gids but reads from a file. Anything after a '#' on any line is
70
+ ignored as comments.
71
+
72
+ --glyphs=<glyphname>[,<glyphname>...]
73
+ Specify comma/whitespace-separated PS glyph names to add to the subset.
74
+ Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc
75
+ that are accepted on the command line. The special string '*' will keep
76
+ the entire glyph set.
77
+
78
+ --glyphs-file=<path>
79
+ Like --glyphs but reads from a file. Anything after a '#' on any line
80
+ is ignored as comments.
81
+
82
+ --text=<text>
83
+ Specify characters to include in the subset, as UTF-8 string.
84
+
85
+ --text-file=<path>
86
+ Like --text but reads from a file. Newline character are not added to
87
+ the subset.
88
+
89
+ --unicodes=<XXXX>[,<XXXX>...]
90
+ Specify comma/whitespace-separated list of Unicode codepoints or
91
+ ranges as hex numbers, optionally prefixed with 'U+', 'u', etc.
92
+ For example, --unicodes=41-5a,61-7a adds ASCII letters, so does
93
+ the more verbose --unicodes=U+0041-005A,U+0061-007A.
94
+ The special strings '*' will choose all Unicode characters mapped
95
+ by the font.
96
+
97
+ --unicodes-file=<path>
98
+ Like --unicodes, but reads from a file. Anything after a '#' on any
99
+ line in the file is ignored as comments.
100
+
101
+ --ignore-missing-glyphs
102
+ Do not fail if some requested glyphs or gids are not available in
103
+ the font.
104
+
105
+ --no-ignore-missing-glyphs
106
+ Stop and fail if some requested glyphs or gids are not available
107
+ in the font. [default]
108
+
109
+ --ignore-missing-unicodes [default]
110
+ Do not fail if some requested Unicode characters (including those
111
+ indirectly specified using --text or --text-file) are not available
112
+ in the font.
113
+
114
+ --no-ignore-missing-unicodes
115
+ Stop and fail if some requested Unicode characters are not available
116
+ in the font.
117
+ Note the default discrepancy between ignoring missing glyphs versus
118
+ unicodes. This is for historical reasons and in the future
119
+ --no-ignore-missing-unicodes might become default.
120
+
121
+ Other options
122
+ ^^^^^^^^^^^^^
123
+
124
+ For the other options listed below, to see the current value of the option,
125
+ pass a value of '?' to it, with or without a '='. In some environments,
126
+ you might need to escape the question mark, like this: '--glyph-names\\?'.
127
+
128
+ Examples::
129
+
130
+ $ pyftsubset --glyph-names?
131
+ Current setting for 'glyph-names' is: False
132
+ $ pyftsubset --name-IDs=?
133
+ Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
134
+ $ pyftsubset --hinting? --no-hinting --hinting?
135
+ Current setting for 'hinting' is: True
136
+ Current setting for 'hinting' is: False
137
+
138
+ Output options
139
+ ^^^^^^^^^^^^^^
140
+
141
+ --output-file=<path>
142
+ The output font file. If not specified, the subsetted font
143
+ will be saved in as font-file.subset.
144
+
145
+ --flavor=<type>
146
+ Specify flavor of output font file. May be 'woff' or 'woff2'.
147
+ Note that WOFF2 requires the Brotli Python extension, available
148
+ at https://github.com/google/brotli
149
+
150
+ --with-zopfli
151
+ Use the Google Zopfli algorithm to compress WOFF. The output is 3-8 %
152
+ smaller than pure zlib, but the compression speed is much slower.
153
+ The Zopfli Python bindings are available at:
154
+ https://pypi.python.org/pypi/zopfli
155
+
156
+ --harfbuzz-repacker
157
+ By default, we serialize GPOS/GSUB using the HarfBuzz Repacker when
158
+ uharfbuzz can be imported and is successful, otherwise fall back to
159
+ the pure-python serializer. Set the option to force using the HarfBuzz
160
+ Repacker (raises an error if uharfbuzz can't be found or fails).
161
+
162
+ --no-harfbuzz-repacker
163
+ Always use the pure-python serializer even if uharfbuzz is available.
164
+
165
+ Glyph set expansion
166
+ ^^^^^^^^^^^^^^^^^^^
167
+
168
+ These options control how additional glyphs are added to the subset.
169
+
170
+ --retain-gids
171
+ Retain glyph indices; just empty glyphs not needed in-place.
172
+
173
+ --notdef-glyph
174
+ Add the '.notdef' glyph to the subset (ie, keep it). [default]
175
+
176
+ --no-notdef-glyph
177
+ Drop the '.notdef' glyph unless specified in the glyph set. This
178
+ saves a few bytes, but is not possible for Postscript-flavored
179
+ fonts, as those require '.notdef'. For TrueType-flavored fonts,
180
+ this works fine as long as no unsupported glyphs are requested
181
+ from the font.
182
+
183
+ --notdef-outline
184
+ Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is
185
+ used when glyphs not supported by the font are to be shown. It is not
186
+ needed otherwise.
187
+
188
+ --no-notdef-outline
189
+ When including a '.notdef' glyph, remove its outline. This saves
190
+ a few bytes. [default]
191
+
192
+ --recommended-glyphs
193
+ Add glyphs 0, 1, 2, and 3 to the subset, as recommended for
194
+ TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'.
195
+ Some legacy software might require this, but no modern system does.
196
+
197
+ --no-recommended-glyphs
198
+ Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in
199
+ glyph set. [default]
200
+
201
+ --no-layout-closure
202
+ Do not expand glyph set to add glyphs produced by OpenType layout
203
+ features. Instead, OpenType layout features will be subset to only
204
+ rules that are relevant to the otherwise-specified glyph set.
205
+
206
+ --layout-features[+|-]=<feature>[,<feature>...]
207
+ Specify (=), add to (+=) or exclude from (-=) the comma-separated
208
+ set of OpenType layout feature tags that will be preserved.
209
+ Glyph variants used by the preserved features are added to the
210
+ specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs',
211
+ 'dnom', 'frac', 'kern', 'liga', 'locl', 'mark', 'mkmk', 'numr', 'rclt',
212
+ 'rlig', 'rvrn', and all features required for script shaping are
213
+ preserved. To see the full list, try '--layout-features=?'.
214
+ Use '*' to keep all features.
215
+ Multiple --layout-features options can be provided if necessary.
216
+ Examples:
217
+
218
+ --layout-features+=onum,pnum,ss01
219
+ * Keep the default set of features and 'onum', 'pnum', 'ss01'.
220
+ --layout-features-='mark','mkmk'
221
+ * Keep the default set of features but drop 'mark' and 'mkmk'.
222
+ --layout-features='kern'
223
+ * Only keep the 'kern' feature, drop all others.
224
+ --layout-features=''
225
+ * Drop all features.
226
+ --layout-features='*'
227
+ * Keep all features.
228
+ --layout-features+=aalt --layout-features-=vrt2
229
+ * Keep default set of features plus 'aalt', but drop 'vrt2'.
230
+
231
+ --layout-scripts[+|-]=<script>[,<script>...]
232
+ Specify (=), add to (+=) or exclude from (-=) the comma-separated
233
+ set of OpenType layout script tags that will be preserved. LangSys tags
234
+ can be appended to script tag, separated by '.', for example:
235
+ 'arab.dflt,arab.URD,latn.TRK'. By default all scripts are retained ('*').
236
+
237
+ Hinting options
238
+ ^^^^^^^^^^^^^^^
239
+
240
+ --hinting
241
+ Keep hinting [default]
242
+
243
+ --no-hinting
244
+ Drop glyph-specific hinting and font-wide hinting tables, as well
245
+ as remove hinting-related bits and pieces from other tables (eg. GPOS).
246
+ See --hinting-tables for list of tables that are dropped by default.
247
+ Instructions and hints are stripped from 'glyf' and 'CFF ' tables
248
+ respectively. This produces (sometimes up to 30%) smaller fonts that
249
+ are suitable for extremely high-resolution systems, like high-end
250
+ mobile devices and retina displays.
251
+
252
+ Optimization options
253
+ ^^^^^^^^^^^^^^^^^^^^
254
+
255
+ --desubroutinize
256
+ Remove CFF use of subroutinizes. Subroutinization is a way to make CFF
257
+ fonts smaller. For small subsets however, desubroutinizing might make
258
+ the font smaller. It has even been reported that desubroutinized CFF
259
+ fonts compress better (produce smaller output) WOFF and WOFF2 fonts.
260
+ Also see note under --no-hinting.
261
+
262
+ --no-desubroutinize [default]
263
+ Leave CFF subroutinizes as is, only throw away unused subroutinizes.
264
+
265
+ Font table options
266
+ ^^^^^^^^^^^^^^^^^^
267
+
268
+ --drop-tables[+|-]=<table>[,<table>...]
269
+ Specify (=), add to (+=) or exclude from (-=) the comma-separated
270
+ set of tables that will be be dropped.
271
+ By default, the following tables are dropped:
272
+ 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
273
+ and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'.
274
+ The tool will attempt to subset the remaining tables.
275
+
276
+ Examples:
277
+
278
+ --drop-tables-=BASE
279
+ * Drop the default set of tables but keep 'BASE'.
280
+
281
+ --drop-tables+=GSUB
282
+ * Drop the default set of tables and 'GSUB'.
283
+
284
+ --drop-tables=DSIG
285
+ * Only drop the 'DSIG' table, keep all others.
286
+
287
+ --drop-tables=
288
+ * Keep all tables.
289
+
290
+ --no-subset-tables+=<table>[,<table>...]
291
+ Add to the set of tables that will not be subsetted.
292
+ By default, the following tables are included in this list, as
293
+ they do not need subsetting (ignore the fact that 'loca' is listed
294
+ here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name',
295
+ 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'.
296
+ By default, tables that the tool does not know how to subset and are not
297
+ specified here will be dropped from the font, unless --passthrough-tables
298
+ option is passed.
299
+
300
+ Example:
301
+
302
+ --no-subset-tables+=FFTM
303
+ * Keep 'FFTM' table in the font by preventing subsetting.
304
+
305
+ --passthrough-tables
306
+ Do not drop tables that the tool does not know how to subset.
307
+
308
+ --no-passthrough-tables
309
+ Tables that the tool does not know how to subset and are not specified
310
+ in --no-subset-tables will be dropped from the font. [default]
311
+
312
+ --hinting-tables[-]=<table>[,<table>...]
313
+ Specify (=), add to (+=) or exclude from (-=) the list of font-wide
314
+ hinting tables that will be dropped if --no-hinting is specified.
315
+
316
+ Examples:
317
+
318
+ --hinting-tables-=VDMX
319
+ * Drop font-wide hinting tables except 'VDMX'.
320
+ --hinting-tables=
321
+ * Keep all font-wide hinting tables (but strip hints from glyphs).
322
+
323
+ --legacy-kern
324
+ Keep TrueType 'kern' table even when OpenType 'GPOS' is available.
325
+
326
+ --no-legacy-kern
327
+ Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default]
328
+
329
+ Font naming options
330
+ ^^^^^^^^^^^^^^^^^^^
331
+
332
+ These options control what is retained in the 'name' table. For numerical
333
+ codes, see: http://www.microsoft.com/typography/otspec/name.htm
334
+
335
+ --name-IDs[+|-]=<nameID>[,<nameID>...]
336
+ Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
337
+ entry nameIDs that will be preserved. By default, only nameIDs between 0
338
+ and 6 are preserved, the rest are dropped. Use '*' to keep all entries.
339
+
340
+ Examples:
341
+
342
+ --name-IDs+=7,8,9
343
+ * Also keep Trademark, Manufacturer and Designer name entries.
344
+ --name-IDs=
345
+ * Drop all 'name' table entries.
346
+ --name-IDs=*
347
+ * keep all 'name' table entries
348
+
349
+ --name-legacy
350
+ Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.).
351
+ XXX Note: This might be needed for some fonts that have no Unicode name
352
+ entires for English. See: https://github.com/fonttools/fonttools/issues/146
353
+
354
+ --no-name-legacy
355
+ Drop legacy (non-Unicode) 'name' table entries [default]
356
+
357
+ --name-languages[+|-]=<langID>[,<langID>]
358
+ Specify (=), add to (+=) or exclude from (-=) the set of 'name' table
359
+ langIDs that will be preserved. By default only records with langID
360
+ 0x0409 (English) are preserved. Use '*' to keep all langIDs.
361
+
362
+ --obfuscate-names
363
+ Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4,
364
+ and 6 with dummy strings (it is still fully functional as webfont).
365
+
366
+ Glyph naming and encoding options
367
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
368
+
369
+ --glyph-names
370
+ Keep PS glyph names in TT-flavored fonts. In general glyph names are
371
+ not needed for correct use of the font. However, some PDF generators
372
+ and PDF viewers might rely on glyph names to extract Unicode text
373
+ from PDF documents.
374
+ --no-glyph-names
375
+ Drop PS glyph names in TT-flavored fonts, by using 'post' table
376
+ version 3.0. [default]
377
+ --legacy-cmap
378
+ Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.).
379
+ --no-legacy-cmap
380
+ Drop the legacy 'cmap' subtables. [default]
381
+ --symbol-cmap
382
+ Keep the 3.0 symbol 'cmap'.
383
+ --no-symbol-cmap
384
+ Drop the 3.0 symbol 'cmap'. [default]
385
+
386
+ Other font-specific options
387
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
388
+
389
+ --recalc-bounds
390
+ Recalculate font bounding boxes.
391
+ --no-recalc-bounds
392
+ Keep original font bounding boxes. This is faster and still safe
393
+ for all practical purposes. [default]
394
+ --recalc-timestamp
395
+ Set font 'modified' timestamp to current time.
396
+ --no-recalc-timestamp
397
+ Do not modify font 'modified' timestamp. [default]
398
+ --canonical-order
399
+ Order tables as recommended in the OpenType standard. This is not
400
+ required by the standard, nor by any known implementation.
401
+ --no-canonical-order
402
+ Keep original order of font tables. This is faster. [default]
403
+ --prune-unicode-ranges
404
+ Update the 'OS/2 ulUnicodeRange*' bits after subsetting. The Unicode
405
+ ranges defined in the OpenType specification v1.7 are intersected with
406
+ the Unicode codepoints specified in the font's Unicode 'cmap' subtables:
407
+ when no overlap is found, the bit will be switched off. However, it will
408
+ *not* be switched on if an intersection is found. [default]
409
+ --no-prune-unicode-ranges
410
+ Don't change the 'OS/2 ulUnicodeRange*' bits.
411
+ --prune-codepage-ranges
412
+ Update the 'OS/2 ulCodePageRange*' bits after subsetting. [default]
413
+ --no-prune-codepage-ranges
414
+ Don't change the 'OS/2 ulCodePageRange*' bits.
415
+ --recalc-average-width
416
+ Update the 'OS/2 xAvgCharWidth' field after subsetting.
417
+ --no-recalc-average-width
418
+ Don't change the 'OS/2 xAvgCharWidth' field. [default]
419
+ --recalc-max-context
420
+ Update the 'OS/2 usMaxContext' field after subsetting.
421
+ --no-recalc-max-context
422
+ Don't change the 'OS/2 usMaxContext' field. [default]
423
+ --font-number=<number>
424
+ Select font number for TrueType Collection (.ttc/.otc), starting from 0.
425
+ --pretty-svg
426
+ When subsetting SVG table, use lxml pretty_print=True option to indent
427
+ the XML output (only recommended for debugging purposes).
428
+
429
+ Application options
430
+ ^^^^^^^^^^^^^^^^^^^
431
+
432
+ --verbose
433
+ Display verbose information of the subsetting process.
434
+ --timing
435
+ Display detailed timing information of the subsetting process.
436
+ --xml
437
+ Display the TTX XML representation of subsetted font.
438
+
439
+ Example
440
+ ^^^^^^^
441
+
442
+ Produce a subset containing the characters ' !"#$%' without performing
443
+ size-reducing optimizations::
444
+
445
+ $ pyftsubset font.ttf --unicodes="U+0020-0025" \\
446
+ --layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\
447
+ --notdef-glyph --notdef-outline --recommended-glyphs \\
448
+ --name-IDs=* --name-legacy --name-languages=*
449
+ """
450
+ )
451
+
452
+
453
+ log = logging.getLogger("fontTools.subset")
454
+
455
+
456
+ def _log_glyphs(self, glyphs, font=None):
457
+ self.info("Glyph names: %s", sorted(glyphs))
458
+ if font:
459
+ reverseGlyphMap = font.getReverseGlyphMap()
460
+ self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs))
461
+
462
+
463
+ # bind "glyphs" function to 'log' object
464
+ log.glyphs = MethodType(_log_glyphs, log)
465
+
466
+ # I use a different timing channel so I can configure it separately from the
467
+ # main module's logger
468
+ timer = Timer(logger=logging.getLogger("fontTools.subset.timer"))
469
+
470
+
471
+ def _dict_subset(d, glyphs):
472
+ return {g: d[g] for g in glyphs}
473
+
474
+
475
+ def _list_subset(l, indices):
476
+ count = len(l)
477
+ return [l[i] for i in indices if i < count]
478
+
479
+
480
+ @_add_method(otTables.Coverage)
481
+ def intersect(self, glyphs):
482
+ """Returns ascending list of matching coverage values."""
483
+ return [i for i, g in enumerate(self.glyphs) if g in glyphs]
484
+
485
+
486
+ @_add_method(otTables.Coverage)
487
+ def intersect_glyphs(self, glyphs):
488
+ """Returns set of intersecting glyphs."""
489
+ return set(g for g in self.glyphs if g in glyphs)
490
+
491
+
492
+ @_add_method(otTables.Coverage)
493
+ def subset(self, glyphs):
494
+ """Returns ascending list of remaining coverage values."""
495
+ indices = self.intersect(glyphs)
496
+ self.glyphs = [g for g in self.glyphs if g in glyphs]
497
+ return indices
498
+
499
+
500
+ @_add_method(otTables.Coverage)
501
+ def remap(self, coverage_map):
502
+ """Remaps coverage."""
503
+ self.glyphs = [self.glyphs[i] for i in coverage_map]
504
+
505
+
506
+ @_add_method(otTables.ClassDef)
507
+ def intersect(self, glyphs):
508
+ """Returns ascending list of matching class values."""
509
+ return _uniq_sort(
510
+ ([0] if any(g not in self.classDefs for g in glyphs) else [])
511
+ + [v for g, v in self.classDefs.items() if g in glyphs]
512
+ )
513
+
514
+
515
+ @_add_method(otTables.ClassDef)
516
+ def intersect_class(self, glyphs, klass):
517
+ """Returns set of glyphs matching class."""
518
+ if klass == 0:
519
+ return set(g for g in glyphs if g not in self.classDefs)
520
+ return set(g for g, v in self.classDefs.items() if v == klass and g in glyphs)
521
+
522
+
523
+ @_add_method(otTables.ClassDef)
524
+ def subset(self, glyphs, remap=False, useClass0=True):
525
+ """Returns ascending list of remaining classes."""
526
+ self.classDefs = {g: v for g, v in self.classDefs.items() if g in glyphs}
527
+ # Note: while class 0 has the special meaning of "not matched",
528
+ # if no glyph will ever /not match/, we can optimize class 0 out too.
529
+ # Only do this if allowed.
530
+ indices = _uniq_sort(
531
+ (
532
+ [0]
533
+ if ((not useClass0) or any(g not in self.classDefs for g in glyphs))
534
+ else []
535
+ )
536
+ + list(self.classDefs.values())
537
+ )
538
+ if remap:
539
+ self.remap(indices)
540
+ return indices
541
+
542
+
543
+ @_add_method(otTables.ClassDef)
544
+ def remap(self, class_map):
545
+ """Remaps classes."""
546
+ self.classDefs = {g: class_map.index(v) for g, v in self.classDefs.items()}
547
+
548
+
549
+ @_add_method(otTables.SingleSubst)
550
+ def closure_glyphs(self, s, cur_glyphs):
551
+ s.glyphs.update(v for g, v in self.mapping.items() if g in cur_glyphs)
552
+
553
+
554
+ @_add_method(otTables.SingleSubst)
555
+ def subset_glyphs(self, s):
556
+ self.mapping = {
557
+ g: v for g, v in self.mapping.items() if g in s.glyphs and v in s.glyphs
558
+ }
559
+ return bool(self.mapping)
560
+
561
+
562
+ @_add_method(otTables.MultipleSubst)
563
+ def closure_glyphs(self, s, cur_glyphs):
564
+ for glyph, subst in self.mapping.items():
565
+ if glyph in cur_glyphs:
566
+ s.glyphs.update(subst)
567
+
568
+
569
+ @_add_method(otTables.MultipleSubst)
570
+ def subset_glyphs(self, s):
571
+ self.mapping = {
572
+ g: v
573
+ for g, v in self.mapping.items()
574
+ if g in s.glyphs and all(sub in s.glyphs for sub in v)
575
+ }
576
+ return bool(self.mapping)
577
+
578
+
579
+ @_add_method(otTables.AlternateSubst)
580
+ def closure_glyphs(self, s, cur_glyphs):
581
+ s.glyphs.update(*(vlist for g, vlist in self.alternates.items() if g in cur_glyphs))
582
+
583
+
584
+ @_add_method(otTables.AlternateSubst)
585
+ def subset_glyphs(self, s):
586
+ self.alternates = {
587
+ g: [v for v in vlist if v in s.glyphs]
588
+ for g, vlist in self.alternates.items()
589
+ if g in s.glyphs and any(v in s.glyphs for v in vlist)
590
+ }
591
+ return bool(self.alternates)
592
+
593
+
594
+ @_add_method(otTables.LigatureSubst)
595
+ def closure_glyphs(self, s, cur_glyphs):
596
+ s.glyphs.update(
597
+ *(
598
+ [seq.LigGlyph for seq in seqs if all(c in s.glyphs for c in seq.Component)]
599
+ for g, seqs in self.ligatures.items()
600
+ if g in cur_glyphs
601
+ )
602
+ )
603
+
604
+
605
+ @_add_method(otTables.LigatureSubst)
606
+ def subset_glyphs(self, s):
607
+ self.ligatures = {g: v for g, v in self.ligatures.items() if g in s.glyphs}
608
+ self.ligatures = {
609
+ g: [
610
+ seq
611
+ for seq in seqs
612
+ if seq.LigGlyph in s.glyphs and all(c in s.glyphs for c in seq.Component)
613
+ ]
614
+ for g, seqs in self.ligatures.items()
615
+ }
616
+ self.ligatures = {g: v for g, v in self.ligatures.items() if v}
617
+ return bool(self.ligatures)
618
+
619
+
620
+ @_add_method(otTables.ReverseChainSingleSubst)
621
+ def closure_glyphs(self, s, cur_glyphs):
622
+ if self.Format == 1:
623
+ indices = self.Coverage.intersect(cur_glyphs)
624
+ if not indices or not all(
625
+ c.intersect(s.glyphs)
626
+ for c in self.LookAheadCoverage + self.BacktrackCoverage
627
+ ):
628
+ return
629
+ s.glyphs.update(self.Substitute[i] for i in indices)
630
+ else:
631
+ assert 0, "unknown format: %s" % self.Format
632
+
633
+
634
+ @_add_method(otTables.ReverseChainSingleSubst)
635
+ def subset_glyphs(self, s):
636
+ if self.Format == 1:
637
+ indices = self.Coverage.subset(s.glyphs)
638
+ self.Substitute = _list_subset(self.Substitute, indices)
639
+ # Now drop rules generating glyphs we don't want
640
+ indices = [i for i, sub in enumerate(self.Substitute) if sub in s.glyphs]
641
+ self.Substitute = _list_subset(self.Substitute, indices)
642
+ self.Coverage.remap(indices)
643
+ self.GlyphCount = len(self.Substitute)
644
+ return bool(
645
+ self.GlyphCount
646
+ and all(
647
+ c.subset(s.glyphs)
648
+ for c in self.LookAheadCoverage + self.BacktrackCoverage
649
+ )
650
+ )
651
+ else:
652
+ assert 0, "unknown format: %s" % self.Format
653
+
654
+
655
+ @_add_method(otTables.Device)
656
+ def is_hinting(self):
657
+ return self.DeltaFormat in (1, 2, 3)
658
+
659
+
660
+ @_add_method(otTables.ValueRecord)
661
+ def prune_hints(self):
662
+ for name in ["XPlaDevice", "YPlaDevice", "XAdvDevice", "YAdvDevice"]:
663
+ v = getattr(self, name, None)
664
+ if v is not None and v.is_hinting():
665
+ delattr(self, name)
666
+
667
+
668
+ @_add_method(otTables.SinglePos)
669
+ def subset_glyphs(self, s):
670
+ if self.Format == 1:
671
+ return len(self.Coverage.subset(s.glyphs))
672
+ elif self.Format == 2:
673
+ indices = self.Coverage.subset(s.glyphs)
674
+ values = self.Value
675
+ count = len(values)
676
+ self.Value = [values[i] for i in indices if i < count]
677
+ self.ValueCount = len(self.Value)
678
+ return bool(self.ValueCount)
679
+ else:
680
+ assert 0, "unknown format: %s" % self.Format
681
+
682
+
683
+ @_add_method(otTables.SinglePos)
684
+ def prune_post_subset(self, font, options):
685
+ if self.Value is None:
686
+ assert self.ValueFormat == 0
687
+ return True
688
+
689
+ # Shrink ValueFormat
690
+ if self.Format == 1:
691
+ if not options.hinting:
692
+ self.Value.prune_hints()
693
+ self.ValueFormat = self.Value.getEffectiveFormat()
694
+ elif self.Format == 2:
695
+ if None in self.Value:
696
+ assert self.ValueFormat == 0
697
+ assert all(v is None for v in self.Value)
698
+ else:
699
+ if not options.hinting:
700
+ for v in self.Value:
701
+ v.prune_hints()
702
+ self.ValueFormat = reduce(
703
+ int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
704
+ )
705
+
706
+ # Downgrade to Format 1 if all ValueRecords are the same
707
+ if self.Format == 2 and all(v == self.Value[0] for v in self.Value):
708
+ self.Format = 1
709
+ self.Value = self.Value[0] if self.ValueFormat != 0 else None
710
+ del self.ValueCount
711
+
712
+ return True
713
+
714
+
715
+ @_add_method(otTables.PairPos)
716
+ def subset_glyphs(self, s):
717
+ if self.Format == 1:
718
+ indices = self.Coverage.subset(s.glyphs)
719
+ pairs = self.PairSet
720
+ count = len(pairs)
721
+ self.PairSet = [pairs[i] for i in indices if i < count]
722
+ for p in self.PairSet:
723
+ p.PairValueRecord = [
724
+ r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs
725
+ ]
726
+ p.PairValueCount = len(p.PairValueRecord)
727
+ # Remove empty pairsets
728
+ indices = [i for i, p in enumerate(self.PairSet) if p.PairValueCount]
729
+ self.Coverage.remap(indices)
730
+ self.PairSet = _list_subset(self.PairSet, indices)
731
+ self.PairSetCount = len(self.PairSet)
732
+ return bool(self.PairSetCount)
733
+ elif self.Format == 2:
734
+ class1_map = [
735
+ c
736
+ for c in self.ClassDef1.subset(
737
+ s.glyphs.intersection(self.Coverage.glyphs), remap=True
738
+ )
739
+ if c < self.Class1Count
740
+ ]
741
+ class2_map = [
742
+ c
743
+ for c in self.ClassDef2.subset(s.glyphs, remap=True, useClass0=False)
744
+ if c < self.Class2Count
745
+ ]
746
+ self.Class1Record = [self.Class1Record[i] for i in class1_map]
747
+ for c in self.Class1Record:
748
+ c.Class2Record = [c.Class2Record[i] for i in class2_map]
749
+ self.Class1Count = len(class1_map)
750
+ self.Class2Count = len(class2_map)
751
+ # If only Class2 0 left, no need to keep anything.
752
+ return bool(
753
+ self.Class1Count
754
+ and (self.Class2Count > 1)
755
+ and self.Coverage.subset(s.glyphs)
756
+ )
757
+ else:
758
+ assert 0, "unknown format: %s" % self.Format
759
+
760
+
761
+ @_add_method(otTables.PairPos)
762
+ def prune_post_subset(self, font, options):
763
+ if not options.hinting:
764
+ attr1, attr2 = {
765
+ 1: ("PairSet", "PairValueRecord"),
766
+ 2: ("Class1Record", "Class2Record"),
767
+ }[self.Format]
768
+
769
+ self.ValueFormat1 = self.ValueFormat2 = 0
770
+ for row in getattr(self, attr1):
771
+ for r in getattr(row, attr2):
772
+ if r.Value1:
773
+ r.Value1.prune_hints()
774
+ self.ValueFormat1 |= r.Value1.getEffectiveFormat()
775
+ if r.Value2:
776
+ r.Value2.prune_hints()
777
+ self.ValueFormat2 |= r.Value2.getEffectiveFormat()
778
+
779
+ return bool(self.ValueFormat1 | self.ValueFormat2)
780
+
781
+
782
+ @_add_method(otTables.CursivePos)
783
+ def subset_glyphs(self, s):
784
+ if self.Format == 1:
785
+ indices = self.Coverage.subset(s.glyphs)
786
+ records = self.EntryExitRecord
787
+ count = len(records)
788
+ self.EntryExitRecord = [records[i] for i in indices if i < count]
789
+ self.EntryExitCount = len(self.EntryExitRecord)
790
+ return bool(self.EntryExitCount)
791
+ else:
792
+ assert 0, "unknown format: %s" % self.Format
793
+
794
+
795
+ @_add_method(otTables.Anchor)
796
+ def prune_hints(self):
797
+ if self.Format == 2:
798
+ self.Format = 1
799
+ elif self.Format == 3:
800
+ for name in ("XDeviceTable", "YDeviceTable"):
801
+ v = getattr(self, name, None)
802
+ if v is not None and v.is_hinting():
803
+ setattr(self, name, None)
804
+ if self.XDeviceTable is None and self.YDeviceTable is None:
805
+ self.Format = 1
806
+
807
+
808
+ @_add_method(otTables.CursivePos)
809
+ def prune_post_subset(self, font, options):
810
+ if not options.hinting:
811
+ for rec in self.EntryExitRecord:
812
+ if rec.EntryAnchor:
813
+ rec.EntryAnchor.prune_hints()
814
+ if rec.ExitAnchor:
815
+ rec.ExitAnchor.prune_hints()
816
+ return True
817
+
818
+
819
+ @_add_method(otTables.MarkBasePos)
820
+ def subset_glyphs(self, s):
821
+ if self.Format == 1:
822
+ mark_indices = self.MarkCoverage.subset(s.glyphs)
823
+ self.MarkArray.MarkRecord = _list_subset(
824
+ self.MarkArray.MarkRecord, mark_indices
825
+ )
826
+ self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
827
+ base_indices = self.BaseCoverage.subset(s.glyphs)
828
+ self.BaseArray.BaseRecord = _list_subset(
829
+ self.BaseArray.BaseRecord, base_indices
830
+ )
831
+ self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
832
+ # Prune empty classes
833
+ class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
834
+ self.ClassCount = len(class_indices)
835
+ for m in self.MarkArray.MarkRecord:
836
+ m.Class = class_indices.index(m.Class)
837
+ for b in self.BaseArray.BaseRecord:
838
+ b.BaseAnchor = _list_subset(b.BaseAnchor, class_indices)
839
+ return bool(
840
+ self.ClassCount and self.MarkArray.MarkCount and self.BaseArray.BaseCount
841
+ )
842
+ else:
843
+ assert 0, "unknown format: %s" % self.Format
844
+
845
+
846
+ @_add_method(otTables.MarkBasePos)
847
+ def prune_post_subset(self, font, options):
848
+ if not options.hinting:
849
+ for m in self.MarkArray.MarkRecord:
850
+ if m.MarkAnchor:
851
+ m.MarkAnchor.prune_hints()
852
+ for b in self.BaseArray.BaseRecord:
853
+ for a in b.BaseAnchor:
854
+ if a:
855
+ a.prune_hints()
856
+ return True
857
+
858
+
859
+ @_add_method(otTables.MarkLigPos)
860
+ def subset_glyphs(self, s):
861
+ if self.Format == 1:
862
+ mark_indices = self.MarkCoverage.subset(s.glyphs)
863
+ self.MarkArray.MarkRecord = _list_subset(
864
+ self.MarkArray.MarkRecord, mark_indices
865
+ )
866
+ self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
867
+ ligature_indices = self.LigatureCoverage.subset(s.glyphs)
868
+ self.LigatureArray.LigatureAttach = _list_subset(
869
+ self.LigatureArray.LigatureAttach, ligature_indices
870
+ )
871
+ self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
872
+ # Prune empty classes
873
+ class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
874
+ self.ClassCount = len(class_indices)
875
+ for m in self.MarkArray.MarkRecord:
876
+ m.Class = class_indices.index(m.Class)
877
+ for l in self.LigatureArray.LigatureAttach:
878
+ if l is None:
879
+ continue
880
+ for c in l.ComponentRecord:
881
+ c.LigatureAnchor = _list_subset(c.LigatureAnchor, class_indices)
882
+ return bool(
883
+ self.ClassCount
884
+ and self.MarkArray.MarkCount
885
+ and self.LigatureArray.LigatureCount
886
+ )
887
+ else:
888
+ assert 0, "unknown format: %s" % self.Format
889
+
890
+
891
+ @_add_method(otTables.MarkLigPos)
892
+ def prune_post_subset(self, font, options):
893
+ if not options.hinting:
894
+ for m in self.MarkArray.MarkRecord:
895
+ if m.MarkAnchor:
896
+ m.MarkAnchor.prune_hints()
897
+ for l in self.LigatureArray.LigatureAttach:
898
+ if l is None:
899
+ continue
900
+ for c in l.ComponentRecord:
901
+ for a in c.LigatureAnchor:
902
+ if a:
903
+ a.prune_hints()
904
+ return True
905
+
906
+
907
+ @_add_method(otTables.MarkMarkPos)
908
+ def subset_glyphs(self, s):
909
+ if self.Format == 1:
910
+ mark1_indices = self.Mark1Coverage.subset(s.glyphs)
911
+ self.Mark1Array.MarkRecord = _list_subset(
912
+ self.Mark1Array.MarkRecord, mark1_indices
913
+ )
914
+ self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
915
+ mark2_indices = self.Mark2Coverage.subset(s.glyphs)
916
+ self.Mark2Array.Mark2Record = _list_subset(
917
+ self.Mark2Array.Mark2Record, mark2_indices
918
+ )
919
+ self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
920
+ # Prune empty classes
921
+ class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
922
+ self.ClassCount = len(class_indices)
923
+ for m in self.Mark1Array.MarkRecord:
924
+ m.Class = class_indices.index(m.Class)
925
+ for b in self.Mark2Array.Mark2Record:
926
+ b.Mark2Anchor = _list_subset(b.Mark2Anchor, class_indices)
927
+ return bool(
928
+ self.ClassCount and self.Mark1Array.MarkCount and self.Mark2Array.MarkCount
929
+ )
930
+ else:
931
+ assert 0, "unknown format: %s" % self.Format
932
+
933
+
934
+ @_add_method(otTables.MarkMarkPos)
935
+ def prune_post_subset(self, font, options):
936
+ if not options.hinting:
937
+ for m in self.Mark1Array.MarkRecord:
938
+ if m.MarkAnchor:
939
+ m.MarkAnchor.prune_hints()
940
+ for b in self.Mark2Array.Mark2Record:
941
+ for m in b.Mark2Anchor:
942
+ if m:
943
+ m.prune_hints()
944
+ return True
945
+
946
+
947
+ @_add_method(
948
+ otTables.SingleSubst,
949
+ otTables.MultipleSubst,
950
+ otTables.AlternateSubst,
951
+ otTables.LigatureSubst,
952
+ otTables.ReverseChainSingleSubst,
953
+ otTables.SinglePos,
954
+ otTables.PairPos,
955
+ otTables.CursivePos,
956
+ otTables.MarkBasePos,
957
+ otTables.MarkLigPos,
958
+ otTables.MarkMarkPos,
959
+ )
960
+ def subset_lookups(self, lookup_indices):
961
+ pass
962
+
963
+
964
+ @_add_method(
965
+ otTables.SingleSubst,
966
+ otTables.MultipleSubst,
967
+ otTables.AlternateSubst,
968
+ otTables.LigatureSubst,
969
+ otTables.ReverseChainSingleSubst,
970
+ otTables.SinglePos,
971
+ otTables.PairPos,
972
+ otTables.CursivePos,
973
+ otTables.MarkBasePos,
974
+ otTables.MarkLigPos,
975
+ otTables.MarkMarkPos,
976
+ )
977
+ def collect_lookups(self):
978
+ return []
979
+
980
+
981
+ @_add_method(
982
+ otTables.SingleSubst,
983
+ otTables.MultipleSubst,
984
+ otTables.AlternateSubst,
985
+ otTables.LigatureSubst,
986
+ otTables.ReverseChainSingleSubst,
987
+ otTables.ContextSubst,
988
+ otTables.ChainContextSubst,
989
+ otTables.ContextPos,
990
+ otTables.ChainContextPos,
991
+ )
992
+ def prune_post_subset(self, font, options):
993
+ return True
994
+
995
+
996
+ @_add_method(
997
+ otTables.SingleSubst, otTables.AlternateSubst, otTables.ReverseChainSingleSubst
998
+ )
999
+ def may_have_non_1to1(self):
1000
+ return False
1001
+
1002
+
1003
+ @_add_method(
1004
+ otTables.MultipleSubst,
1005
+ otTables.LigatureSubst,
1006
+ otTables.ContextSubst,
1007
+ otTables.ChainContextSubst,
1008
+ )
1009
+ def may_have_non_1to1(self):
1010
+ return True
1011
+
1012
+
1013
+ @_add_method(
1014
+ otTables.ContextSubst,
1015
+ otTables.ChainContextSubst,
1016
+ otTables.ContextPos,
1017
+ otTables.ChainContextPos,
1018
+ )
1019
+ def __subset_classify_context(self):
1020
+ class ContextHelper(object):
1021
+ def __init__(self, klass, Format):
1022
+ if klass.__name__.endswith("Subst"):
1023
+ Typ = "Sub"
1024
+ Type = "Subst"
1025
+ else:
1026
+ Typ = "Pos"
1027
+ Type = "Pos"
1028
+ if klass.__name__.startswith("Chain"):
1029
+ Chain = "Chain"
1030
+ InputIdx = 1
1031
+ DataLen = 3
1032
+ else:
1033
+ Chain = ""
1034
+ InputIdx = 0
1035
+ DataLen = 1
1036
+ ChainTyp = Chain + Typ
1037
+
1038
+ self.Typ = Typ
1039
+ self.Type = Type
1040
+ self.Chain = Chain
1041
+ self.ChainTyp = ChainTyp
1042
+ self.InputIdx = InputIdx
1043
+ self.DataLen = DataLen
1044
+
1045
+ self.LookupRecord = Type + "LookupRecord"
1046
+
1047
+ if Format == 1:
1048
+ Coverage = lambda r: r.Coverage
1049
+ ChainCoverage = lambda r: r.Coverage
1050
+ ContextData = lambda r: (None,)
1051
+ ChainContextData = lambda r: (None, None, None)
1052
+ SetContextData = None
1053
+ SetChainContextData = None
1054
+ RuleData = lambda r: (r.Input,)
1055
+ ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
1056
+
1057
+ def SetRuleData(r, d):
1058
+ (r.Input,) = d
1059
+ (r.GlyphCount,) = (len(x) + 1 for x in d)
1060
+
1061
+ def ChainSetRuleData(r, d):
1062
+ (r.Backtrack, r.Input, r.LookAhead) = d
1063
+ (
1064
+ r.BacktrackGlyphCount,
1065
+ r.InputGlyphCount,
1066
+ r.LookAheadGlyphCount,
1067
+ ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
1068
+
1069
+ elif Format == 2:
1070
+ Coverage = lambda r: r.Coverage
1071
+ ChainCoverage = lambda r: r.Coverage
1072
+ ContextData = lambda r: (r.ClassDef,)
1073
+ ChainContextData = lambda r: (
1074
+ r.BacktrackClassDef,
1075
+ r.InputClassDef,
1076
+ r.LookAheadClassDef,
1077
+ )
1078
+
1079
+ def SetContextData(r, d):
1080
+ (r.ClassDef,) = d
1081
+
1082
+ def SetChainContextData(r, d):
1083
+ (r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
1084
+
1085
+ RuleData = lambda r: (r.Class,)
1086
+ ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
1087
+
1088
+ def SetRuleData(r, d):
1089
+ (r.Class,) = d
1090
+ (r.GlyphCount,) = (len(x) + 1 for x in d)
1091
+
1092
+ def ChainSetRuleData(r, d):
1093
+ (r.Backtrack, r.Input, r.LookAhead) = d
1094
+ (
1095
+ r.BacktrackGlyphCount,
1096
+ r.InputGlyphCount,
1097
+ r.LookAheadGlyphCount,
1098
+ ) = (len(d[0]), len(d[1]) + 1, len(d[2]))
1099
+
1100
+ elif Format == 3:
1101
+ Coverage = lambda r: r.Coverage[0]
1102
+ ChainCoverage = lambda r: r.InputCoverage[0]
1103
+ ContextData = None
1104
+ ChainContextData = None
1105
+ SetContextData = None
1106
+ SetChainContextData = None
1107
+ RuleData = lambda r: r.Coverage
1108
+ ChainRuleData = lambda r: (
1109
+ r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage
1110
+ )
1111
+
1112
+ def SetRuleData(r, d):
1113
+ (r.Coverage,) = d
1114
+ (r.GlyphCount,) = (len(x) for x in d)
1115
+
1116
+ def ChainSetRuleData(r, d):
1117
+ (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
1118
+ (
1119
+ r.BacktrackGlyphCount,
1120
+ r.InputGlyphCount,
1121
+ r.LookAheadGlyphCount,
1122
+ ) = (len(x) for x in d)
1123
+
1124
+ else:
1125
+ assert 0, "unknown format: %s" % Format
1126
+
1127
+ if Chain:
1128
+ self.Coverage = ChainCoverage
1129
+ self.ContextData = ChainContextData
1130
+ self.SetContextData = SetChainContextData
1131
+ self.RuleData = ChainRuleData
1132
+ self.SetRuleData = ChainSetRuleData
1133
+ else:
1134
+ self.Coverage = Coverage
1135
+ self.ContextData = ContextData
1136
+ self.SetContextData = SetContextData
1137
+ self.RuleData = RuleData
1138
+ self.SetRuleData = SetRuleData
1139
+
1140
+ if Format == 1:
1141
+ self.Rule = ChainTyp + "Rule"
1142
+ self.RuleCount = ChainTyp + "RuleCount"
1143
+ self.RuleSet = ChainTyp + "RuleSet"
1144
+ self.RuleSetCount = ChainTyp + "RuleSetCount"
1145
+ self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
1146
+ elif Format == 2:
1147
+ self.Rule = ChainTyp + "ClassRule"
1148
+ self.RuleCount = ChainTyp + "ClassRuleCount"
1149
+ self.RuleSet = ChainTyp + "ClassSet"
1150
+ self.RuleSetCount = ChainTyp + "ClassSetCount"
1151
+ self.Intersect = lambda glyphs, c, r: (
1152
+ c.intersect_class(glyphs, r)
1153
+ if c
1154
+ else (set(glyphs) if r == 0 else set())
1155
+ )
1156
+
1157
+ self.ClassDef = "InputClassDef" if Chain else "ClassDef"
1158
+ self.ClassDefIndex = 1 if Chain else 0
1159
+ self.Input = "Input" if Chain else "Class"
1160
+ elif Format == 3:
1161
+ self.Input = "InputCoverage" if Chain else "Coverage"
1162
+
1163
+ if self.Format not in [1, 2, 3]:
1164
+ return None # Don't shoot the messenger; let it go
1165
+ if not hasattr(self.__class__, "_subset__ContextHelpers"):
1166
+ self.__class__._subset__ContextHelpers = {}
1167
+ if self.Format not in self.__class__._subset__ContextHelpers:
1168
+ helper = ContextHelper(self.__class__, self.Format)
1169
+ self.__class__._subset__ContextHelpers[self.Format] = helper
1170
+ return self.__class__._subset__ContextHelpers[self.Format]
1171
+
1172
+
1173
+ @_add_method(otTables.ContextSubst, otTables.ChainContextSubst)
1174
+ def closure_glyphs(self, s, cur_glyphs):
1175
+ c = self.__subset_classify_context()
1176
+
1177
+ indices = c.Coverage(self).intersect(cur_glyphs)
1178
+ if not indices:
1179
+ return []
1180
+ cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs)
1181
+
1182
+ if self.Format == 1:
1183
+ ContextData = c.ContextData(self)
1184
+ rss = getattr(self, c.RuleSet)
1185
+ rssCount = getattr(self, c.RuleSetCount)
1186
+ for i in indices:
1187
+ if i >= rssCount or not rss[i]:
1188
+ continue
1189
+ for r in getattr(rss[i], c.Rule):
1190
+ if not r:
1191
+ continue
1192
+ if not all(
1193
+ all(c.Intersect(s.glyphs, cd, k) for k in klist)
1194
+ for cd, klist in zip(ContextData, c.RuleData(r))
1195
+ ):
1196
+ continue
1197
+ chaos = set()
1198
+ for ll in getattr(r, c.LookupRecord):
1199
+ if not ll:
1200
+ continue
1201
+ seqi = ll.SequenceIndex
1202
+ if seqi in chaos:
1203
+ # TODO Can we improve this?
1204
+ pos_glyphs = None
1205
+ else:
1206
+ if seqi == 0:
1207
+ pos_glyphs = frozenset([c.Coverage(self).glyphs[i]])
1208
+ else:
1209
+ pos_glyphs = frozenset([r.Input[seqi - 1]])
1210
+ lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1211
+ chaos.add(seqi)
1212
+ if lookup.may_have_non_1to1():
1213
+ chaos.update(range(seqi, len(r.Input) + 2))
1214
+ lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1215
+ elif self.Format == 2:
1216
+ ClassDef = getattr(self, c.ClassDef)
1217
+ indices = ClassDef.intersect(cur_glyphs)
1218
+ ContextData = c.ContextData(self)
1219
+ rss = getattr(self, c.RuleSet)
1220
+ rssCount = getattr(self, c.RuleSetCount)
1221
+ for i in indices:
1222
+ if i >= rssCount or not rss[i]:
1223
+ continue
1224
+ for r in getattr(rss[i], c.Rule):
1225
+ if not r:
1226
+ continue
1227
+ if not all(
1228
+ all(c.Intersect(s.glyphs, cd, k) for k in klist)
1229
+ for cd, klist in zip(ContextData, c.RuleData(r))
1230
+ ):
1231
+ continue
1232
+ chaos = set()
1233
+ for ll in getattr(r, c.LookupRecord):
1234
+ if not ll:
1235
+ continue
1236
+ seqi = ll.SequenceIndex
1237
+ if seqi in chaos:
1238
+ # TODO Can we improve this?
1239
+ pos_glyphs = None
1240
+ else:
1241
+ if seqi == 0:
1242
+ pos_glyphs = frozenset(
1243
+ ClassDef.intersect_class(cur_glyphs, i)
1244
+ )
1245
+ else:
1246
+ pos_glyphs = frozenset(
1247
+ ClassDef.intersect_class(
1248
+ s.glyphs, getattr(r, c.Input)[seqi - 1]
1249
+ )
1250
+ )
1251
+ lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1252
+ chaos.add(seqi)
1253
+ if lookup.may_have_non_1to1():
1254
+ chaos.update(range(seqi, len(getattr(r, c.Input)) + 2))
1255
+ lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1256
+ elif self.Format == 3:
1257
+ if not all(x is not None and x.intersect(s.glyphs) for x in c.RuleData(self)):
1258
+ return []
1259
+ r = self
1260
+ input_coverages = getattr(r, c.Input)
1261
+ chaos = set()
1262
+ for ll in getattr(r, c.LookupRecord):
1263
+ if not ll:
1264
+ continue
1265
+ seqi = ll.SequenceIndex
1266
+ if seqi in chaos:
1267
+ # TODO Can we improve this?
1268
+ pos_glyphs = None
1269
+ else:
1270
+ if seqi == 0:
1271
+ pos_glyphs = frozenset(cur_glyphs)
1272
+ else:
1273
+ pos_glyphs = frozenset(
1274
+ input_coverages[seqi].intersect_glyphs(s.glyphs)
1275
+ )
1276
+ lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
1277
+ chaos.add(seqi)
1278
+ if lookup.may_have_non_1to1():
1279
+ chaos.update(range(seqi, len(input_coverages) + 1))
1280
+ lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
1281
+ else:
1282
+ assert 0, "unknown format: %s" % self.Format
1283
+
1284
+
1285
+ @_add_method(
1286
+ otTables.ContextSubst,
1287
+ otTables.ContextPos,
1288
+ otTables.ChainContextSubst,
1289
+ otTables.ChainContextPos,
1290
+ )
1291
+ def subset_glyphs(self, s):
1292
+ c = self.__subset_classify_context()
1293
+
1294
+ if self.Format == 1:
1295
+ indices = self.Coverage.subset(s.glyphs)
1296
+ rss = getattr(self, c.RuleSet)
1297
+ rssCount = getattr(self, c.RuleSetCount)
1298
+ rss = [rss[i] for i in indices if i < rssCount]
1299
+ for rs in rss:
1300
+ if not rs:
1301
+ continue
1302
+ ss = getattr(rs, c.Rule)
1303
+ ss = [
1304
+ r
1305
+ for r in ss
1306
+ if r
1307
+ and all(all(g in s.glyphs for g in glist) for glist in c.RuleData(r))
1308
+ ]
1309
+ setattr(rs, c.Rule, ss)
1310
+ setattr(rs, c.RuleCount, len(ss))
1311
+ # Prune empty rulesets
1312
+ indices = [i for i, rs in enumerate(rss) if rs and getattr(rs, c.Rule)]
1313
+ self.Coverage.remap(indices)
1314
+ rss = _list_subset(rss, indices)
1315
+ setattr(self, c.RuleSet, rss)
1316
+ setattr(self, c.RuleSetCount, len(rss))
1317
+ return bool(rss)
1318
+ elif self.Format == 2:
1319
+ if not self.Coverage.subset(s.glyphs):
1320
+ return False
1321
+ ContextData = c.ContextData(self)
1322
+ klass_maps = [
1323
+ x.subset(s.glyphs, remap=True) if x else None for x in ContextData
1324
+ ]
1325
+
1326
+ # Keep rulesets for class numbers that survived.
1327
+ indices = klass_maps[c.ClassDefIndex]
1328
+ rss = getattr(self, c.RuleSet)
1329
+ rssCount = getattr(self, c.RuleSetCount)
1330
+ rss = [rss[i] for i in indices if i < rssCount]
1331
+ del rssCount
1332
+ # Delete, but not renumber, unreachable rulesets.
1333
+ indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs)
1334
+ rss = [rss if i in indices else None for i, rss in enumerate(rss)]
1335
+
1336
+ for rs in rss:
1337
+ if not rs:
1338
+ continue
1339
+ ss = getattr(rs, c.Rule)
1340
+ ss = [
1341
+ r
1342
+ for r in ss
1343
+ if r
1344
+ and all(
1345
+ all(k in klass_map for k in klist)
1346
+ for klass_map, klist in zip(klass_maps, c.RuleData(r))
1347
+ )
1348
+ ]
1349
+ setattr(rs, c.Rule, ss)
1350
+ setattr(rs, c.RuleCount, len(ss))
1351
+
1352
+ # Remap rule classes
1353
+ for r in ss:
1354
+ c.SetRuleData(
1355
+ r,
1356
+ [
1357
+ [klass_map.index(k) for k in klist]
1358
+ for klass_map, klist in zip(klass_maps, c.RuleData(r))
1359
+ ],
1360
+ )
1361
+
1362
+ # Prune empty rulesets
1363
+ rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss]
1364
+ while rss and rss[-1] is None:
1365
+ del rss[-1]
1366
+ setattr(self, c.RuleSet, rss)
1367
+ setattr(self, c.RuleSetCount, len(rss))
1368
+
1369
+ # TODO: We can do a second round of remapping class values based
1370
+ # on classes that are actually used in at least one rule. Right
1371
+ # now we subset classes to c.glyphs only. Or better, rewrite
1372
+ # the above to do that.
1373
+
1374
+ return bool(rss)
1375
+ elif self.Format == 3:
1376
+ return all(x is not None and x.subset(s.glyphs) for x in c.RuleData(self))
1377
+ else:
1378
+ assert 0, "unknown format: %s" % self.Format
1379
+
1380
+
1381
+ @_add_method(
1382
+ otTables.ContextSubst,
1383
+ otTables.ChainContextSubst,
1384
+ otTables.ContextPos,
1385
+ otTables.ChainContextPos,
1386
+ )
1387
+ def subset_lookups(self, lookup_indices):
1388
+ c = self.__subset_classify_context()
1389
+
1390
+ if self.Format in [1, 2]:
1391
+ for rs in getattr(self, c.RuleSet):
1392
+ if not rs:
1393
+ continue
1394
+ for r in getattr(rs, c.Rule):
1395
+ if not r:
1396
+ continue
1397
+ setattr(
1398
+ r,
1399
+ c.LookupRecord,
1400
+ [
1401
+ ll
1402
+ for ll in getattr(r, c.LookupRecord)
1403
+ if ll and ll.LookupListIndex in lookup_indices
1404
+ ],
1405
+ )
1406
+ for ll in getattr(r, c.LookupRecord):
1407
+ if not ll:
1408
+ continue
1409
+ ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
1410
+ elif self.Format == 3:
1411
+ setattr(
1412
+ self,
1413
+ c.LookupRecord,
1414
+ [
1415
+ ll
1416
+ for ll in getattr(self, c.LookupRecord)
1417
+ if ll and ll.LookupListIndex in lookup_indices
1418
+ ],
1419
+ )
1420
+ for ll in getattr(self, c.LookupRecord):
1421
+ if not ll:
1422
+ continue
1423
+ ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex)
1424
+ else:
1425
+ assert 0, "unknown format: %s" % self.Format
1426
+
1427
+
1428
+ @_add_method(
1429
+ otTables.ContextSubst,
1430
+ otTables.ChainContextSubst,
1431
+ otTables.ContextPos,
1432
+ otTables.ChainContextPos,
1433
+ )
1434
+ def collect_lookups(self):
1435
+ c = self.__subset_classify_context()
1436
+
1437
+ if self.Format in [1, 2]:
1438
+ return [
1439
+ ll.LookupListIndex
1440
+ for rs in getattr(self, c.RuleSet)
1441
+ if rs
1442
+ for r in getattr(rs, c.Rule)
1443
+ if r
1444
+ for ll in getattr(r, c.LookupRecord)
1445
+ if ll
1446
+ ]
1447
+ elif self.Format == 3:
1448
+ return [ll.LookupListIndex for ll in getattr(self, c.LookupRecord) if ll]
1449
+ else:
1450
+ assert 0, "unknown format: %s" % self.Format
1451
+
1452
+
1453
+ @_add_method(otTables.ExtensionSubst)
1454
+ def closure_glyphs(self, s, cur_glyphs):
1455
+ if self.Format == 1:
1456
+ self.ExtSubTable.closure_glyphs(s, cur_glyphs)
1457
+ else:
1458
+ assert 0, "unknown format: %s" % self.Format
1459
+
1460
+
1461
+ @_add_method(otTables.ExtensionSubst)
1462
+ def may_have_non_1to1(self):
1463
+ if self.Format == 1:
1464
+ return self.ExtSubTable.may_have_non_1to1()
1465
+ else:
1466
+ assert 0, "unknown format: %s" % self.Format
1467
+
1468
+
1469
+ @_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1470
+ def subset_glyphs(self, s):
1471
+ if self.Format == 1:
1472
+ return self.ExtSubTable.subset_glyphs(s)
1473
+ else:
1474
+ assert 0, "unknown format: %s" % self.Format
1475
+
1476
+
1477
+ @_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1478
+ def prune_post_subset(self, font, options):
1479
+ if self.Format == 1:
1480
+ return self.ExtSubTable.prune_post_subset(font, options)
1481
+ else:
1482
+ assert 0, "unknown format: %s" % self.Format
1483
+
1484
+
1485
+ @_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1486
+ def subset_lookups(self, lookup_indices):
1487
+ if self.Format == 1:
1488
+ return self.ExtSubTable.subset_lookups(lookup_indices)
1489
+ else:
1490
+ assert 0, "unknown format: %s" % self.Format
1491
+
1492
+
1493
+ @_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
1494
+ def collect_lookups(self):
1495
+ if self.Format == 1:
1496
+ return self.ExtSubTable.collect_lookups()
1497
+ else:
1498
+ assert 0, "unknown format: %s" % self.Format
1499
+
1500
+
1501
+ @_add_method(otTables.Lookup)
1502
+ def closure_glyphs(self, s, cur_glyphs=None):
1503
+ if cur_glyphs is None:
1504
+ cur_glyphs = frozenset(s.glyphs)
1505
+
1506
+ # Memoize
1507
+ key = id(self)
1508
+ doneLookups = s._doneLookups
1509
+ count, covered = doneLookups.get(key, (0, None))
1510
+ if count != len(s.glyphs):
1511
+ count, covered = doneLookups[key] = (len(s.glyphs), set())
1512
+ if cur_glyphs.issubset(covered):
1513
+ return
1514
+ covered.update(cur_glyphs)
1515
+
1516
+ for st in self.SubTable:
1517
+ if not st:
1518
+ continue
1519
+ st.closure_glyphs(s, cur_glyphs)
1520
+
1521
+
1522
+ @_add_method(otTables.Lookup)
1523
+ def subset_glyphs(self, s):
1524
+ self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)]
1525
+ self.SubTableCount = len(self.SubTable)
1526
+ if hasattr(self, "MarkFilteringSet") and self.MarkFilteringSet is not None:
1527
+ if self.MarkFilteringSet not in s.used_mark_sets:
1528
+ self.MarkFilteringSet = None
1529
+ self.LookupFlag &= ~0x10
1530
+ else:
1531
+ self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet)
1532
+ return bool(self.SubTableCount)
1533
+
1534
+
1535
+ @_add_method(otTables.Lookup)
1536
+ def prune_post_subset(self, font, options):
1537
+ ret = False
1538
+ for st in self.SubTable:
1539
+ if not st:
1540
+ continue
1541
+ if st.prune_post_subset(font, options):
1542
+ ret = True
1543
+ return ret
1544
+
1545
+
1546
+ @_add_method(otTables.Lookup)
1547
+ def subset_lookups(self, lookup_indices):
1548
+ for s in self.SubTable:
1549
+ s.subset_lookups(lookup_indices)
1550
+
1551
+
1552
+ @_add_method(otTables.Lookup)
1553
+ def collect_lookups(self):
1554
+ return sum((st.collect_lookups() for st in self.SubTable if st), [])
1555
+
1556
+
1557
+ @_add_method(otTables.Lookup)
1558
+ def may_have_non_1to1(self):
1559
+ return any(st.may_have_non_1to1() for st in self.SubTable if st)
1560
+
1561
+
1562
+ @_add_method(otTables.LookupList)
1563
+ def subset_glyphs(self, s):
1564
+ """Returns the indices of nonempty lookups."""
1565
+ return [i for i, l in enumerate(self.Lookup) if l and l.subset_glyphs(s)]
1566
+
1567
+
1568
+ @_add_method(otTables.LookupList)
1569
+ def prune_post_subset(self, font, options):
1570
+ ret = False
1571
+ for l in self.Lookup:
1572
+ if not l:
1573
+ continue
1574
+ if l.prune_post_subset(font, options):
1575
+ ret = True
1576
+ return ret
1577
+
1578
+
1579
+ @_add_method(otTables.LookupList)
1580
+ def subset_lookups(self, lookup_indices):
1581
+ self.ensureDecompiled()
1582
+ self.Lookup = [self.Lookup[i] for i in lookup_indices if i < self.LookupCount]
1583
+ self.LookupCount = len(self.Lookup)
1584
+ for l in self.Lookup:
1585
+ l.subset_lookups(lookup_indices)
1586
+
1587
+
1588
+ @_add_method(otTables.LookupList)
1589
+ def neuter_lookups(self, lookup_indices):
1590
+ """Sets lookups not in lookup_indices to None."""
1591
+ self.ensureDecompiled()
1592
+ self.Lookup = [
1593
+ l if i in lookup_indices else None for i, l in enumerate(self.Lookup)
1594
+ ]
1595
+
1596
+
1597
+ @_add_method(otTables.LookupList)
1598
+ def closure_lookups(self, lookup_indices):
1599
+ """Returns sorted index of all lookups reachable from lookup_indices."""
1600
+ lookup_indices = _uniq_sort(lookup_indices)
1601
+ recurse = lookup_indices
1602
+ while True:
1603
+ recurse_lookups = sum(
1604
+ (self.Lookup[i].collect_lookups() for i in recurse if i < self.LookupCount),
1605
+ [],
1606
+ )
1607
+ recurse_lookups = [
1608
+ l
1609
+ for l in recurse_lookups
1610
+ if l not in lookup_indices and l < self.LookupCount
1611
+ ]
1612
+ if not recurse_lookups:
1613
+ return _uniq_sort(lookup_indices)
1614
+ recurse_lookups = _uniq_sort(recurse_lookups)
1615
+ lookup_indices.extend(recurse_lookups)
1616
+ recurse = recurse_lookups
1617
+
1618
+
1619
+ @_add_method(otTables.Feature)
1620
+ def subset_lookups(self, lookup_indices):
1621
+ """ "Returns True if feature is non-empty afterwards."""
1622
+ self.LookupListIndex = [l for l in self.LookupListIndex if l in lookup_indices]
1623
+ # Now map them.
1624
+ self.LookupListIndex = [lookup_indices.index(l) for l in self.LookupListIndex]
1625
+ self.LookupCount = len(self.LookupListIndex)
1626
+ # keep 'size' feature even if it contains no lookups; but drop any other
1627
+ # empty feature (e.g. FeatureParams for stylistic set names)
1628
+ # https://github.com/fonttools/fonttools/issues/2324
1629
+ return self.LookupCount or isinstance(
1630
+ self.FeatureParams, otTables.FeatureParamsSize
1631
+ )
1632
+
1633
+
1634
+ @_add_method(otTables.FeatureList)
1635
+ def subset_lookups(self, lookup_indices):
1636
+ """Returns the indices of nonempty features."""
1637
+ # Note: Never ever drop feature 'pref', even if it's empty.
1638
+ # HarfBuzz chooses shaper for Khmer based on presence of this
1639
+ # feature. See thread at:
1640
+ # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html
1641
+ return [
1642
+ i
1643
+ for i, f in enumerate(self.FeatureRecord)
1644
+ if (f.Feature.subset_lookups(lookup_indices) or f.FeatureTag == "pref")
1645
+ ]
1646
+
1647
+
1648
+ @_add_method(otTables.FeatureList)
1649
+ def collect_lookups(self, feature_indices):
1650
+ return sum(
1651
+ (
1652
+ self.FeatureRecord[i].Feature.LookupListIndex
1653
+ for i in feature_indices
1654
+ if i < self.FeatureCount
1655
+ ),
1656
+ [],
1657
+ )
1658
+
1659
+
1660
+ @_add_method(otTables.FeatureList)
1661
+ def subset_features(self, feature_indices):
1662
+ self.ensureDecompiled()
1663
+ self.FeatureRecord = _list_subset(self.FeatureRecord, feature_indices)
1664
+ self.FeatureCount = len(self.FeatureRecord)
1665
+ return bool(self.FeatureCount)
1666
+
1667
+
1668
+ @_add_method(otTables.FeatureTableSubstitution)
1669
+ def subset_lookups(self, lookup_indices):
1670
+ """Returns the indices of nonempty features."""
1671
+ return [
1672
+ r.FeatureIndex
1673
+ for r in self.SubstitutionRecord
1674
+ if r.Feature.subset_lookups(lookup_indices)
1675
+ ]
1676
+
1677
+
1678
+ @_add_method(otTables.FeatureVariations)
1679
+ def subset_lookups(self, lookup_indices):
1680
+ """Returns the indices of nonempty features."""
1681
+ return sum(
1682
+ (
1683
+ f.FeatureTableSubstitution.subset_lookups(lookup_indices)
1684
+ for f in self.FeatureVariationRecord
1685
+ ),
1686
+ [],
1687
+ )
1688
+
1689
+
1690
+ @_add_method(otTables.FeatureVariations)
1691
+ def collect_lookups(self, feature_indices):
1692
+ return sum(
1693
+ (
1694
+ r.Feature.LookupListIndex
1695
+ for vr in self.FeatureVariationRecord
1696
+ for r in vr.FeatureTableSubstitution.SubstitutionRecord
1697
+ if r.FeatureIndex in feature_indices
1698
+ ),
1699
+ [],
1700
+ )
1701
+
1702
+
1703
+ @_add_method(otTables.FeatureTableSubstitution)
1704
+ def subset_features(self, feature_indices):
1705
+ self.ensureDecompiled()
1706
+ self.SubstitutionRecord = [
1707
+ r for r in self.SubstitutionRecord if r.FeatureIndex in feature_indices
1708
+ ]
1709
+ # remap feature indices
1710
+ for r in self.SubstitutionRecord:
1711
+ r.FeatureIndex = feature_indices.index(r.FeatureIndex)
1712
+ self.SubstitutionCount = len(self.SubstitutionRecord)
1713
+ return bool(self.SubstitutionCount)
1714
+
1715
+
1716
+ @_add_method(otTables.FeatureVariations)
1717
+ def subset_features(self, feature_indices):
1718
+ self.ensureDecompiled()
1719
+ for r in self.FeatureVariationRecord:
1720
+ r.FeatureTableSubstitution.subset_features(feature_indices)
1721
+ # Prune empty records at the end only
1722
+ # https://github.com/fonttools/fonttools/issues/1881
1723
+ while (
1724
+ self.FeatureVariationRecord
1725
+ and not self.FeatureVariationRecord[
1726
+ -1
1727
+ ].FeatureTableSubstitution.SubstitutionCount
1728
+ ):
1729
+ self.FeatureVariationRecord.pop()
1730
+ self.FeatureVariationCount = len(self.FeatureVariationRecord)
1731
+ return bool(self.FeatureVariationCount)
1732
+
1733
+
1734
+ @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1735
+ def subset_features(self, feature_indices):
1736
+ if self.ReqFeatureIndex in feature_indices:
1737
+ self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex)
1738
+ else:
1739
+ self.ReqFeatureIndex = 65535
1740
+ self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices]
1741
+ # Now map them.
1742
+ self.FeatureIndex = [
1743
+ feature_indices.index(f) for f in self.FeatureIndex if f in feature_indices
1744
+ ]
1745
+ self.FeatureCount = len(self.FeatureIndex)
1746
+ return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
1747
+
1748
+
1749
+ @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1750
+ def collect_features(self):
1751
+ feature_indices = self.FeatureIndex[:]
1752
+ if self.ReqFeatureIndex != 65535:
1753
+ feature_indices.append(self.ReqFeatureIndex)
1754
+ return _uniq_sort(feature_indices)
1755
+
1756
+
1757
+ @_add_method(otTables.Script)
1758
+ def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False):
1759
+ if (
1760
+ self.DefaultLangSys
1761
+ and not self.DefaultLangSys.subset_features(feature_indices)
1762
+ and not keepEmptyDefaultLangSys
1763
+ ):
1764
+ self.DefaultLangSys = None
1765
+ self.LangSysRecord = [
1766
+ l for l in self.LangSysRecord if l.LangSys.subset_features(feature_indices)
1767
+ ]
1768
+ self.LangSysCount = len(self.LangSysRecord)
1769
+ return bool(self.LangSysCount or self.DefaultLangSys)
1770
+
1771
+
1772
+ @_add_method(otTables.Script)
1773
+ def collect_features(self):
1774
+ feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
1775
+ if self.DefaultLangSys:
1776
+ feature_indices.append(self.DefaultLangSys.collect_features())
1777
+ return _uniq_sort(sum(feature_indices, []))
1778
+
1779
+
1780
+ @_add_method(otTables.ScriptList)
1781
+ def subset_features(self, feature_indices, retain_empty):
1782
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32
1783
+ self.ScriptRecord = [
1784
+ s
1785
+ for s in self.ScriptRecord
1786
+ if s.Script.subset_features(feature_indices, s.ScriptTag == "DFLT")
1787
+ or retain_empty
1788
+ ]
1789
+ self.ScriptCount = len(self.ScriptRecord)
1790
+ return bool(self.ScriptCount)
1791
+
1792
+
1793
+ @_add_method(otTables.ScriptList)
1794
+ def collect_features(self):
1795
+ return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), []))
1796
+
1797
+
1798
+ # CBLC will inherit it
1799
+ @_add_method(ttLib.getTableClass("EBLC"))
1800
+ def subset_glyphs(self, s):
1801
+ for strike in self.strikes:
1802
+ for indexSubTable in strike.indexSubTables:
1803
+ indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs]
1804
+ strike.indexSubTables = [i for i in strike.indexSubTables if i.names]
1805
+ self.strikes = [s for s in self.strikes if s.indexSubTables]
1806
+
1807
+ return True
1808
+
1809
+
1810
+ # CBDT will inherit it
1811
+ @_add_method(ttLib.getTableClass("EBDT"))
1812
+ def subset_glyphs(self, s):
1813
+ strikeData = [
1814
+ {g: strike[g] for g in s.glyphs if g in strike} for strike in self.strikeData
1815
+ ]
1816
+ # Prune empty strikes
1817
+ # https://github.com/fonttools/fonttools/issues/1633
1818
+ self.strikeData = [strike for strike in strikeData if strike]
1819
+ return True
1820
+
1821
+
1822
+ @_add_method(ttLib.getTableClass("sbix"))
1823
+ def subset_glyphs(self, s):
1824
+ for strike in self.strikes.values():
1825
+ strike.glyphs = {g: strike.glyphs[g] for g in s.glyphs if g in strike.glyphs}
1826
+
1827
+ return True
1828
+
1829
+
1830
+ @_add_method(ttLib.getTableClass("GSUB"))
1831
+ def closure_glyphs(self, s):
1832
+ s.table = self.table
1833
+ if self.table.ScriptList:
1834
+ feature_indices = self.table.ScriptList.collect_features()
1835
+ else:
1836
+ feature_indices = []
1837
+ if self.table.FeatureList:
1838
+ lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
1839
+ else:
1840
+ lookup_indices = []
1841
+ if getattr(self.table, "FeatureVariations", None):
1842
+ lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices)
1843
+ lookup_indices = _uniq_sort(lookup_indices)
1844
+ if self.table.LookupList:
1845
+ s._doneLookups = {}
1846
+ while True:
1847
+ orig_glyphs = frozenset(s.glyphs)
1848
+ for i in lookup_indices:
1849
+ if i >= self.table.LookupList.LookupCount:
1850
+ continue
1851
+ if not self.table.LookupList.Lookup[i]:
1852
+ continue
1853
+ self.table.LookupList.Lookup[i].closure_glyphs(s)
1854
+ if orig_glyphs == s.glyphs:
1855
+ break
1856
+ del s._doneLookups
1857
+ del s.table
1858
+
1859
+
1860
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1861
+ def subset_glyphs(self, s):
1862
+ s.glyphs = s.glyphs_gsubed
1863
+ if self.table.LookupList:
1864
+ lookup_indices = self.table.LookupList.subset_glyphs(s)
1865
+ else:
1866
+ lookup_indices = []
1867
+ self.subset_lookups(lookup_indices)
1868
+ return True
1869
+
1870
+
1871
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1872
+ def retain_empty_scripts(self):
1873
+ # https://github.com/fonttools/fonttools/issues/518
1874
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15
1875
+ return self.__class__ == ttLib.getTableClass("GSUB")
1876
+
1877
+
1878
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1879
+ def subset_lookups(self, lookup_indices):
1880
+ """Retains specified lookups, then removes empty features, language
1881
+ systems, and scripts."""
1882
+ if self.table.LookupList:
1883
+ self.table.LookupList.subset_lookups(lookup_indices)
1884
+ if self.table.FeatureList:
1885
+ feature_indices = self.table.FeatureList.subset_lookups(lookup_indices)
1886
+ else:
1887
+ feature_indices = []
1888
+ if getattr(self.table, "FeatureVariations", None):
1889
+ feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices)
1890
+ feature_indices = _uniq_sort(feature_indices)
1891
+ if self.table.FeatureList:
1892
+ self.table.FeatureList.subset_features(feature_indices)
1893
+ if getattr(self.table, "FeatureVariations", None):
1894
+ self.table.FeatureVariations.subset_features(feature_indices)
1895
+ if self.table.ScriptList:
1896
+ self.table.ScriptList.subset_features(
1897
+ feature_indices, self.retain_empty_scripts()
1898
+ )
1899
+
1900
+
1901
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1902
+ def neuter_lookups(self, lookup_indices):
1903
+ """Sets lookups not in lookup_indices to None."""
1904
+ if self.table.LookupList:
1905
+ self.table.LookupList.neuter_lookups(lookup_indices)
1906
+
1907
+
1908
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1909
+ def prune_lookups(self, remap=True):
1910
+ """Remove (default) or neuter unreferenced lookups"""
1911
+ if self.table.ScriptList:
1912
+ feature_indices = self.table.ScriptList.collect_features()
1913
+ else:
1914
+ feature_indices = []
1915
+ if self.table.FeatureList:
1916
+ lookup_indices = self.table.FeatureList.collect_lookups(feature_indices)
1917
+ else:
1918
+ lookup_indices = []
1919
+ if getattr(self.table, "FeatureVariations", None):
1920
+ lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices)
1921
+ lookup_indices = _uniq_sort(lookup_indices)
1922
+ if self.table.LookupList:
1923
+ lookup_indices = self.table.LookupList.closure_lookups(lookup_indices)
1924
+ else:
1925
+ lookup_indices = []
1926
+ if remap:
1927
+ self.subset_lookups(lookup_indices)
1928
+ else:
1929
+ self.neuter_lookups(lookup_indices)
1930
+
1931
+
1932
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1933
+ def subset_feature_tags(self, feature_tags):
1934
+ if self.table.FeatureList:
1935
+ feature_indices = [
1936
+ i
1937
+ for i, f in enumerate(self.table.FeatureList.FeatureRecord)
1938
+ if f.FeatureTag in feature_tags
1939
+ ]
1940
+ self.table.FeatureList.subset_features(feature_indices)
1941
+ if getattr(self.table, "FeatureVariations", None):
1942
+ self.table.FeatureVariations.subset_features(feature_indices)
1943
+ else:
1944
+ feature_indices = []
1945
+ if self.table.ScriptList:
1946
+ self.table.ScriptList.subset_features(
1947
+ feature_indices, self.retain_empty_scripts()
1948
+ )
1949
+
1950
+
1951
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1952
+ def subset_script_tags(self, tags):
1953
+ langsys = {}
1954
+ script_tags = set()
1955
+ for tag in tags:
1956
+ script_tag, lang_tag = tag.split(".") if "." in tag else (tag, "*")
1957
+ script_tags.add(script_tag.ljust(4))
1958
+ langsys.setdefault(script_tag, set()).add(lang_tag.ljust(4))
1959
+
1960
+ if self.table.ScriptList:
1961
+ self.table.ScriptList.ScriptRecord = [
1962
+ s for s in self.table.ScriptList.ScriptRecord if s.ScriptTag in script_tags
1963
+ ]
1964
+ self.table.ScriptList.ScriptCount = len(self.table.ScriptList.ScriptRecord)
1965
+
1966
+ for record in self.table.ScriptList.ScriptRecord:
1967
+ if record.ScriptTag in langsys and "* " not in langsys[record.ScriptTag]:
1968
+ record.Script.LangSysRecord = [
1969
+ l
1970
+ for l in record.Script.LangSysRecord
1971
+ if l.LangSysTag in langsys[record.ScriptTag]
1972
+ ]
1973
+ record.Script.LangSysCount = len(record.Script.LangSysRecord)
1974
+ if "dflt" not in langsys[record.ScriptTag]:
1975
+ record.Script.DefaultLangSys = None
1976
+
1977
+
1978
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1979
+ def prune_features(self):
1980
+ """Remove unreferenced features"""
1981
+ if self.table.ScriptList:
1982
+ feature_indices = self.table.ScriptList.collect_features()
1983
+ else:
1984
+ feature_indices = []
1985
+ if self.table.FeatureList:
1986
+ self.table.FeatureList.subset_features(feature_indices)
1987
+ if getattr(self.table, "FeatureVariations", None):
1988
+ self.table.FeatureVariations.subset_features(feature_indices)
1989
+ if self.table.ScriptList:
1990
+ self.table.ScriptList.subset_features(
1991
+ feature_indices, self.retain_empty_scripts()
1992
+ )
1993
+
1994
+
1995
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1996
+ def prune_pre_subset(self, font, options):
1997
+ # Drop undesired features
1998
+ if "*" not in options.layout_scripts:
1999
+ self.subset_script_tags(options.layout_scripts)
2000
+ if "*" not in options.layout_features:
2001
+ self.subset_feature_tags(options.layout_features)
2002
+ # Neuter unreferenced lookups
2003
+ self.prune_lookups(remap=False)
2004
+ return True
2005
+
2006
+
2007
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2008
+ def remove_redundant_langsys(self):
2009
+ table = self.table
2010
+ if not table.ScriptList or not table.FeatureList:
2011
+ return
2012
+
2013
+ features = table.FeatureList.FeatureRecord
2014
+
2015
+ for s in table.ScriptList.ScriptRecord:
2016
+ d = s.Script.DefaultLangSys
2017
+ if not d:
2018
+ continue
2019
+ for lr in s.Script.LangSysRecord[:]:
2020
+ l = lr.LangSys
2021
+ # Compare d and l
2022
+ if len(d.FeatureIndex) != len(l.FeatureIndex):
2023
+ continue
2024
+ if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535):
2025
+ continue
2026
+
2027
+ if d.ReqFeatureIndex != 65535:
2028
+ if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]:
2029
+ continue
2030
+
2031
+ for i in range(len(d.FeatureIndex)):
2032
+ if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]:
2033
+ break
2034
+ else:
2035
+ # LangSys and default are equal; delete LangSys
2036
+ s.Script.LangSysRecord.remove(lr)
2037
+
2038
+
2039
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2040
+ def prune_post_subset(self, font, options):
2041
+ table = self.table
2042
+
2043
+ self.prune_lookups() # XXX Is this actually needed?!
2044
+
2045
+ if table.LookupList:
2046
+ table.LookupList.prune_post_subset(font, options)
2047
+ # XXX Next two lines disabled because OTS is stupid and
2048
+ # doesn't like NULL offsets here.
2049
+ # if not table.LookupList.Lookup:
2050
+ # table.LookupList = None
2051
+
2052
+ if not table.LookupList:
2053
+ table.FeatureList = None
2054
+
2055
+ if table.FeatureList:
2056
+ self.remove_redundant_langsys()
2057
+ # Remove unreferenced features
2058
+ self.prune_features()
2059
+
2060
+ # XXX Next two lines disabled because OTS is stupid and
2061
+ # doesn't like NULL offsets here.
2062
+ # if table.FeatureList and not table.FeatureList.FeatureRecord:
2063
+ # table.FeatureList = None
2064
+
2065
+ # Never drop scripts themselves as them just being available
2066
+ # holds semantic significance.
2067
+ # XXX Next two lines disabled because OTS is stupid and
2068
+ # doesn't like NULL offsets here.
2069
+ # if table.ScriptList and not table.ScriptList.ScriptRecord:
2070
+ # table.ScriptList = None
2071
+
2072
+ if hasattr(table, "FeatureVariations"):
2073
+ # drop FeatureVariations if there are no features to substitute
2074
+ if table.FeatureVariations and not (
2075
+ table.FeatureList and table.FeatureVariations.FeatureVariationRecord
2076
+ ):
2077
+ table.FeatureVariations = None
2078
+
2079
+ # downgrade table version if there are no FeatureVariations
2080
+ if not table.FeatureVariations and table.Version == 0x00010001:
2081
+ table.Version = 0x00010000
2082
+
2083
+ return True
2084
+
2085
+
2086
+ @_add_method(ttLib.getTableClass("GDEF"))
2087
+ def subset_glyphs(self, s):
2088
+ glyphs = s.glyphs_gsubed
2089
+ table = self.table
2090
+ if table.LigCaretList:
2091
+ indices = table.LigCaretList.Coverage.subset(glyphs)
2092
+ table.LigCaretList.LigGlyph = _list_subset(table.LigCaretList.LigGlyph, indices)
2093
+ table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph)
2094
+ if table.MarkAttachClassDef:
2095
+ table.MarkAttachClassDef.classDefs = {
2096
+ g: v for g, v in table.MarkAttachClassDef.classDefs.items() if g in glyphs
2097
+ }
2098
+ if table.GlyphClassDef:
2099
+ table.GlyphClassDef.classDefs = {
2100
+ g: v for g, v in table.GlyphClassDef.classDefs.items() if g in glyphs
2101
+ }
2102
+ if table.AttachList:
2103
+ indices = table.AttachList.Coverage.subset(glyphs)
2104
+ GlyphCount = table.AttachList.GlyphCount
2105
+ table.AttachList.AttachPoint = [
2106
+ table.AttachList.AttachPoint[i] for i in indices if i < GlyphCount
2107
+ ]
2108
+ table.AttachList.GlyphCount = len(table.AttachList.AttachPoint)
2109
+ if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef:
2110
+ markGlyphSets = table.MarkGlyphSetsDef
2111
+ for coverage in markGlyphSets.Coverage:
2112
+ if coverage:
2113
+ coverage.subset(glyphs)
2114
+
2115
+ s.used_mark_sets = [i for i, c in enumerate(markGlyphSets.Coverage) if c.glyphs]
2116
+ markGlyphSets.Coverage = [c for c in markGlyphSets.Coverage if c.glyphs]
2117
+
2118
+ return True
2119
+
2120
+
2121
+ def _pruneGDEF(font):
2122
+ if "GDEF" not in font:
2123
+ return
2124
+ gdef = font["GDEF"]
2125
+ table = gdef.table
2126
+ if not hasattr(table, "VarStore"):
2127
+ return
2128
+
2129
+ store = table.VarStore
2130
+
2131
+ usedVarIdxes = set()
2132
+
2133
+ # Collect.
2134
+ table.collect_device_varidxes(usedVarIdxes)
2135
+ if "GPOS" in font:
2136
+ font["GPOS"].table.collect_device_varidxes(usedVarIdxes)
2137
+
2138
+ # Subset.
2139
+ varidx_map = store.subset_varidxes(usedVarIdxes)
2140
+
2141
+ # Map.
2142
+ table.remap_device_varidxes(varidx_map)
2143
+ if "GPOS" in font:
2144
+ font["GPOS"].table.remap_device_varidxes(varidx_map)
2145
+
2146
+
2147
+ @_add_method(ttLib.getTableClass("GDEF"))
2148
+ def prune_post_subset(self, font, options):
2149
+ table = self.table
2150
+ # XXX check these against OTS
2151
+ if table.LigCaretList and not table.LigCaretList.LigGlyphCount:
2152
+ table.LigCaretList = None
2153
+ if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs:
2154
+ table.MarkAttachClassDef = None
2155
+ if table.GlyphClassDef and not table.GlyphClassDef.classDefs:
2156
+ table.GlyphClassDef = None
2157
+ if table.AttachList and not table.AttachList.GlyphCount:
2158
+ table.AttachList = None
2159
+ if hasattr(table, "VarStore"):
2160
+ _pruneGDEF(font)
2161
+ if table.VarStore.VarDataCount == 0:
2162
+ if table.Version == 0x00010003:
2163
+ table.Version = 0x00010002
2164
+ if (
2165
+ not hasattr(table, "MarkGlyphSetsDef")
2166
+ or not table.MarkGlyphSetsDef
2167
+ or not table.MarkGlyphSetsDef.Coverage
2168
+ ):
2169
+ table.MarkGlyphSetsDef = None
2170
+ if table.Version == 0x00010002:
2171
+ table.Version = 0x00010000
2172
+ return bool(
2173
+ table.LigCaretList
2174
+ or table.MarkAttachClassDef
2175
+ or table.GlyphClassDef
2176
+ or table.AttachList
2177
+ or (table.Version >= 0x00010002 and table.MarkGlyphSetsDef)
2178
+ or (table.Version >= 0x00010003 and table.VarStore)
2179
+ )
2180
+
2181
+
2182
+ @_add_method(ttLib.getTableClass("kern"))
2183
+ def prune_pre_subset(self, font, options):
2184
+ # Prune unknown kern table types
2185
+ self.kernTables = [t for t in self.kernTables if hasattr(t, "kernTable")]
2186
+ return bool(self.kernTables)
2187
+
2188
+
2189
+ @_add_method(ttLib.getTableClass("kern"))
2190
+ def subset_glyphs(self, s):
2191
+ glyphs = s.glyphs_gsubed
2192
+ for t in self.kernTables:
2193
+ t.kernTable = {
2194
+ (a, b): v
2195
+ for (a, b), v in t.kernTable.items()
2196
+ if a in glyphs and b in glyphs
2197
+ }
2198
+ self.kernTables = [t for t in self.kernTables if t.kernTable]
2199
+ return bool(self.kernTables)
2200
+
2201
+
2202
+ @_add_method(ttLib.getTableClass("vmtx"))
2203
+ def subset_glyphs(self, s):
2204
+ self.metrics = _dict_subset(self.metrics, s.glyphs)
2205
+ for g in s.glyphs_emptied:
2206
+ self.metrics[g] = (0, 0)
2207
+ return bool(self.metrics)
2208
+
2209
+
2210
+ @_add_method(ttLib.getTableClass("hmtx"))
2211
+ def subset_glyphs(self, s):
2212
+ self.metrics = _dict_subset(self.metrics, s.glyphs)
2213
+ for g in s.glyphs_emptied:
2214
+ self.metrics[g] = (0, 0)
2215
+ return True # Required table
2216
+
2217
+
2218
+ @_add_method(ttLib.getTableClass("hdmx"))
2219
+ def subset_glyphs(self, s):
2220
+ self.hdmx = {sz: _dict_subset(l, s.glyphs) for sz, l in self.hdmx.items()}
2221
+ for sz in self.hdmx:
2222
+ for g in s.glyphs_emptied:
2223
+ self.hdmx[sz][g] = 0
2224
+ return bool(self.hdmx)
2225
+
2226
+
2227
+ @_add_method(ttLib.getTableClass("ankr"))
2228
+ def subset_glyphs(self, s):
2229
+ table = self.table.AnchorPoints
2230
+ assert table.Format == 0, "unknown 'ankr' format %s" % table.Format
2231
+ table.Anchors = {
2232
+ glyph: table.Anchors[glyph] for glyph in s.glyphs if glyph in table.Anchors
2233
+ }
2234
+ return len(table.Anchors) > 0
2235
+
2236
+
2237
+ @_add_method(ttLib.getTableClass("bsln"))
2238
+ def closure_glyphs(self, s):
2239
+ table = self.table.Baseline
2240
+ if table.Format in (2, 3):
2241
+ s.glyphs.add(table.StandardGlyph)
2242
+
2243
+
2244
+ @_add_method(ttLib.getTableClass("bsln"))
2245
+ def subset_glyphs(self, s):
2246
+ table = self.table.Baseline
2247
+ if table.Format in (1, 3):
2248
+ baselines = {
2249
+ glyph: table.BaselineValues.get(glyph, table.DefaultBaseline)
2250
+ for glyph in s.glyphs
2251
+ }
2252
+ if len(baselines) > 0:
2253
+ mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0]
2254
+ table.DefaultBaseline = mostCommon
2255
+ baselines = {glyph: b for glyph, b in baselines.items() if b != mostCommon}
2256
+ if len(baselines) > 0:
2257
+ table.BaselineValues = baselines
2258
+ else:
2259
+ table.Format = {1: 0, 3: 2}[table.Format]
2260
+ del table.BaselineValues
2261
+ return True
2262
+
2263
+
2264
+ @_add_method(ttLib.getTableClass("lcar"))
2265
+ def subset_glyphs(self, s):
2266
+ table = self.table.LigatureCarets
2267
+ if table.Format in (0, 1):
2268
+ table.Carets = {
2269
+ glyph: table.Carets[glyph] for glyph in s.glyphs if glyph in table.Carets
2270
+ }
2271
+ return len(table.Carets) > 0
2272
+ else:
2273
+ assert False, "unknown 'lcar' format %s" % table.Format
2274
+
2275
+
2276
+ @_add_method(ttLib.getTableClass("gvar"))
2277
+ def prune_pre_subset(self, font, options):
2278
+ if options.notdef_glyph and not options.notdef_outline:
2279
+ self.variations[font.glyphOrder[0]] = []
2280
+ return True
2281
+
2282
+
2283
+ @_add_method(ttLib.getTableClass("gvar"))
2284
+ def subset_glyphs(self, s):
2285
+ self.variations = _dict_subset(self.variations, s.glyphs)
2286
+ self.glyphCount = len(self.variations)
2287
+ return bool(self.variations)
2288
+
2289
+
2290
+ def _remap_index_map(s, varidx_map, table_map):
2291
+ map_ = {k: varidx_map[v] for k, v in table_map.mapping.items()}
2292
+ # Emptied glyphs are remapped to:
2293
+ # if GID <= last retained GID, 0/0: delta set for 0/0 is expected to exist & zeros compress well
2294
+ # if GID > last retained GID, major/minor of the last retained glyph: will be optimized out by table compiler
2295
+ last_idx = varidx_map[table_map.mapping[s.last_retained_glyph]]
2296
+ for g, i in s.reverseEmptiedGlyphMap.items():
2297
+ map_[g] = last_idx if i > s.last_retained_order else 0
2298
+ return map_
2299
+
2300
+
2301
+ @_add_method(ttLib.getTableClass("HVAR"))
2302
+ def subset_glyphs(self, s):
2303
+ table = self.table
2304
+
2305
+ used = set()
2306
+ advIdxes_ = set()
2307
+ retainAdvMap = False
2308
+
2309
+ if table.AdvWidthMap:
2310
+ table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs)
2311
+ used.update(table.AdvWidthMap.mapping.values())
2312
+ else:
2313
+ used.update(s.reverseOrigGlyphMap.values())
2314
+ advIdxes_ = used.copy()
2315
+ retainAdvMap = s.options.retain_gids
2316
+
2317
+ if table.LsbMap:
2318
+ table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs)
2319
+ used.update(table.LsbMap.mapping.values())
2320
+ if table.RsbMap:
2321
+ table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs)
2322
+ used.update(table.RsbMap.mapping.values())
2323
+
2324
+ varidx_map = table.VarStore.subset_varidxes(
2325
+ used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2326
+ )
2327
+
2328
+ if table.AdvWidthMap:
2329
+ table.AdvWidthMap.mapping = _remap_index_map(s, varidx_map, table.AdvWidthMap)
2330
+ if table.LsbMap:
2331
+ table.LsbMap.mapping = _remap_index_map(s, varidx_map, table.LsbMap)
2332
+ if table.RsbMap:
2333
+ table.RsbMap.mapping = _remap_index_map(s, varidx_map, table.RsbMap)
2334
+
2335
+ # TODO Return emptiness...
2336
+ return True
2337
+
2338
+
2339
+ @_add_method(ttLib.getTableClass("VVAR"))
2340
+ def subset_glyphs(self, s):
2341
+ table = self.table
2342
+
2343
+ used = set()
2344
+ advIdxes_ = set()
2345
+ retainAdvMap = False
2346
+
2347
+ if table.AdvHeightMap:
2348
+ table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs)
2349
+ used.update(table.AdvHeightMap.mapping.values())
2350
+ else:
2351
+ used.update(s.reverseOrigGlyphMap.values())
2352
+ advIdxes_ = used.copy()
2353
+ retainAdvMap = s.options.retain_gids
2354
+
2355
+ if table.TsbMap:
2356
+ table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs)
2357
+ used.update(table.TsbMap.mapping.values())
2358
+ if table.BsbMap:
2359
+ table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs)
2360
+ used.update(table.BsbMap.mapping.values())
2361
+ if table.VOrgMap:
2362
+ table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs)
2363
+ used.update(table.VOrgMap.mapping.values())
2364
+
2365
+ varidx_map = table.VarStore.subset_varidxes(
2366
+ used, retainFirstMap=retainAdvMap, advIdxes=advIdxes_
2367
+ )
2368
+
2369
+ if table.AdvHeightMap:
2370
+ table.AdvHeightMap.mapping = _remap_index_map(s, varidx_map, table.AdvHeightMap)
2371
+ if table.TsbMap:
2372
+ table.TsbMap.mapping = _remap_index_map(s, varidx_map, table.TsbMap)
2373
+ if table.BsbMap:
2374
+ table.BsbMap.mapping = _remap_index_map(s, varidx_map, table.BsbMap)
2375
+ if table.VOrgMap:
2376
+ table.VOrgMap.mapping = _remap_index_map(s, varidx_map, table.VOrgMap)
2377
+
2378
+ # TODO Return emptiness...
2379
+ return True
2380
+
2381
+
2382
+ @_add_method(ttLib.getTableClass("VORG"))
2383
+ def subset_glyphs(self, s):
2384
+ self.VOriginRecords = {
2385
+ g: v for g, v in self.VOriginRecords.items() if g in s.glyphs
2386
+ }
2387
+ self.numVertOriginYMetrics = len(self.VOriginRecords)
2388
+ return True # Never drop; has default metrics
2389
+
2390
+
2391
+ @_add_method(ttLib.getTableClass("opbd"))
2392
+ def subset_glyphs(self, s):
2393
+ table = self.table.OpticalBounds
2394
+ if table.Format == 0:
2395
+ table.OpticalBoundsDeltas = {
2396
+ glyph: table.OpticalBoundsDeltas[glyph]
2397
+ for glyph in s.glyphs
2398
+ if glyph in table.OpticalBoundsDeltas
2399
+ }
2400
+ return len(table.OpticalBoundsDeltas) > 0
2401
+ elif table.Format == 1:
2402
+ table.OpticalBoundsPoints = {
2403
+ glyph: table.OpticalBoundsPoints[glyph]
2404
+ for glyph in s.glyphs
2405
+ if glyph in table.OpticalBoundsPoints
2406
+ }
2407
+ return len(table.OpticalBoundsPoints) > 0
2408
+ else:
2409
+ assert False, "unknown 'opbd' format %s" % table.Format
2410
+
2411
+
2412
+ @_add_method(ttLib.getTableClass("post"))
2413
+ def prune_pre_subset(self, font, options):
2414
+ if not options.glyph_names:
2415
+ self.formatType = 3.0
2416
+ return True # Required table
2417
+
2418
+
2419
+ @_add_method(ttLib.getTableClass("post"))
2420
+ def subset_glyphs(self, s):
2421
+ self.extraNames = [] # This seems to do it
2422
+ return True # Required table
2423
+
2424
+
2425
+ @_add_method(ttLib.getTableClass("prop"))
2426
+ def subset_glyphs(self, s):
2427
+ prop = self.table.GlyphProperties
2428
+ if prop.Format == 0:
2429
+ return prop.DefaultProperties != 0
2430
+ elif prop.Format == 1:
2431
+ prop.Properties = {
2432
+ g: prop.Properties.get(g, prop.DefaultProperties) for g in s.glyphs
2433
+ }
2434
+ mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0]
2435
+ prop.DefaultProperties = mostCommon
2436
+ prop.Properties = {
2437
+ g: prop for g, prop in prop.Properties.items() if prop != mostCommon
2438
+ }
2439
+ if len(prop.Properties) == 0:
2440
+ del prop.Properties
2441
+ prop.Format = 0
2442
+ return prop.DefaultProperties != 0
2443
+ return True
2444
+ else:
2445
+ assert False, "unknown 'prop' format %s" % prop.Format
2446
+
2447
+
2448
+ def _paint_glyph_names(paint, colr):
2449
+ result = set()
2450
+
2451
+ def callback(paint):
2452
+ if paint.Format in {
2453
+ otTables.PaintFormat.PaintGlyph,
2454
+ otTables.PaintFormat.PaintColrGlyph,
2455
+ }:
2456
+ result.add(paint.Glyph)
2457
+
2458
+ paint.traverse(colr, callback)
2459
+ return result
2460
+
2461
+
2462
+ @_add_method(ttLib.getTableClass("COLR"))
2463
+ def closure_glyphs(self, s):
2464
+ if self.version > 0:
2465
+ # on decompiling COLRv1, we only keep around the raw otTables
2466
+ # but for subsetting we need dicts with fully decompiled layers;
2467
+ # we store them temporarily in the C_O_L_R_ instance and delete
2468
+ # them after we have finished subsetting.
2469
+ self.ColorLayers = self._decompileColorLayersV0(self.table)
2470
+ self.ColorLayersV1 = {
2471
+ rec.BaseGlyph: rec.Paint
2472
+ for rec in self.table.BaseGlyphList.BaseGlyphPaintRecord
2473
+ }
2474
+
2475
+ decompose = s.glyphs
2476
+ while decompose:
2477
+ layers = set()
2478
+ for g in decompose:
2479
+ for layer in self.ColorLayers.get(g, []):
2480
+ layers.add(layer.name)
2481
+
2482
+ if self.version > 0:
2483
+ paint = self.ColorLayersV1.get(g)
2484
+ if paint is not None:
2485
+ layers.update(_paint_glyph_names(paint, self.table))
2486
+
2487
+ layers -= s.glyphs
2488
+ s.glyphs.update(layers)
2489
+ decompose = layers
2490
+
2491
+
2492
+ @_add_method(ttLib.getTableClass("COLR"))
2493
+ def subset_glyphs(self, s):
2494
+ from fontTools.colorLib.unbuilder import unbuildColrV1
2495
+ from fontTools.colorLib.builder import buildColrV1, populateCOLRv0
2496
+
2497
+ # only include glyphs after COLR closure, which in turn comes after cmap and GSUB
2498
+ # closure, but importantly before glyf/CFF closures. COLR layers can refer to
2499
+ # composite glyphs, and that's ok, since glyf/CFF closures happen after COLR closure
2500
+ # and take care of those. If we also included glyphs resulting from glyf/CFF closures
2501
+ # when deciding which COLR base glyphs to retain, then we may end up with a situation
2502
+ # whereby a COLR base glyph is kept, not because directly requested (cmap)
2503
+ # or substituted (GSUB) or referenced by another COLRv1 PaintColrGlyph, but because
2504
+ # it corresponds to (has same GID as) a non-COLR glyph that happens to be used as a
2505
+ # component in glyf or CFF table. Best case scenario we retain more glyphs than
2506
+ # required; worst case we retain incomplete COLR records that try to reference
2507
+ # glyphs that are no longer in the final subset font.
2508
+ # https://github.com/fonttools/fonttools/issues/2461
2509
+ s.glyphs = s.glyphs_colred
2510
+
2511
+ self.ColorLayers = {
2512
+ g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers
2513
+ }
2514
+ if self.version == 0:
2515
+ return bool(self.ColorLayers)
2516
+
2517
+ colorGlyphsV1 = unbuildColrV1(self.table.LayerList, self.table.BaseGlyphList)
2518
+ self.table.LayerList, self.table.BaseGlyphList = buildColrV1(
2519
+ {g: colorGlyphsV1[g] for g in colorGlyphsV1 if g in s.glyphs}
2520
+ )
2521
+ del self.ColorLayersV1
2522
+
2523
+ if self.table.ClipList is not None:
2524
+ clips = self.table.ClipList.clips
2525
+ self.table.ClipList.clips = {g: clips[g] for g in clips if g in s.glyphs}
2526
+
2527
+ layersV0 = self.ColorLayers
2528
+ if not self.table.BaseGlyphList.BaseGlyphPaintRecord:
2529
+ # no more COLRv1 glyphs: downgrade to version 0
2530
+ self.version = 0
2531
+ del self.table
2532
+ return bool(layersV0)
2533
+
2534
+ populateCOLRv0(
2535
+ self.table,
2536
+ {g: [(layer.name, layer.colorID) for layer in layersV0[g]] for g in layersV0},
2537
+ )
2538
+ del self.ColorLayers
2539
+
2540
+ # TODO: also prune ununsed varIndices in COLR.VarStore
2541
+ return True
2542
+
2543
+
2544
+ @_add_method(ttLib.getTableClass("CPAL"))
2545
+ def prune_post_subset(self, font, options):
2546
+ # Keep whole "CPAL" if "SVG " is present as it may be referenced by the latter
2547
+ # via 'var(--color{palette_entry_index}, ...)' CSS color variables.
2548
+ # For now we just assume this is the case by the mere presence of "SVG " table,
2549
+ # for parsing SVG to collect all the used indices is too much work...
2550
+ # TODO(anthrotype): Do The Right Thing (TM).
2551
+ if "SVG " in font:
2552
+ return True
2553
+
2554
+ colr = font.get("COLR")
2555
+ if not colr: # drop CPAL if COLR was subsetted to empty
2556
+ return False
2557
+
2558
+ colors_by_index = defaultdict(list)
2559
+
2560
+ def collect_colors_by_index(paint):
2561
+ if hasattr(paint, "PaletteIndex"): # either solid colors...
2562
+ colors_by_index[paint.PaletteIndex].append(paint)
2563
+ elif hasattr(paint, "ColorLine"): # ... or gradient color stops
2564
+ for stop in paint.ColorLine.ColorStop:
2565
+ colors_by_index[stop.PaletteIndex].append(stop)
2566
+
2567
+ if colr.version == 0:
2568
+ for layers in colr.ColorLayers.values():
2569
+ for layer in layers:
2570
+ colors_by_index[layer.colorID].append(layer)
2571
+ else:
2572
+ if colr.table.LayerRecordArray:
2573
+ for layer in colr.table.LayerRecordArray.LayerRecord:
2574
+ colors_by_index[layer.PaletteIndex].append(layer)
2575
+ for record in colr.table.BaseGlyphList.BaseGlyphPaintRecord:
2576
+ record.Paint.traverse(colr.table, collect_colors_by_index)
2577
+
2578
+ # don't remap palette entry index 0xFFFF, this is always the foreground color
2579
+ # https://github.com/fonttools/fonttools/issues/2257
2580
+ retained_palette_indices = set(colors_by_index.keys()) - {0xFFFF}
2581
+ for palette in self.palettes:
2582
+ palette[:] = [c for i, c in enumerate(palette) if i in retained_palette_indices]
2583
+ assert len(palette) == len(retained_palette_indices)
2584
+
2585
+ for new_index, old_index in enumerate(sorted(retained_palette_indices)):
2586
+ for record in colors_by_index[old_index]:
2587
+ if hasattr(record, "colorID"): # v0
2588
+ record.colorID = new_index
2589
+ elif hasattr(record, "PaletteIndex"): # v1
2590
+ record.PaletteIndex = new_index
2591
+ else:
2592
+ raise AssertionError(record)
2593
+
2594
+ self.numPaletteEntries = len(self.palettes[0])
2595
+
2596
+ if self.version == 1:
2597
+ kept_labels = []
2598
+ for i, label in enumerate(self.paletteEntryLabels):
2599
+ if i in retained_palette_indices:
2600
+ kept_labels.append(label)
2601
+ self.paletteEntryLabels = kept_labels
2602
+ return bool(self.numPaletteEntries)
2603
+
2604
+
2605
+ @_add_method(otTables.MathGlyphConstruction)
2606
+ def closure_glyphs(self, glyphs):
2607
+ variants = set()
2608
+ for v in self.MathGlyphVariantRecord:
2609
+ variants.add(v.VariantGlyph)
2610
+ if self.GlyphAssembly:
2611
+ for p in self.GlyphAssembly.PartRecords:
2612
+ variants.add(p.glyph)
2613
+ return variants
2614
+
2615
+
2616
+ @_add_method(otTables.MathVariants)
2617
+ def closure_glyphs(self, s):
2618
+ glyphs = frozenset(s.glyphs)
2619
+ variants = set()
2620
+
2621
+ if self.VertGlyphCoverage:
2622
+ indices = self.VertGlyphCoverage.intersect(glyphs)
2623
+ for i in indices:
2624
+ variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs))
2625
+
2626
+ if self.HorizGlyphCoverage:
2627
+ indices = self.HorizGlyphCoverage.intersect(glyphs)
2628
+ for i in indices:
2629
+ variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs))
2630
+
2631
+ s.glyphs.update(variants)
2632
+
2633
+
2634
+ @_add_method(ttLib.getTableClass("VARC"))
2635
+ def subset_glyphs(self, s):
2636
+ indices = self.table.Coverage.subset(s.glyphs)
2637
+ self.table.VarCompositeGlyphs.VarCompositeGlyph = _list_subset(
2638
+ self.table.VarCompositeGlyphs.VarCompositeGlyph, indices
2639
+ )
2640
+ return bool(self.table.VarCompositeGlyphs.VarCompositeGlyph)
2641
+
2642
+
2643
+ @_add_method(ttLib.getTableClass("VARC"))
2644
+ def closure_glyphs(self, s):
2645
+ if self.table.VarCompositeGlyphs is None:
2646
+ return
2647
+
2648
+ glyphMap = {glyphName: i for i, glyphName in enumerate(self.table.Coverage.glyphs)}
2649
+ glyphRecords = self.table.VarCompositeGlyphs.VarCompositeGlyph
2650
+
2651
+ glyphs = s.glyphs
2652
+ covered = set()
2653
+ new = set(glyphs)
2654
+ while new:
2655
+ oldNew = new
2656
+ new = set()
2657
+ for glyphName in oldNew:
2658
+ if glyphName in covered:
2659
+ continue
2660
+ idx = glyphMap.get(glyphName)
2661
+ if idx is None:
2662
+ continue
2663
+ glyph = glyphRecords[idx]
2664
+ for comp in glyph.components:
2665
+ name = comp.glyphName
2666
+ glyphs.add(name)
2667
+ if name not in covered:
2668
+ new.add(name)
2669
+
2670
+
2671
+ @_add_method(ttLib.getTableClass("VARC"))
2672
+ def prune_post_subset(self, font, options):
2673
+ table = self.table
2674
+
2675
+ store = table.MultiVarStore
2676
+ if store is not None:
2677
+ usedVarIdxes = set()
2678
+ table.collect_varidxes(usedVarIdxes)
2679
+ varidx_map = store.subset_varidxes(usedVarIdxes)
2680
+ table.remap_varidxes(varidx_map)
2681
+
2682
+ axisIndicesList = table.AxisIndicesList.Item
2683
+ if axisIndicesList is not None:
2684
+ usedIndices = set()
2685
+ for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
2686
+ for comp in glyph.components:
2687
+ if comp.axisIndicesIndex is not None:
2688
+ usedIndices.add(comp.axisIndicesIndex)
2689
+ usedIndices = sorted(usedIndices)
2690
+ table.AxisIndicesList.Item = _list_subset(axisIndicesList, usedIndices)
2691
+ mapping = {old: new for new, old in enumerate(usedIndices)}
2692
+ for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
2693
+ for comp in glyph.components:
2694
+ if comp.axisIndicesIndex is not None:
2695
+ comp.axisIndicesIndex = mapping[comp.axisIndicesIndex]
2696
+
2697
+ conditionList = table.ConditionList
2698
+ if conditionList is not None:
2699
+ conditionTables = conditionList.ConditionTable
2700
+ usedIndices = set()
2701
+ for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
2702
+ for comp in glyph.components:
2703
+ if comp.conditionIndex is not None:
2704
+ usedIndices.add(comp.conditionIndex)
2705
+ usedIndices = sorted(usedIndices)
2706
+ conditionList.ConditionTable = _list_subset(conditionTables, usedIndices)
2707
+ mapping = {old: new for new, old in enumerate(usedIndices)}
2708
+ for glyph in table.VarCompositeGlyphs.VarCompositeGlyph:
2709
+ for comp in glyph.components:
2710
+ if comp.conditionIndex is not None:
2711
+ comp.conditionIndex = mapping[comp.conditionIndex]
2712
+
2713
+ return True
2714
+
2715
+
2716
+ @_add_method(ttLib.getTableClass("MATH"))
2717
+ def closure_glyphs(self, s):
2718
+ if self.table.MathVariants:
2719
+ self.table.MathVariants.closure_glyphs(s)
2720
+
2721
+
2722
+ @_add_method(otTables.MathItalicsCorrectionInfo)
2723
+ def subset_glyphs(self, s):
2724
+ indices = self.Coverage.subset(s.glyphs)
2725
+ self.ItalicsCorrection = _list_subset(self.ItalicsCorrection, indices)
2726
+ self.ItalicsCorrectionCount = len(self.ItalicsCorrection)
2727
+ return bool(self.ItalicsCorrectionCount)
2728
+
2729
+
2730
+ @_add_method(otTables.MathTopAccentAttachment)
2731
+ def subset_glyphs(self, s):
2732
+ indices = self.TopAccentCoverage.subset(s.glyphs)
2733
+ self.TopAccentAttachment = _list_subset(self.TopAccentAttachment, indices)
2734
+ self.TopAccentAttachmentCount = len(self.TopAccentAttachment)
2735
+ return bool(self.TopAccentAttachmentCount)
2736
+
2737
+
2738
+ @_add_method(otTables.MathKernInfo)
2739
+ def subset_glyphs(self, s):
2740
+ indices = self.MathKernCoverage.subset(s.glyphs)
2741
+ self.MathKernInfoRecords = _list_subset(self.MathKernInfoRecords, indices)
2742
+ self.MathKernCount = len(self.MathKernInfoRecords)
2743
+ return bool(self.MathKernCount)
2744
+
2745
+
2746
+ @_add_method(otTables.MathGlyphInfo)
2747
+ def subset_glyphs(self, s):
2748
+ if self.MathItalicsCorrectionInfo:
2749
+ self.MathItalicsCorrectionInfo.subset_glyphs(s)
2750
+ if self.MathTopAccentAttachment:
2751
+ self.MathTopAccentAttachment.subset_glyphs(s)
2752
+ if self.MathKernInfo:
2753
+ self.MathKernInfo.subset_glyphs(s)
2754
+ if self.ExtendedShapeCoverage:
2755
+ self.ExtendedShapeCoverage.subset(s.glyphs)
2756
+ return True
2757
+
2758
+
2759
+ @_add_method(otTables.MathVariants)
2760
+ def subset_glyphs(self, s):
2761
+ if self.VertGlyphCoverage:
2762
+ indices = self.VertGlyphCoverage.subset(s.glyphs)
2763
+ self.VertGlyphConstruction = _list_subset(self.VertGlyphConstruction, indices)
2764
+ self.VertGlyphCount = len(self.VertGlyphConstruction)
2765
+
2766
+ if self.HorizGlyphCoverage:
2767
+ indices = self.HorizGlyphCoverage.subset(s.glyphs)
2768
+ self.HorizGlyphConstruction = _list_subset(self.HorizGlyphConstruction, indices)
2769
+ self.HorizGlyphCount = len(self.HorizGlyphConstruction)
2770
+
2771
+ return True
2772
+
2773
+
2774
+ @_add_method(ttLib.getTableClass("MATH"))
2775
+ def subset_glyphs(self, s):
2776
+ s.glyphs = s.glyphs_mathed
2777
+ if self.table.MathGlyphInfo:
2778
+ self.table.MathGlyphInfo.subset_glyphs(s)
2779
+ if self.table.MathVariants:
2780
+ self.table.MathVariants.subset_glyphs(s)
2781
+ return True
2782
+
2783
+
2784
+ @_add_method(ttLib.getTableModule("glyf").Glyph)
2785
+ def remapComponentsFast(self, glyphidmap):
2786
+ if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
2787
+ return # Not composite
2788
+ data = self.data = bytearray(self.data)
2789
+ i = 10
2790
+ more = 1
2791
+ while more:
2792
+ flags = (data[i] << 8) | data[i + 1]
2793
+ glyphID = (data[i + 2] << 8) | data[i + 3]
2794
+ # Remap
2795
+ glyphID = glyphidmap[glyphID]
2796
+ data[i + 2] = glyphID >> 8
2797
+ data[i + 3] = glyphID & 0xFF
2798
+ i += 4
2799
+ flags = int(flags)
2800
+
2801
+ if flags & 0x0001:
2802
+ i += 4 # ARG_1_AND_2_ARE_WORDS
2803
+ else:
2804
+ i += 2
2805
+ if flags & 0x0008:
2806
+ i += 2 # WE_HAVE_A_SCALE
2807
+ elif flags & 0x0040:
2808
+ i += 4 # WE_HAVE_AN_X_AND_Y_SCALE
2809
+ elif flags & 0x0080:
2810
+ i += 8 # WE_HAVE_A_TWO_BY_TWO
2811
+ more = flags & 0x0020 # MORE_COMPONENTS
2812
+
2813
+
2814
+ @_add_method(ttLib.getTableClass("glyf"))
2815
+ def closure_glyphs(self, s):
2816
+ glyphSet = self.glyphs
2817
+ decompose = s.glyphs
2818
+ while decompose:
2819
+ components = set()
2820
+ for g in decompose:
2821
+ if g not in glyphSet:
2822
+ continue
2823
+ gl = glyphSet[g]
2824
+ for c in gl.getComponentNames(self):
2825
+ components.add(c)
2826
+ components -= s.glyphs
2827
+ s.glyphs.update(components)
2828
+ decompose = components
2829
+
2830
+
2831
+ @_add_method(ttLib.getTableClass("glyf"))
2832
+ def prune_pre_subset(self, font, options):
2833
+ if options.notdef_glyph and not options.notdef_outline:
2834
+ g = self[self.glyphOrder[0]]
2835
+ # Yay, easy!
2836
+ g.__dict__.clear()
2837
+ g.data = b""
2838
+ return True
2839
+
2840
+
2841
+ @_add_method(ttLib.getTableClass("glyf"))
2842
+ def subset_glyphs(self, s):
2843
+ self.glyphs = _dict_subset(self.glyphs, s.glyphs)
2844
+ if not s.options.retain_gids:
2845
+ indices = [i for i, g in enumerate(self.glyphOrder) if g in s.glyphs]
2846
+ glyphmap = {o: n for n, o in enumerate(indices)}
2847
+ for v in self.glyphs.values():
2848
+ if hasattr(v, "data"):
2849
+ v.remapComponentsFast(glyphmap)
2850
+ Glyph = ttLib.getTableModule("glyf").Glyph
2851
+ for g in s.glyphs_emptied:
2852
+ self.glyphs[g] = Glyph()
2853
+ self.glyphs[g].data = b""
2854
+ self.glyphOrder = [
2855
+ g for g in self.glyphOrder if g in s.glyphs or g in s.glyphs_emptied
2856
+ ]
2857
+ # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset.
2858
+ return True
2859
+
2860
+
2861
+ @_add_method(ttLib.getTableClass("glyf"))
2862
+ def prune_post_subset(self, font, options):
2863
+ remove_hinting = not options.hinting
2864
+ for v in self.glyphs.values():
2865
+ v.trim(remove_hinting=remove_hinting)
2866
+ return True
2867
+
2868
+
2869
+ @_add_method(ttLib.getTableClass("cmap"))
2870
+ def closure_glyphs(self, s):
2871
+ tables = [t for t in self.tables if t.isUnicode()]
2872
+
2873
+ # Close glyphs
2874
+ for table in tables:
2875
+ if table.format == 14:
2876
+ for varSelector, cmap in table.uvsDict.items():
2877
+ if varSelector not in s.unicodes_requested:
2878
+ continue
2879
+ glyphs = {g for u, g in cmap if u in s.unicodes_requested}
2880
+ if None in glyphs:
2881
+ glyphs.remove(None)
2882
+ s.glyphs.update(glyphs)
2883
+ else:
2884
+ cmap = table.cmap
2885
+ intersection = s.unicodes_requested.intersection(cmap.keys())
2886
+ s.glyphs.update(cmap[u] for u in intersection)
2887
+
2888
+ # Calculate unicodes_missing
2889
+ s.unicodes_missing = s.unicodes_requested.copy()
2890
+ for table in tables:
2891
+ s.unicodes_missing.difference_update(table.cmap)
2892
+
2893
+
2894
+ @_add_method(ttLib.getTableClass("cmap"))
2895
+ def prune_pre_subset(self, font, options):
2896
+ if not options.legacy_cmap:
2897
+ # Drop non-Unicode / non-Symbol cmaps
2898
+ self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()]
2899
+ if not options.symbol_cmap:
2900
+ self.tables = [t for t in self.tables if not t.isSymbol()]
2901
+ # TODO(behdad) Only keep one subtable?
2902
+ # For now, drop format=0 which can't be subset_glyphs easily?
2903
+ self.tables = [t for t in self.tables if t.format != 0]
2904
+ self.numSubTables = len(self.tables)
2905
+ return True # Required table
2906
+
2907
+
2908
+ @_add_method(ttLib.getTableClass("cmap"))
2909
+ def subset_glyphs(self, s):
2910
+ s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only
2911
+
2912
+ tables_format12_bmp = []
2913
+ table_plat0_enc3 = {} # Unicode platform, Unicode BMP only, keyed by language
2914
+ table_plat3_enc1 = {} # Windows platform, Unicode BMP, keyed by language
2915
+
2916
+ for t in self.tables:
2917
+ if t.platformID == 0 and t.platEncID == 3:
2918
+ table_plat0_enc3[t.language] = t
2919
+ if t.platformID == 3 and t.platEncID == 1:
2920
+ table_plat3_enc1[t.language] = t
2921
+
2922
+ if t.format == 14:
2923
+ # TODO(behdad) We drop all the default-UVS mappings
2924
+ # for glyphs_requested. So it's the caller's responsibility to make
2925
+ # sure those are included.
2926
+ t.uvsDict = {
2927
+ v: [
2928
+ (u, g)
2929
+ for u, g in l
2930
+ if g in s.glyphs_requested or u in s.unicodes_requested
2931
+ ]
2932
+ for v, l in t.uvsDict.items()
2933
+ if v in s.unicodes_requested
2934
+ }
2935
+ t.uvsDict = {v: l for v, l in t.uvsDict.items() if l}
2936
+ elif t.isUnicode():
2937
+ t.cmap = {
2938
+ u: g
2939
+ for u, g in t.cmap.items()
2940
+ if g in s.glyphs_requested or u in s.unicodes_requested
2941
+ }
2942
+ # Collect format 12 tables that hold only basic multilingual plane
2943
+ # codepoints.
2944
+ if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000:
2945
+ tables_format12_bmp.append(t)
2946
+ else:
2947
+ t.cmap = {u: g for u, g in t.cmap.items() if g in s.glyphs_requested}
2948
+
2949
+ # Fomat 12 tables are redundant if they contain just the same BMP codepoints
2950
+ # their little BMP-only encoding siblings contain.
2951
+ for t in tables_format12_bmp:
2952
+ if (
2953
+ t.platformID == 0 # Unicode platform
2954
+ and t.platEncID == 4 # Unicode full repertoire
2955
+ and t.language in table_plat0_enc3 # Have a BMP-only sibling?
2956
+ and table_plat0_enc3[t.language].cmap == t.cmap
2957
+ ):
2958
+ t.cmap.clear()
2959
+ elif (
2960
+ t.platformID == 3 # Windows platform
2961
+ and t.platEncID == 10 # Unicode full repertoire
2962
+ and t.language in table_plat3_enc1 # Have a BMP-only sibling?
2963
+ and table_plat3_enc1[t.language].cmap == t.cmap
2964
+ ):
2965
+ t.cmap.clear()
2966
+
2967
+ self.tables = [t for t in self.tables if (t.cmap if t.format != 14 else t.uvsDict)]
2968
+ self.numSubTables = len(self.tables)
2969
+ # TODO(behdad) Convert formats when needed.
2970
+ # In particular, if we have a format=12 without non-BMP
2971
+ # characters, convert it to format=4 if there's not one.
2972
+ return True # Required table
2973
+
2974
+
2975
+ @_add_method(ttLib.getTableClass("DSIG"))
2976
+ def prune_pre_subset(self, font, options):
2977
+ # Drop all signatures since they will be invalid
2978
+ self.usNumSigs = 0
2979
+ self.signatureRecords = []
2980
+ return True
2981
+
2982
+
2983
+ @_add_method(ttLib.getTableClass("maxp"))
2984
+ def prune_pre_subset(self, font, options):
2985
+ if not options.hinting:
2986
+ if self.tableVersion == 0x00010000:
2987
+ self.maxZones = 1
2988
+ self.maxTwilightPoints = 0
2989
+ self.maxStorage = 0
2990
+ self.maxFunctionDefs = 0
2991
+ self.maxInstructionDefs = 0
2992
+ self.maxStackElements = 0
2993
+ self.maxSizeOfInstructions = 0
2994
+ return True
2995
+
2996
+
2997
+ @_add_method(ttLib.getTableClass("name"))
2998
+ def prune_post_subset(self, font, options):
2999
+ visitor = NameRecordVisitor()
3000
+ visitor.visit(font)
3001
+ nameIDs = set(options.name_IDs) | visitor.seen
3002
+ if "*" in options.name_IDs:
3003
+ nameIDs |= {n.nameID for n in self.names if n.nameID < 256}
3004
+ self.names = [n for n in self.names if n.nameID in nameIDs]
3005
+ if not options.name_legacy:
3006
+ # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman
3007
+ # entry for Latin and no Unicode names.
3008
+ self.names = [n for n in self.names if n.isUnicode()]
3009
+ # TODO(behdad) Option to keep only one platform's
3010
+ if "*" not in options.name_languages:
3011
+ # TODO(behdad) This is Windows-platform specific!
3012
+ self.names = [n for n in self.names if n.langID in options.name_languages]
3013
+ if options.obfuscate_names:
3014
+ namerecs = []
3015
+ for n in self.names:
3016
+ if n.nameID in [1, 4]:
3017
+ n.string = ".\x7f".encode("utf_16_be") if n.isUnicode() else ".\x7f"
3018
+ elif n.nameID in [2, 6]:
3019
+ n.string = "\x7f".encode("utf_16_be") if n.isUnicode() else "\x7f"
3020
+ elif n.nameID == 3:
3021
+ n.string = ""
3022
+ elif n.nameID in [16, 17, 18]:
3023
+ continue
3024
+ namerecs.append(n)
3025
+ self.names = namerecs
3026
+ return True # Required table
3027
+
3028
+
3029
+ @_add_method(ttLib.getTableClass("head"))
3030
+ def prune_post_subset(self, font, options):
3031
+ # Force re-compiling head table, to update any recalculated values.
3032
+ return True
3033
+
3034
+
3035
+ # TODO(behdad) OS/2 ulCodePageRange?
3036
+ # TODO(behdad) Drop AAT tables.
3037
+ # TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries.
3038
+ # TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left
3039
+ # TODO(behdad) Drop GDEF subitems if unused by lookups
3040
+ # TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF)
3041
+ # TODO(behdad) Text direction considerations.
3042
+ # TODO(behdad) Text script / language considerations.
3043
+ # TODO(behdad) Optionally drop 'kern' table if GPOS available
3044
+ # TODO(behdad) Implement --unicode='*' to choose all cmap'ed
3045
+ # TODO(behdad) Drop old-spec Indic scripts
3046
+
3047
+
3048
+ class Options(object):
3049
+ class OptionError(Exception):
3050
+ pass
3051
+
3052
+ class UnknownOptionError(OptionError):
3053
+ pass
3054
+
3055
+ # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser
3056
+ _drop_tables_default = [
3057
+ "BASE",
3058
+ "JSTF",
3059
+ "DSIG",
3060
+ "EBDT",
3061
+ "EBLC",
3062
+ "EBSC",
3063
+ "PCLT",
3064
+ "LTSH",
3065
+ ]
3066
+ _drop_tables_default += ["Feat", "Glat", "Gloc", "Silf", "Sill"] # Graphite
3067
+ _no_subset_tables_default = [
3068
+ "avar",
3069
+ "fvar",
3070
+ "gasp",
3071
+ "head",
3072
+ "hhea",
3073
+ "maxp",
3074
+ "vhea",
3075
+ "OS/2",
3076
+ "loca",
3077
+ "name",
3078
+ "cvt",
3079
+ "fpgm",
3080
+ "prep",
3081
+ "VDMX",
3082
+ "DSIG",
3083
+ "CPAL",
3084
+ "MVAR",
3085
+ "cvar",
3086
+ "STAT",
3087
+ ]
3088
+ _hinting_tables_default = ["cvt", "cvar", "fpgm", "prep", "hdmx", "VDMX"]
3089
+
3090
+ # Based on HarfBuzz shapers
3091
+ _layout_features_groups = {
3092
+ # Default shaper
3093
+ "common": ["rvrn", "ccmp", "liga", "locl", "mark", "mkmk", "rlig"],
3094
+ "fractions": ["frac", "numr", "dnom"],
3095
+ "horizontal": ["calt", "clig", "curs", "kern", "rclt"],
3096
+ "vertical": ["valt", "vert", "vkrn", "vpal", "vrt2"],
3097
+ "ltr": ["ltra", "ltrm"],
3098
+ "rtl": ["rtla", "rtlm"],
3099
+ "rand": ["rand"],
3100
+ "justify": ["jalt"],
3101
+ "private": ["Harf", "HARF", "Buzz", "BUZZ"],
3102
+ "east_asian_spacing": ["chws", "vchw", "halt", "vhal"],
3103
+ # Complex shapers
3104
+ "arabic": [
3105
+ "init",
3106
+ "medi",
3107
+ "fina",
3108
+ "isol",
3109
+ "med2",
3110
+ "fin2",
3111
+ "fin3",
3112
+ "cswh",
3113
+ "mset",
3114
+ "stch",
3115
+ ],
3116
+ "hangul": ["ljmo", "vjmo", "tjmo"],
3117
+ "tibetan": ["abvs", "blws", "abvm", "blwm"],
3118
+ "indic": [
3119
+ "nukt",
3120
+ "akhn",
3121
+ "rphf",
3122
+ "rkrf",
3123
+ "pref",
3124
+ "blwf",
3125
+ "half",
3126
+ "abvf",
3127
+ "pstf",
3128
+ "cfar",
3129
+ "vatu",
3130
+ "cjct",
3131
+ "init",
3132
+ "pres",
3133
+ "abvs",
3134
+ "blws",
3135
+ "psts",
3136
+ "haln",
3137
+ "dist",
3138
+ "abvm",
3139
+ "blwm",
3140
+ ],
3141
+ }
3142
+ _layout_features_default = _uniq_sort(
3143
+ sum(iter(_layout_features_groups.values()), [])
3144
+ )
3145
+
3146
+ def __init__(self, **kwargs):
3147
+ self.drop_tables = self._drop_tables_default[:]
3148
+ self.no_subset_tables = self._no_subset_tables_default[:]
3149
+ self.passthrough_tables = False # keep/drop tables we can't subset
3150
+ self.hinting_tables = self._hinting_tables_default[:]
3151
+ self.legacy_kern = False # drop 'kern' table if GPOS available
3152
+ self.layout_closure = True
3153
+ self.layout_features = self._layout_features_default[:]
3154
+ self.layout_scripts = ["*"]
3155
+ self.ignore_missing_glyphs = False
3156
+ self.ignore_missing_unicodes = True
3157
+ self.hinting = True
3158
+ self.glyph_names = False
3159
+ self.legacy_cmap = False
3160
+ self.symbol_cmap = False
3161
+ self.name_IDs = [
3162
+ 0,
3163
+ 1,
3164
+ 2,
3165
+ 3,
3166
+ 4,
3167
+ 5,
3168
+ 6,
3169
+ ] # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225
3170
+ self.name_legacy = False
3171
+ self.name_languages = [0x0409] # English
3172
+ self.obfuscate_names = False # to make webfont unusable as a system font
3173
+ self.retain_gids = False
3174
+ self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF
3175
+ self.notdef_outline = False # No need for notdef to have an outline really
3176
+ self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType
3177
+ self.recalc_bounds = False # Recalculate font bounding boxes
3178
+ self.recalc_timestamp = False # Recalculate font modified timestamp
3179
+ self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits
3180
+ self.prune_codepage_ranges = True # Clear unused 'ulCodePageRange' bits
3181
+ self.recalc_average_width = False # update 'xAvgCharWidth'
3182
+ self.recalc_max_context = False # update 'usMaxContext'
3183
+ self.canonical_order = None # Order tables as recommended
3184
+ self.flavor = None # May be 'woff' or 'woff2'
3185
+ self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0
3186
+ self.desubroutinize = False # Desubroutinize CFF CharStrings
3187
+ self.harfbuzz_repacker = USE_HARFBUZZ_REPACKER.default
3188
+ self.verbose = False
3189
+ self.timing = False
3190
+ self.xml = False
3191
+ self.font_number = -1
3192
+ self.pretty_svg = False
3193
+ self.lazy = True
3194
+
3195
+ self.set(**kwargs)
3196
+
3197
+ def set(self, **kwargs):
3198
+ for k, v in kwargs.items():
3199
+ if not hasattr(self, k):
3200
+ raise self.UnknownOptionError("Unknown option '%s'" % k)
3201
+ setattr(self, k, v)
3202
+
3203
+ def parse_opts(self, argv, ignore_unknown=[]):
3204
+ posargs = []
3205
+ passthru_options = []
3206
+ for a in argv:
3207
+ orig_a = a
3208
+ if not a.startswith("--"):
3209
+ posargs.append(a)
3210
+ continue
3211
+ a = a[2:]
3212
+ i = a.find("=")
3213
+ op = "="
3214
+ if i == -1:
3215
+ if a.startswith("no-"):
3216
+ k = a[3:]
3217
+ if k == "canonical-order":
3218
+ # reorderTables=None is faster than False (the latter
3219
+ # still reorders to "keep" the original table order)
3220
+ v = None
3221
+ else:
3222
+ v = False
3223
+ else:
3224
+ k = a
3225
+ v = True
3226
+ if k.endswith("?"):
3227
+ k = k[:-1]
3228
+ v = "?"
3229
+ else:
3230
+ k = a[:i]
3231
+ if k[-1] in "-+":
3232
+ op = k[-1] + "=" # Op is '-=' or '+=' now.
3233
+ k = k[:-1]
3234
+ v = a[i + 1 :]
3235
+ ok = k
3236
+ k = k.replace("-", "_")
3237
+ if not hasattr(self, k):
3238
+ if ignore_unknown is True or ok in ignore_unknown:
3239
+ passthru_options.append(orig_a)
3240
+ continue
3241
+ else:
3242
+ raise self.UnknownOptionError("Unknown option '%s'" % a)
3243
+
3244
+ ov = getattr(self, k)
3245
+ if v == "?":
3246
+ print("Current setting for '%s' is: %s" % (ok, ov))
3247
+ continue
3248
+ if isinstance(ov, bool):
3249
+ v = bool(v)
3250
+ elif isinstance(ov, int):
3251
+ v = int(v)
3252
+ elif isinstance(ov, str):
3253
+ v = str(v) # redundant
3254
+ elif isinstance(ov, list):
3255
+ if isinstance(v, bool):
3256
+ raise self.OptionError(
3257
+ "Option '%s' requires values to be specified using '='" % a
3258
+ )
3259
+ vv = v.replace(",", " ").split()
3260
+ if vv == [""]:
3261
+ vv = []
3262
+ vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
3263
+ if op == "=":
3264
+ v = vv
3265
+ elif op == "+=":
3266
+ v = ov
3267
+ v.extend(vv)
3268
+ elif op == "-=":
3269
+ v = ov
3270
+ for x in vv:
3271
+ if x in v:
3272
+ v.remove(x)
3273
+ else:
3274
+ assert False
3275
+
3276
+ setattr(self, k, v)
3277
+
3278
+ return posargs + passthru_options
3279
+
3280
+
3281
+ class Subsetter(object):
3282
+ class SubsettingError(Exception):
3283
+ pass
3284
+
3285
+ class MissingGlyphsSubsettingError(SubsettingError):
3286
+ pass
3287
+
3288
+ class MissingUnicodesSubsettingError(SubsettingError):
3289
+ pass
3290
+
3291
+ def __init__(self, options=None):
3292
+ if not options:
3293
+ options = Options()
3294
+
3295
+ self.options = options
3296
+ self.unicodes_requested = set()
3297
+ self.glyph_names_requested = set()
3298
+ self.glyph_ids_requested = set()
3299
+
3300
+ def populate(self, glyphs=[], gids=[], unicodes=[], text=""):
3301
+ self.unicodes_requested.update(unicodes)
3302
+ if isinstance(text, bytes):
3303
+ text = text.decode("utf_8")
3304
+ text_utf32 = text.encode("utf-32-be")
3305
+ nchars = len(text_utf32) // 4
3306
+ for u in struct.unpack(">%dL" % nchars, text_utf32):
3307
+ self.unicodes_requested.add(u)
3308
+ self.glyph_names_requested.update(glyphs)
3309
+ self.glyph_ids_requested.update(gids)
3310
+
3311
+ def _prune_pre_subset(self, font):
3312
+ for tag in self._sort_tables(font):
3313
+ if (
3314
+ tag.strip() in self.options.drop_tables
3315
+ or (
3316
+ tag.strip() in self.options.hinting_tables
3317
+ and not self.options.hinting
3318
+ )
3319
+ or (tag == "kern" and (not self.options.legacy_kern and "GPOS" in font))
3320
+ ):
3321
+ log.info("%s dropped", tag)
3322
+ del font[tag]
3323
+ continue
3324
+
3325
+ clazz = ttLib.getTableClass(tag)
3326
+
3327
+ if hasattr(clazz, "prune_pre_subset"):
3328
+ with timer("load '%s'" % tag):
3329
+ table = font[tag]
3330
+ with timer("prune '%s'" % tag):
3331
+ retain = table.prune_pre_subset(font, self.options)
3332
+ if not retain:
3333
+ log.info("%s pruned to empty; dropped", tag)
3334
+ del font[tag]
3335
+ continue
3336
+ else:
3337
+ log.info("%s pruned", tag)
3338
+
3339
+ def _closure_glyphs(self, font):
3340
+ realGlyphs = set(font.getGlyphOrder())
3341
+ self.orig_glyph_order = glyph_order = font.getGlyphOrder()
3342
+
3343
+ self.glyphs_requested = set()
3344
+ self.glyphs_requested.update(self.glyph_names_requested)
3345
+ self.glyphs_requested.update(
3346
+ glyph_order[i] for i in self.glyph_ids_requested if i < len(glyph_order)
3347
+ )
3348
+
3349
+ self.glyphs_missing = set()
3350
+ self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs))
3351
+ self.glyphs_missing.update(
3352
+ i for i in self.glyph_ids_requested if i >= len(glyph_order)
3353
+ )
3354
+ if self.glyphs_missing:
3355
+ log.info("Missing requested glyphs: %s", self.glyphs_missing)
3356
+ if not self.options.ignore_missing_glyphs:
3357
+ raise self.MissingGlyphsSubsettingError(self.glyphs_missing)
3358
+
3359
+ self.glyphs = self.glyphs_requested.copy()
3360
+
3361
+ self.unicodes_missing = set()
3362
+ if "cmap" in font:
3363
+ with timer("close glyph list over 'cmap'"):
3364
+ font["cmap"].closure_glyphs(self)
3365
+ self.glyphs.intersection_update(realGlyphs)
3366
+ self.glyphs_cmaped = frozenset(self.glyphs)
3367
+ if self.unicodes_missing:
3368
+ missing = ["U+%04X" % u for u in self.unicodes_missing]
3369
+ log.info("Missing glyphs for requested Unicodes: %s", missing)
3370
+ if not self.options.ignore_missing_unicodes:
3371
+ raise self.MissingUnicodesSubsettingError(missing)
3372
+ del missing
3373
+
3374
+ if self.options.notdef_glyph:
3375
+ if "glyf" in font:
3376
+ self.glyphs.add(font.getGlyphName(0))
3377
+ log.info("Added gid0 to subset")
3378
+ else:
3379
+ self.glyphs.add(".notdef")
3380
+ log.info("Added .notdef to subset")
3381
+ if self.options.recommended_glyphs:
3382
+ if "glyf" in font:
3383
+ for i in range(min(4, len(font.getGlyphOrder()))):
3384
+ self.glyphs.add(font.getGlyphName(i))
3385
+ log.info("Added first four glyphs to subset")
3386
+
3387
+ if "MATH" in font:
3388
+ with timer("close glyph list over 'MATH'"):
3389
+ log.info(
3390
+ "Closing glyph list over 'MATH': %d glyphs before", len(self.glyphs)
3391
+ )
3392
+ log.glyphs(self.glyphs, font=font)
3393
+ font["MATH"].closure_glyphs(self)
3394
+ self.glyphs.intersection_update(realGlyphs)
3395
+ log.info(
3396
+ "Closed glyph list over 'MATH': %d glyphs after", len(self.glyphs)
3397
+ )
3398
+ log.glyphs(self.glyphs, font=font)
3399
+ self.glyphs_mathed = frozenset(self.glyphs)
3400
+
3401
+ if self.options.layout_closure and "GSUB" in font:
3402
+ with timer("close glyph list over 'GSUB'"):
3403
+ log.info(
3404
+ "Closing glyph list over 'GSUB': %d glyphs before", len(self.glyphs)
3405
+ )
3406
+ log.glyphs(self.glyphs, font=font)
3407
+ font["GSUB"].closure_glyphs(self)
3408
+ self.glyphs.intersection_update(realGlyphs)
3409
+ log.info(
3410
+ "Closed glyph list over 'GSUB': %d glyphs after", len(self.glyphs)
3411
+ )
3412
+ log.glyphs(self.glyphs, font=font)
3413
+ self.glyphs_gsubed = frozenset(self.glyphs)
3414
+
3415
+ for table in ("COLR", "bsln"):
3416
+ if table in font:
3417
+ with timer("close glyph list over '%s'" % table):
3418
+ log.info(
3419
+ "Closing glyph list over '%s': %d glyphs before",
3420
+ table,
3421
+ len(self.glyphs),
3422
+ )
3423
+ log.glyphs(self.glyphs, font=font)
3424
+ font[table].closure_glyphs(self)
3425
+ self.glyphs.intersection_update(realGlyphs)
3426
+ log.info(
3427
+ "Closed glyph list over '%s': %d glyphs after",
3428
+ table,
3429
+ len(self.glyphs),
3430
+ )
3431
+ log.glyphs(self.glyphs, font=font)
3432
+ setattr(self, f"glyphs_{table.lower()}ed", frozenset(self.glyphs))
3433
+
3434
+ if "VARC" in font:
3435
+ with timer("close glyph list over 'VARC'"):
3436
+ log.info(
3437
+ "Closing glyph list over 'VARC': %d glyphs before", len(self.glyphs)
3438
+ )
3439
+ log.glyphs(self.glyphs, font=font)
3440
+ font["VARC"].closure_glyphs(self)
3441
+ self.glyphs.intersection_update(realGlyphs)
3442
+ log.info(
3443
+ "Closed glyph list over 'VARC': %d glyphs after", len(self.glyphs)
3444
+ )
3445
+ log.glyphs(self.glyphs, font=font)
3446
+ self.glyphs_glyfed = frozenset(self.glyphs)
3447
+
3448
+ if "glyf" in font:
3449
+ with timer("close glyph list over 'glyf'"):
3450
+ log.info(
3451
+ "Closing glyph list over 'glyf': %d glyphs before", len(self.glyphs)
3452
+ )
3453
+ log.glyphs(self.glyphs, font=font)
3454
+ font["glyf"].closure_glyphs(self)
3455
+ self.glyphs.intersection_update(realGlyphs)
3456
+ log.info(
3457
+ "Closed glyph list over 'glyf': %d glyphs after", len(self.glyphs)
3458
+ )
3459
+ log.glyphs(self.glyphs, font=font)
3460
+ self.glyphs_glyfed = frozenset(self.glyphs)
3461
+
3462
+ if "CFF " in font:
3463
+ with timer("close glyph list over 'CFF '"):
3464
+ log.info(
3465
+ "Closing glyph list over 'CFF ': %d glyphs before", len(self.glyphs)
3466
+ )
3467
+ log.glyphs(self.glyphs, font=font)
3468
+ font["CFF "].closure_glyphs(self)
3469
+ self.glyphs.intersection_update(realGlyphs)
3470
+ log.info(
3471
+ "Closed glyph list over 'CFF ': %d glyphs after", len(self.glyphs)
3472
+ )
3473
+ log.glyphs(self.glyphs, font=font)
3474
+ self.glyphs_cffed = frozenset(self.glyphs)
3475
+
3476
+ self.glyphs_retained = frozenset(self.glyphs)
3477
+
3478
+ order = font.getReverseGlyphMap()
3479
+ self.reverseOrigGlyphMap = {g: order[g] for g in self.glyphs_retained}
3480
+
3481
+ self.last_retained_order = max(self.reverseOrigGlyphMap.values())
3482
+ self.last_retained_glyph = font.getGlyphOrder()[self.last_retained_order]
3483
+
3484
+ self.glyphs_emptied = frozenset()
3485
+ if self.options.retain_gids:
3486
+ self.glyphs_emptied = {
3487
+ g
3488
+ for g in realGlyphs - self.glyphs_retained
3489
+ if order[g] <= self.last_retained_order
3490
+ }
3491
+
3492
+ self.reverseEmptiedGlyphMap = {g: order[g] for g in self.glyphs_emptied}
3493
+
3494
+ if not self.options.retain_gids:
3495
+ new_glyph_order = [g for g in glyph_order if g in self.glyphs_retained]
3496
+ else:
3497
+ new_glyph_order = [
3498
+ g for g in glyph_order if font.getGlyphID(g) <= self.last_retained_order
3499
+ ]
3500
+ # We'll call font.setGlyphOrder() at the end of _subset_glyphs when all
3501
+ # tables have been subsetted. Below, we use the new glyph order to get
3502
+ # a map from old to new glyph indices, which can be useful when
3503
+ # subsetting individual tables (e.g. SVG) that refer to GIDs.
3504
+ self.new_glyph_order = new_glyph_order
3505
+ self.glyph_index_map = {
3506
+ order[new_glyph_order[i]]: i for i in range(len(new_glyph_order))
3507
+ }
3508
+
3509
+ log.info("Retaining %d glyphs", len(self.glyphs_retained))
3510
+
3511
+ del self.glyphs
3512
+
3513
+ def _subset_glyphs(self, font):
3514
+ self.used_mark_sets = []
3515
+ for tag in self._sort_tables(font):
3516
+ clazz = ttLib.getTableClass(tag)
3517
+
3518
+ if tag.strip() in self.options.no_subset_tables:
3519
+ log.info("%s subsetting not needed", tag)
3520
+ elif hasattr(clazz, "subset_glyphs"):
3521
+ with timer("subset '%s'" % tag):
3522
+ table = font[tag]
3523
+ self.glyphs = self.glyphs_retained
3524
+ retain = table.subset_glyphs(self)
3525
+ del self.glyphs
3526
+ if not retain:
3527
+ log.info("%s subsetted to empty; dropped", tag)
3528
+ del font[tag]
3529
+ else:
3530
+ log.info("%s subsetted", tag)
3531
+ elif self.options.passthrough_tables:
3532
+ log.info("%s NOT subset; don't know how to subset", tag)
3533
+ else:
3534
+ log.warning("%s NOT subset; don't know how to subset; dropped", tag)
3535
+ del font[tag]
3536
+
3537
+ with timer("subset GlyphOrder"):
3538
+ font.setGlyphOrder(self.new_glyph_order)
3539
+
3540
+ def _prune_post_subset(self, font):
3541
+ tableTags = font.keys()
3542
+ # Prune the name table last because when we're pruning the name table,
3543
+ # we visit each table in the font to see what name table records are
3544
+ # still in use.
3545
+ if "name" in tableTags:
3546
+ tableTags.remove("name")
3547
+ tableTags.append("name")
3548
+ for tag in tableTags:
3549
+ if tag == "GlyphOrder":
3550
+ continue
3551
+ if tag == "OS/2":
3552
+ if self.options.prune_unicode_ranges:
3553
+ old_uniranges = font[tag].getUnicodeRanges()
3554
+ new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True)
3555
+ if old_uniranges != new_uniranges:
3556
+ log.info(
3557
+ "%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)
3558
+ )
3559
+ if self.options.prune_codepage_ranges and font[tag].version >= 1:
3560
+ # codepage range fields were added with OS/2 format 1
3561
+ # https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version-1
3562
+ old_codepages = font[tag].getCodePageRanges()
3563
+ new_codepages = font[tag].recalcCodePageRanges(font, pruneOnly=True)
3564
+ if old_codepages != new_codepages:
3565
+ log.info(
3566
+ "%s CodePage ranges pruned: %s",
3567
+ tag,
3568
+ sorted(new_codepages),
3569
+ )
3570
+ if self.options.recalc_average_width:
3571
+ old_avg_width = font[tag].xAvgCharWidth
3572
+ new_avg_width = font[tag].recalcAvgCharWidth(font)
3573
+ if old_avg_width != new_avg_width:
3574
+ log.info("%s xAvgCharWidth updated: %d", tag, new_avg_width)
3575
+ if self.options.recalc_max_context:
3576
+ max_context = maxCtxFont(font)
3577
+ if max_context != font[tag].usMaxContext:
3578
+ font[tag].usMaxContext = max_context
3579
+ log.info("%s usMaxContext updated: %d", tag, max_context)
3580
+ clazz = ttLib.getTableClass(tag)
3581
+ if hasattr(clazz, "prune_post_subset"):
3582
+ with timer("prune '%s'" % tag):
3583
+ table = font[tag]
3584
+ retain = table.prune_post_subset(font, self.options)
3585
+ if not retain:
3586
+ log.info("%s pruned to empty; dropped", tag)
3587
+ del font[tag]
3588
+ else:
3589
+ log.info("%s pruned", tag)
3590
+
3591
+ def _sort_tables(self, font):
3592
+ tagOrder = ["GDEF", "GPOS", "GSUB", "fvar", "avar", "gvar", "name", "glyf"]
3593
+ tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)}
3594
+ tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0))
3595
+ return [t for t in tags if t != "GlyphOrder"]
3596
+
3597
+ def subset(self, font):
3598
+ self._prune_pre_subset(font)
3599
+ self._closure_glyphs(font)
3600
+ self._subset_glyphs(font)
3601
+ self._prune_post_subset(font)
3602
+
3603
+
3604
+ @timer("load font")
3605
+ def load_font(fontFile, options, checkChecksums=0, dontLoadGlyphNames=False, lazy=True):
3606
+ font = ttLib.TTFont(
3607
+ fontFile,
3608
+ checkChecksums=checkChecksums,
3609
+ recalcBBoxes=options.recalc_bounds,
3610
+ recalcTimestamp=options.recalc_timestamp,
3611
+ lazy=lazy,
3612
+ fontNumber=options.font_number,
3613
+ )
3614
+
3615
+ # Hack:
3616
+ #
3617
+ # If we don't need glyph names, change 'post' class to not try to
3618
+ # load them. It avoid lots of headache with broken fonts as well
3619
+ # as loading time.
3620
+ #
3621
+ # Ideally ttLib should provide a way to ask it to skip loading
3622
+ # glyph names. But it currently doesn't provide such a thing.
3623
+ #
3624
+ if dontLoadGlyphNames:
3625
+ post = ttLib.getTableClass("post")
3626
+ saved = post.decode_format_2_0
3627
+ post.decode_format_2_0 = post.decode_format_3_0
3628
+ f = font["post"]
3629
+ if f.formatType == 2.0:
3630
+ f.formatType = 3.0
3631
+ post.decode_format_2_0 = saved
3632
+
3633
+ return font
3634
+
3635
+
3636
+ @timer("compile and save font")
3637
+ def save_font(font, outfile, options):
3638
+ if options.with_zopfli and options.flavor == "woff":
3639
+ from fontTools.ttLib import sfnt
3640
+
3641
+ sfnt.USE_ZOPFLI = True
3642
+ font.flavor = options.flavor
3643
+ font.cfg[USE_HARFBUZZ_REPACKER] = options.harfbuzz_repacker
3644
+ font.save(outfile, reorderTables=options.canonical_order)
3645
+
3646
+
3647
+ def parse_unicodes(s):
3648
+ import re
3649
+
3650
+ s = re.sub(r"0[xX]", " ", s)
3651
+ s = re.sub(r"[<+>,;&#\\xXuU\n ]", " ", s)
3652
+ l = []
3653
+ for item in s.split():
3654
+ fields = item.split("-")
3655
+ if len(fields) == 1:
3656
+ l.append(int(item, 16))
3657
+ else:
3658
+ start, end = fields
3659
+ l.extend(range(int(start, 16), int(end, 16) + 1))
3660
+ return l
3661
+
3662
+
3663
+ def parse_gids(s):
3664
+ l = []
3665
+ for item in s.replace(",", " ").split():
3666
+ fields = item.split("-")
3667
+ if len(fields) == 1:
3668
+ l.append(int(fields[0]))
3669
+ else:
3670
+ l.extend(range(int(fields[0]), int(fields[1]) + 1))
3671
+ return l
3672
+
3673
+
3674
+ def parse_glyphs(s):
3675
+ return s.replace(",", " ").split()
3676
+
3677
+
3678
+ def usage():
3679
+ print("usage:", __usage__, file=sys.stderr)
3680
+ print("Try pyftsubset --help for more information.\n", file=sys.stderr)
3681
+
3682
+
3683
+ @timer("make one with everything (TOTAL TIME)")
3684
+ def main(args=None):
3685
+ """OpenType font subsetter and optimizer"""
3686
+ from os.path import splitext
3687
+ from fontTools import configLogger
3688
+
3689
+ if args is None:
3690
+ args = sys.argv[1:]
3691
+
3692
+ if "--help" in args:
3693
+ print(__doc__)
3694
+ return 0
3695
+
3696
+ options = Options()
3697
+ try:
3698
+ args = options.parse_opts(
3699
+ args,
3700
+ ignore_unknown=[
3701
+ "gids",
3702
+ "gids-file",
3703
+ "glyphs",
3704
+ "glyphs-file",
3705
+ "text",
3706
+ "text-file",
3707
+ "unicodes",
3708
+ "unicodes-file",
3709
+ "output-file",
3710
+ ],
3711
+ )
3712
+ except options.OptionError as e:
3713
+ usage()
3714
+ print("ERROR:", e, file=sys.stderr)
3715
+ return 2
3716
+
3717
+ if len(args) < 2:
3718
+ usage()
3719
+ return 1
3720
+
3721
+ configLogger(level=logging.INFO if options.verbose else logging.WARNING)
3722
+ if options.timing:
3723
+ timer.logger.setLevel(logging.DEBUG)
3724
+ else:
3725
+ timer.logger.disabled = True
3726
+
3727
+ fontfile = args[0]
3728
+ args = args[1:]
3729
+
3730
+ subsetter = Subsetter(options=options)
3731
+ outfile = None
3732
+ glyphs = []
3733
+ gids = []
3734
+ unicodes = []
3735
+ wildcard_glyphs = False
3736
+ wildcard_unicodes = False
3737
+ text = ""
3738
+ for g in args:
3739
+ if g == "*":
3740
+ wildcard_glyphs = True
3741
+ continue
3742
+ if g.startswith("--output-file="):
3743
+ outfile = g[14:]
3744
+ continue
3745
+ if g.startswith("--text="):
3746
+ text += g[7:]
3747
+ continue
3748
+ if g.startswith("--text-file="):
3749
+ with open(g[12:], encoding="utf-8") as f:
3750
+ text += f.read().replace("\n", "")
3751
+ continue
3752
+ if g.startswith("--unicodes="):
3753
+ if g[11:] == "*":
3754
+ wildcard_unicodes = True
3755
+ else:
3756
+ unicodes.extend(parse_unicodes(g[11:]))
3757
+ continue
3758
+ if g.startswith("--unicodes-file="):
3759
+ with open(g[16:]) as f:
3760
+ for line in f.readlines():
3761
+ unicodes.extend(parse_unicodes(line.split("#")[0]))
3762
+ continue
3763
+ if g.startswith("--gids="):
3764
+ gids.extend(parse_gids(g[7:]))
3765
+ continue
3766
+ if g.startswith("--gids-file="):
3767
+ with open(g[12:]) as f:
3768
+ for line in f.readlines():
3769
+ gids.extend(parse_gids(line.split("#")[0]))
3770
+ continue
3771
+ if g.startswith("--glyphs="):
3772
+ if g[9:] == "*":
3773
+ wildcard_glyphs = True
3774
+ else:
3775
+ glyphs.extend(parse_glyphs(g[9:]))
3776
+ continue
3777
+ if g.startswith("--glyphs-file="):
3778
+ with open(g[14:]) as f:
3779
+ for line in f.readlines():
3780
+ glyphs.extend(parse_glyphs(line.split("#")[0]))
3781
+ continue
3782
+ glyphs.append(g)
3783
+
3784
+ dontLoadGlyphNames = not options.glyph_names and not glyphs
3785
+ lazy = options.lazy
3786
+ font = load_font(
3787
+ fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames, lazy=lazy
3788
+ )
3789
+
3790
+ if outfile is None:
3791
+ ext = "." + options.flavor.lower() if options.flavor is not None else None
3792
+ outfile = makeOutputFileName(
3793
+ fontfile, extension=ext, overWrite=True, suffix=".subset"
3794
+ )
3795
+
3796
+ with timer("compile glyph list"):
3797
+ if wildcard_glyphs:
3798
+ glyphs.extend(font.getGlyphOrder())
3799
+ if wildcard_unicodes:
3800
+ for t in font["cmap"].tables:
3801
+ if t.isUnicode():
3802
+ unicodes.extend(t.cmap.keys())
3803
+ if t.format == 14:
3804
+ unicodes.extend(t.uvsDict.keys())
3805
+ assert "" not in glyphs
3806
+
3807
+ log.info("Text: '%s'" % text)
3808
+ log.info("Unicodes: %s", unicodes)
3809
+ log.info("Glyphs: %s", glyphs)
3810
+ log.info("Gids: %s", gids)
3811
+
3812
+ subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text)
3813
+ subsetter.subset(font)
3814
+
3815
+ save_font(font, outfile, options)
3816
+
3817
+ if options.verbose:
3818
+ import os
3819
+
3820
+ log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile))
3821
+ log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile))
3822
+
3823
+ if options.xml:
3824
+ font.saveXML(sys.stdout)
3825
+
3826
+ font.close()
3827
+
3828
+
3829
+ __all__ = [
3830
+ "Options",
3831
+ "Subsetter",
3832
+ "load_font",
3833
+ "save_font",
3834
+ "parse_gids",
3835
+ "parse_glyphs",
3836
+ "parse_unicodes",
3837
+ "main",
3838
+ ]