fonttools 4.59.1__cp310-cp310-win_amd64.whl → 4.60.0__cp310-cp310-win_amd64.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 (51) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/annotations.py +30 -0
  3. fontTools/cu2qu/cu2qu.c +1185 -971
  4. fontTools/cu2qu/cu2qu.cp310-win_amd64.pyd +0 -0
  5. fontTools/cu2qu/cu2qu.py +36 -4
  6. fontTools/feaLib/builder.py +9 -3
  7. fontTools/feaLib/lexer.c +18 -12
  8. fontTools/feaLib/lexer.cp310-win_amd64.pyd +0 -0
  9. fontTools/feaLib/parser.py +11 -1
  10. fontTools/feaLib/variableScalar.py +6 -1
  11. fontTools/misc/bezierTools.c +18 -12
  12. fontTools/misc/bezierTools.cp310-win_amd64.pyd +0 -0
  13. fontTools/misc/enumTools.py +23 -0
  14. fontTools/misc/textTools.py +4 -2
  15. fontTools/pens/filterPen.py +218 -26
  16. fontTools/pens/momentsPen.c +18 -12
  17. fontTools/pens/momentsPen.cp310-win_amd64.pyd +0 -0
  18. fontTools/pens/pointPen.py +40 -6
  19. fontTools/qu2cu/qu2cu.c +30 -16
  20. fontTools/qu2cu/qu2cu.cp310-win_amd64.pyd +0 -0
  21. fontTools/subset/__init__.py +1 -0
  22. fontTools/ttLib/tables/_a_v_a_r.py +4 -2
  23. fontTools/ttLib/tables/_n_a_m_e.py +11 -6
  24. fontTools/ttLib/tables/_p_o_s_t.py +5 -5
  25. fontTools/ufoLib/__init__.py +279 -176
  26. fontTools/ufoLib/converters.py +14 -5
  27. fontTools/ufoLib/filenames.py +16 -6
  28. fontTools/ufoLib/glifLib.py +286 -190
  29. fontTools/ufoLib/kerning.py +32 -12
  30. fontTools/ufoLib/utils.py +41 -13
  31. fontTools/ufoLib/validators.py +121 -97
  32. fontTools/varLib/__init__.py +80 -1
  33. fontTools/varLib/avar/__init__.py +0 -0
  34. fontTools/varLib/avar/__main__.py +72 -0
  35. fontTools/varLib/avar/build.py +79 -0
  36. fontTools/varLib/avar/map.py +108 -0
  37. fontTools/varLib/avar/plan.py +1004 -0
  38. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  39. fontTools/varLib/avarPlanner.py +3 -999
  40. fontTools/varLib/instancer/__init__.py +56 -18
  41. fontTools/varLib/interpolatableHelpers.py +3 -0
  42. fontTools/varLib/iup.c +24 -14
  43. fontTools/varLib/iup.cp310-win_amd64.pyd +0 -0
  44. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/METADATA +43 -2
  45. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/RECORD +51 -44
  46. {fonttools-4.59.1.data → fonttools-4.60.0.data}/data/share/man/man1/ttx.1 +0 -0
  47. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/WHEEL +0 -0
  48. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/entry_points.txt +0 -0
  49. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE +0 -0
  50. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE.external +0 -0
  51. {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/top_level.txt +0 -0
Binary file
fontTools/cu2qu/cu2qu.py CHANGED
@@ -37,7 +37,7 @@ NAN = float("NaN")
37
37
  @cython.cfunc
38
38
  @cython.inline
39
39
  @cython.returns(cython.double)
40
- @cython.locals(v1=cython.complex, v2=cython.complex)
40
+ @cython.locals(v1=cython.complex, v2=cython.complex, result=cython.double)
41
41
  def dot(v1, v2):
42
42
  """Return the dot product of two vectors.
43
43
 
@@ -48,7 +48,33 @@ def dot(v1, v2):
48
48
  Returns:
49
49
  double: Dot product.
50
50
  """
51
- return (v1 * v2.conjugate()).real
51
+ result = (v1 * v2.conjugate()).real
52
+ # When vectors are perpendicular (i.e. dot product is 0), the above expression may
53
+ # yield slightly different results when running in pure Python vs C/Cython,
54
+ # both of which are correct within IEEE-754 floating-point precision.
55
+ # It's probably due to the different order of operations and roundings in each
56
+ # implementation. Because we are using the result in a denominator and catching
57
+ # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here.
58
+ if abs(result) < 1e-15:
59
+ result = 0.0
60
+ return result
61
+
62
+
63
+ @cython.cfunc
64
+ @cython.locals(z=cython.complex, den=cython.double)
65
+ @cython.locals(zr=cython.double, zi=cython.double)
66
+ def _complex_div_by_real(z, den):
67
+ """Divide complex by real using Python's method (two separate divisions).
68
+
69
+ This ensures bit-exact compatibility with Python's complex division,
70
+ avoiding C's multiply-by-reciprocal optimization that can cause 1 ULP differences
71
+ on some platforms/compilers (e.g. clang on macOS arm64).
72
+
73
+ https://github.com/fonttools/fonttools/issues/3928
74
+ """
75
+ zr = z.real
76
+ zi = z.imag
77
+ return complex(zr / den, zi / den)
52
78
 
53
79
 
54
80
  @cython.cfunc
@@ -59,8 +85,8 @@ def dot(v1, v2):
59
85
  )
