fonttools 4.59.2__cp312-cp312-win_amd64.whl → 4.60.0__cp312-cp312-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 (42) 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.cp312-win_amd64.pyd +0 -0
  5. fontTools/cu2qu/cu2qu.py +19 -2
  6. fontTools/feaLib/lexer.c +18 -12
  7. fontTools/feaLib/lexer.cp312-win_amd64.pyd +0 -0
  8. fontTools/misc/bezierTools.c +18 -12
  9. fontTools/misc/bezierTools.cp312-win_amd64.pyd +0 -0
  10. fontTools/misc/enumTools.py +23 -0
  11. fontTools/pens/filterPen.py +218 -26
  12. fontTools/pens/momentsPen.c +18 -12
  13. fontTools/pens/momentsPen.cp312-win_amd64.pyd +0 -0
  14. fontTools/pens/pointPen.py +40 -6
  15. fontTools/qu2cu/qu2cu.c +30 -16
  16. fontTools/qu2cu/qu2cu.cp312-win_amd64.pyd +0 -0
  17. fontTools/ttLib/tables/_p_o_s_t.py +5 -5
  18. fontTools/ufoLib/__init__.py +279 -176
  19. fontTools/ufoLib/converters.py +14 -5
  20. fontTools/ufoLib/filenames.py +16 -6
  21. fontTools/ufoLib/glifLib.py +286 -190
  22. fontTools/ufoLib/kerning.py +32 -12
  23. fontTools/ufoLib/utils.py +41 -13
  24. fontTools/ufoLib/validators.py +121 -97
  25. fontTools/varLib/avar/__init__.py +0 -0
  26. fontTools/varLib/avar/__main__.py +72 -0
  27. fontTools/varLib/avar/build.py +79 -0
  28. fontTools/varLib/avar/map.py +108 -0
  29. fontTools/varLib/avar/plan.py +1004 -0
  30. fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
  31. fontTools/varLib/avarPlanner.py +3 -999
  32. fontTools/varLib/interpolatableHelpers.py +3 -0
  33. fontTools/varLib/iup.c +24 -14
  34. fontTools/varLib/iup.cp312-win_amd64.pyd +0 -0
  35. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/METADATA +29 -2
  36. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/RECORD +42 -35
  37. {fonttools-4.59.2.data → fonttools-4.60.0.data}/data/share/man/man1/ttx.1 +0 -0
  38. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/WHEEL +0 -0
  39. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/entry_points.txt +0 -0
  40. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE +0 -0
  41. {fonttools-4.59.2.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE.external +0 -0
  42. {fonttools-4.59.2.dist-info → fonttools-4.60.0.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
@@ -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
@@ -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)