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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cffLib/CFF2ToCFF.py +65 -10
  4. fontTools/cffLib/__init__.py +61 -26
  5. fontTools/cffLib/specializer.py +4 -1
  6. fontTools/cffLib/transforms.py +11 -6
  7. fontTools/config/__init__.py +15 -0
  8. fontTools/cu2qu/cu2qu.c +6567 -5579
  9. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
  10. fontTools/cu2qu/cu2qu.py +36 -4
  11. fontTools/cu2qu/ufo.py +14 -0
  12. fontTools/designspaceLib/__init__.py +8 -3
  13. fontTools/designspaceLib/statNames.py +14 -7
  14. fontTools/feaLib/ast.py +24 -15
  15. fontTools/feaLib/builder.py +139 -66
  16. fontTools/feaLib/error.py +1 -1
  17. fontTools/feaLib/lexer.c +7038 -7995
  18. fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
  19. fontTools/feaLib/parser.py +75 -40
  20. fontTools/feaLib/variableScalar.py +6 -1
  21. fontTools/fontBuilder.py +50 -44
  22. fontTools/merge/__init__.py +1 -1
  23. fontTools/merge/cmap.py +33 -1
  24. fontTools/merge/tables.py +12 -1
  25. fontTools/misc/bezierTools.c +14913 -17013
  26. fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
  27. fontTools/misc/bezierTools.py +4 -1
  28. fontTools/misc/configTools.py +3 -1
  29. fontTools/misc/enumTools.py +23 -0
  30. fontTools/misc/etree.py +4 -27
  31. fontTools/misc/filesystem/__init__.py +68 -0
  32. fontTools/misc/filesystem/_base.py +134 -0
  33. fontTools/misc/filesystem/_copy.py +45 -0
  34. fontTools/misc/filesystem/_errors.py +54 -0
  35. fontTools/misc/filesystem/_info.py +75 -0
  36. fontTools/misc/filesystem/_osfs.py +164 -0
  37. fontTools/misc/filesystem/_path.py +67 -0
  38. fontTools/misc/filesystem/_subfs.py +92 -0
  39. fontTools/misc/filesystem/_tempfs.py +34 -0
  40. fontTools/misc/filesystem/_tools.py +34 -0
  41. fontTools/misc/filesystem/_walk.py +55 -0
  42. fontTools/misc/filesystem/_zipfs.py +204 -0
  43. fontTools/misc/fixedTools.py +1 -1
  44. fontTools/misc/loggingTools.py +1 -1
  45. fontTools/misc/psCharStrings.py +17 -2
  46. fontTools/misc/sstruct.py +2 -6
  47. fontTools/misc/symfont.py +6 -8
  48. fontTools/misc/testTools.py +5 -1
  49. fontTools/misc/textTools.py +4 -2
  50. fontTools/misc/visitor.py +32 -16
  51. fontTools/misc/xmlWriter.py +44 -8
  52. fontTools/mtiLib/__init__.py +1 -3
  53. fontTools/otlLib/builder.py +402 -155
  54. fontTools/otlLib/optimize/gpos.py +49 -63
  55. fontTools/pens/filterPen.py +218 -26
  56. fontTools/pens/momentsPen.c +5514 -5584
  57. fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
  58. fontTools/pens/pointPen.py +61 -18
  59. fontTools/pens/roundingPen.py +2 -2
  60. fontTools/pens/t2CharStringPen.py +31 -11
  61. fontTools/qu2cu/qu2cu.c +6581 -6168
  62. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
  63. fontTools/subset/__init__.py +283 -25
  64. fontTools/subset/svg.py +2 -3
  65. fontTools/ttLib/__init__.py +4 -0
  66. fontTools/ttLib/__main__.py +47 -8
  67. fontTools/ttLib/removeOverlaps.py +7 -5
  68. fontTools/ttLib/reorderGlyphs.py +8 -7
  69. fontTools/ttLib/sfnt.py +11 -9
  70. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  71. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  72. fontTools/ttLib/tables/S__i_l_f.py +2 -2
  73. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  74. fontTools/ttLib/tables/T_S_I__1.py +2 -5
  75. fontTools/ttLib/tables/T_S_I__5.py +18 -7
  76. fontTools/ttLib/tables/__init__.py +1 -0
  77. fontTools/ttLib/tables/_a_v_a_r.py +12 -3
  78. fontTools/ttLib/tables/_c_m_a_p.py +20 -7
  79. fontTools/ttLib/tables/_c_v_t.py +3 -2
  80. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  81. fontTools/ttLib/tables/_g_l_y_f.py +45 -21
  82. fontTools/ttLib/tables/_g_v_a_r.py +67 -19
  83. fontTools/ttLib/tables/_h_d_m_x.py +4 -4
  84. fontTools/ttLib/tables/_h_m_t_x.py +7 -3
  85. fontTools/ttLib/tables/_l_o_c_a.py +2 -2
  86. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  87. fontTools/ttLib/tables/_p_o_s_t.py +9 -7
  88. fontTools/ttLib/tables/otBase.py +5 -12
  89. fontTools/ttLib/tables/otConverters.py +5 -2
  90. fontTools/ttLib/tables/otData.py +1 -1
  91. fontTools/ttLib/tables/otTables.py +33 -30
  92. fontTools/ttLib/tables/otTraverse.py +2 -1
  93. fontTools/ttLib/tables/sbixStrike.py +3 -3
  94. fontTools/ttLib/ttFont.py +666 -120
  95. fontTools/ttLib/ttGlyphSet.py +0 -10
  96. fontTools/ttLib/woff2.py +10 -13
  97. fontTools/ttx.py +13 -1
  98. fontTools/ufoLib/__init__.py +300 -202
  99. fontTools/ufoLib/converters.py +103 -30
  100. fontTools/ufoLib/errors.py +8 -0
  101. fontTools/ufoLib/etree.py +1 -1
  102. fontTools/ufoLib/filenames.py +171 -106
  103. fontTools/ufoLib/glifLib.py +303 -205
  104. fontTools/ufoLib/kerning.py +98 -48
  105. fontTools/ufoLib/utils.py +46 -15
  106. fontTools/ufoLib/validators.py +121 -99
  107. fontTools/unicodedata/Blocks.py +35 -20
  108. fontTools/unicodedata/Mirrored.py +446 -0
  109. fontTools/unicodedata/ScriptExtensions.py +63 -37
  110. fontTools/unicodedata/Scripts.py +173 -152
  111. fontTools/unicodedata/__init__.py +10 -2
  112. fontTools/varLib/__init__.py +198 -109
  113. fontTools/varLib/avar/__init__.py +0 -0
  114. fontTools/varLib/avar/__main__.py +72 -0
  115. fontTools/varLib/avar/build.py +79 -0
  116. fontTools/varLib/avar/map.py +108 -0
  117. fontTools/varLib/avar/plan.py +1004 -0
  118. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  119. fontTools/varLib/avarPlanner.py +3 -999
  120. fontTools/varLib/featureVars.py +21 -7
  121. fontTools/varLib/hvar.py +113 -0
  122. fontTools/varLib/instancer/__init__.py +180 -65
  123. fontTools/varLib/interpolatableHelpers.py +3 -0
  124. fontTools/varLib/iup.c +7564 -6903
  125. fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
  126. fontTools/varLib/models.py +17 -2
  127. fontTools/varLib/mutator.py +11 -0
  128. fontTools/varLib/varStore.py +10 -38
  129. fontTools/voltLib/__main__.py +206 -0
  130. fontTools/voltLib/ast.py +4 -0
  131. fontTools/voltLib/parser.py +16 -8
  132. fontTools/voltLib/voltToFea.py +347 -166
  133. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
  134. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
  135. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
  136. fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
  137. {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
  138. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
  139. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
  140. {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # Google Author(s): Behdad Esfahbod
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  from fontTools import config
6
8
  from fontTools.misc.roundTools import otRound
7
9
  from fontTools import ttLib
@@ -15,7 +17,8 @@ from fontTools.subset.util import _add_method, _uniq_sort
15
17
  from fontTools.subset.cff import *
16
18
  from fontTools.subset.svg import *
17
19
  from fontTools.varLib import varStore, multiVarStore # For monkey-patching
18
- from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor
20
+ from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor, makeName
21
+ from fontTools.unicodedata import mirrored
19
22
  import sys
20
23
  import struct
21
24
  import array
@@ -24,16 +27,16 @@ from collections import Counter, defaultdict
24
27
  from functools import reduce
25
28
  from types import MethodType
26
29
 
27
- __usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
30
+ __usage__ = "fonttools subset font-file [glyph...] [--option=value]..."
28
31
 
29
32
  __doc__ = (
30
33
  """\
31
- pyftsubset -- OpenType font subsetter and optimizer
34
+ fonttools subset -- OpenType font subsetter and optimizer
32
35
 
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.
36
+ fonttools subset is an OpenType font subsetter and optimizer, based on
37
+ fontTools. It accepts any TT- or CFF-flavored OpenType (.otf or .ttf)
38
+ or WOFF (.woff) font file. The subsetted glyph set is based on the
39
+ specified glyphs or characters, and specified OpenType layout features.
37
40
 
38
41
  The tool also performs some size-reducing optimizations, aimed for using
39
42
  subset fonts as webfonts. Individual optimizations can be enabled or
@@ -127,11 +130,11 @@ you might need to escape the question mark, like this: '--glyph-names\\?'.
127
130
 
128
131
  Examples::
129
132
 
130
- $ pyftsubset --glyph-names?
133
+ $ fonttools subset --glyph-names?
131
134
  Current setting for 'glyph-names' is: False
132
- $ pyftsubset --name-IDs=?
135
+ $ fonttools subset --name-IDs=?
133
136
  Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6]
134
- $ pyftsubset --hinting? --no-hinting --hinting?
137
+ $ fonttools subset --hinting? --no-hinting --hinting?
135
138
  Current setting for 'hinting' is: True
136
139
  Current setting for 'hinting' is: False
137
140
 
@@ -269,7 +272,7 @@ Font table options
269
272
  Specify (=), add to (+=) or exclude from (-=) the comma-separated
270
273
  set of tables that will be be dropped.
271
274
  By default, the following tables are dropped:
272
- 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
275
+ 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
273
276
  and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'.
274
277
  The tool will attempt to subset the remaining tables.
275
278
 
@@ -442,7 +445,7 @@ Example
442
445
  Produce a subset containing the characters ' !"#$%' without performing
443
446
  size-reducing optimizations::
444
447
 
445
- $ pyftsubset font.ttf --unicodes="U+0020-0025" \\
448
+ $ fonttools subset font.ttf --unicodes="U+0020-0025" \\
446
449
  --layout-features=* --glyph-names --symbol-cmap --legacy-cmap \\
447
450
  --notdef-glyph --notdef-outline --recommended-glyphs \\
448
451
  --name-IDs=* --name-legacy --name-languages=*
@@ -824,13 +827,26 @@ def subset_glyphs(self, s):
824
827
  self.MarkArray.MarkRecord, mark_indices
825
828
  )
826
829
  self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
827
- base_indices = self.BaseCoverage.subset(s.glyphs)
830
+ class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
831
+
832
+ intersect_base_indices = self.BaseCoverage.intersect(s.glyphs)
833
+ base_records = self.BaseArray.BaseRecord
834
+ num_base_records = len(base_records)
835
+ base_indices = [
836
+ i
837
+ for i in intersect_base_indices
838
+ if i < num_base_records
839
+ and any(base_records[i].BaseAnchor[j] is not None for j in class_indices)
840
+ ]
841
+ if not base_indices:
842
+ return False
843
+
844
+ self.BaseCoverage.remap(base_indices)
828
845
  self.BaseArray.BaseRecord = _list_subset(
829
846
  self.BaseArray.BaseRecord, base_indices
830
847
  )
831
848
  self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
832
849
  # Prune empty classes
833
- class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
834
850
  self.ClassCount = len(class_indices)
835
851
  for m in self.MarkArray.MarkRecord:
836
852
  m.Class = class_indices.index(m.Class)
@@ -864,13 +880,31 @@ def subset_glyphs(self, s):
864
880
  self.MarkArray.MarkRecord, mark_indices
865
881
  )
866
882
  self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
867
- ligature_indices = self.LigatureCoverage.subset(s.glyphs)
883
+ class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
884
+
885
+ intersect_ligature_indices = self.LigatureCoverage.intersect(s.glyphs)
886
+ ligature_array = self.LigatureArray.LigatureAttach
887
+ num_ligatures = self.LigatureArray.LigatureCount
888
+
889
+ ligature_indices = [
890
+ i
891
+ for i in intersect_ligature_indices
892
+ if i < num_ligatures
893
+ and any(
894
+ any(component.LigatureAnchor[j] is not None for j in class_indices)
895
+ for component in ligature_array[i].ComponentRecord
896
+ )
897
+ ]
898
+
899
+ if not ligature_indices:
900
+ return False
901
+
902
+ self.LigatureCoverage.remap(ligature_indices)
868
903
  self.LigatureArray.LigatureAttach = _list_subset(
869
904
  self.LigatureArray.LigatureAttach, ligature_indices
870
905
  )
871
906
  self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
872
907
  # Prune empty classes
873
- class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
874
908
  self.ClassCount = len(class_indices)
875
909
  for m in self.MarkArray.MarkRecord:
876
910
  m.Class = class_indices.index(m.Class)
@@ -912,13 +946,26 @@ def subset_glyphs(self, s):
912
946
  self.Mark1Array.MarkRecord, mark1_indices
913
947
  )
914
948
  self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
915
- mark2_indices = self.Mark2Coverage.subset(s.glyphs)
949
+ class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
950
+
951
+ intersect_mark2_indices = self.Mark2Coverage.intersect(s.glyphs)
952
+ mark2_records = self.Mark2Array.Mark2Record
953
+ num_mark2_records = len(mark2_records)
954
+ mark2_indices = [
955
+ i
956
+ for i in intersect_mark2_indices
957
+ if i < num_mark2_records
958
+ and any(mark2_records[i].Mark2Anchor[j] is not None for j in class_indices)
959
+ ]
960
+ if not mark2_indices:
961
+ return False
962
+
963
+ self.Mark2Coverage.remap(mark2_indices)
916
964
  self.Mark2Array.Mark2Record = _list_subset(
917
965
  self.Mark2Array.Mark2Record, mark2_indices
918
966
  )
919
967
  self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
920
968
  # Prune empty classes
921
- class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
922
969
  self.ClassCount = len(class_indices)
923
970
  for m in self.Mark1Array.MarkRecord:
924
971
  m.Class = class_indices.index(m.Class)
@@ -1527,6 +1574,7 @@ def subset_glyphs(self, s):
1527
1574
  if self.MarkFilteringSet not in s.used_mark_sets:
1528
1575
  self.MarkFilteringSet = None
1529
1576
  self.LookupFlag &= ~0x10
1577
+ self.LookupFlag |= 0x8
1530
1578
  else:
1531
1579
  self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet)
1532
1580
  return bool(self.SubTableCount)
@@ -1713,6 +1761,19 @@ def subset_features(self, feature_indices):
1713
1761
  return bool(self.SubstitutionCount)
1714
1762
 
1715
1763
 
1764
+ @_add_method(otTables.FeatureTableSubstitution)
1765
+ def prune_features(self, feature_index_map):
1766
+ self.ensureDecompiled()
1767
+ self.SubstitutionRecord = [
1768
+ r for r in self.SubstitutionRecord if r.FeatureIndex in feature_index_map.keys()
1769
+ ]
1770
+ # remap feature indices
1771
+ for r in self.SubstitutionRecord:
1772
+ r.FeatureIndex = feature_index_map[r.FeatureIndex]
1773
+ self.SubstitutionCount = len(self.SubstitutionRecord)
1774
+ return bool(self.SubstitutionCount)
1775
+
1776
+
1716
1777
  @_add_method(otTables.FeatureVariations)
1717
1778
  def subset_features(self, feature_indices):
1718
1779
  self.ensureDecompiled()
@@ -1731,6 +1792,24 @@ def subset_features(self, feature_indices):
1731
1792
  return bool(self.FeatureVariationCount)
1732
1793
 
1733
1794
 
1795
+ @_add_method(otTables.FeatureVariations)
1796
+ def prune_features(self, feature_index_map):
1797
+ self.ensureDecompiled()
1798
+ for r in self.FeatureVariationRecord:
1799
+ r.FeatureTableSubstitution.prune_features(feature_index_map)
1800
+ # Prune empty records at the end only
1801
+ # https://github.com/fonttools/fonttools/issues/1881
1802
+ while (
1803
+ self.FeatureVariationRecord
1804
+ and not self.FeatureVariationRecord[
1805
+ -1
1806
+ ].FeatureTableSubstitution.SubstitutionCount
1807
+ ):
1808
+ self.FeatureVariationRecord.pop()
1809
+ self.FeatureVariationCount = len(self.FeatureVariationRecord)
1810
+ return bool(self.FeatureVariationCount)
1811
+
1812
+
1734
1813
  @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1735
1814
  def subset_features(self, feature_indices):
1736
1815
  if self.ReqFeatureIndex in feature_indices:
@@ -1746,6 +1825,16 @@ def subset_features(self, feature_indices):
1746
1825
  return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
1747
1826
 
1748
1827
 
1828
+ @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1829
+ def prune_features(self, feature_index_map):
1830
+ self.ReqFeatureIndex = feature_index_map.get(self.ReqFeatureIndex, 65535)
1831
+ self.FeatureIndex = [
1832
+ feature_index_map[f] for f in self.FeatureIndex if f in feature_index_map.keys()
1833
+ ]
1834
+ self.FeatureCount = len(self.FeatureIndex)
1835
+ return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
1836
+
1837
+
1749
1838
  @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1750
1839
  def collect_features(self):
1751
1840
  feature_indices = self.FeatureIndex[:]
@@ -1769,6 +1858,21 @@ def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False):
1769
1858
  return bool(self.LangSysCount or self.DefaultLangSys)
1770
1859
 
1771
1860
 
1861
+ @_add_method(otTables.Script)
1862
+ def prune_features(self, feature_index_map, keepEmptyDefaultLangSys=False):
1863
+ if (
1864
+ self.DefaultLangSys
1865
+ and not self.DefaultLangSys.prune_features(feature_index_map)
1866
+ and not keepEmptyDefaultLangSys
1867
+ ):
1868
+ self.DefaultLangSys = None
1869
+ self.LangSysRecord = [
1870
+ l for l in self.LangSysRecord if l.LangSys.prune_features(feature_index_map)
1871
+ ]
1872
+ self.LangSysCount = len(self.LangSysRecord)
1873
+ return bool(self.LangSysCount or self.DefaultLangSys)
1874
+
1875
+
1772
1876
  @_add_method(otTables.Script)
1773
1877
  def collect_features(self):
1774
1878
  feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
@@ -1790,6 +1894,19 @@ def subset_features(self, feature_indices, retain_empty):
1790
1894
  return bool(self.ScriptCount)
1791
1895
 
1792
1896
 
1897
+ @_add_method(otTables.ScriptList)
1898
+ def prune_features(self, feature_index_map, retain_empty):
1899
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32
1900
+ self.ScriptRecord = [
1901
+ s
1902
+ for s in self.ScriptRecord
1903
+ if s.Script.prune_features(feature_index_map, s.ScriptTag == "DFLT")
1904
+ or retain_empty
1905
+ ]
1906
+ self.ScriptCount = len(self.ScriptRecord)
1907
+ return bool(self.ScriptCount)
1908
+
1909
+
1793
1910
  @_add_method(otTables.ScriptList)
1794
1911
  def collect_features(self):
1795
1912
  return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), []))
@@ -1977,21 +2094,74 @@ def subset_script_tags(self, tags):
1977
2094
 
1978
2095
  @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1979
2096
  def prune_features(self):
1980
- """Remove unreferenced features"""
2097
+ """Remove unreferenced and duplicate features in FeatureList
2098
+ Remove unreferenced features and remap duplicate feature indices in ScriptList and FeatureVariations
2099
+ """
1981
2100
  if self.table.ScriptList:
1982
2101
  feature_indices = self.table.ScriptList.collect_features()
1983
2102
  else:
1984
2103
  feature_indices = []
2104
+ (feature_indices, feature_index_map) = self.remap_duplicate_features(
2105
+ feature_indices
2106
+ )
2107
+
1985
2108
  if self.table.FeatureList:
1986
2109
  self.table.FeatureList.subset_features(feature_indices)
1987
2110
  if getattr(self.table, "FeatureVariations", None):
1988
- self.table.FeatureVariations.subset_features(feature_indices)
2111
+ self.table.FeatureVariations.prune_features(feature_index_map)
1989
2112
  if self.table.ScriptList:
1990
- self.table.ScriptList.subset_features(
1991
- feature_indices, self.retain_empty_scripts()
2113
+ self.table.ScriptList.prune_features(
2114
+ feature_index_map, self.retain_empty_scripts()
1992
2115
  )
1993
2116
 
1994
2117
 
2118
+ @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2119
+ def remap_duplicate_features(self, feature_indices):
2120
+ """Return retained feature indices(without duplicates) and remapped feature indices"""
2121
+ features = self.table.FeatureList.FeatureRecord
2122
+
2123
+ unique_features = {}
2124
+ duplicate_features = {}
2125
+ for i in feature_indices:
2126
+ f = features[i]
2127
+ tag = f.FeatureTag
2128
+
2129
+ same_tag_features = unique_features.get(tag)
2130
+ if same_tag_features is None:
2131
+ unique_features[tag] = set([i])
2132
+ duplicate_features[i] = i
2133
+ continue
2134
+
2135
+ found = False
2136
+ for other_i in same_tag_features:
2137
+ if features[other_i] == f:
2138
+ found = True
2139
+ duplicate_features[i] = other_i
2140
+ break
2141
+
2142
+ if not found:
2143
+ same_tag_features.add(i)
2144
+ duplicate_features[i] = i
2145
+
2146
+ ## remap retained feature indices
2147
+ feature_map = {}
2148
+ new_idx = 0
2149
+
2150
+ for i in feature_indices:
2151
+ unique_i = duplicate_features.get(i, i)
2152
+ v = feature_map.get(unique_i)
2153
+ if v is None:
2154
+ feature_map[i] = new_idx
2155
+ new_idx += 1
2156
+ else:
2157
+ feature_map[i] = v
2158
+
2159
+ retained_feature_indices = _uniq_sort(
2160
+ sum((list(s) for s in unique_features.values()), [])
2161
+ )
2162
+ return (retained_feature_indices, feature_map)
2163
+
2164
+
1995
2165
  @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1996
2166
  def prune_pre_subset(self, font, options):
1997
2167
  # Drop undesired features
@@ -2870,6 +3040,15 @@ def prune_post_subset(self, font, options):
2870
3040
  def closure_glyphs(self, s):
2871
3041
  tables = [t for t in self.tables if t.isUnicode()]
2872
3042
 
3043
+ # Closure unicodes, which for now is pulling in bidi mirrored variants
3044
+ if s.options.bidi_closure:
3045
+ additional_unicodes = set()
3046
+ for u in s.unicodes_requested:
3047
+ mirror_u = mirrored(u)
3048
+ if mirror_u is not None:
3049
+ additional_unicodes.add(mirror_u)
3050
+ s.unicodes_requested.update(additional_unicodes)
3051
+
2873
3052
  # Close glyphs
2874
3053
  for table in tables:
2875
3054
  if table.format == 14:
@@ -2994,6 +3173,9 @@ def prune_pre_subset(self, font, options):
2994
3173
  return True
2995
3174
 
2996
3175
 
3176
+ NAME_IDS_TO_OBFUSCATE = {1, 2, 3, 4, 6, 16, 17, 18}
3177
+
3178
+
2997
3179
  @_add_method(ttLib.getTableClass("name"))
2998
3180
  def prune_post_subset(self, font, options):
2999
3181
  visitor = NameRecordVisitor()
@@ -3012,6 +3194,11 @@ def prune_post_subset(self, font, options):
3012
3194
  self.names = [n for n in self.names if n.langID in options.name_languages]
3013
3195
  if options.obfuscate_names:
3014
3196
  namerecs = []
3197
+ # Preserve names to be scrambled or dropped elsewhere so that other
3198
+ # parts of the font don't break.
3199
+ needRemapping = visitor.seen.intersection(NAME_IDS_TO_OBFUSCATE)
3200
+ if needRemapping:
3201
+ _remap_select_name_ids(font, needRemapping)
3015
3202
  for n in self.names:
3016
3203
  if n.nameID in [1, 4]:
3017
3204
  n.string = ".\x7f".encode("utf_16_be") if n.isUnicode() else ".\x7f"
@@ -3026,6 +3213,76 @@ def prune_post_subset(self, font, options):
3026
3213
  return True # Required table
3027
3214
 
3028
3215
 
3216
+ def _remap_select_name_ids(font: ttLib.TTFont, needRemapping: set[int]) -> None:
3217
+ """Remap a set of IDs so that the originals can be safely scrambled or
3218
+ dropped.
3219
+
3220
+ For each name record whose name id is in the `needRemapping` set, make a copy
3221
+ and allocate a new unused name id in the font-specific range (> 255).
3222
+
3223
+ Finally update references to these in the `fvar` and `STAT` tables.
3224
+ """
3225
+
3226
+ if "fvar" not in font and "STAT" not in font:
3227
+ return
3228
+
3229
+ name = font["name"]
3230
+
3231
+ # 1. Assign new IDs for names to be preserved.
3232
+ existingIds = {record.nameID for record in name.names}
3233
+ remapping = {}
3234
+ nextId = name._findUnusedNameID() - 1 # Should skip gaps in name IDs.
3235
+ for nameId in needRemapping:
3236
+ nextId += 1 # We should have complete freedom until 32767.
3237
+ assert nextId not in existingIds, "_findUnusedNameID did not skip gaps"
3238
+ if nextId > 32767:
3239
+ raise ValueError("Ran out of name IDs while trying to remap existing ones.")
3240
+ remapping[nameId] = nextId
3241
+
3242
+ # 2. Copy records to use the new ID. We can't rewrite them in place, because
3243
+ # that could make IDs 1 to 6 "disappear" from code that follows. Some
3244
+ # tools that produce EOT fonts expect them to exist, even when they're
3245
+ # scrambled. See https://github.com/fonttools/fonttools/issues/165.
3246
+ copiedRecords = []
3247
+ for record in name.names:
3248
+ if record.nameID not in needRemapping:
3249
+ continue
3250
+ recordCopy = makeName(
3251
+ record.string,
3252
+ remapping[record.nameID],
3253
+ record.platformID,
3254
+ record.platEncID,
3255
+ record.langID,
3256
+ )
3257
+ copiedRecords.append(recordCopy)
3258
+ name.names.extend(copiedRecords)
3259
+
3260
+ # 3. Rewrite the corresponding IDs in other tables. For now, care only about
3261
+ # STAT and fvar. If more tables need to be changed, consider adapting
3262
+ # NameRecordVisitor to rewrite IDs wherever it finds them.
3263
+ fvar = font.get("fvar")
3264
+ if fvar is not None:
3265
+ for axis in fvar.axes:
3266
+ axis.axisNameID = remapping.get(axis.axisNameID, axis.axisNameID)
3267
+ for instance in fvar.instances:
3268
+ nameID = instance.subfamilyNameID
3269
+ instance.subfamilyNameID = remapping.get(nameID, nameID)
3270
+ nameID = instance.postscriptNameID
3271
+ instance.postscriptNameID = remapping.get(nameID, nameID)
3272
+
3273
+ stat = font.get("STAT")
3274
+ if stat is None:
3275
+ return
3276
+ elidedNameID = stat.table.ElidedFallbackNameID
3277
+ stat.table.ElidedFallbackNameID = remapping.get(elidedNameID, elidedNameID)
3278
+ if stat.table.DesignAxisRecord:
3279
+ for axis in stat.table.DesignAxisRecord.Axis:
3280
+ axis.AxisNameID = remapping.get(axis.AxisNameID, axis.AxisNameID)
3281
+ if stat.table.AxisValueArray:
3282
+ for value in stat.table.AxisValueArray.AxisValue:
3283
+ value.ValueNameID = remapping.get(value.ValueNameID, value.ValueNameID)
3284
+
3285
+
3029
3286
  @_add_method(ttLib.getTableClass("head"))
3030
3287
  def prune_post_subset(self, font, options):
3031
3288
  # Force re-compiling head table, to update any recalculated values.
@@ -3054,7 +3311,6 @@ class Options(object):
3054
3311
 
3055
3312
  # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser
3056
3313
  _drop_tables_default = [
3057
- "BASE",
3058
3314
  "JSTF",
3059
3315
  "DSIG",
3060
3316
  "EBDT",
@@ -3066,6 +3322,7 @@ class Options(object):
3066
3322
  _drop_tables_default += ["Feat", "Glat", "Gloc", "Silf", "Sill"] # Graphite
3067
3323
  _no_subset_tables_default = [
3068
3324
  "avar",
3325
+ "BASE",
3069
3326
  "fvar",
3070
3327
  "gasp",
3071
3328
  "head",
@@ -3191,6 +3448,7 @@ class Options(object):
3191
3448
  self.font_number = -1
3192
3449
  self.pretty_svg = False
3193
3450
  self.lazy = True
3451
+ self.bidi_closure = True
3194
3452
 
3195
3453
  self.set(**kwargs)
3196
3454
 
@@ -3677,7 +3935,7 @@ def parse_glyphs(s):
3677
3935
 
3678
3936
  def usage():
3679
3937
  print("usage:", __usage__, file=sys.stderr)
3680
- print("Try pyftsubset --help for more information.\n", file=sys.stderr)
3938
+ print("Try fonttools subset --help for more information.\n", file=sys.stderr)
3681
3939
 
3682
3940
 
3683
3941
  @timer("make one with everything (TOTAL TIME)")
@@ -3746,7 +4004,7 @@ def main(args=None):
3746
4004
  text += g[7:]
3747
4005
  continue
3748
4006
  if g.startswith("--text-file="):
3749
- with open(g[12:], encoding="utf-8") as f:
4007
+ with open(g[12:], encoding="utf-8-sig") as f:
3750
4008
  text += f.read().replace("\n", "")
3751
4009
  continue
3752
4010
  if g.startswith("--unicodes="):
fontTools/subset/svg.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from functools import lru_cache
4
+ from functools import cache
5
5
  from itertools import chain, count
6
6
  from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
7
7
 
@@ -29,8 +29,7 @@ NAMESPACES = {
29
29
  XLINK_HREF = f'{{{NAMESPACES["xlink"]}}}href'
30
30
 
31
31
 
32
- # TODO(antrotype): Replace with functools.cache once we are 3.9+
33
- @lru_cache(maxsize=None)
32
+ @cache
34
33
  def xpath(path):
35
34
  # compile XPath upfront, caching result to reuse on multiple elements
36
35
  return etree.XPath(path, namespaces=NAMESPACES)
@@ -1,5 +1,6 @@
1
1
  """fontTools.ttLib -- a package for dealing with TrueType fonts."""
2
2
 
3
+ from fontTools.config import OPTIONS
3
4
  from fontTools.misc.loggingTools import deprecateFunction
4
5
  import logging
5
6
 
@@ -7,6 +8,9 @@ import logging
7
8
  log = logging.getLogger(__name__)
8
9
 
9
10
 
11
+ OPTIMIZE_FONT_SPEED = OPTIONS["fontTools.ttLib:OPTIMIZE_FONT_SPEED"]
12
+
13
+
10
14
  class TTLibError(Exception):
11
15
  pass
12
16
 
@@ -1,5 +1,5 @@
1
1
  import sys
2
- from fontTools.ttLib import TTLibError, TTLibFileIsCollectionError
2
+ from fontTools.ttLib import OPTIMIZE_FONT_SPEED, TTLibError, TTLibFileIsCollectionError
3
3
  from fontTools.ttLib.ttFont import *
4
4
  from fontTools.ttLib.ttCollection import TTCollection
5
5
 
@@ -51,7 +51,7 @@ def main(args=None):
51
51
  )
52
52
  parser.add_argument("font", metavar="font", nargs="*", help="Font file.")
53
53
  parser.add_argument(
54
- "-t", "--table", metavar="table", nargs="*", help="Tables to decompile."
54
+ "-t", "--table", metavar="table", action="append", help="Tables to decompile."
55
55
  )
56
56
  parser.add_argument(
57
57
  "-o", "--output", metavar="FILE", default=None, help="Output file."
@@ -71,27 +71,66 @@ def main(args=None):
71
71
  default=None,
72
72
  help="Flavor of output font. 'woff' or 'woff2'.",
73
73
  )
74
+ parser.add_argument(
75
+ "--no-recalc-timestamp",
76
+ dest="recalcTimestamp",
77
+ action="store_false",
78
+ help="Keep the original font 'modified' timestamp.",
79
+ )
80
+ parser.add_argument(
81
+ "-b",
82
+ dest="recalcBBoxes",
83
+ action="store_false",
84
+ help="Don't recalc glyph bounding boxes: use the values in the original font.",
85
+ )
86
+ parser.add_argument(
87
+ "--optimize-font-speed",
88
+ action="store_true",
89
+ help=(
90
+ "Enable optimizations that prioritize speed over file size. This "
91
+ "mainly affects how glyf table and gvar / VARC tables are compiled."
92
+ ),
93
+ )
74
94
  options = parser.parse_args(args)
75
95
 
76
96
  fontNumber = int(options.y) if options.y is not None else None
77
97
  outFile = options.output
78
98
  lazy = options.lazy
79
99
  flavor = options.flavor
80
- tables = options.table if options.table is not None else ["*"]
100
+ tables = options.table
101
+ recalcBBoxes = options.recalcBBoxes
102
+ recalcTimestamp = options.recalcTimestamp
103
+ optimizeFontSpeed = options.optimize_font_speed
81
104
 
82
105
  fonts = []
83
106
  for f in options.font:
84
107
  try:
85
- font = TTFont(f, fontNumber=fontNumber, lazy=lazy)
108
+ font = TTFont(
109
+ f,
110
+ recalcBBoxes=recalcBBoxes,
111
+ recalcTimestamp=recalcTimestamp,
112
+ fontNumber=fontNumber,
113
+ lazy=lazy,
114
+ )
115
+ if optimizeFontSpeed:
116
+ font.cfg[OPTIMIZE_FONT_SPEED] = optimizeFontSpeed
86
117
  fonts.append(font)
87
118
  except TTLibFileIsCollectionError:
88
119
  collection = TTCollection(f, lazy=lazy)
89
120
  fonts.extend(collection.fonts)
90
121
 
91
- if lazy is False:
92
- for font in fonts:
93
- for table in tables if "*" not in tables else font.keys():
94
- font[table] # Decompiles
122
+ if tables is None:
123
+ if lazy is False:
124
+ tables = ["*"]
125
+ elif optimizeFontSpeed:
126
+ tables = {"glyf", "gvar", "VARC"}.intersection(font.keys())
127
+ else:
128
+ tables = []
129
+ for font in fonts:
130
+ if "GlyphOrder" in tables:
131
+ font.getGlyphOrder()
132
+ for table in tables if "*" not in tables else font.keys():
133
+ font[table] # Decompiles
95
134
 
96
135
  if outFile is not None:
97
136
  if len(fonts) == 1:
@@ -1,4 +1,4 @@
1
- """ Simplify TrueType glyphs by merging overlapping contours/components.
1
+ """Simplify TrueType glyphs by merging overlapping contours/components.
2
2
 
3
3
  Requires https://github.com/fonttools/skia-pathops
4
4
  """
@@ -250,9 +250,10 @@ def _remove_cff_overlaps(
250
250
  glyphSet: _TTGlyphMapping,
251
251
  removeHinting: bool,
252
252
  ignoreErrors: bool,
253
+ table_tag: str,
253
254
  removeUnusedSubroutines: bool = True,
254
255
  ) -> None:
255
- cffFontSet = font["CFF "].cff
256
+ cffFontSet = font[table_tag].cff
256
257
  modified = set()
257
258
  for glyphName in glyphNames:
258
259
  try:
@@ -311,9 +312,9 @@ def removeOverlaps(
311
312
  any glyphs are modified.
312
313
  """
313
314
 
314
- if "glyf" not in font and "CFF " not in font:
315
+ if "glyf" not in font and "CFF " not in font and "CFF2" not in font:
315
316
  raise NotImplementedError(
316
- "No outline data found in the font: missing 'glyf' or 'CFF ' table"
317
+ "No outline data found in the font: missing 'glyf', 'CFF ', or 'CFF2' table"
317
318
  )
318
319
 
319
320
  if glyphNames is None:
@@ -331,13 +332,14 @@ def removeOverlaps(
331
332
  ignoreErrors=ignoreErrors,
332
333
  )
333
334
 
334
- if "CFF " in font:
335
+ if "CFF " in font or "CFF2" in font:
335
336
  _remove_cff_overlaps(
336
337
  font=font,
337
338
  glyphNames=glyphNames,
338
339
  glyphSet=glyphSet,
339
340
  removeHinting=removeHinting,
340
341
  ignoreErrors=ignoreErrors,
342
+ table_tag="CFF " if "CFF " in font else "CFF2",
341
343
  removeUnusedSubroutines=removeUnusedSubroutines,
342
344
  )
343
345