60
86
  def calc_cubic_points(a, b, c, d):
61
87
  _1 = d
62
- _2 = (c / 3.0) + d
63
- _3 = (b + c) / 3.0 + _2
88
+ _2 = _complex_div_by_real(c, 3.0) + d
89
+ _3 = _complex_div_by_real(b + c, 3.0) + _2
64
90
  _4 = a + d + c + b
65
91
  return _1, _2, _3, _4
66
92
 
@@ -273,6 +299,12 @@ def calc_intersect(a, b, c, d):
273
299
  try:
274
300
  h = dot(p, a - c) / dot(p, cd)
275
301
  except ZeroDivisionError:
302
+ # if 3 or 4 points are equal, we do have an intersection despite the zero-div:
303
+ # return one of the off-curves so that the algorithm can attempt a one-curve
304
+ # solution if it's within tolerance:
305
+ # https://github.com/linebender/kurbo/pull/484
306
+ if b == c and (a == b or c == d):
307
+ return b
276
308
  return complex(NAN, NAN)
277
309
  return c + cd * h
278
310
 
@@ -32,6 +32,7 @@ from fontTools.otlLib.builder import (
32
32
  AnySubstBuilder,
33
33
  )
34
34
  from fontTools.otlLib.error import OpenTypeLibError
35
+ from fontTools.varLib.errors import VarLibError
35
36
  from fontTools.varLib.varStore import OnlineVarStoreBuilder
36
37
  from fontTools.varLib.builder import buildVarDevTable
37
38
  from fontTools.varLib.featureVars import addFeatureVariationsRaw
@@ -1728,9 +1729,14 @@ class Builder(object):
1728
1729
  if not varscalar.does_vary:
1729
1730
  return varscalar.default, None
1730
1731
 
1731
- default, index = varscalar.add_to_variation_store(
1732
- self.varstorebuilder, self.model_cache, self.font.get("avar")
1733
- )
1732
+ try:
1733
+ default, index = varscalar.add_to_variation_store(
1734
+ self.varstorebuilder, self.model_cache, self.font.get("avar")
1735
+ )
1736
+ except VarLibError as e:
1737
+ raise FeatureLibError(
1738
+ "Failed to compute deltas for variable scalar", location
1739
+ ) from e
1734
1740
 
1735
1741
  device = None
1736
1742
  if index is not None and index != 0xFFFFFFFF:
fontTools/feaLib/lexer.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
  {
@@ -26,8 +26,8 @@ END: Cython Metadata */
26
26
  #elif PY_VERSION_HEX < 0x03080000
27
27
  #error Cython requires Python 3.8+.
28
28
  #else
29
- #define __PYX_ABI_VERSION "3_1_3"
30
- #define CYTHON_HEX_VERSION 0x030103F0
29
+ #define __PYX_ABI_VERSION "3_1_4"
30
+ #define CYTHON_HEX_VERSION 0x030104F0
31
31
  #define CYTHON_FUTURE_DIVISION 1
32
32
  /* CModulePreamble */
33
33
  #include <stddef.h>
@@ -10825,15 +10825,16 @@ static int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate) {
10825
10825
  return -1;
10826
10826
  }
10827
10827
  /* #### Code section: init_codeobjects ### */
