fonttools 4.59.1__cp39-cp39-win32.whl → 4.60.0__cp39-cp39-win32.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.cp39-win32.pyd +0 -0
- fontTools/cu2qu/cu2qu.py +36 -4
- fontTools/feaLib/builder.py +9 -3
- fontTools/feaLib/lexer.cp39-win32.pyd +0 -0
- fontTools/feaLib/parser.py +11 -1
- fontTools/feaLib/variableScalar.py +6 -1
- fontTools/misc/bezierTools.cp39-win32.pyd +0 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/textTools.py +4 -2
- fontTools/pens/filterPen.py +218 -26
- fontTools/pens/momentsPen.cp39-win32.pyd +0 -0
- fontTools/pens/pointPen.py +40 -6
- fontTools/qu2cu/qu2cu.cp39-win32.pyd +0 -0
- fontTools/subset/__init__.py +1 -0
- fontTools/ttLib/tables/_a_v_a_r.py +4 -2
- fontTools/ttLib/tables/_n_a_m_e.py +11 -6
- fontTools/ttLib/tables/_p_o_s_t.py +5 -5
- fontTools/ufoLib/__init__.py +279 -176
- 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/__init__.py +80 -1
- 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/instancer/__init__.py +56 -18
- fontTools/varLib/interpolatableHelpers.py +3 -0
- fontTools/varLib/iup.cp39-win32.pyd +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/METADATA +43 -2
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/RECORD +45 -38
- {fonttools-4.59.1.data → fonttools-4.60.0.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/WHEEL +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/entry_points.txt +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/licenses/LICENSE.external +0 -0
- {fonttools-4.59.1.dist-info → fonttools-4.60.0.dist-info}/top_level.txt +0 -0
fontTools/__init__.py
CHANGED
fontTools/annotations.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from fontTools.misc.filesystem._base import FS
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from xml.etree.ElementTree import Element as ElementTreeElement
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from fontTools.ufoLib import UFOFormatVersion
|
|
10
|
+
from fontTools.ufoLib.glifLib import GLIFFormatVersion
|
|
11
|
+
from lxml.etree import _Element as LxmlElement
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T") # Generic type
|
|
15
|
+
K = TypeVar("K") # Generic dict key type
|
|
16
|
+
V = TypeVar("V") # Generic dict value type
|
|
17
|
+
|
|
18
|
+
GlyphNameToFileNameFunc = Optional[Callable[[str, set[str]], str]]
|
|
19
|
+
ElementType = Union[ElementTreeElement, "LxmlElement"]
|
|
20
|
+
FormatVersion = Union[int, tuple[int, int]]
|
|
21
|
+
FormatVersions = Optional[Iterable[FormatVersion]]
|
|
22
|
+
GLIFFormatVersionInput = Optional[Union[int, tuple[int, int], "GLIFFormatVersion"]]
|
|
23
|
+
UFOFormatVersionInput = Optional[Union[int, tuple[int, int], "UFOFormatVersion"]]
|
|
24
|
+
IntFloat = Union[int, float]
|
|
25
|
+
KerningPair = tuple[str, str]
|
|
26
|
+
KerningDict = dict[KerningPair, IntFloat]
|
|
27
|
+
KerningGroups = dict[str, Sequence[str]]
|
|
28
|
+
KerningNested = dict[str, dict[str, IntFloat]]
|
|
29
|
+
PathStr = Union[str, PathLike[str]]
|
|
30
|
+
PathOrFS = Union[PathStr, FS]
|
|
Binary file
|
fontTools/cu2qu/cu2qu.py
CHANGED
|
@@ -37,7 +37,7 @@ NAN = float("NaN")
|
|
|
37
37
|
@cython.cfunc
|
|
38
38
|
@cython.inline
|
|
39
39
|
@cython.returns(cython.double)
|
|
40
|
-
@cython.locals(v1=cython.complex, v2=cython.complex)
|
|
40
|
+
@cython.locals(v1=cython.complex, v2=cython.complex, result=cython.double)
|
|
41
41
|
def dot(v1, v2):
|
|
42
42
|
"""Return the dot product of two vectors.
|
|
43
43
|
|
|
@@ -48,7 +48,33 @@ def dot(v1, v2):
|
|
|
48
48
|
Returns:
|
|
49
49
|
double: Dot product.
|
|
50
50
|
"""
|
|
51
|
-
|
|
51
|
+
result = (v1 * v2.conjugate()).real
|
|
52
|
+
# When vectors are perpendicular (i.e. dot product is 0), the above expression may
|
|
53
|
+
# yield slightly different results when running in pure Python vs C/Cython,
|
|
54
|
+
# both of which are correct within IEEE-754 floating-point precision.
|
|
55
|
+
# It's probably due to the different order of operations and roundings in each
|
|
56
|
+
# implementation. Because we are using the result in a denominator and catching
|
|
57
|
+
# ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here.
|
|
58
|
+
if abs(result) < 1e-15:
|
|
59
|
+
result = 0.0
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@cython.cfunc
|
|
64
|
+
@cython.locals(z=cython.complex, den=cython.double)
|
|
65
|
+
@cython.locals(zr=cython.double, zi=cython.double)
|
|
66
|
+
def _complex_div_by_real(z, den):
|
|
67
|
+
"""Divide complex by real using Python's method (two separate divisions).
|
|
68
|
+
|
|
69
|
+
This ensures bit-exact compatibility with Python's complex division,
|
|
70
|
+
avoiding C's multiply-by-reciprocal optimization that can cause 1 ULP differences
|
|
71
|
+
on some platforms/compilers (e.g. clang on macOS arm64).
|
|
72
|
+
|
|
73
|
+
https://github.com/fonttools/fonttools/issues/3928
|
|
74
|
+
"""
|
|
75
|
+
zr = z.real
|
|
76
|
+
zi = z.imag
|
|
77
|
+
return complex(zr / den, zi / den)
|
|
52
78
|
|
|
53
79
|
|
|
54
80
|
@cython.cfunc
|
|
@@ -59,8 +85,8 @@ def dot(v1, v2):
|
|
|
59
85
|
)
|
|
60
86
|
def calc_cubic_points(a, b, c, d):
|
|
61
87
|
_1 = d
|
|
62
|
-
_2 = (c
|
|
63
|
-
_3 = (b + c
|
|
88
|
+
_2 = _complex_div_by_real(c, 3.0) + d
|
|
89
|
+
_3 = _complex_div_by_real(b + c, 3.0) + _2
|
|
64
90
|
_4 = a + d + c + b
|
|
65
91
|
return _1, _2, _3, _4
|
|
66
92
|
|
|
@@ -273,6 +299,12 @@ def calc_intersect(a, b, c, d):
|
|
|
273
299
|
try:
|
|
274
300
|
h = dot(p, a - c) / dot(p, cd)
|
|
275
301
|
except ZeroDivisionError:
|
|
302
|
+
# if 3 or 4 points are equal, we do have an intersection despite the zero-div:
|
|
303
|
+
# return one of the off-curves so that the algorithm can attempt a one-curve
|
|
304
|
+
# solution if it's within tolerance:
|
|
305
|
+
# https://github.com/linebender/kurbo/pull/484
|
|
306
|
+
if b == c and (a == b or c == d):
|
|
307
|
+
return b
|
|
276
308
|
return complex(NAN, NAN)
|
|
277
309
|
return c + cd * h
|
|
278
310
|
|
fontTools/feaLib/builder.py
CHANGED
|
@@ -32,6 +32,7 @@ from fontTools.otlLib.builder import (
|
|
|
32
32
|
AnySubstBuilder,
|
|
33
33
|
)
|
|
34
34
|
from fontTools.otlLib.error import OpenTypeLibError
|
|
35
|
+
from fontTools.varLib.errors import VarLibError
|
|
35
36
|
from fontTools.varLib.varStore import OnlineVarStoreBuilder
|
|
36
37
|
from fontTools.varLib.builder import buildVarDevTable
|
|
37
38
|
from fontTools.varLib.featureVars import addFeatureVariationsRaw
|
|
@@ -1728,9 +1729,14 @@ class Builder(object):
|
|
|
1728
1729
|
if not varscalar.does_vary:
|
|
1729
1730
|
return varscalar.default, None
|
|
1730
1731
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1732
|
+
try:
|
|
1733
|
+
default, index = varscalar.add_to_variation_store(
|
|
1734
|
+
self.varstorebuilder, self.model_cache, self.font.get("avar")
|
|
1735
|
+
)
|
|
1736
|
+
except VarLibError as e:
|
|
1737
|
+
raise FeatureLibError(
|
|
1738
|
+
"Failed to compute deltas for variable scalar", location
|
|
1739
|
+
) from e
|
|
1734
1740
|
|
|
1735
1741
|
device = None
|
|
1736
1742
|
if index is not None and index != 0xFFFFFFFF:
|
|
Binary file
|
fontTools/feaLib/parser.py
CHANGED
|
@@ -2198,7 +2198,7 @@ class Parser(object):
|
|
|
2198
2198
|
raise FeatureLibError(
|
|
2199
2199
|
"Expected an equals sign", self.cur_token_location_
|
|
2200
2200
|
)
|
|
2201
|
-
value = self.
|
|
2201
|
+
value = self.expect_integer_or_float_()
|
|
2202
2202
|
location[axis] = value
|
|
2203
2203
|
if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
|
|
2204
2204
|
# Lexer has just read the value as a glyph name. We'll correct it later
|
|
@@ -2230,6 +2230,16 @@ class Parser(object):
|
|
|
2230
2230
|
"Expected a floating-point number", self.cur_token_location_
|
|
2231
2231
|
)
|
|
2232
2232
|
|
|
2233
|
+
def expect_integer_or_float_(self):
|
|
2234
|
+
if self.next_token_type_ == Lexer.FLOAT:
|
|
2235
|
+
return self.expect_float_()
|
|
2236
|
+
elif self.next_token_type_ is Lexer.NUMBER:
|
|
2237
|
+
return self.expect_number_()
|
|
2238
|
+
else:
|
|
2239
|
+
raise FeatureLibError(
|
|
2240
|
+
"Expected an integer or floating-point number", self.cur_token_location_
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2233
2243
|
def expect_decipoint_(self):
|
|
2234
2244
|
if self.next_token_type_ == Lexer.FLOAT:
|
|
2235
2245
|
return self.expect_float_()
|
|
@@ -17,7 +17,12 @@ class VariableScalar:
|
|
|
17
17
|
def __repr__(self):
|
|
18
18
|
items = []
|
|
19
19
|
for location, value in self.values.items():
|
|
20
|
-
loc = ",".join(
|
|
20
|
+
loc = ",".join(
|
|
21
|
+
[
|
|
22
|
+
f"{ax}={int(coord) if float(coord).is_integer() else coord}"
|
|
23
|
+
for ax, coord in location
|
|
24
|
+
]
|
|
25
|
+
)
|
|
21
26
|
items.append("%s:%i" % (loc, value))
|
|
22
27
|
return "(" + (" ".join(items)) + ")"
|
|
23
28
|
|
|
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/textTools.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""fontTools.misc.textTools.py -- miscellaneous routines."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import ast
|
|
4
6
|
import string
|
|
5
7
|
|
|
@@ -118,14 +120,14 @@ def pad(data, size):
|
|
|
118
120
|
return data
|
|
119
121
|
|
|
120
122
|
|
|
121
|
-
def tostr(s, encoding="ascii", errors="strict"):
|
|
123
|
+
def tostr(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> str:
|
|
122
124
|
if not isinstance(s, str):
|
|
123
125
|
return s.decode(encoding, errors)
|
|
124
126
|
else:
|
|
125
127
|
return s
|
|
126
128
|
|
|
127
129
|
|
|
128
|
-
def tobytes(s, encoding="ascii", errors="strict"):
|
|
130
|
+
def tobytes(s: str | bytes, encoding: str = "ascii", errors: str = "strict") -> bytes:
|
|
129
131
|
if isinstance(s, str):
|
|
130
132
|
return s.encode(encoding, errors)
|
|
131
133
|
else:
|
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]
|
|
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)
|
|
Binary file
|
fontTools/subset/__init__.py
CHANGED
|
@@ -1530,6 +1530,7 @@ def subset_glyphs(self, s):
|
|
|
1530
1530
|
if self.MarkFilteringSet not in s.used_mark_sets:
|
|
1531
1531
|
self.MarkFilteringSet = None
|
|
1532
1532
|
self.LookupFlag &= ~0x10
|
|
1533
|
+
self.LookupFlag |= 0x8
|
|
1533
1534
|
else:
|
|
1534
1535
|
self.MarkFilteringSet = s.used_mark_sets.index(self.MarkFilteringSet)
|
|
1535
1536
|
return bool(self.SubTableCount)
|
|
@@ -143,7 +143,7 @@ class table__a_v_a_r(BaseTTXConverter):
|
|
|
143
143
|
else:
|
|
144
144
|
super().fromXML(name, attrs, content, ttFont)
|
|
145
145
|
|
|
146
|
-
def renormalizeLocation(self, location, font):
|
|
146
|
+
def renormalizeLocation(self, location, font, dropZeroes=True):
|
|
147
147
|
|
|
148
148
|
majorVersion = getattr(self, "majorVersion", 1)
|
|
149
149
|
|
|
@@ -185,7 +185,9 @@ class table__a_v_a_r(BaseTTXConverter):
|
|
|
185
185
|
out.append(v)
|
|
186
186
|
|
|
187
187
|
mappedLocation = {
|
|
188
|
-
axis.axisTag: fi2fl(v, 14)
|
|
188
|
+
axis.axisTag: fi2fl(v, 14)
|
|
189
|
+
for v, axis in zip(out, axes)
|
|
190
|
+
if v != 0 or not dropZeroes
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
return mappedLocation
|