fonttools 4.59.2__cp311-cp311-win32.whl → 4.60.1__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (44) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cu2qu/cu2qu.c +1067 -946
  4. fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
  5. fontTools/cu2qu/cu2qu.py +19 -2
  6. fontTools/feaLib/lexer.c +18 -12
  7. fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
  8. fontTools/misc/bezierTools.c +18 -12
  9. fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
  10. fontTools/misc/enumTools.py +23 -0
  11. fontTools/misc/visitor.py +24 -16
  12. fontTools/pens/filterPen.py +218 -26
  13. fontTools/pens/momentsPen.c +18 -12
  14. fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
  15. fontTools/pens/pointPen.py +40 -6
  16. fontTools/qu2cu/qu2cu.c +30 -16
  17. fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
  18. fontTools/subset/__init__.py +178 -12
  19. fontTools/ttLib/tables/_p_o_s_t.py +5 -5
  20. fontTools/ufoLib/__init__.py +278 -175
  21. fontTools/ufoLib/converters.py +14 -5
  22. fontTools/ufoLib/filenames.py +16 -6
  23. fontTools/ufoLib/glifLib.py +286 -190
  24. fontTools/ufoLib/kerning.py +32 -12
  25. fontTools/ufoLib/utils.py +41 -13
  26. fontTools/ufoLib/validators.py +121 -97
  27. fontTools/varLib/avar/__init__.py +0 -0
  28. fontTools/varLib/avar/__main__.py +72 -0
  29. fontTools/varLib/avar/build.py +79 -0
  30. fontTools/varLib/avar/map.py +108 -0
  31. fontTools/varLib/avar/plan.py +1004 -0
  32. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  33. fontTools/varLib/avarPlanner.py +3 -999
  34. fontTools/varLib/interpolatableHelpers.py +3 -0
  35. fontTools/varLib/iup.c +24 -14
  36. fontTools/varLib/iup.cp311-win32.pyd +0 -0
  37. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/METADATA +41 -2
  38. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/RECORD +44 -37
  39. {fonttools-4.59.2.data → fonttools-4.60.1.data}/data/share/man/man1/ttx.1 +0 -0
  40. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/WHEEL +0 -0
  41. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/entry_points.txt +0 -0
  42. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/licenses/LICENSE +0 -0
  43. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/licenses/LICENSE.external +0 -0
  44. {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import math
18
18
  from typing import Any, Dict, List, Optional, Tuple
19
19
 
20
+ from fontTools.misc.enumTools import StrEnum
20
21
  from fontTools.misc.loggingTools import LogMixin
21
22
  from fontTools.misc.transform import DecomposedTransform, Identity
22
23
  from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError
@@ -28,6 +29,7 @@ __all__ = [
28
29
  "SegmentToPointPen",
29
30
  "GuessSmoothPointPen",
30
31
  "ReverseContourPointPen",
32
+ "ReverseFlipped",
31
33
  ]
32
34
 
33
35
  # Some type aliases to make it easier below
@@ -39,6 +41,19 @@ SegmentType = Optional[str]
39
41
  SegmentList = List[Tuple[SegmentType, SegmentPointList]]
40
42
 
41
43
 
44
+ class ReverseFlipped(StrEnum):
45
+ """How to handle flipped components during decomposition.
46
+
47
+ NO: Don't reverse flipped components
48
+ KEEP_START: Reverse flipped components, keeping original starting point
49
+ ON_CURVE_FIRST: Reverse flipped components, ensuring first point is on-curve
50
+ """
51
+
52
+ NO = "no"
53
+ KEEP_START = "keep_start"
54
+ ON_CURVE_FIRST = "on_curve_first"
55
+
56
+
42
57
  class AbstractPointPen:
43
58
  """Baseclass for all PointPens."""
44
59
 
@@ -559,15 +574,20 @@ class DecomposingPointPen(LogMixin, AbstractPointPen):
559
574
  glyphSet,
560
575
  *args,
561
576
  skipMissingComponents=None,
562
- reverseFlipped=False,
577
+ reverseFlipped: bool | ReverseFlipped = False,
563
578
  **kwargs,
564
579
  ):