10828
- typedef struct {
10829
- unsigned int argcount : 2;
10830
- unsigned int num_posonly_args : 1;
10831
- unsigned int num_kwonly_args : 1;
10832
- unsigned int nlocals : 4;
10833
- unsigned int flags : 10;
10834
- unsigned int first_line : 9;
10835
- unsigned int line_table_length : 15;
10836
- } __Pyx_PyCode_New_function_description;
10828
+ \
10829
+ typedef struct {
10830
+ unsigned int argcount : 2;
10831
+ unsigned int num_posonly_args : 1;
10832
+ unsigned int num_kwonly_args : 1;
10833
+ unsigned int nlocals : 4;
10834
+ unsigned int flags : 10;
10835
+ unsigned int first_line : 9;
10836
+ unsigned int line_table_length : 15;
10837
+ } __Pyx_PyCode_New_function_description;
10837
10838
  /* NewCodeObj.proto */
10838
10839
  static PyObject* __Pyx_PyCode_New(
10839
10840
  const __Pyx_PyCode_New_function_description descr,
@@ -14312,6 +14313,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
14312
14313
  return -1;
14313
14314
  }
14314
14315
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
14316
+ Py_DECREF(bases);
14315
14317
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
14316
14318
  return -1;
14317
14319
  }
@@ -16695,6 +16697,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
16695
16697
  PyCode_NewWithPosOnlyArgs
16696
16698
  #endif
16697
16699
  (a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes);
16700
+ #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1
16701
+ if (likely(result))
16702
+ result->_co_firsttraceable = 0;
16703
+ #endif
16698
16704
  return result;
16699
16705
  }
16700
16706
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
Binary file
@@ -2198,7 +2198,7 @@ class Parser(object):
2198
2198
  raise FeatureLibError(
2199
2199
  "Expected an equals sign", self.cur_token_location_
2200
2200
  )
2201
- value = self.expect_number_()
2201
+ value = self.expect_integer_or_float_()
2202
2202
  location[axis] = value
2203
2203
  if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
2204
2204
  # Lexer has just read the value as a glyph name. We'll correct it later
@@ -2230,6 +2230,16 @@ class Parser(object):
2230
2230
  "Expected a floating-point number", self.cur_token_location_
2231
2231
  )
2232
2232
 
2233
+ def expect_integer_or_float_(self):
2234
+ if self.next_token_type_ == Lexer.FLOAT:
2235
+ return self.expect_float_()
2236
+ elif self.next_token_type_ is Lexer.NUMBER:
2237
+ return self.expect_number_()
2238
+ else:
2239
+ raise FeatureLibError(
2240
+ "Expected an integer or floating-point number", self.cur_token_location_
2241
+ )
2242
+
2233
2243
  def expect_decipoint_(self):
2234
2244
  if self.next_token_type_ == Lexer.FLOAT:
2235
2245
  return self.expect_float_()
@@ -17,7 +17,12 @@ class VariableScalar:
17
17
  def __repr__(self):
18
18
  items = []
19
19
  for location, value in self.values.items():
20
- loc = ",".join(["%s=%i" % (ax, loc) for ax, loc in location])
20
+ loc = ",".join(
21
+ [
22
+ f"{ax}={int(coord) if float(coord).is_integer() else coord}"
23
+ for ax, coord in location
24
+ ]
25
+ )
21
26
  items.append("%s:%i" % (loc, value))
22
27
  return "(" + (" ".join(items)) + ")"
23
28
 
@@ -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
  {
@@ -26,8 +26,8 @@ END: Cython Metadata */
26
26
  #elif PY_VERSION_HEX < 0x03080000
27
27
  #error Cython requires Python 3.8+.
28
28
  #else
29
- #define __PYX_ABI_VERSION "3_1_3"
30
- #define CYTHON_HEX_VERSION 0x030103F0
29
+ #define __PYX_ABI_VERSION "3_1_4"
30
+ #define CYTHON_HEX_VERSION 0x030104F0
31
31
  #define CYTHON_FUTURE_DIVISION 1
32
32
  /* CModulePreamble */
33
33
  #include <stddef.h>
@@ -31615,15 +31615,16 @@ static int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate) {
31615
31615
  return -1;
31616
31616
  }
31617
31617
  /* #### Code section: init_codeobjects ### */
