fonttools 4.59.2__cp311-cp311-musllinux_1_2_aarch64.whl → 4.60.0__cp311-cp311-musllinux_1_2_aarch64.whl

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

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 +1057 -937
  4. fontTools/cu2qu/cu2qu.cpython-311-aarch64-linux-musl.so +0 -0
  5. fontTools/cu2qu/cu2qu.py +19 -2
  6. fontTools/feaLib/lexer.c +8 -3
  7. fontTools/feaLib/lexer.cpython-311-aarch64-linux-musl.so +0 -0
  8. fontTools/misc/bezierTools.c +8 -3
  9. fontTools/misc/bezierTools.cpython-311-aarch64-linux-musl.so +0 -0
  10. fontTools/misc/enumTools.py +23 -0
  11. fontTools/pens/filterPen.py +218 -26
  12. fontTools/pens/momentsPen.c +8 -3
  13. fontTools/pens/momentsPen.cpython-311-aarch64-linux-musl.so +0 -0
  14. fontTools/pens/pointPen.py +40 -6
  15. fontTools/qu2cu/qu2cu.c +20 -7
  16. fontTools/qu2cu/qu2cu.cpython-311-aarch64-linux-musl.so +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 +14 -5
  34. fontTools/varLib/iup.cpython-311-aarch64-linux-musl.so +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
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>
@@ -14313,6 +14313,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
14313
14313
  return -1;
14314
14314
  }
14315
14315
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
14316
+ Py_DECREF(bases);
14316
14317
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
14317
14318
  return -1;
14318
14319
  }
@@ -16696,6 +16697,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
16696
16697
  PyCode_NewWithPosOnlyArgs
16697
16698
  #endif
16698
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
16699
16704
  return result;
16700
16705
  }
16701
16706
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
@@ -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>
@@ -34353,6 +34353,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
34353
34353
  return -1;
34354
34354
  }
34355
34355
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
34356
+ Py_DECREF(bases);
34356
34357
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
34357
34358
  return -1;
34358
34359
  }
@@ -39496,6 +39497,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
39496
39497
  PyCode_NewWithPosOnlyArgs
39497
39498
  #endif
39498
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
39499
39504
  return result;
39500
39505
  }
39501
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>
@@ -10390,6 +10390,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
10390
10390
  return -1;
10391
10391
  }
10392
10392
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
10393
+ Py_DECREF(bases);
10393
10394
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
10394
10395
  return -1;
10395
10396
  }
@@ -12811,6 +12812,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
12811
12812
  PyCode_NewWithPosOnlyArgs
12812
12813
  #endif
12813
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
12814
12819
  return result;
12815
12820
  }
12816
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)
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);
@@ -12271,6 +12279,7 @@ static int __pyx_CommonTypesMetaclass_init(PyObject *module) {
12271
12279
  return -1;
12272
12280
  }
12273
12281
  mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
12282
+ Py_DECREF(bases);
12274
12283
  if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
12275
12284
  return -1;
12276
12285
  }
@@ -16098,6 +16107,10 @@ static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt
16098
16107
  PyCode_NewWithPosOnlyArgs
16099
16108
  #endif
16100
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
16101
16114
  return result;
16102
16115
  }
16103
16116
  #elif PY_VERSION_HEX >= 0x030800B2 && !CYTHON_COMPILING_IN_PYPY
@@ -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