svg-ultralight 0.64.0__py3-none-any.whl → 0.73.1__py3-none-any.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.
- svg_ultralight/__init__.py +8 -6
- svg_ultralight/bounding_boxes/bound_helpers.py +37 -50
- svg_ultralight/bounding_boxes/padded_text_initializers.py +148 -182
- svg_ultralight/bounding_boxes/type_bounding_box.py +31 -14
- svg_ultralight/bounding_boxes/type_padded_list.py +2 -7
- svg_ultralight/bounding_boxes/type_padded_text.py +240 -53
- svg_ultralight/constructors/new_element.py +37 -3
- svg_ultralight/font_tools/comp_results.py +18 -18
- svg_ultralight/font_tools/font_info.py +117 -36
- svg_ultralight/layout.py +37 -18
- svg_ultralight/main.py +26 -16
- svg_ultralight/root_elements.py +6 -4
- svg_ultralight/string_conversion.py +40 -8
- svg_ultralight/unit_conversion.py +104 -27
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/METADATA +1 -1
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/RECORD +17 -18
- svg_ultralight/read_svg.py +0 -58
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/WHEEL +0 -0
|
@@ -12,6 +12,7 @@ import math
|
|
|
12
12
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
13
13
|
from svg_ultralight.strings import svg_matrix
|
|
14
14
|
from svg_ultralight.transformations import mat_apply, mat_dot, new_transformation_matrix
|
|
15
|
+
from svg_ultralight.unit_conversion import MeasurementArg, to_user_units
|
|
15
16
|
|
|
16
17
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
17
18
|
|
|
@@ -39,7 +40,7 @@ class HasBoundingBox(SupportsBounds):
|
|
|
39
40
|
y = self.bbox.base_y
|
|
40
41
|
x2 = x + self.bbox.base_width
|
|
41
42
|
y2 = y + self.bbox.base_height
|
|
42
|
-
return (x, y), (
|
|
43
|
+
return (x, y), (x, y2), (x2, y2), (x2, y)
|
|
43
44
|
|
|
44
45
|
def values(self) -> tuple[float, float, float, float]:
|
|
45
46
|
"""Get the values of the bounding box.
|
|
@@ -65,12 +66,28 @@ class HasBoundingBox(SupportsBounds):
|
|
|
65
66
|
|
|
66
67
|
:return: four corners counter-clockwise starting at top left, transformed by
|
|
67
68
|
self.transformation
|
|
69
|
+
|
|
70
|
+
These quadrilateral defined by these corners may not be axis aligned. This is
|
|
71
|
+
purely for determining the bounds of the transformed box.
|
|
68
72
|
"""
|
|
69
73
|
c0, c1, c2, c3 = (
|
|
70
74
|
mat_apply(self.bbox.transformation, c) for c in self._get_input_corners()
|
|
71
75
|
)
|
|
72
76
|
return c0, c1, c2, c3
|
|
73
77
|
|
|
78
|
+
@property
|
|
79
|
+
def corners(
|
|
80
|
+
self,
|
|
81
|
+
) -> tuple[
|
|
82
|
+
tuple[float, float],
|
|
83
|
+
tuple[float, float],
|
|
84
|
+
tuple[float, float],
|
|
85
|
+
tuple[float, float],
|
|
86
|
+
]:
|
|
87
|
+
"""Get the corners of the bbox in the current state. CW from top left."""
|
|
88
|
+
x, y, x2, y2 = self.x, self.y, self.x2, self.y2
|
|
89
|
+
return ((x, y), (x, y2), (x2, y2), (x2, y))
|
|
90
|
+
|
|
74
91
|
def transform(
|
|
75
92
|
self,
|
|
76
93
|
transformation: _Matrix | None = None,
|
|
@@ -106,7 +123,7 @@ class HasBoundingBox(SupportsBounds):
|
|
|
106
123
|
def scale(self) -> tuple[float, float]:
|
|
107
124
|
"""Get scale of the bounding box.
|
|
108
125
|
|
|
109
|
-
:return:
|
|
126
|
+
:return: x and y scale of the bounding box
|
|
110
127
|
|
|
111
128
|
Use caution, the scale attribute can cause errors in intuition. Changing
|
|
112
129
|
width or height will change the scale attribute, but not the x or y values.
|
|
@@ -301,13 +318,13 @@ class BoundingBox(HasBoundingBox):
|
|
|
301
318
|
:param transformation: transformation matrix
|
|
302
319
|
|
|
303
320
|
Functions that return a bounding box will return a BoundingBox instance. This
|
|
304
|
-
instance can be transformed
|
|
305
|
-
|
|
321
|
+
instance can be transformed. Transformations will be combined and stored to be
|
|
322
|
+
passed to new_element as a transform value.
|
|
306
323
|
|
|
307
324
|
Define the bbox with x=, y=, width=, height=
|
|
308
325
|
|
|
309
326
|
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
310
|
-
x2, y, cy, y2, width, or height, private transformation value transformation
|
|
327
|
+
x2, y, cy, y2, width, or height, private transformation value `transformation`
|
|
311
328
|
will be updated.
|
|
312
329
|
|
|
313
330
|
The ultimate transformation can be accessed through ``.transform_string``.
|
|
@@ -316,7 +333,7 @@ class BoundingBox(HasBoundingBox):
|
|
|
316
333
|
1. Get the bounding box of an svg element
|
|
317
334
|
2. Update the bounding box x, y, width, and height
|
|
318
335
|
3. Transform the original svg element with
|
|
319
|
-
|
|
336
|
+
svg_ultralight.transform_element(elem, bbox.transformation)
|
|
320
337
|
4. The transformed element will lie in the transformed BoundingBox
|
|
321
338
|
|
|
322
339
|
In addition to x, y, width, and height, x2 and y2 can be set to establish the
|
|
@@ -352,10 +369,10 @@ class BoundingBox(HasBoundingBox):
|
|
|
352
369
|
|
|
353
370
|
def __init__(
|
|
354
371
|
self,
|
|
355
|
-
x:
|
|
356
|
-
y:
|
|
357
|
-
width:
|
|
358
|
-
height:
|
|
372
|
+
x: MeasurementArg,
|
|
373
|
+
y: MeasurementArg,
|
|
374
|
+
width: MeasurementArg,
|
|
375
|
+
height: MeasurementArg,
|
|
359
376
|
transformation: _Matrix = (1, 0, 0, 1, 0, 0),
|
|
360
377
|
) -> None:
|
|
361
378
|
"""Initialize a BoundingBox instance.
|
|
@@ -365,10 +382,10 @@ class BoundingBox(HasBoundingBox):
|
|
|
365
382
|
:param width: width of the bounding box
|
|
366
383
|
:param height: height of the bounding box
|
|
367
384
|
"""
|
|
368
|
-
self.base_x = x
|
|
369
|
-
self.base_y = y
|
|
370
|
-
self.base_width = width
|
|
371
|
-
self.base_height = height
|
|
385
|
+
self.base_x = to_user_units(x)
|
|
386
|
+
self.base_y = to_user_units(y)
|
|
387
|
+
self.base_width = to_user_units(width)
|
|
388
|
+
self.base_height = to_user_units(height)
|
|
372
389
|
self.transformation = transformation
|
|
373
390
|
self.bbox = self
|
|
374
391
|
|
|
@@ -32,7 +32,7 @@ from svg_ultralight.attrib_hints import ElemAttrib
|
|
|
32
32
|
from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
|
|
33
33
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
34
34
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
35
|
-
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
35
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText, new_padded_union
|
|
36
36
|
from svg_ultralight.constructors import update_element
|
|
37
37
|
from svg_ultralight.transformations import new_transformation_matrix
|
|
38
38
|
|
|
@@ -123,12 +123,7 @@ class PaddedList:
|
|
|
123
123
|
|
|
124
124
|
def padded_union(self, **attribs: ElemAttrib) -> PaddedText:
|
|
125
125
|
"""Return a PaddedText inst where the elem is a `g` of all the padded text."""
|
|
126
|
-
|
|
127
|
-
tpad = self.tbox.y - self.bbox.y
|
|
128
|
-
rpad = self.bbox.x2 - self.tbox.x2
|
|
129
|
-
bpad = self.bbox.y2 - self.tbox.y2
|
|
130
|
-
lpad = self.tbox.x - self.bbox.x
|
|
131
|
-
return PaddedText(union.elem, union.bbox, tpad, rpad, bpad, lpad)
|
|
126
|
+
return new_padded_union(*self.plems, **attribs)
|
|
132
127
|
|
|
133
128
|
def get_dim(self, dim: str) -> float:
|
|
134
129
|
"""Get a dimension from bbox or tbox."""
|
|
@@ -51,35 +51,77 @@ PaddedText instances with sensible defaults.
|
|
|
51
51
|
|
|
52
52
|
from __future__ import annotations
|
|
53
53
|
|
|
54
|
+
import dataclasses
|
|
54
55
|
import math
|
|
55
56
|
from typing import TYPE_CHECKING
|
|
56
57
|
|
|
57
|
-
from paragraphs import par
|
|
58
|
-
|
|
59
58
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
60
59
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
61
|
-
from svg_ultralight.
|
|
60
|
+
from svg_ultralight.constructors.new_element import new_element_union
|
|
61
|
+
from svg_ultralight.transformations import (
|
|
62
|
+
mat_apply,
|
|
63
|
+
new_transformation_matrix,
|
|
64
|
+
transform_element,
|
|
65
|
+
)
|
|
62
66
|
|
|
63
67
|
if TYPE_CHECKING:
|
|
64
68
|
from lxml.etree import (
|
|
65
69
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
66
70
|
)
|
|
67
71
|
|
|
72
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
73
|
+
|
|
68
74
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
69
75
|
|
|
70
|
-
_no_line_gap_msg = par(
|
|
71
|
-
"""No line_gap defined. Line gap is an inherent font attribute defined within a
|
|
72
|
-
font file. If this PaddedText instance was created with `pad_text` from reference
|
|
73
|
-
elements, a line_gap was not defined. Reading line_gap from the font file
|
|
74
|
-
requires creating a PaddedText instance with `pad_text_ft` or `pad_text_mixed`.
|
|
75
|
-
You can set an arbitrary line_gap after init with `instance.line_gap = value`."""
|
|
76
|
-
)
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
@dataclasses.dataclass
|
|
78
|
+
class FontMetrics:
|
|
79
|
+
"""Font metrics."""
|
|
80
|
+
|
|
81
|
+
_font_size: float
|
|
82
|
+
_ascent: float
|
|
83
|
+
_descent: float
|
|
84
|
+
_cap_height: float
|
|
85
|
+
_x_height: float
|
|
86
|
+
_line_gap: float
|
|
87
|
+
_scalar: float = dataclasses.field(default=1.0, init=False)
|
|
88
|
+
|
|
89
|
+
def scale(self, scalar: float) -> None:
|
|
90
|
+
"""Scale the font metrics by a scalar.
|
|
91
|
+
|
|
92
|
+
:param scalar: The scaling factor.
|
|
93
|
+
"""
|
|
94
|
+
self._scalar *= scalar
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def font_size(self) -> float:
|
|
98
|
+
"""The font size."""
|
|
99
|
+
return self._font_size * self._scalar
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def ascent(self) -> float:
|
|
103
|
+
"""Return the ascent."""
|
|
104
|
+
return self._ascent * self._scalar
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def descent(self) -> float:
|
|
108
|
+
"""Return the descent."""
|
|
109
|
+
return self._descent * self._scalar
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def cap_height(self) -> float:
|
|
113
|
+
"""Return the cap height."""
|
|
114
|
+
return self._cap_height * self._scalar
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def x_height(self) -> float:
|
|
118
|
+
"""Return the x height."""
|
|
119
|
+
return self._x_height * self._scalar
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def line_gap(self) -> float:
|
|
123
|
+
"""Return the line gap."""
|
|
124
|
+
return self._line_gap * self._scalar
|
|
83
125
|
|
|
84
126
|
|
|
85
127
|
class PaddedText(BoundElement):
|
|
@@ -93,8 +135,7 @@ class PaddedText(BoundElement):
|
|
|
93
135
|
rpad: float,
|
|
94
136
|
bpad: float,
|
|
95
137
|
lpad: float,
|
|
96
|
-
|
|
97
|
-
font_size: float | None = None,
|
|
138
|
+
metrics: FontMetrics | None = None,
|
|
98
139
|
) -> None:
|
|
99
140
|
"""Initialize a PaddedText instance.
|
|
100
141
|
|
|
@@ -113,8 +154,26 @@ class PaddedText(BoundElement):
|
|
|
113
154
|
self.rpad = rpad
|
|
114
155
|
self.base_bpad = bpad
|
|
115
156
|
self.lpad = lpad
|
|
116
|
-
self.
|
|
117
|
-
|
|
157
|
+
self._metrics = metrics
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def metrics(self) -> FontMetrics:
|
|
161
|
+
"""The font metrics for this PaddedText.
|
|
162
|
+
|
|
163
|
+
:return: The font metrics for this PaddedText.
|
|
164
|
+
"""
|
|
165
|
+
if self._metrics is None:
|
|
166
|
+
msg = "No font metrics defined for this PaddedText."
|
|
167
|
+
raise AttributeError(msg)
|
|
168
|
+
return self._metrics
|
|
169
|
+
|
|
170
|
+
@metrics.setter
|
|
171
|
+
def metrics(self, value: FontMetrics) -> None:
|
|
172
|
+
"""Set the font metrics for this PaddedText.
|
|
173
|
+
|
|
174
|
+
:param value: The new font metrics.
|
|
175
|
+
"""
|
|
176
|
+
self._metrics = value
|
|
118
177
|
|
|
119
178
|
@property
|
|
120
179
|
def tbox(self) -> BoundingBox:
|
|
@@ -181,17 +240,14 @@ class PaddedText(BoundElement):
|
|
|
181
240
|
transformed by tmat.
|
|
182
241
|
"""
|
|
183
242
|
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
184
|
-
self.
|
|
243
|
+
self.tbox.transform(tmat, reverse=reverse)
|
|
185
244
|
_ = transform_element(self.elem, tmat, reverse=reverse)
|
|
186
245
|
x_norm = pow(tmat[0] ** 2 + tmat[1] ** 2, 1 / 2)
|
|
187
246
|
self.lpad *= x_norm
|
|
188
247
|
self.rpad *= x_norm
|
|
189
|
-
if self.
|
|
248
|
+
if self._metrics:
|
|
190
249
|
y_norm = pow(tmat[2] ** 2 + tmat[3] ** 2, 1 / 2)
|
|
191
|
-
|
|
192
|
-
self._line_gap *= y_norm
|
|
193
|
-
if self._font_size:
|
|
194
|
-
self._font_size *= y_norm
|
|
250
|
+
self._metrics.scale(y_norm)
|
|
195
251
|
|
|
196
252
|
def transform_preserve_sidebearings(
|
|
197
253
|
self,
|
|
@@ -222,22 +278,152 @@ class PaddedText(BoundElement):
|
|
|
222
278
|
self.y2 = y2
|
|
223
279
|
|
|
224
280
|
@property
|
|
225
|
-
def
|
|
226
|
-
"""The
|
|
281
|
+
def tx(self) -> float:
|
|
282
|
+
"""The x value of the tight element bounding box.
|
|
283
|
+
|
|
284
|
+
:return: The x value of the tight element bounding box.
|
|
285
|
+
"""
|
|
286
|
+
return self.tbox.x
|
|
287
|
+
|
|
288
|
+
@tx.setter
|
|
289
|
+
def tx(self, value: float) -> None:
|
|
290
|
+
"""Set the x value of the tight element bounding box.
|
|
291
|
+
|
|
292
|
+
:param value: The new x value of the tight element bounding box.
|
|
293
|
+
"""
|
|
294
|
+
self.transform(dx=value - self.tbox.x)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def tx2(self) -> float:
|
|
298
|
+
"""The x2 value of the tight element bounding box.
|
|
299
|
+
|
|
300
|
+
:return: The x2 value of the tight element bounding box.
|
|
301
|
+
"""
|
|
302
|
+
return self.tbox.x2
|
|
303
|
+
|
|
304
|
+
@tx2.setter
|
|
305
|
+
def tx2(self, value: float) -> None:
|
|
306
|
+
"""Set the x2 value of the tight element bounding box.
|
|
307
|
+
|
|
308
|
+
:param value: The new x2 value of the tight element bounding box.
|
|
309
|
+
"""
|
|
310
|
+
self.transform(dx=value - self.tbox.x2)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def ty(self) -> float:
|
|
314
|
+
"""The y value of the tight element bounding box.
|
|
315
|
+
|
|
316
|
+
:return: The y value of the tight element bounding box.
|
|
317
|
+
"""
|
|
318
|
+
return self.tbox.y
|
|
227
319
|
|
|
228
|
-
|
|
320
|
+
@ty.setter
|
|
321
|
+
def ty(self, value: float) -> None:
|
|
322
|
+
"""Set the y value of the tight element bounding box.
|
|
323
|
+
|
|
324
|
+
:param value: The new y value of the tight element bounding box.
|
|
325
|
+
"""
|
|
326
|
+
self.transform(dy=value - self.tbox.y)
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def ty2(self) -> float:
|
|
330
|
+
"""The y2 value of the tight element bounding box.
|
|
331
|
+
|
|
332
|
+
:return: The y2 value of the tight element bounding box.
|
|
333
|
+
"""
|
|
334
|
+
return self.tbox.y2
|
|
335
|
+
|
|
336
|
+
@ty2.setter
|
|
337
|
+
def ty2(self, value: float) -> None:
|
|
338
|
+
"""Set the y2 value of the tight element bounding box.
|
|
339
|
+
|
|
340
|
+
:param value: The new y2 value of the tight element bounding box.
|
|
341
|
+
"""
|
|
342
|
+
self.transform(dy=value - self.tbox.y2)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def twidth(self) -> float:
|
|
346
|
+
"""The width of the tight element bounding box.
|
|
347
|
+
|
|
348
|
+
:return: The width of the tight element bounding box.
|
|
229
349
|
"""
|
|
230
|
-
|
|
231
|
-
raise AttributeError(_no_line_gap_msg)
|
|
232
|
-
return self._line_gap
|
|
350
|
+
return self.tbox.width
|
|
233
351
|
|
|
234
|
-
@
|
|
235
|
-
def
|
|
236
|
-
"""Set the
|
|
352
|
+
@twidth.setter
|
|
353
|
+
def twidth(self, value: float) -> None:
|
|
354
|
+
"""Set the width of the tight element bounding box.
|
|
237
355
|
|
|
238
|
-
:param value: The new
|
|
356
|
+
:param value: The new width of the tight element bounding box.
|
|
239
357
|
"""
|
|
240
|
-
self.
|
|
358
|
+
self.transform(scale=value / self.tbox.width)
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def theight(self) -> float:
|
|
362
|
+
"""The height of the tight element bounding box.
|
|
363
|
+
|
|
364
|
+
:return: The height of the tight element bounding box.
|
|
365
|
+
"""
|
|
366
|
+
return self.tbox.height
|
|
367
|
+
|
|
368
|
+
@theight.setter
|
|
369
|
+
def theight(self, value: float) -> None:
|
|
370
|
+
"""Set the height of the tight element bounding box.
|
|
371
|
+
|
|
372
|
+
:param value: The new height of the tight element bounding box.
|
|
373
|
+
"""
|
|
374
|
+
self.transform(scale=value / self.tbox.height)
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def baseline(self) -> float:
|
|
378
|
+
"""The y value of the baseline for the font.
|
|
379
|
+
|
|
380
|
+
:return: The baseline y value of this line of text.
|
|
381
|
+
"""
|
|
382
|
+
return mat_apply(self.tbox.transformation, (0, 0))[1]
|
|
383
|
+
return self.y2 + (self.metrics.descent)
|
|
384
|
+
|
|
385
|
+
@baseline.setter
|
|
386
|
+
def baseline(self, value: float) -> None:
|
|
387
|
+
"""Set the y value of the baseline for the font.
|
|
388
|
+
|
|
389
|
+
:param value: The new baseline y value.
|
|
390
|
+
"""
|
|
391
|
+
dy = value - self.baseline
|
|
392
|
+
self.transform(dy=dy)
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def capline(self) -> float:
|
|
396
|
+
"""The y value of the top of flat-topped capital letters for the font.
|
|
397
|
+
|
|
398
|
+
:return: The capline y value of this line of text.
|
|
399
|
+
"""
|
|
400
|
+
return self.baseline - self.metrics.cap_height
|
|
401
|
+
|
|
402
|
+
@capline.setter
|
|
403
|
+
def capline(self, value: float) -> None:
|
|
404
|
+
"""Set the capline y value for the font.
|
|
405
|
+
|
|
406
|
+
:param value: The new capline y value.
|
|
407
|
+
"""
|
|
408
|
+
dy = value - self.capline
|
|
409
|
+
self.transform(dy=dy)
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def xline(self) -> float:
|
|
413
|
+
"""The y value of the x-height for the font.
|
|
414
|
+
|
|
415
|
+
:return: The xline y value of this line of text.
|
|
416
|
+
"""
|
|
417
|
+
return self.baseline - self.metrics.x_height
|
|
418
|
+
|
|
419
|
+
@xline.setter
|
|
420
|
+
def xline(self, value: float) -> None:
|
|
421
|
+
"""Set the xline y value for the font.
|
|
422
|
+
|
|
423
|
+
:param value: The new xline y value.
|
|
424
|
+
"""
|
|
425
|
+
dy = value - self.xline
|
|
426
|
+
self.transform(dy=dy)
|
|
241
427
|
|
|
242
428
|
@property
|
|
243
429
|
def font_size(self) -> float:
|
|
@@ -245,9 +431,7 @@ class PaddedText(BoundElement):
|
|
|
245
431
|
|
|
246
432
|
:return: The font size of this line of text.
|
|
247
433
|
"""
|
|
248
|
-
|
|
249
|
-
raise AttributeError(_no_font_size_msg)
|
|
250
|
-
return self._font_size
|
|
434
|
+
return self.metrics.font_size
|
|
251
435
|
|
|
252
436
|
@font_size.setter
|
|
253
437
|
def font_size(self, value: float) -> None:
|
|
@@ -263,7 +447,7 @@ class PaddedText(BoundElement):
|
|
|
263
447
|
|
|
264
448
|
:return: The line gap plus the height of this line of text.
|
|
265
449
|
"""
|
|
266
|
-
return self.height + self.line_gap
|
|
450
|
+
return self.height + self.metrics.line_gap
|
|
267
451
|
|
|
268
452
|
@property
|
|
269
453
|
def tpad(self) -> float:
|
|
@@ -332,19 +516,6 @@ class PaddedText(BoundElement):
|
|
|
332
516
|
)
|
|
333
517
|
self.transform(scale=new_scale)
|
|
334
518
|
|
|
335
|
-
@property
|
|
336
|
-
def uniform_scale(self) -> float:
|
|
337
|
-
"""Get uniform scale of the bounding box.
|
|
338
|
-
|
|
339
|
-
:return: uniform scale of the bounding box
|
|
340
|
-
:raises ValueError: if the scale is non-uniform.
|
|
341
|
-
"""
|
|
342
|
-
scale = self.scale
|
|
343
|
-
if math.isclose(scale[0], scale[1]):
|
|
344
|
-
return scale[0]
|
|
345
|
-
msg = f"Non-uniform scale detected: sx={scale[0]}, sy={scale[1]}"
|
|
346
|
-
raise ValueError(msg)
|
|
347
|
-
|
|
348
519
|
@property
|
|
349
520
|
def width(self) -> float:
|
|
350
521
|
"""The width of this line of text with padding.
|
|
@@ -500,3 +671,19 @@ class PaddedText(BoundElement):
|
|
|
500
671
|
:param value: The bottom of this line of text.
|
|
501
672
|
"""
|
|
502
673
|
self.transform(dy=value - self.bpad - self.tbox.y2)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def new_padded_union(*plems: PaddedText, **attributes: ElemAttrib) -> PaddedText:
|
|
677
|
+
"""Create a new PaddedText instance that is the union of multiple PaddedText.
|
|
678
|
+
|
|
679
|
+
:param plems: The PaddedText instances to union.
|
|
680
|
+
:return: A new PaddedText instance that is the union of the input instances.
|
|
681
|
+
"""
|
|
682
|
+
bbox = BoundingBox.union(*(t.bbox for t in plems))
|
|
683
|
+
tbox = BoundingBox.union(*(t.tbox for t in plems))
|
|
684
|
+
tpad = tbox.y - bbox.y
|
|
685
|
+
rpad = bbox.x2 - tbox.x2
|
|
686
|
+
bpad = bbox.y2 - tbox.y2
|
|
687
|
+
lpad = tbox.x - bbox.x
|
|
688
|
+
elem = new_element_union(*(t.elem for t in plems), **attributes)
|
|
689
|
+
return PaddedText(elem, tbox, tpad, rpad, bpad, lpad, plems[0]._metrics) # pyright: ignore[reportPrivateUsage]
|
|
@@ -16,6 +16,7 @@ import warnings
|
|
|
16
16
|
from typing import TYPE_CHECKING
|
|
17
17
|
|
|
18
18
|
from lxml import etree
|
|
19
|
+
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
19
20
|
|
|
20
21
|
from svg_ultralight.string_conversion import set_attributes
|
|
21
22
|
|
|
@@ -23,9 +24,6 @@ if TYPE_CHECKING:
|
|
|
23
24
|
from lxml.etree import (
|
|
24
25
|
QName,
|
|
25
26
|
)
|
|
26
|
-
from lxml.etree import (
|
|
27
|
-
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
28
|
-
)
|
|
29
27
|
|
|
30
28
|
from svg_ultralight.attrib_hints import ElemAttrib
|
|
31
29
|
|
|
@@ -115,3 +113,39 @@ def deepcopy_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeEleme
|
|
|
115
113
|
elem = copy.deepcopy(elem)
|
|
116
114
|
_ = update_element(elem, **attributes)
|
|
117
115
|
return elem
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def new_element_union(
|
|
119
|
+
*elems: EtreeElement | object, **attributes: ElemAttrib
|
|
120
|
+
) -> EtreeElement:
|
|
121
|
+
"""Get the union of any elements found in the given arguments.
|
|
122
|
+
|
|
123
|
+
:param elems: EtreeElements or containers like BoundElements, PaddedTexts, or
|
|
124
|
+
others that have an `elem` attribute that is an EtreeElement. Other arguments
|
|
125
|
+
will be ignored.
|
|
126
|
+
:return: a new group element containing all elements.
|
|
127
|
+
|
|
128
|
+
This does not support consolidating attributes. E.g., if all elements have the
|
|
129
|
+
same fill color, this will not be recognized and consolidated into a single
|
|
130
|
+
attribute for the group. Too many attributes change their behavior when applied
|
|
131
|
+
to a group.
|
|
132
|
+
"""
|
|
133
|
+
elements_found: list[EtreeElement] = []
|
|
134
|
+
for elem in elems:
|
|
135
|
+
if isinstance(elem, EtreeElement):
|
|
136
|
+
elements_found.append(elem)
|
|
137
|
+
continue
|
|
138
|
+
elem_elem = getattr(elem, "elem", None)
|
|
139
|
+
if isinstance(elem_elem, EtreeElement):
|
|
140
|
+
elements_found.append(elem_elem)
|
|
141
|
+
|
|
142
|
+
if not elements_found:
|
|
143
|
+
msg = (
|
|
144
|
+
"Cannot find any elements to union. "
|
|
145
|
+
+ "At least one argument must be a "
|
|
146
|
+
+ "BoundElement, PaddedText, or EtreeElement."
|
|
147
|
+
)
|
|
148
|
+
raise ValueError(msg)
|
|
149
|
+
group = new_element("g", **attributes)
|
|
150
|
+
group.extend(elements_found)
|
|
151
|
+
return group
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Compare results between Inkscape and fontTools.
|
|
2
2
|
|
|
3
3
|
Function `check_font_tools_alignment` will let you know if it's relatively safe to
|
|
4
|
-
use `
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
use `pad_text_ft`, which improves `pad_text_inkscape` by assigning `line_gap` values
|
|
5
|
+
to the resulting PaddedText instance and by aligning with the actual descent and
|
|
6
|
+
ascent of a font instead of by attempting to infer these from a referenve string.
|
|
7
7
|
|
|
8
8
|
See Enum `FontBboxError` for the possible error codes and their meanings returned by
|
|
9
9
|
`check_font`.
|
|
@@ -27,8 +27,8 @@ from typing import TYPE_CHECKING
|
|
|
27
27
|
from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_rect, pad_bbox
|
|
28
28
|
from svg_ultralight.bounding_boxes.padded_text_initializers import (
|
|
29
29
|
DEFAULT_Y_BOUNDS_REFERENCE,
|
|
30
|
-
pad_text,
|
|
31
30
|
pad_text_ft,
|
|
31
|
+
pad_text_inkscape,
|
|
32
32
|
)
|
|
33
33
|
from svg_ultralight.constructors import new_element
|
|
34
34
|
from svg_ultralight.font_tools.font_info import FTFontInfo, get_svg_font_attributes
|
|
@@ -45,14 +45,14 @@ if TYPE_CHECKING:
|
|
|
45
45
|
class FontBboxError(enum.Enum):
|
|
46
46
|
"""Classify the type of error between Inkscape and fontTools bounding boxes.
|
|
47
47
|
|
|
48
|
-
INIT: Use `
|
|
48
|
+
INIT: Use `pad_text_inkscape`.
|
|
49
49
|
|
|
50
|
-
FontTools failed to run. This can happen with fonts that, inentionally or
|
|
51
|
-
not
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
FontTools failed to run. This can happen with fonts that, inentionally or not, do
|
|
51
|
+
not have the required tables or character sets to build a bounding box around the
|
|
52
|
+
TEXT_TEXT. You can only use the `pad_text_inkscape` PaddedText constructor. This
|
|
53
|
+
font may work with other or ascii-only text.
|
|
54
54
|
|
|
55
|
-
ELEM_Y: Use `
|
|
55
|
+
ELEM_Y: Use `pad_text_inkscape` with cautions.
|
|
56
56
|
|
|
57
57
|
The y coordinate of the element bounding box is off by more than 1% of
|
|
58
58
|
the height. This error matters, because the y coordinates are used by
|
|
@@ -60,21 +60,21 @@ class FontBboxError(enum.Enum):
|
|
|
60
60
|
y_bounds_reference element and accept some potential error in `line_gap` or
|
|
61
61
|
explicitly pass `ascent` and `descent` values to `pad_text_ft` or `pad_text_mix`.
|
|
62
62
|
|
|
63
|
-
SAFE_ELEM_X:
|
|
63
|
+
SAFE_ELEM_X:
|
|
64
64
|
|
|
65
65
|
The y bounds are accurate, but the x coordinate of the element
|
|
66
66
|
bounding box is off by more than 1%. This is called "safe" because it is not used
|
|
67
67
|
by pad_bbox_mix, but you cannot use `pad_text_ft` without expecting BoundingBox
|
|
68
68
|
inaccuracies.
|
|
69
69
|
|
|
70
|
-
LINE_Y: Use `
|
|
70
|
+
LINE_Y: Use `pad_text_inkscape` with caution.
|
|
71
71
|
|
|
72
72
|
All of the above match, but the y coordinate of the line bounding box
|
|
73
73
|
(the padded bounding box) is off by more than 1% of the height. This error
|
|
74
74
|
matters as does ELEM_Y, but it does not exist for any font on my system. Fonts
|
|
75
75
|
without ELEM_Y errors should not have LINE_Y errors.
|
|
76
76
|
|
|
77
|
-
SAFE_LINE_X:
|
|
77
|
+
SAFE_LINE_X:
|
|
78
78
|
|
|
79
79
|
All of the above match, but the x coordinate of the line bounding
|
|
80
80
|
box (the padded bounding box) is off by more than 1%. This is safe or unsafe as
|
|
@@ -82,9 +82,9 @@ class FontBboxError(enum.Enum):
|
|
|
82
82
|
|
|
83
83
|
NO_ERROR: Use `pad_text_ft`.
|
|
84
84
|
|
|
85
|
-
No errors were found. The bounding boxes match within 1% of the height.
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
No errors were found. The bounding boxes match within 1% of the height. You can
|
|
86
|
+
use `pad_text_ft` to get the same result as `pad_text_inkscape` without the delay
|
|
87
|
+
caused by an Inkscape call.
|
|
88
88
|
"""
|
|
89
89
|
|
|
90
90
|
INIT = enum.auto()
|
|
@@ -154,7 +154,7 @@ def check_font_tools_alignment(
|
|
|
154
154
|
try:
|
|
155
155
|
svg_attribs = get_svg_font_attributes(font)
|
|
156
156
|
text_elem = new_element("text", **svg_attribs, text=text)
|
|
157
|
-
rslt_pt =
|
|
157
|
+
rslt_pt = pad_text_inkscape(inkscape, text_elem)
|
|
158
158
|
rslt_ft = pad_text_ft(
|
|
159
159
|
font,
|
|
160
160
|
text,
|
|
@@ -212,7 +212,7 @@ def draw_comparison(
|
|
|
212
212
|
stroke="green",
|
|
213
213
|
stroke_width=0.1,
|
|
214
214
|
)
|
|
215
|
-
padded_pt =
|
|
215
|
+
padded_pt = pad_text_inkscape(inkscape, text_elem)
|
|
216
216
|
padded_ft = pad_text_ft(
|
|
217
217
|
font,
|
|
218
218
|
text,
|