31618
- typedef struct {
31619
- unsigned int argcount : 3;
31620
- unsigned int num_posonly_args : 1;
31621
- unsigned int num_kwonly_args : 1;
31622
- unsigned int nlocals : 6;
31623
- unsigned int flags : 10;
31624
- unsigned int first_line : 11;
31625
- unsigned int line_table_length : 14;
31626
- } __Pyx_PyCode_New_function_description;
31618
+ \
31619
+ typedef struct {
31620
+ unsigned int argcount : 3;
31621
+ unsigned int num_posonly_args : 1;
31622
+ unsigned int num_kwonly_args : 1;
31623
+ unsigned int nlocals : 6;
31624
+ unsigned int flags : 10;
31625
+ unsigned int first_line : 11;
31626
+ unsigned int line_table_length : 14;
31627
+ } __Pyx_PyCode_New_function_description;
31627
31628
  /* NewCodeObj.proto */
31628
31629
  static PyObject* __Pyx_PyCode_New(
31629
31630
  const __Pyx_PyCode_New_function_description descr,
@@ -34352,6 +34353,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
34352
34353
  return -1;
34353
34354
  }
34354
34355
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
34356
+ Py_DECREF(bases);
34355
34357
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
34356
34358
  return -1;
34357
34359
  }
@@ -39495,6 +39497,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
39495
39497
  PyCode_NewWithPosOnlyArgs
39496
39498
  #endif
39497
39499
  (a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes);
39500
+ #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1
39501
+ if (likely(result))
39502
+ result->_co_firsttraceable = 0;
39503
+ #endif
39498
39504
  return result;
39499
39505
  }
39500
39506
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
@@ -0,0 +1,23 @@
1
+ """Enum-related utilities, including backports for older Python versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ __all__ = ["StrEnum"]
9
+
10
+ # StrEnum is only available in Python 3.11+
11
+ try:
12
+ from enum import StrEnum
13
+ except ImportError:
14
+
15
+ class StrEnum(str, Enum):
16
+ """
17
+ Minimal backport of Python 3.11's StrEnum for older versions.
18
+
19
+ An Enum where all members are also strings.
20
+ """
21
+
22
+ def __str__(self) -> str:
23
+ return self.value
@@ -1,5 +1,7 @@
1
1
  """fontTools.misc.textTools.py -- miscellaneous routines."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import ast
4
6
  import string
5
7
 
@@ -118,14 +120,14 @@ def pad(data, size):
118
120
  return data
119
121
 
120
122
 