565
580
  """Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced
566
581
  as components are looked up by their name.
567
582
 
568
- If the optional 'reverseFlipped' argument is True, components whose transformation
569
- matrix has a negative determinant will be decomposed with a reversed path direction
570
- to compensate for the flip.
583
+ If the optional 'reverseFlipped' argument is True or a ReverseFlipped enum value,
584
+ components whose transformation matrix has a negative determinant will be decomposed
585
+ with a reversed path direction to compensate for the flip.
586
+
587
+ The reverseFlipped parameter can be:
588
+ - False or ReverseFlipped.NO: Don't reverse flipped components
589
+ - True or ReverseFlipped.KEEP_START: Reverse, keeping original starting point
590
+ - ReverseFlipped.ON_CURVE_FIRST: Reverse, ensuring first point is on-curve
571
591
 
572
592
  The optional 'skipMissingComponents' argument can be set to True/False to
573
593
  override the homonymous class attribute for a given pen instance.
@@ -579,7 +599,13 @@ class DecomposingPointPen(LogMixin, AbstractPointPen):
579
599
  if skipMissingComponents is None
580
600
  else skipMissingComponents
581
601
  )
582
- self.reverseFlipped = reverseFlipped
602
+ # Handle backward compatibility and validate string inputs
603
+ if reverseFlipped is False:
604
+ self.reverseFlipped = ReverseFlipped.NO
605
+ elif reverseFlipped is True:
606
+ self.reverseFlipped = ReverseFlipped.KEEP_START
607
+ else:
608
+ self.reverseFlipped = ReverseFlipped(reverseFlipped)
583
609
 
584
610
  def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
585
611
  """Transform the points of the base glyph and draw it onto self.
@@ -600,10 +626,18 @@ class DecomposingPointPen(LogMixin, AbstractPointPen):
600
626
  pen = self
601
627
  if transformation != Identity:
602
628
  pen = TransformPointPen(pen, transformation)
603
- if self.reverseFlipped:
629
+ if self.reverseFlipped != ReverseFlipped.NO:
604
630
  # if the transformation has a negative determinant, it will
605
631
  # reverse the contour direction of the component
606
632
  a, b, c, d = transformation[:4]
607
633
  if a * d - b * c < 0:
608
634
  pen = ReverseContourPointPen(pen)
635
+
636
+ if self.reverseFlipped == ReverseFlipped.ON_CURVE_FIRST:
637
+ from fontTools.pens.filterPen import OnCurveFirstPointPen
638
+
639
+ # Ensure the starting point is an on-curve.
640
+ # Wrap last so this filter runs first during drawPoints
641
+ pen = OnCurveFirstPointPen(pen)
642
+
609
643
  glyph.drawPoints(pen)
fontTools/qu2cu/qu2cu.c CHANGED
@@ -1,4 +1,4 @@
1
- /* Generated by Cython 3.1.3 */
1
+ /* Generated by Cython 3.1.4 */
2
2
 
3
3
  /* BEGIN: Cython Metadata
4
4
  {
@@ -32,8 +32,8 @@ END: Cython Metadata */
32
32
  #elif PY_VERSION_HEX < 0x03080000
33
33
  #error Cython requires Python 3.8+.
34
34
  #else
35
- #define __PYX_ABI_VERSION "3_1_3"
36
- #define CYTHON_HEX_VERSION 0x030103F0
35
+ #define __PYX_ABI_VERSION "3_1_4"
36
+ #define CYTHON_HEX_VERSION 0x030104F0
37
37
  #define CYTHON_FUTURE_DIVISION 1
38
38
  /* CModulePreamble */
39
39
  #include <stddef.h>
@@ -3887,8 +3887,12 @@ static PyObject *__pyx_f_9fontTools_5qu2cu_5qu2cu_merge_curves(PyObject *__pyx_v
3887
3887
  __pyx_t_14 = __Pyx_c_diff_double(__pyx_v_p1, __pyx_v_p0);
3888
3888
  __pyx_t_7 = __pyx_PyComplex_FromComplex(__pyx_t_14); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 146, __pyx_L1_error)
