fonttools 4.59.2__cp313-cp313-win_amd64.whl → 4.60.1__cp313-cp313-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 (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.cp313-win_amd64.pyd +0 -0
  5. fontTools/cu2qu/cu2qu.py +19 -2
  6. fontTools/feaLib/lexer.c +18 -12
  7. fontTools/feaLib/lexer.cp313-win_amd64.pyd +0 -0
  8. fontTools/misc/bezierTools.c +18 -12
  9. fontTools/misc/bezierTools.cp313-win_amd64.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.cp313-win_amd64.pyd +0 -0
  15. fontTools/pens/pointPen.py +40 -6
  16. fontTools/qu2cu/qu2cu.c +30 -16
  17. fontTools/qu2cu/qu2cu.cp313-win_amd64.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.cp313-win_amd64.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
Binary file
fontTools/cu2qu/cu2qu.py CHANGED
@@ -60,6 +60,23 @@ def dot(v1, v2):
60
60
  return result
61
61
 
62
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)
78
+
79
+
63
80
  @cython.cfunc
64
81
  @cython.inline
65
82
  @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@@ -68,8 +85,8 @@ def dot(v1, v2):
68
85
  )
69
86
  def calc_cubic_points(a, b, c, d):
70
87
  _1 = d
71
- _2 = (c / 3.0) + d
72
- _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
73
90
  _4 = a + d + c + b
74
91
  return _1, _2, _3, _4
75
92
 
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
@@ -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
fontTools/misc/visitor.py CHANGED
@@ -79,12 +79,15 @@ class Visitor(object):
79
79
  def visitObject(self, obj, *args, **kwargs):
80
80
  """Called to visit an object. This function loops over all non-private
81
81
  attributes of the objects and calls any user-registered (via
82
- @register_attr() or @register_attrs()) visit() functions.
82
+ ``@register_attr()`` or ``@register_attrs()``) ``visit()`` functions.
83
83
 
84
- If there is no user-registered visit function, of if there is and it
85
- returns True, or it returns None (or doesn't return anything) and
86
- visitor.defaultStop is False (default), then the visitor will proceed
87
- to call self.visitAttr()"""
84
+ The visitor will proceed to call ``self.visitAttr()``, unless there is a
85
+ user-registered visit function and:
86
+
87
+ * It returns ``False``; or
88
+ * It returns ``None`` (or doesn't return anything) and
89
+ ``visitor.defaultStop`` is ``True`` (non-default).
90
+ """
88
91
 
89
92
  keys = sorted(vars(obj).keys())
90
93
  _visitors = self._visitorsFor(obj)
@@ -121,19 +124,24 @@ class Visitor(object):
121
124
 
122
125
  def visit(self, obj, *args, **kwargs):
123
126
  """This is the main entry to the visitor. The visitor will visit object
124
- obj.
127
+ ``obj``.
125
128
 
126
129
  The visitor will first determine if there is a registered (via
127
- @register()) visit function for the type of object. If there is, it
128
- will be called, and (visitor, obj, *args, **kwargs) will be passed to
129
- the user visit function.
130
-
131
- If there is no user-registered visit function, of if there is and it
132
- returns True, or it returns None (or doesn't return anything) and
133
- visitor.defaultStop is False (default), then the visitor will proceed
134
- to dispatch to one of self.visitObject(), self.visitList(),
135
- self.visitDict(), or self.visitLeaf() (any of which can be overriden in
136
- a subclass)."""
130
+ ``@register()``) visit function for the type of object. If there is, it
131
+ will be called, and ``(visitor, obj, *args, **kwargs)`` will be passed
132
+ to the user visit function.
133
+
134
+ The visitor will not recurse if there is a user-registered visit
135
+ function and:
136
+
137
+ * It returns ``False``; or
138
+ * It returns ``None`` (or doesn't return anything) and
139
+ ``visitor.defaultStop`` is ``True`` (non-default)
140
+
141
+ Otherwise, the visitor will proceed to dispatch to one of
142
+ ``self.visitObject()``, ``self.visitList()``, ``self.visitDict()``, or
143
+ ``self.visitLeaf()`` (any of which can be overriden in a subclass).
144
+ """
137
145
 
138
146
  visitorFunc = self._visitorsFor(obj).get(None, None)
139
147
  if visitorFunc is not None:
@@ -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