121
- def tostr(s, encoding="ascii", errors="strict"):
123
+ def tostr(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> str:
122
124
  if not isinstance(s, str):
123
125
  return s.decode(encoding, errors)
124
126
  else:
125
127
  return s
126
128
 
127
129
 
128
- def tobytes(s, encoding="ascii", errors="strict"):
130
+ def tobytes(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> bytes:
129
131
  if isinstance(s, str):
130
132
  return s.encode(encoding, errors)
131
133
  else:
@@ -1,7 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from fontTools.pens.basePen import AbstractPen, DecomposingPen
4
- from fontTools.pens.pointPen import AbstractPointPen, DecomposingPointPen
4
+ from fontTools.pens.pointPen import (
5
+ AbstractPointPen,
6
+ DecomposingPointPen,
7
+ ReverseFlipped,
8
+ )
5
9
  from fontTools.pens.recordingPen import RecordingPen
6
10
 
7
11
 
@@ -155,26 +159,61 @@ class FilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
155
159
  def __init__(self, outPen):
156
160
  self._outPen = outPen
157
161
 
158
- def beginPath(self, **kwargs):
162
+ def beginPath(self, identifier=None, **kwargs):
163
+ kwargs = dict(kwargs)
164
+ if identifier is not None:
165
+ kwargs["identifier"] = identifier
159
166
  self._outPen.beginPath(**kwargs)
160
167
 
161
168
  def endPath(self):
162
169
  self._outPen.endPath()
163
170
 
164
- def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
171
+ def addPoint(
172
+ self,
173
+ pt,
174
+ segmentType=None,
175
+ smooth=False,
176
+ name=None,
177
+ identifier=None,
178
+ **kwargs,
179
+ ):
180
+ kwargs = dict(kwargs)
181
+ if identifier is not None:
182
+ kwargs["identifier"] = identifier
165
183
  self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
166
184
 
167
185
 
168
- class _DecomposingFilterPenMixin:
169
- """Mixin class that decomposes components as regular contours.
186
+ class _DecomposingFilterMixinBase:
187
+ """Base mixin class with common `addComponent` logic for decomposing filter pens."""
188
+
189
+ def addComponent(self, baseGlyphName, transformation, **kwargs):
190
+ # only decompose the component if it's included in the set
191
+ if self.include is None or baseGlyphName in self.include:
192
+ # if we're decomposing nested components, temporarily set include to None
193
+ include_bak = self.include
194
+ if self.decomposeNested and self.include:
195
+ self.include = None
196
+ try:
197
+ super().addComponent(baseGlyphName, transformation, **kwargs)
198
+ finally:
199
+ if self.include != include_bak:
200
+ self.include = include_bak
201
+ else:
202
+ _PassThruComponentsMixin.addComponent(
203
+ self, baseGlyphName, transformation, **kwargs
204
+ )
205
+
206
+
207
+ class _DecomposingFilterPenMixin(_DecomposingFilterMixinBase):
208
+ """Mixin class that decomposes components as regular contours for segment pens.
170
209
 
171
- Shared by both DecomposingFilterPen and DecomposingFilterPointPen.
210
+ Used by DecomposingFilterPen.
172
211
 
173
- Takes two required parameters, another (segment or point) pen 'outPen' to draw
212
+ Takes two required parameters, another segment pen 'outPen' to draw
174
213
  with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
175
214
 
176
215
  The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
177
- same as in the DecomposingPen/DecomposingPointPen. Both are False by default.
216
+ same as in the DecomposingPen. reverseFlipped is bool only (True/False).
178
217
 
179
218
  In addition, the decomposing filter pens also take the following two options:
180
219
 
@@ -196,35 +235,69 @@ class _DecomposingFilterPenMixin:
196
235
  outPen,
197
236
  glyphSet,
198
237
  skipMissingComponents=None,
199
- reverseFlipped=False,
238
+ reverseFlipped: bool = False,
200
239
  include: set[str] | None = None,
201
240
  decomposeNested: bool = True,
241
+ **kwargs,
202
242
  ):
243
+ assert isinstance(
244
+ reverseFlipped, bool
245
+ ), f"Expected bool, got {type(reverseFlipped).__name__}"
203
246
  super().__init__(
204
247
  outPen=outPen,
205
248
  glyphSet=glyphSet,
206
249
  skipMissingComponents=skipMissingComponents,
207
250
  reverseFlipped=reverseFlipped,
251
+ **kwargs,
208
252
  )
209
253
  self.include = include
210
254
  self.decomposeNested = decomposeNested
211
255
 
212
- def addComponent(self, baseGlyphName, transformation, **kwargs):
213
- # only decompose the component if it's included in the set
214
- if self.include is None or baseGlyphName in self.include:
215
- # if we're decomposing nested components, temporarily set include to None
216
- include_bak = self.include
217
- if self.decomposeNested and self.include:
218
- self.include = None
219
- try:
220
- super().addComponent(baseGlyphName, transformation, **kwargs)
221
- finally:
222
- if self.include != include_bak:
223
- self.include = include_bak
224
- else:
225
- _PassThruComponentsMixin.addComponent(
226
- self, baseGlyphName, transformation, **kwargs
227
- )
256
+
257
+ class _DecomposingFilterPointPenMixin(_DecomposingFilterMixinBase):
258
+ """Mixin class that decomposes components as regular contours for point pens.
259
+
260
+ Takes two required parameters, another point pen 'outPen' to draw
261
+ with, and a 'glyphSet' dict of drawable glyph objects to draw components from.
262
+
263
+ The 'skipMissingComponents' and 'reverseFlipped' optional arguments work the
264
+ same as in the DecomposingPointPen. reverseFlipped accepts bool | ReverseFlipped
265
+ (see DecomposingPointPen).
266
+
267
+ In addition, the decomposing filter pens also take the following two options:
268
+
269
+ 'include' is an optional set of component base glyph names to consider for
270
+ decomposition; the default include=None means decompose all components no matter
271
+ the base glyph name).
272
+
273
+ 'decomposeNested' (bool) controls whether to recurse decomposition into nested
274
+ components of components (this only matters when 'include' was also provided);
275
+ if False, only decompose top-level components included in the set, but not
276
+ also their children.
277
+ """
278
+
279
+ # raises MissingComponentError if base glyph is not found in glyphSet
280
+ skipMissingComponents = False
281
+
282
+ def __init__(
283
+ self,
284
+ outPen,
285
+ glyphSet,
286
+ skipMissingComponents=None,
287
+ reverseFlipped: bool | ReverseFlipped = False,
288
+ include: set[str] | None = None,
289
+ decomposeNested: bool = True,
290
+ **kwargs,
291
+ ):
292
+ super().__init__(
293
+ outPen=outPen,
294
+ glyphSet=glyphSet,
295
+ skipMissingComponents=skipMissingComponents,
296
+ reverseFlipped=reverseFlipped,
297
+ **kwargs,
298
+ )
299
+ self.include = include
300
+ self.decomposeNested = decomposeNested
228
301
 
229
302
 
230
303
  class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen):
