fonttools 4.55.4__cp313-cp313-musllinux_1_2_aarch64.whl → 4.61.1__cp313-cp313-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.
- fontTools/__init__.py +1 -1
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +65 -10
- fontTools/cffLib/__init__.py +61 -26
- fontTools/cffLib/specializer.py +4 -1
- fontTools/cffLib/transforms.py +11 -6
- fontTools/config/__init__.py +15 -0
- fontTools/cu2qu/cu2qu.c +6567 -5579
- fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/cu2qu/cu2qu.py +36 -4
- fontTools/cu2qu/ufo.py +14 -0
- fontTools/designspaceLib/__init__.py +8 -3
- fontTools/designspaceLib/statNames.py +14 -7
- fontTools/feaLib/ast.py +24 -15
- fontTools/feaLib/builder.py +139 -66
- fontTools/feaLib/error.py +1 -1
- fontTools/feaLib/lexer.c +7038 -7995
- fontTools/feaLib/lexer.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/feaLib/parser.py +75 -40
- fontTools/feaLib/variableScalar.py +6 -1
- fontTools/fontBuilder.py +50 -44
- fontTools/merge/__init__.py +1 -1
- fontTools/merge/cmap.py +33 -1
- fontTools/merge/tables.py +12 -1
- fontTools/misc/bezierTools.c +14913 -17013
- fontTools/misc/bezierTools.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/misc/bezierTools.py +4 -1
- fontTools/misc/configTools.py +3 -1
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +4 -27
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +1 -1
- fontTools/misc/loggingTools.py +1 -1
- fontTools/misc/psCharStrings.py +17 -2
- fontTools/misc/sstruct.py +2 -6
- fontTools/misc/symfont.py +6 -8
- fontTools/misc/testTools.py +5 -1
- fontTools/misc/textTools.py +4 -2
- fontTools/misc/visitor.py +32 -16
- fontTools/misc/xmlWriter.py +44 -8
- fontTools/mtiLib/__init__.py +1 -3
- fontTools/otlLib/builder.py +402 -155
- fontTools/otlLib/optimize/gpos.py +49 -63
- fontTools/pens/filterPen.py +218 -26
- fontTools/pens/momentsPen.c +5514 -5584
- fontTools/pens/momentsPen.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/pens/pointPen.py +61 -18
- fontTools/pens/roundingPen.py +2 -2
- fontTools/pens/t2CharStringPen.py +31 -11
- fontTools/qu2cu/qu2cu.c +6581 -6168
- fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/subset/__init__.py +283 -25
- fontTools/subset/svg.py +2 -3
- fontTools/ttLib/__init__.py +4 -0
- fontTools/ttLib/__main__.py +47 -8
- fontTools/ttLib/removeOverlaps.py +7 -5
- fontTools/ttLib/reorderGlyphs.py +8 -7
- fontTools/ttLib/sfnt.py +11 -9
- fontTools/ttLib/tables/D__e_b_g.py +20 -2
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/S__i_l_f.py +2 -2
- fontTools/ttLib/tables/T_S_I__0.py +14 -3
- fontTools/ttLib/tables/T_S_I__1.py +2 -5
- fontTools/ttLib/tables/T_S_I__5.py +18 -7
- fontTools/ttLib/tables/__init__.py +1 -0
- fontTools/ttLib/tables/_a_v_a_r.py +12 -3
- fontTools/ttLib/tables/_c_m_a_p.py +20 -7
- fontTools/ttLib/tables/_c_v_t.py +3 -2
- fontTools/ttLib/tables/_f_p_g_m.py +3 -1
- fontTools/ttLib/tables/_g_l_y_f.py +45 -21
- fontTools/ttLib/tables/_g_v_a_r.py +67 -19
- fontTools/ttLib/tables/_h_d_m_x.py +4 -4
- fontTools/ttLib/tables/_h_m_t_x.py +7 -3
- fontTools/ttLib/tables/_l_o_c_a.py +2 -2
- fontTools/ttLib/tables/_n_a_m_e.py +11 -6
- fontTools/ttLib/tables/_p_o_s_t.py +9 -7
- fontTools/ttLib/tables/otBase.py +5 -12
- fontTools/ttLib/tables/otConverters.py +5 -2
- fontTools/ttLib/tables/otData.py +1 -1
- fontTools/ttLib/tables/otTables.py +33 -30
- fontTools/ttLib/tables/otTraverse.py +2 -1
- fontTools/ttLib/tables/sbixStrike.py +3 -3
- fontTools/ttLib/ttFont.py +666 -120
- fontTools/ttLib/ttGlyphSet.py +0 -10
- fontTools/ttLib/woff2.py +10 -13
- fontTools/ttx.py +13 -1
- fontTools/ufoLib/__init__.py +300 -202
- fontTools/ufoLib/converters.py +103 -30
- fontTools/ufoLib/errors.py +8 -0
- fontTools/ufoLib/etree.py +1 -1
- fontTools/ufoLib/filenames.py +171 -106
- fontTools/ufoLib/glifLib.py +303 -205
- fontTools/ufoLib/kerning.py +98 -48
- fontTools/ufoLib/utils.py +46 -15
- fontTools/ufoLib/validators.py +121 -99
- fontTools/unicodedata/Blocks.py +35 -20
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/ScriptExtensions.py +63 -37
- fontTools/unicodedata/Scripts.py +173 -152
- fontTools/unicodedata/__init__.py +10 -2
- fontTools/varLib/__init__.py +198 -109
- 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/featureVars.py +21 -7
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +180 -65
- fontTools/varLib/interpolatableHelpers.py +3 -0
- fontTools/varLib/iup.c +7564 -6903
- fontTools/varLib/iup.cpython-313-aarch64-linux-musl.so +0 -0
- fontTools/varLib/models.py +17 -2
- fontTools/varLib/mutator.py +11 -0
- fontTools/varLib/varStore.py +10 -38
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +4 -0
- fontTools/voltLib/parser.py +16 -8
- fontTools/voltLib/voltToFea.py +347 -166
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/METADATA +269 -1410
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/RECORD +318 -294
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/WHEEL +1 -1
- fonttools-4.61.1.dist-info/licenses/LICENSE.external +388 -0
- {fonttools-4.55.4.data → fonttools-4.61.1.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/entry_points.txt +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info/licenses}/LICENSE +0 -0
- {fonttools-4.55.4.dist-info → fonttools-4.61.1.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from collections import defaultdict, namedtuple
|
|
4
|
-
from
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import cached_property, reduce
|
|
5
6
|
from itertools import chain
|
|
6
7
|
from math import log2
|
|
7
8
|
from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple
|
|
@@ -53,12 +54,18 @@ def compact(font: TTFont, level: int) -> TTFont:
|
|
|
53
54
|
# are not grouped together first; instead each subtable is treated
|
|
54
55
|
# independently, so currently this step is:
|
|
55
56
|
# Split existing subtables into more smaller subtables
|
|
56
|
-
gpos = font
|
|
57
|
+
gpos = font.get("GPOS")
|
|
58
|
+
|
|
59
|
+
# If the font does not contain a GPOS table, there is nothing to do.
|
|
60
|
+
if gpos is None:
|
|
61
|
+
return font
|
|
62
|
+
|
|
57
63
|
for lookup in gpos.table.LookupList.Lookup:
|
|
58
64
|
if lookup.LookupType == 2:
|
|
59
65
|
compact_lookup(font, level, lookup)
|
|
60
66
|
elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
|
|
61
67
|
compact_ext_lookup(font, level, lookup)
|
|
68
|
+
|
|
62
69
|
return font
|
|
63
70
|
|
|
64
71
|
|
|
@@ -186,79 +193,58 @@ ClusteringContext = namedtuple(
|
|
|
186
193
|
)
|
|
187
194
|
|
|
188
195
|
|
|
196
|
+
@dataclass
|
|
189
197
|
class Cluster:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# indices: int
|
|
193
|
-
# Caches
|
|
194
|
-
# TODO(Python 3.8): use functools.cached_property instead of the
|
|
195
|
-
# manually cached properties, and remove the cache fields listed below.
|
|
196
|
-
# _indices: Optional[List[int]] = None
|
|
197
|
-
# _column_indices: Optional[List[int]] = None
|
|
198
|
-
# _cost: Optional[int] = None
|
|
199
|
-
|
|
200
|
-
__slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost"
|
|
201
|
-
|
|
202
|
-
def __init__(self, ctx: ClusteringContext, indices_bitmask: int):
|
|
203
|
-
self.ctx = ctx
|
|
204
|
-
self.indices_bitmask = indices_bitmask
|
|
205
|
-
self._indices = None
|
|
206
|
-
self._column_indices = None
|
|
207
|
-
self._cost = None
|
|
198
|
+
ctx: ClusteringContext
|
|
199
|
+
indices_bitmask: int
|
|
208
200
|
|
|
209
|
-
@
|
|
201
|
+
@cached_property
|
|
210
202
|
def indices(self):
|
|
211
|
-
|
|
212
|
-
self._indices = bit_indices(self.indices_bitmask)
|
|
213
|
-
return self._indices
|
|
203
|
+
return bit_indices(self.indices_bitmask)
|
|
214
204
|
|
|
215
|
-
@
|
|
205
|
+
@cached_property
|
|
216
206
|
def column_indices(self):
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self._column_indices = bit_indices(bitmask)
|
|
222
|
-
return self._column_indices
|
|
207
|
+
# Indices of columns that have a 1 in at least 1 line
|
|
208
|
+
# => binary OR all the lines
|
|
209
|
+
bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices))
|
|
210
|
+
return bit_indices(bitmask)
|
|
223
211
|
|
|
224
212
|
@property
|
|
225
213
|
def width(self):
|
|
226
214
|
# Add 1 because Class2=0 cannot be used but needs to be encoded.
|
|
227
215
|
return len(self.column_indices) + 1
|
|
228
216
|
|
|
229
|
-
@
|
|
217
|
+
@cached_property
|
|
230
218
|
def cost(self):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
)
|
|
261
|
-
return self._cost
|
|
219
|
+
return (
|
|
220
|
+
# 2 bytes to store the offset to this subtable in the Lookup table above
|
|
221
|
+
2
|
|
222
|
+
# Contents of the subtable
|
|
223
|
+
# From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment
|
|
224
|
+
# uint16 posFormat Format identifier: format = 2
|
|
225
|
+
+ 2
|
|
226
|
+
# Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable.
|
|
227
|
+
+ 2
|
|
228
|
+
+ self.coverage_bytes
|
|
229
|
+
# uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero).
|
|
230
|
+
+ 2
|
|
231
|
+
# uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero).
|
|
232
|
+
+ 2
|
|
233
|
+
# Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
|
|
234
|
+
+ 2
|
|
235
|
+
+ self.classDef1_bytes
|
|
236
|
+
# Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
|
|
237
|
+
+ 2
|
|
238
|
+
+ self.classDef2_bytes
|
|
239
|
+
# uint16 class1Count Number of classes in classDef1 table — includes Class 0.
|
|
240
|
+
+ 2
|
|
241
|
+
# uint16 class2Count Number of classes in classDef2 table — includes Class 0.
|
|
242
|
+
+ 2
|
|
243
|
+
# Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1.
|
|
244
|
+
+ (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes)
|
|
245
|
+
* len(self.indices)
|
|
246
|
+
* self.width
|
|
247
|
+
)
|
|
262
248
|
|
|
263
249
|
@property
|
|
264
250
|
def coverage_bytes(self):
|
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]
|