3889
3889
  __Pyx_GOTREF(__pyx_t_7);
3890
- __pyx_t_9 = (__Pyx_PyList_GET_SIZE(__pyx_v_ts) != 0);
3891
- if (unlikely(((!CYTHON_ASSUME_SAFE_MACROS) && __pyx_t_9 < 0))) __PYX_ERR(0, 146, __pyx_L1_error)
3890
+ {
3891
+ Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_v_ts);
3892
+ if (unlikely(((!CYTHON_ASSUME_SAFE_SIZE) && __pyx_temp < 0))) __PYX_ERR(0, 146, __pyx_L1_error)
3893
+ __pyx_t_9 = (__pyx_temp != 0);
3894
+ }
3895
+
3892
3896
  if (__pyx_t_9) {
3893
3897
  __pyx_t_1 = __Pyx_GetItemInt_List(__pyx_v_ts, 0, long, 1, __Pyx_PyLong_From_long, 1, 0, 1, 1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 146, __pyx_L1_error)
3894
3898
  __Pyx_GOTREF(__pyx_t_1);
@@ -3922,8 +3926,12 @@ static PyObject *__pyx_f_9fontTools_5qu2cu_5qu2cu_merge_curves(PyObject *__pyx_v
3922
3926
  __pyx_t_14 = __Pyx_c_diff_double(__pyx_v_p2, __pyx_v_p3);
3923
3927
  __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_t_14); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 147, __pyx_L1_error)
3924
3928
  __Pyx_GOTREF(__pyx_t_1);
3925
- __pyx_t_9 = (__Pyx_PyList_GET_SIZE(__pyx_v_ts) != 0);
3926
- if (unlikely(((!CYTHON_ASSUME_SAFE_MACROS) && __pyx_t_9 < 0))) __PYX_ERR(0, 147, __pyx_L1_error)
3929
+ {
3930
+ Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_v_ts);
3931
+ if (unlikely(((!CYTHON_ASSUME_SAFE_SIZE) && __pyx_temp < 0))) __PYX_ERR(0, 147, __pyx_L1_error)
3932
+ __pyx_t_9 = (__pyx_temp != 0);
3933
+ }
3934
+
3927
3935
  if (__pyx_t_9) {
3928
3936
  __pyx_t_7 = __Pyx_GetItemInt_List(__pyx_v_ts, -1L, long, 1, __Pyx_PyLong_From_long, 1, 1, 1, 1); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 147, __pyx_L1_error)
3929
3937
  __Pyx_GOTREF(__pyx_t_7);
@@ -9031,15 +9039,16 @@ static int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate) {
9031
9039
  return -1;
9032
9040
  }
9033
9041
  /* #### Code section: init_codeobjects ### */
9034
- typedef struct {
9035
- unsigned int argcount : 3;
9036
- unsigned int num_posonly_args : 1;
9037
- unsigned int num_kwonly_args : 1;
9038
- unsigned int nlocals : 6;
9039
- unsigned int flags : 10;
9040
- unsigned int first_line : 9;
9041
- unsigned int line_table_length : 15;
9042
- } __Pyx_PyCode_New_function_description;
9042
+ \
9043
+ typedef struct {
9044
+ unsigned int argcount : 3;
9045
+ unsigned int num_posonly_args : 1;
9046
+ unsigned int num_kwonly_args : 1;
9047
+ unsigned int nlocals : 6;
9048
+ unsigned int flags : 10;
9049
+ unsigned int first_line : 9;
9050
+ unsigned int line_table_length : 15;
9051
+ } __Pyx_PyCode_New_function_description;
9043
9052
  /* NewCodeObj.proto */
9044
9053
  static PyObject* __Pyx_PyCode_New(
9045
9054
  const __Pyx_PyCode_New_function_description descr,
@@ -12270,6 +12279,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
12270
12279
  return -1;
12271
12280
  }