@@ -234,8 +307,127 @@ class DecomposingFilterPen(_DecomposingFilterPenMixin, DecomposingPen, FilterPen
234
307
 
235
308
 
236
309
  class DecomposingFilterPointPen(
237
- _DecomposingFilterPenMixin, DecomposingPointPen, FilterPointPen
310
+ _DecomposingFilterPointPenMixin, DecomposingPointPen, FilterPointPen
238
311
  ):
239
312
  """Filter point pen that draws components as regular contours."""
240
313
 
241
314
  pass
315
+
316
+
317
+ class ContourFilterPointPen(_PassThruComponentsMixin, AbstractPointPen):
318
+ """A "buffered" filter point pen that accumulates contour data, passes
319
+ it through a ``filterContour`` method when the contour is closed or ended,
320
+ and finally draws the result with the output point pen.
321
+
322
+ Components are passed through unchanged.
323
+
324
+ The ``filterContour`` method can modify the contour in-place (return None)
325
+ or return a new contour to replace it.
326
+ """
327
+
328
+ def __init__(self, outPen):
329
+ self._outPen = outPen
330
+ self.currentContour = None
331
+ self.currentContourKwargs = None
332
+
333
+ def beginPath(self, identifier=None, **kwargs):
334
+ if self.currentContour is not None:
335
+ raise ValueError("Path already begun")
336
+ kwargs = dict(kwargs)
337
+ if identifier is not None:
338
+ kwargs["identifier"] = identifier
339
+ self.currentContour = []
340
+ self.currentContourKwargs = kwargs
341
+
342
+ def endPath(self):
343
+ if self.currentContour is None:
344
+ raise ValueError("Path not begun")
345
+ self._flushContour()
346
+ self.currentContour = None
347
+ self.currentContourKwargs = None
348
+
349
+ def _flushContour(self):
350
+ """Flush the current contour to the output pen."""
351
+ result = self.filterContour(self.currentContour)
352
+ if result is not None:
353
+ self.currentContour = result
354
+
355
+ # Draw the filtered contour
356
+ self._outPen.beginPath(**self.currentContourKwargs)
357
+ for pt, segmentType, smooth, name, kwargs in self.currentContour:
358
+ self._outPen.addPoint(pt, segmentType, smooth, name, **kwargs)
359
+ self._outPen.endPath()
360
+
361
+ def filterContour(self, contour):
362
+ """Subclasses must override this to perform the filtering.
363
+
364
+ The contour is a list of (pt, segmentType, smooth, name, kwargs) tuples.
365
+ If the method doesn't return a value (i.e. returns None), it's
366
+ assumed that the contour was modified in-place.
367
+ Otherwise, the return value replaces the original contour.
368
+ """
369
+ return # or return contour
370
+
371
+ def addPoint(
372
+ self,
373
+ pt,
374
+ segmentType=None,
375
+ smooth=False,
376
+ name=None,
377
+ identifier=None,
378
+ **kwargs,
379
+ ):
380
+ if self.currentContour is None:
381
+ raise ValueError("Path not begun")
382
+ kwargs = dict(kwargs)
383
+ if identifier is not None:
384
+ kwargs["identifier"] = identifier
385
+ self.currentContour.append((pt, segmentType, smooth, name, kwargs))
386
+
387
+
388
+ class OnCurveFirstPointPen(ContourFilterPointPen):
389
+ """Filter point pen that ensures closed contours start with an on-curve point.
390
+
391
+ If a closed contour starts with an off-curve point (segmentType=None), it rotates
392
+ the points list so that the first on-curve point (segmentType != None) becomes
393
+ the start point. Open contours and contours already starting with on-curve points
394
+ are passed through unchanged.
395
+
396
+ >>> from fontTools.pens.recordingPen import RecordingPointPen
397
+ >>> rec = RecordingPointPen()
398
+ >>> pen = OnCurveFirstPointPen(rec)
399
+ >>> # Closed contour starting with off-curve - will be rotated
400
+ >>> pen.beginPath()
401
+ >>> pen.addPoint((0, 0), None) # off-curve
402
+ >>> pen.addPoint((100, 100), "line") # on-curve - will become start
403
+ >>> pen.addPoint((200, 0), None) # off-curve
404
+ >>> pen.addPoint((300, 100), "curve") # on-curve
405
+ >>> pen.endPath()
406
+ >>> # The contour should now start with (100, 100) "line"
407
+ >>> rec.value[0]
408
+ ('beginPath', (), {})
409
+ >>> rec.value[1]
410
+ ('addPoint', ((100, 100), 'line', False, None), {})
411
+ >>> rec.value[2]
412
+ ('addPoint', ((200, 0), None, False, None), {})
413
+ >>> rec.value[3]
414
+ ('addPoint', ((300, 100), 'curve', False, None), {})
415
+ >>> rec.value[4]
416
+ ('addPoint', ((0, 0), None, False, None), {})
417
+ """
418
+
419
+ def filterContour(self, contour):
420
+ """Rotate closed contour to start with first on-curve point if needed."""
421
+ if not contour:
422
+ return
423
+
424
+ # Check if it's a closed contour (no "move" segmentType)
425
+ is_closed = contour[0][1] != "move"
426
+
427
+ if is_closed and contour[0][1] is None:
428
+ # Closed contour starting with off-curve - need to rotate
429
+ # Find the first on-curve point
430
+ for i, (pt, segmentType, smooth, name, kwargs) in enumerate(contour):
431
+ if segmentType is not None:
432
+ # Rotate the points list so it starts with the first on-curve point
433
+ return contour[i:] + contour[:i]
@@ -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
  {
@@ -26,8 +26,8 @@ END: Cython Metadata */
26
26
  #elif PY_VERSION_HEX < 0x03080000
27
27
  #error Cython requires Python 3.8+.
28
28
  #else
29
- #define __PYX_ABI_VERSION "3_1_3"
30
- #define CYTHON_HEX_VERSION 0x030103F0
29
+ #define __PYX_ABI_VERSION "3_1_4"
30
+ #define CYTHON_HEX_VERSION 0x030104F0
31
31
  #define CYTHON_FUTURE_DIVISION 1
32
32
  /* CModulePreamble */
33
33
  #include <stddef.h>
@@ -8297,15 +8297,16 @@ static int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate) {
8297
8297
  return -1;
8298
8298
  }
8299
8299
  /* #### Code section: init_codeobjects ### */
8300
- typedef struct {
8301
- unsigned int argcount : 3;
8302
- unsigned int num_posonly_args : 1;
8303
- unsigned int num_kwonly_args : 1;
8304
- unsigned int nlocals : 8;
8305
- unsigned int flags : 10;
8306
- unsigned int first_line : 9;
8307
- unsigned int line_table_length : 17;
8308
- } __Pyx_PyCode_New_function_description;
8300
+ \
8301
+ typedef struct {
8302
+ unsigned int argcount : 3;
8303
+ unsigned int num_posonly_args : 1;
8304
+ unsigned int num_kwonly_args : 1;
8305
+ unsigned int nlocals : 8;
8306
+ unsigned int flags : 10;
8307
+ unsigned int first_line : 9;
8308
+ unsigned int line_table_length : 17;
8309
+ } __Pyx_PyCode_New_function_description;
8309
8310
  /* NewCodeObj.proto */
8310
8311
  static PyObject* __Pyx_PyCode_New(
8311
8312
  const __Pyx_PyCode_New_function_description descr,
@@ -10389,6 +10390,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
10389
10390
  return -1;
10390
10391
  }
10391
10392
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
10393
+ Py_DECREF(bases);
10392
10394
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
10393
10395
  return -1;
10394
10396
  }
@@ -12810,6 +12812,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
12810
12812
  PyCode_NewWithPosOnlyArgs
12811
12813
  #endif
12812
12814
  (a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes);
12815
+ #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1
12816
+ if (likely(result))
12817
+ result->_co_firsttraceable = 0;
12818
+ #endif
12813
12819
  return result;
12814
12820
  }
12815
12821
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY