fonttools 4.59.2__cp314-cp314-musllinux_1_2_aarch64.whl → 4.60.1__cp314-cp314-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.
- fontTools/__init__.py +1 -1
- fontTools/annotations.py +30 -0
- fontTools/cu2qu/cu2qu.c +1057 -937
- fontTools/cu2qu/cu2qu.cpython-314-aarch64-linux-musl.so +0 -0
- fontTools/cu2qu/cu2qu.py +19 -2
- fontTools/feaLib/lexer.c +8 -3
- fontTools/feaLib/lexer.cpython-314-aarch64-linux-musl.so +0 -0
- fontTools/misc/bezierTools.c +8 -3
- fontTools/misc/bezierTools.cpython-314-aarch64-linux-musl.so +0 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/visitor.py +24 -16
- fontTools/pens/filterPen.py +218 -26
- fontTools/pens/momentsPen.c +8 -3
- fontTools/pens/momentsPen.cpython-314-aarch64-linux-musl.so +0 -0
- fontTools/pens/pointPen.py +40 -6
- fontTools/qu2cu/qu2cu.c +20 -7
- fontTools/qu2cu/qu2cu.cpython-314-aarch64-linux-musl.so +0 -0
- fontTools/subset/__init__.py +178 -12
- fontTools/ttLib/tables/_p_o_s_t.py +5 -5
- fontTools/ufoLib/__init__.py +278 -175
- fontTools/ufoLib/converters.py +14 -5
- fontTools/ufoLib/filenames.py +16 -6
- fontTools/ufoLib/glifLib.py +286 -190
- fontTools/ufoLib/kerning.py +32 -12
- fontTools/ufoLib/utils.py +41 -13
- fontTools/ufoLib/validators.py +121 -97
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/{avar.py → avar/unbuild.py} +70 -59
- fontTools/varLib/avarPlanner.py +3 -999
- fontTools/varLib/interpolatableHelpers.py +3 -0
- fontTools/varLib/iup.c +14 -5
- fontTools/varLib/iup.cpython-314-aarch64-linux-musl.so +0 -0
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/METADATA +41 -2
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/RECORD +44 -37
- {fonttools-4.59.2.data → fonttools-4.60.1.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/WHEEL +0 -0
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/entry_points.txt +0 -0
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/licenses/LICENSE +0 -0
- {fonttools-4.59.2.dist-info → fonttools-4.60.1.dist-info}/licenses/LICENSE.external +0 -0
- {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
|
|
72
|
-
_3 = (b + c
|
|
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.
|
|
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 "
|
|
30
|
-
#define CYTHON_HEX_VERSION
|
|
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
|
|
Binary file
|
fontTools/misc/bezierTools.c
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Generated by Cython 3.1.
|
|
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 "
|
|
30
|
-
#define CYTHON_HEX_VERSION
|
|
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
|
|
Binary file
|
|
@@ -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
|
-
|
|
82
|
+
``@register_attr()`` or ``@register_attrs()``) ``visit()`` functions.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
128
|
-
will be called, and (visitor, obj, *args, **kwargs) will be passed
|
|
129
|
-
the user visit function.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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:
|
fontTools/pens/filterPen.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
169
|
-
"""
|
|
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
|
-
|
|
210
|
+
Used by DecomposingFilterPen.
|
|
172
211
|
|
|
173
|
-
Takes two required parameters, another
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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]
|
fontTools/pens/momentsPen.c
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Generated by Cython 3.1.
|
|
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 "
|
|
30
|
-
#define CYTHON_HEX_VERSION
|
|
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
|
|
Binary file
|
fontTools/pens/pointPen.py
CHANGED
|
@@ -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
|
|
569
|
-
matrix has a negative determinant will be decomposed
|
|
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
|
-
|
|
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)
|