12272
12281
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
12282
+ Py_DECREF(bases);
12273
12283
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
12274
12284
  return -1;
12275
12285
  }
@@ -16097,6 +16107,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
16097
16107
  PyCode_NewWithPosOnlyArgs
16098
16108
  #endif
16099
16109
  (a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes);
16110
+ #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1
16111
+ if (likely(result))
16112
+ result->_co_firsttraceable = 0;
16113
+ #endif
16100
16114
  return result;
16101
16115
  }
16102
16116
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
Binary file
@@ -272,7 +272,7 @@ Font table options
272
272
  Specify (=), add to (+=) or exclude from (-=) the comma-separated
273
273
  set of tables that will be be dropped.
274
274
  By default, the following tables are dropped:
275
- 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
275
+ 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'PCLT', 'LTSH'
276
276
  and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill'.
277
277
  The tool will attempt to subset the remaining tables.
278
278
 
@@ -827,13 +827,26 @@ def subset_glyphs(self, s):
827
827
  self.MarkArray.MarkRecord, mark_indices
828
828
  )
829
829
  self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
830
- 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)
831
845
  self.BaseArray.BaseRecord = _list_subset(
832
846
  self.BaseArray.BaseRecord, base_indices
833
847
  )
834
848
  self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord)
835
849
  # Prune empty classes
836
- class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
837
850
  self.ClassCount = len(class_indices)
838
851
  for m in self.MarkArray.MarkRecord:
839
852
  m.Class = class_indices.index(m.Class)
@@ -867,13 +880,31 @@ def subset_glyphs(self, s):
867
880
  self.MarkArray.MarkRecord, mark_indices
868
881
  )
869
882
  self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord)
870
- 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)
871
903
  self.LigatureArray.LigatureAttach = _list_subset(
872
904
  self.LigatureArray.LigatureAttach, ligature_indices
873
905
  )
874
906
  self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach)
875
907
  # Prune empty classes
876
- class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord)
877
908
  self.ClassCount = len(class_indices)
878
909
  for m in self.MarkArray.MarkRecord:
879
910
  m.Class = class_indices.index(m.Class)
@@ -915,13 +946,26 @@ def subset_glyphs(self, s):
915
946
  self.Mark1Array.MarkRecord, mark1_indices
916
947
  )
917
948
  self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord)
918
- 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)
919
964
  self.Mark2Array.Mark2Record = _list_subset(
920
965
  self.Mark2Array.Mark2Record, mark2_indices
921
966
  )
922
967
  self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record)
923
968
  # Prune empty classes
924
- class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord)
925
969
  self.ClassCount = len(class_indices)
926
970
  for m in self.Mark1Array.MarkRecord:
927
971
  m.Class = class_indices.index(m.Class)
@@ -1717,6 +1761,19 @@ def subset_features(self, feature_indices):
1717
1761
  return bool(self.SubstitutionCount)
1718
1762
 
1719
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
+
1720
1777
  @_add_method(otTables.FeatureVariations)
1721
1778
  def subset_features(self, feature_indices):
1722
1779
  self.ensureDecompiled()
@@ -1735,6 +1792,24 @@ def subset_features(self, feature_indices):
1735
1792
  return bool(self.FeatureVariationCount)
1736
1793
 
1737
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
+
1738
1813
  @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1739
1814
  def subset_features(self, feature_indices):
1740
1815
  if self.ReqFeatureIndex in feature_indices:
@@ -1750,6 +1825,16 @@ def subset_features(self, feature_indices):
1750
1825
  return bool(self.FeatureCount or self.ReqFeatureIndex != 65535)
1751
1826
 
1752
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
+
1753
1838
  @_add_method(otTables.DefaultLangSys, otTables.LangSys)
1754
1839
  def collect_features(self):
1755
1840
  feature_indices = self.FeatureIndex[:]
@@ -1773,6 +1858,21 @@ def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False):
1773
1858
  return bool(self.LangSysCount or self.DefaultLangSys)
1774
1859
 
1775
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
+
1776
1876
  @_add_method(otTables.Script)
1777
1877
  def collect_features(self):
1778
1878
  feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord]
@@ -1794,6 +1894,19 @@ def subset_features(self, feature_indices, retain_empty):
1794
1894
  return bool(self.ScriptCount)
1795
1895
 
1796
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
+
1797
1910
  @_add_method(otTables.ScriptList)
1798
1911
  def collect_features(self):
1799
1912
  return _uniq_sort(sum((s.Script.collect_features() for s in self.ScriptRecord), []))
@@ -1981,21 +2094,74 @@ def subset_script_tags(self, tags):
1981
2094
 
1982
2095
  @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
1983
2096
  def prune_features(self):
1984
- """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
+ """
1985
2100
  if self.table.ScriptList:
1986
2101
  feature_indices = self.table.ScriptList.collect_features()
1987
2102
  else:
1988
2103
  feature_indices = []
2104
+ (feature_indices, feature_index_map) = self.remap_duplicate_features(
2105
+ feature_indices
2106
+ )
2107
+
1989
2108
  if self.table.FeatureList:
1990
2109
  self.table.FeatureList.subset_features(feature_indices)
1991
2110
  if getattr(self.table, "FeatureVariations", None):
1992
- self.table.FeatureVariations.subset_features(feature_indices)
2111
+ self.table.FeatureVariations.prune_features(feature_index_map)
1993
2112
  if self.table.ScriptList:
1994
- self.table.ScriptList.subset_features(
1995
- feature_indices, self.retain_empty_scripts()
2113
+ self.table.ScriptList.prune_features(
2114
+ feature_index_map, self.retain_empty_scripts()
1996
2115
  )
1997
2116
 
1998
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
+
1999
2165
  @_add_method(ttLib.getTableClass("GSUB"), ttLib.getTableClass("GPOS"))
2000
2166
  def prune_pre_subset(self, font, options):
2001
2167
  # Drop undesired features
@@ -3145,7 +3311,6 @@ class Options(object):
3145
3311
 
3146
3312
  # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser
3147
3313
  _drop_tables_default = [
3148
- "BASE",
3149
3314
  "JSTF",
3150
3315
  "DSIG",
3151
3316
  "EBDT",
@@ -3157,6 +3322,7 @@ class Options(object):
3157
3322
  _drop_tables_default += ["Feat", "Glat", "Gloc", "Silf", "Sill"] # Graphite
3158
3323
  _no_subset_tables_default = [
3159
3324
  "avar",
3325
+ "BASE",
3160
3326
  "fvar",
3161
3327
  "gasp",
3162
3328
  "head",
@@ -118,6 +118,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
118
118
  def build_psNameMapping(self, ttFont):
119
119
  mapping = {}
120
120
  allNames = {}
121
+ glyphOrderNames = set(self.glyphOrder)
121
122
  for i in range(ttFont["maxp"].numGlyphs):
122
123
  glyphName = psName = self.glyphOrder[i]
123
124
  if glyphName == "":
@@ -126,16 +127,15 @@ class table__p_o_s_t(DefaultTable.DefaultTable):
126
127
  if glyphName in allNames:
127
128
  # make up a new glyphName that's unique
128
129
  n = allNames[glyphName]
129
- # check if the exists in any of the seen names or later ones
130
- names = set(allNames.keys()) | set(self.glyphOrder)
131
- while (glyphName + "." + str(n)) in names:
130
+ # check if the glyph name exists in the glyph order
131
+ while f"{glyphName}.{n}" in glyphOrderNames:
132
132
  n += 1
133
133
  allNames[glyphName] = n + 1
134
- glyphName = glyphName + "." + str(n)
134
+ glyphName = f"{glyphName}.{n}"
135
135
 
136
- self.glyphOrder[i] = glyphName
137
136
  allNames[glyphName] = 1
138
137
  if glyphName != psName:
138
+ self.glyphOrder[i] = glyphName
139
139
  mapping[glyphName] = psName
140
140
 
141
141
  self.mapping = mapping