svg-ultralight 0.38.0__py3-none-any.whl → 0.39.0__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.
Potentially problematic release.
This version of svg-ultralight might be problematic. Click here for more details.
- svg_ultralight/bounding_boxes/bound_helpers.py +3 -10
- svg_ultralight/bounding_boxes/supports_bounds.py +4 -9
- svg_ultralight/bounding_boxes/type_bound_collection.py +1 -1
- svg_ultralight/bounding_boxes/type_bound_element.py +1 -4
- svg_ultralight/bounding_boxes/type_bounding_box.py +155 -262
- svg_ultralight/bounding_boxes/type_padded_text.py +62 -204
- svg_ultralight/layout.py +22 -13
- svg_ultralight/query.py +33 -23
- svg_ultralight/transformations.py +12 -6
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.0.dist-info}/METADATA +1 -1
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.0.dist-info}/RECORD +13 -13
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.0.dist-info}/WHEEL +1 -1
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.0.dist-info}/top_level.txt +0 -0
|
@@ -10,8 +10,9 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
|
|
11
11
|
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
12
12
|
|
|
13
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
13
14
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
14
|
-
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
15
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox, HasBoundingBox
|
|
15
16
|
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
16
17
|
from svg_ultralight.constructors import new_element
|
|
17
18
|
|
|
@@ -66,15 +67,7 @@ def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
|
|
|
66
67
|
|
|
67
68
|
Will used the padded_box attribute of PaddedText instances.
|
|
68
69
|
"""
|
|
69
|
-
bboxes
|
|
70
|
-
for blem in blems:
|
|
71
|
-
if isinstance(blem, BoundingBox):
|
|
72
|
-
bboxes.append(blem)
|
|
73
|
-
elif isinstance(blem, BoundElement):
|
|
74
|
-
bboxes.append(blem.bbox)
|
|
75
|
-
elif isinstance(blem, PaddedText):
|
|
76
|
-
bboxes.append(blem.padded_bbox)
|
|
77
|
-
|
|
70
|
+
bboxes = [x.bbox for x in blems if isinstance(x, HasBoundingBox)]
|
|
78
71
|
if not bboxes:
|
|
79
72
|
msg = (
|
|
80
73
|
"Cannot find any bounding boxes to union. "
|
|
@@ -37,7 +37,7 @@ class SupportsBounds(Protocol):
|
|
|
37
37
|
cy (float): The center y coordinate.
|
|
38
38
|
width (float): The width of the object.
|
|
39
39
|
height(float): The height of the object.
|
|
40
|
-
scale (float): The scale of the object.
|
|
40
|
+
scale ((float, float)): The x and yx and y scale of the object.
|
|
41
41
|
|
|
42
42
|
There is no setter for scale. Scale is a function of width and height.
|
|
43
43
|
Setting scale would be ambiguous. because the typical implementation of
|
|
@@ -45,16 +45,11 @@ class SupportsBounds(Protocol):
|
|
|
45
45
|
set width and height.
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
|
-
@property
|
|
49
|
-
def transformation(self) -> _Matrix:
|
|
50
|
-
"""Return an svg-style transformation matrix."""
|
|
51
|
-
...
|
|
52
|
-
|
|
53
48
|
def transform(
|
|
54
49
|
self,
|
|
55
50
|
transformation: _Matrix | None = None,
|
|
56
51
|
*,
|
|
57
|
-
scale: float | None = None,
|
|
52
|
+
scale: tuple[float, float] | None = None,
|
|
58
53
|
dx: float | None = None,
|
|
59
54
|
dy: float | None = None,
|
|
60
55
|
):
|
|
@@ -158,12 +153,12 @@ class SupportsBounds(Protocol):
|
|
|
158
153
|
"""
|
|
159
154
|
|
|
160
155
|
@property
|
|
161
|
-
def scale(self) -> float:
|
|
156
|
+
def scale(self) -> tuple[float, float]:
|
|
162
157
|
"""Return scale of the object."""
|
|
163
158
|
...
|
|
164
159
|
|
|
165
160
|
@scale.setter
|
|
166
|
-
def scale(self, value: float):
|
|
161
|
+
def scale(self, value: tuple[float, float]):
|
|
167
162
|
"""Return scale of the object.
|
|
168
163
|
|
|
169
164
|
:param value: The scale of the object.
|
|
@@ -45,14 +45,11 @@ class BoundElement(HasBoundingBox):
|
|
|
45
45
|
self.elem = element
|
|
46
46
|
self.bbox = bounding_box
|
|
47
47
|
|
|
48
|
-
def _update_elem(self):
|
|
49
|
-
self.elem.attrib["transform"] = self.bbox.transform_string
|
|
50
|
-
|
|
51
48
|
def transform(
|
|
52
49
|
self,
|
|
53
50
|
transformation: _Matrix | None = None,
|
|
54
51
|
*,
|
|
55
|
-
scale: float | None = None,
|
|
52
|
+
scale: tuple[float, float] | None = None,
|
|
56
53
|
dx: float | None = None,
|
|
57
54
|
dy: float | None = None,
|
|
58
55
|
):
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import dataclasses
|
|
10
|
+
import math
|
|
10
11
|
|
|
11
12
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
12
13
|
from svg_ultralight.string_conversion import format_number
|
|
@@ -15,85 +16,64 @@ from svg_ultralight.transformations import mat_apply, mat_dot, new_transformatio
|
|
|
15
16
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
class
|
|
20
|
-
"""Mutable bounding box object for svg_ultralight.
|
|
21
|
-
|
|
22
|
-
:param x: left x value
|
|
23
|
-
:param y: top y value
|
|
24
|
-
:param width: width of the bounding box
|
|
25
|
-
:param height: height of the bounding box
|
|
26
|
-
|
|
27
|
-
The below optional parameter, in addition to the required parameters, captures
|
|
28
|
-
the entire state of a BoundingBox instance. It could be used to make a copy or
|
|
29
|
-
to initialize a transformed box with the same transform_string as another box.
|
|
30
|
-
Under most circumstances, it will not be used.
|
|
31
|
-
|
|
32
|
-
:param _transformation: transformation matrix
|
|
33
|
-
|
|
34
|
-
Functions that return a bounding box will return a BoundingBox instance. This
|
|
35
|
-
instance can be transformed (uniform scale and translate only). Transformations
|
|
36
|
-
will be combined and scored to be passed to new_element as a transform value.
|
|
37
|
-
|
|
38
|
-
Define the bbox with x=, y=, width=, height=
|
|
39
|
-
|
|
40
|
-
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
41
|
-
x2, y, cy, y2, width, or height, private transformation value _transformation
|
|
42
|
-
will be updated.
|
|
43
|
-
|
|
44
|
-
The ultimate transformation can be accessed through ``.transform_string``.
|
|
45
|
-
So the workflow will look like :
|
|
46
|
-
|
|
47
|
-
1. Get the bounding box of an svg element
|
|
48
|
-
2. Update the bounding box x, y, width, and height
|
|
49
|
-
3. Transform the original svg element with
|
|
50
|
-
update_element(elem, transform=bbox.transform_string)
|
|
51
|
-
4. The transformed element will lie in the transformed BoundingBox
|
|
52
|
-
|
|
53
|
-
In addition to x, y, width, and height, x2 and y2 can be set to establish the
|
|
54
|
-
right x value or bottom y value.
|
|
55
|
-
|
|
56
|
-
The point of all of this is to simplify stacking and aligning elements. To stack:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
elem_a = new_element(*args)
|
|
60
|
-
bbox_a = get_bounding_box(elem_a)
|
|
61
|
-
|
|
62
|
-
elem_b = new_element(*args)
|
|
63
|
-
bbox_b = get_bounding_box(elem_b)
|
|
64
|
-
|
|
65
|
-
# align at same x
|
|
66
|
-
bbox_b.x = bbox_a.x
|
|
67
|
-
|
|
68
|
-
# make the same width
|
|
69
|
-
bbox_b.width = bbox_a.width
|
|
19
|
+
class HasBoundingBox(SupportsBounds):
|
|
20
|
+
"""A parent class for BoundElement and others that have a bbox attribute."""
|
|
70
21
|
|
|
71
|
-
|
|
72
|
-
|
|
22
|
+
def __init__(self, bbox: BoundingBox) -> None:
|
|
23
|
+
"""Initialize the HasBoundingBox instance."""
|
|
24
|
+
self.bbox = bbox
|
|
73
25
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
26
|
+
def _get_input_corners(
|
|
27
|
+
self,
|
|
28
|
+
) -> tuple[
|
|
29
|
+
tuple[float, float],
|
|
30
|
+
tuple[float, float],
|
|
31
|
+
tuple[float, float],
|
|
32
|
+
tuple[float, float],
|
|
33
|
+
]:
|
|
34
|
+
"""Get the input corners of the bounding box.
|
|
35
|
+
|
|
36
|
+
:return: four corners counter-clockwise starting at top left
|
|
37
|
+
"""
|
|
38
|
+
x = self.bbox.base_x
|
|
39
|
+
y = self.bbox.base_y
|
|
40
|
+
x2 = x + self.bbox.base_width
|
|
41
|
+
y2 = y + self.bbox.base_height
|
|
42
|
+
return (x, y), (x2, y), (x2, y2), (x, y2)
|
|
43
|
+
|
|
44
|
+
def _get_transformed_corners(
|
|
45
|
+
self,
|
|
46
|
+
) -> tuple[
|
|
47
|
+
tuple[float, float],
|
|
48
|
+
tuple[float, float],
|
|
49
|
+
tuple[float, float],
|
|
50
|
+
tuple[float, float],
|
|
51
|
+
]:
|
|
52
|
+
"""Get the transformed corners of the bounding box.
|
|
77
53
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
:return: four corners counter-clockwise starting at top left, transformed by
|
|
55
|
+
self.transformation
|
|
56
|
+
"""
|
|
57
|
+
c0, c1, c2, c3 = (
|
|
58
|
+
mat_apply(self.bbox.transformation, c) for c in self._get_input_corners()
|
|
59
|
+
)
|
|
60
|
+
return c0, c1, c2, c3
|
|
83
61
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"""Return transformation matrix.
|
|
62
|
+
def _scale_scale_by_uniform_scalar(self, scalar: float) -> None:
|
|
63
|
+
"""Scale the bounding box uniformly by a factor.
|
|
87
64
|
|
|
88
|
-
:
|
|
65
|
+
:param scale: scale factor
|
|
66
|
+
Unlike self.scale, this does not set the scale, but scales the scale. So if
|
|
67
|
+
the current scale is (2, 6), and you call this with a scalar of 2, the new
|
|
68
|
+
scale will be (4, 12).
|
|
89
69
|
"""
|
|
90
|
-
|
|
70
|
+
self.transform(scale=(scalar, scalar))
|
|
91
71
|
|
|
92
72
|
def transform(
|
|
93
73
|
self,
|
|
94
74
|
transformation: _Matrix | None = None,
|
|
95
75
|
*,
|
|
96
|
-
scale: float | None = None,
|
|
76
|
+
scale: tuple[float, float] | None = None,
|
|
97
77
|
dx: float | None = None,
|
|
98
78
|
dy: float | None = None,
|
|
99
79
|
):
|
|
@@ -112,10 +92,10 @@ class BoundingBox(SupportsBounds):
|
|
|
112
92
|
when applying a transformation from another bounding box instance.
|
|
113
93
|
"""
|
|
114
94
|
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
115
|
-
self.
|
|
95
|
+
self.bbox.transformation = mat_dot(tmat, self.bbox.transformation)
|
|
116
96
|
|
|
117
97
|
@property
|
|
118
|
-
def scale(self) -> float:
|
|
98
|
+
def scale(self) -> tuple[float, float]:
|
|
119
99
|
"""Get scale of the bounding box.
|
|
120
100
|
|
|
121
101
|
:return: uniform scale of the bounding box
|
|
@@ -127,10 +107,11 @@ class BoundingBox(SupportsBounds):
|
|
|
127
107
|
width*scale, height => height*scale, scale => scale*scale. This matches how
|
|
128
108
|
scale works in almost every other context.
|
|
129
109
|
"""
|
|
130
|
-
|
|
110
|
+
xx, xy, yx, yy, *_ = self.bbox.transformation
|
|
111
|
+
return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
|
|
131
112
|
|
|
132
113
|
@scale.setter
|
|
133
|
-
def scale(self, value: float) -> None:
|
|
114
|
+
def scale(self, value: tuple[float, float]) -> None:
|
|
134
115
|
"""Scale the bounding box by a uniform factor.
|
|
135
116
|
|
|
136
117
|
:param value: new scale value
|
|
@@ -142,7 +123,8 @@ class BoundingBox(SupportsBounds):
|
|
|
142
123
|
`scale = 2` -> ignore whatever scale was previously defined and set scale to 2
|
|
143
124
|
`scale *= 2` -> make it twice as big as it was.
|
|
144
125
|
"""
|
|
145
|
-
self.
|
|
126
|
+
new_scale = value[0] / self.scale[0], value[1] / self.scale[1]
|
|
127
|
+
self.transform(scale=new_scale)
|
|
146
128
|
|
|
147
129
|
@property
|
|
148
130
|
def x(self) -> float:
|
|
@@ -150,13 +132,13 @@ class BoundingBox(SupportsBounds):
|
|
|
150
132
|
|
|
151
133
|
:return: internal _x value transformed by scale and translation
|
|
152
134
|
"""
|
|
153
|
-
return
|
|
135
|
+
return min(x for x, _ in self._get_transformed_corners())
|
|
154
136
|
|
|
155
137
|
@x.setter
|
|
156
|
-
def x(self, value: float)
|
|
157
|
-
"""
|
|
138
|
+
def x(self, value: float):
|
|
139
|
+
"""Set the x coordinate of the left edge of the bounding box.
|
|
158
140
|
|
|
159
|
-
:param value: new x
|
|
141
|
+
:param value: the new x coordinate of the left edge of the bounding box
|
|
160
142
|
"""
|
|
161
143
|
self.transform(dx=value - self.x)
|
|
162
144
|
|
|
@@ -182,7 +164,7 @@ class BoundingBox(SupportsBounds):
|
|
|
182
164
|
|
|
183
165
|
:return: transformed x + transformed width
|
|
184
166
|
"""
|
|
185
|
-
return
|
|
167
|
+
return max(x for x, _ in self._get_transformed_corners())
|
|
186
168
|
|
|
187
169
|
@x2.setter
|
|
188
170
|
def x2(self, value: float) -> None:
|
|
@@ -198,7 +180,7 @@ class BoundingBox(SupportsBounds):
|
|
|
198
180
|
|
|
199
181
|
:return: internal _y value transformed by scale and translation
|
|
200
182
|
"""
|
|
201
|
-
return
|
|
183
|
+
return min(y for _, y in self._get_transformed_corners())
|
|
202
184
|
|
|
203
185
|
@y.setter
|
|
204
186
|
def y(self, value: float) -> None:
|
|
@@ -230,7 +212,7 @@ class BoundingBox(SupportsBounds):
|
|
|
230
212
|
|
|
231
213
|
:return: transformed y + transformed height
|
|
232
214
|
"""
|
|
233
|
-
return
|
|
215
|
+
return max(y for _, y in self._get_transformed_corners())
|
|
234
216
|
|
|
235
217
|
@y2.setter
|
|
236
218
|
def y2(self, value: float) -> None:
|
|
@@ -246,7 +228,7 @@ class BoundingBox(SupportsBounds):
|
|
|
246
228
|
|
|
247
229
|
:return: internal _width value transformed by scale
|
|
248
230
|
"""
|
|
249
|
-
return self.
|
|
231
|
+
return self.x2 - self.x
|
|
250
232
|
|
|
251
233
|
@width.setter
|
|
252
234
|
def width(self, value: float) -> None:
|
|
@@ -259,7 +241,7 @@ class BoundingBox(SupportsBounds):
|
|
|
259
241
|
"""
|
|
260
242
|
current_x = self.x
|
|
261
243
|
current_y = self.y
|
|
262
|
-
self.
|
|
244
|
+
self._scale_scale_by_uniform_scalar(value / self.width)
|
|
263
245
|
self.x = current_x
|
|
264
246
|
self.y = current_y
|
|
265
247
|
|
|
@@ -269,7 +251,7 @@ class BoundingBox(SupportsBounds):
|
|
|
269
251
|
|
|
270
252
|
:return: internal _height value transformed by scale
|
|
271
253
|
"""
|
|
272
|
-
return self.
|
|
254
|
+
return self.y2 - self.y
|
|
273
255
|
|
|
274
256
|
@height.setter
|
|
275
257
|
def height(self, value: float) -> None:
|
|
@@ -291,7 +273,96 @@ class BoundingBox(SupportsBounds):
|
|
|
291
273
|
Use with
|
|
292
274
|
``update_element(elem, transform=bbox.transform_string)``
|
|
293
275
|
"""
|
|
294
|
-
return f"matrix({' '.join(map(format_number, self.transformation))})"
|
|
276
|
+
return f"matrix({' '.join(map(format_number, self.bbox.transformation))})"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclasses.dataclass
|
|
280
|
+
class BoundingBox(HasBoundingBox):
|
|
281
|
+
"""Mutable bounding box object for svg_ultralight.
|
|
282
|
+
|
|
283
|
+
:param x: left x value
|
|
284
|
+
:param y: top y value
|
|
285
|
+
:param width: width of the bounding box
|
|
286
|
+
:param height: height of the bounding box
|
|
287
|
+
|
|
288
|
+
The below optional parameter, in addition to the required parameters, captures
|
|
289
|
+
the entire state of a BoundingBox instance. It could be used to make a copy or
|
|
290
|
+
to initialize a transformed box with the same transform_string as another box.
|
|
291
|
+
Under most circumstances, it will not be used.
|
|
292
|
+
|
|
293
|
+
:param transformation: transformation matrix
|
|
294
|
+
|
|
295
|
+
Functions that return a bounding box will return a BoundingBox instance. This
|
|
296
|
+
instance can be transformed (uniform scale and translate only). Transformations
|
|
297
|
+
will be combined and scored to be passed to new_element as a transform value.
|
|
298
|
+
|
|
299
|
+
Define the bbox with x=, y=, width=, height=
|
|
300
|
+
|
|
301
|
+
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
302
|
+
x2, y, cy, y2, width, or height, private transformation value transformation
|
|
303
|
+
will be updated.
|
|
304
|
+
|
|
305
|
+
The ultimate transformation can be accessed through ``.transform_string``.
|
|
306
|
+
So the workflow will look like :
|
|
307
|
+
|
|
308
|
+
1. Get the bounding box of an svg element
|
|
309
|
+
2. Update the bounding box x, y, width, and height
|
|
310
|
+
3. Transform the original svg element with
|
|
311
|
+
update_element(elem, transform=bbox.transform_string)
|
|
312
|
+
4. The transformed element will lie in the transformed BoundingBox
|
|
313
|
+
|
|
314
|
+
In addition to x, y, width, and height, x2 and y2 can be set to establish the
|
|
315
|
+
right x value or bottom y value.
|
|
316
|
+
|
|
317
|
+
The point of all of this is to simplify stacking and aligning elements. To stack:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
elem_a = new_element(*args)
|
|
321
|
+
bbox_a = get_bounding_box(elem_a)
|
|
322
|
+
|
|
323
|
+
elem_b = new_element(*args)
|
|
324
|
+
bbox_b = get_bounding_box(elem_b)
|
|
325
|
+
|
|
326
|
+
# align at same x
|
|
327
|
+
bbox_b.x = bbox_a.x
|
|
328
|
+
|
|
329
|
+
# make the same width
|
|
330
|
+
bbox_b.width = bbox_a.width
|
|
331
|
+
|
|
332
|
+
# stack a on top of b
|
|
333
|
+
bbox_a.y2 = bbox_b.y
|
|
334
|
+
|
|
335
|
+
update_element(elem_a, transform=bbox_a.transform_string)
|
|
336
|
+
update_element(elem_b, transform=bbox_b.transform_string)
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
base_x: float = dataclasses.field(init=False)
|
|
340
|
+
base_y: float = dataclasses.field(init=False)
|
|
341
|
+
base_width: float = dataclasses.field(init=False)
|
|
342
|
+
base_height: float = dataclasses.field(init=False)
|
|
343
|
+
transformation: _Matrix = dataclasses.field(init=False)
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
x: float,
|
|
348
|
+
y: float,
|
|
349
|
+
width: float,
|
|
350
|
+
height: float,
|
|
351
|
+
transformation: _Matrix = (1, 0, 0, 1, 0, 0),
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Initialize a BoundingBox instance.
|
|
354
|
+
|
|
355
|
+
:param x: left x value
|
|
356
|
+
:param y: top y value
|
|
357
|
+
:param width: width of the bounding box
|
|
358
|
+
:param height: height of the bounding box
|
|
359
|
+
"""
|
|
360
|
+
self.base_x = x
|
|
361
|
+
self.base_y = y
|
|
362
|
+
self.base_width = width
|
|
363
|
+
self.base_height = height
|
|
364
|
+
self.transformation = transformation
|
|
365
|
+
self.bbox = self
|
|
295
366
|
|
|
296
367
|
def merge(self, *others: BoundingBox) -> BoundingBox:
|
|
297
368
|
"""Create a bounding box around all other bounding boxes.
|
|
@@ -321,181 +392,3 @@ class BoundingBox(SupportsBounds):
|
|
|
321
392
|
min_y = min(x.y for x in bboxes)
|
|
322
393
|
max_y = max(x.y + x.height for x in bboxes)
|
|
323
394
|
return BoundingBox(min_x, min_y, max_x - min_x, max_y - min_y)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
class HasBoundingBox(SupportsBounds):
|
|
327
|
-
"""A parent class for BoundElement and others that have a bbox attribute."""
|
|
328
|
-
|
|
329
|
-
def __init__(self, bbox: BoundingBox) -> None:
|
|
330
|
-
"""Initialize the HasBoundingBox instance."""
|
|
331
|
-
self.bbox = bbox
|
|
332
|
-
|
|
333
|
-
@property
|
|
334
|
-
def transformation(self) -> _Matrix:
|
|
335
|
-
"""The transformation matrix of the bounding box."""
|
|
336
|
-
return self.bbox.transformation
|
|
337
|
-
|
|
338
|
-
def transform(
|
|
339
|
-
self,
|
|
340
|
-
transformation: _Matrix | None = None,
|
|
341
|
-
*,
|
|
342
|
-
scale: float | None = None,
|
|
343
|
-
dx: float | None = None,
|
|
344
|
-
dy: float | None = None,
|
|
345
|
-
):
|
|
346
|
-
"""Transform the element and bounding box.
|
|
347
|
-
|
|
348
|
-
:param transformation: a 6-tuple transformation matrix
|
|
349
|
-
:param scale: a scaling factor
|
|
350
|
-
:param dx: the x translation
|
|
351
|
-
:param dy: the y translation
|
|
352
|
-
"""
|
|
353
|
-
self.bbox.transform(transformation, scale=scale, dx=dx, dy=dy)
|
|
354
|
-
|
|
355
|
-
@property
|
|
356
|
-
def scale(self) -> float:
|
|
357
|
-
"""The scale of the bounding box.
|
|
358
|
-
|
|
359
|
-
:return: the scale of the bounding box
|
|
360
|
-
"""
|
|
361
|
-
return self.transformation[0]
|
|
362
|
-
|
|
363
|
-
@scale.setter
|
|
364
|
-
def scale(self, value: float):
|
|
365
|
-
"""Set the scale of the bounding box.
|
|
366
|
-
|
|
367
|
-
:param value: the scale of the bounding box
|
|
368
|
-
"""
|
|
369
|
-
self.transform(scale=value / self.scale)
|
|
370
|
-
|
|
371
|
-
@property
|
|
372
|
-
def x(self) -> float:
|
|
373
|
-
"""The x coordinate of the left edge of the bounding box.
|
|
374
|
-
|
|
375
|
-
:return: the x coordinate of the left edge of the bounding box
|
|
376
|
-
"""
|
|
377
|
-
return self.bbox.x
|
|
378
|
-
|
|
379
|
-
@x.setter
|
|
380
|
-
def x(self, value: float):
|
|
381
|
-
"""Set the x coordinate of the left edge of the bounding box.
|
|
382
|
-
|
|
383
|
-
:param value: the new x coordinate of the left edge of the bounding box
|
|
384
|
-
"""
|
|
385
|
-
self.transform(dx=value - self.x)
|
|
386
|
-
|
|
387
|
-
@property
|
|
388
|
-
def x2(self) -> float:
|
|
389
|
-
"""The x coordinate of the right edge of the bounding box.
|
|
390
|
-
|
|
391
|
-
:return: the x coordinate of the right edge of the bounding box
|
|
392
|
-
"""
|
|
393
|
-
return self.bbox.x2
|
|
394
|
-
|
|
395
|
-
@x2.setter
|
|
396
|
-
def x2(self, value: float):
|
|
397
|
-
"""Set the x coordinate of the right edge of the bounding box.
|
|
398
|
-
|
|
399
|
-
:param value: the new x coordinate of the right edge of the bounding box
|
|
400
|
-
"""
|
|
401
|
-
self.x += value - self.x2
|
|
402
|
-
|
|
403
|
-
@property
|
|
404
|
-
def cx(self) -> float:
|
|
405
|
-
"""The x coordinate of the center of the bounding box.
|
|
406
|
-
|
|
407
|
-
:return: the x coordinate of the center of the bounding box
|
|
408
|
-
"""
|
|
409
|
-
return self.bbox.cx
|
|
410
|
-
|
|
411
|
-
@cx.setter
|
|
412
|
-
def cx(self, value: float):
|
|
413
|
-
"""Set the x coordinate of the center of the bounding box.
|
|
414
|
-
|
|
415
|
-
:param value: the new x coordinate of the center of the bounding box
|
|
416
|
-
"""
|
|
417
|
-
self.x += value - self.cx
|
|
418
|
-
|
|
419
|
-
@property
|
|
420
|
-
def y(self) -> float:
|
|
421
|
-
"""The y coordinate of the top edge of the bounding box.
|
|
422
|
-
|
|
423
|
-
:return: the y coordinate of the top edge of the bounding box
|
|
424
|
-
"""
|
|
425
|
-
return self.bbox.y
|
|
426
|
-
|
|
427
|
-
@y.setter
|
|
428
|
-
def y(self, value: float):
|
|
429
|
-
"""Set the y coordinate of the top edge of the bounding box.
|
|
430
|
-
|
|
431
|
-
:param value: the new y coordinate of the top edge of the bounding box
|
|
432
|
-
"""
|
|
433
|
-
self.transform(dy=value - self.y)
|
|
434
|
-
|
|
435
|
-
@property
|
|
436
|
-
def y2(self) -> float:
|
|
437
|
-
"""The y coordinate of the bottom edge of the bounding box.
|
|
438
|
-
|
|
439
|
-
:return: the y coordinate of the bottom edge of the bounding box
|
|
440
|
-
"""
|
|
441
|
-
return self.bbox.y2
|
|
442
|
-
|
|
443
|
-
@y2.setter
|
|
444
|
-
def y2(self, value: float):
|
|
445
|
-
"""Set the y coordinate of the bottom edge of the bounding box.
|
|
446
|
-
|
|
447
|
-
:param value: the new y coordinate of the bottom edge of the bounding box
|
|
448
|
-
"""
|
|
449
|
-
self.y += value - self.y2
|
|
450
|
-
|
|
451
|
-
@property
|
|
452
|
-
def cy(self) -> float:
|
|
453
|
-
"""The y coordinate of the center of the bounding box.
|
|
454
|
-
|
|
455
|
-
:return: the y coordinate of the center of the bounding box
|
|
456
|
-
"""
|
|
457
|
-
return self.bbox.cy
|
|
458
|
-
|
|
459
|
-
@cy.setter
|
|
460
|
-
def cy(self, value: float):
|
|
461
|
-
"""Set the y coordinate of the center of the bounding box.
|
|
462
|
-
|
|
463
|
-
:param value: the new y coordinate of the center of the bounding box
|
|
464
|
-
"""
|
|
465
|
-
self.y += value - self.cy
|
|
466
|
-
|
|
467
|
-
@property
|
|
468
|
-
def width(self) -> float:
|
|
469
|
-
"""The width of the bounding box.
|
|
470
|
-
|
|
471
|
-
:return: the width of the bounding box
|
|
472
|
-
"""
|
|
473
|
-
return self.bbox.width
|
|
474
|
-
|
|
475
|
-
@width.setter
|
|
476
|
-
def width(self, value: float):
|
|
477
|
-
"""Set the width of the bounding box.
|
|
478
|
-
|
|
479
|
-
:param value: the new width of the bounding box
|
|
480
|
-
"""
|
|
481
|
-
current_x = self.x
|
|
482
|
-
current_y = self.y
|
|
483
|
-
self.scale *= value / self.width
|
|
484
|
-
self.x = current_x
|
|
485
|
-
self.y = current_y
|
|
486
|
-
|
|
487
|
-
@property
|
|
488
|
-
def height(self) -> float:
|
|
489
|
-
"""The height of the bounding box.
|
|
490
|
-
|
|
491
|
-
:return: the height of the bounding box
|
|
492
|
-
"""
|
|
493
|
-
return self.bbox.height
|
|
494
|
-
|
|
495
|
-
@height.setter
|
|
496
|
-
def height(self, value: float):
|
|
497
|
-
"""Set the height of the bounding box.
|
|
498
|
-
|
|
499
|
-
:param value: the new height of the bounding box
|
|
500
|
-
"""
|
|
501
|
-
self.width *= value / self.height
|
|
@@ -12,7 +12,7 @@ There is a getter and setter for each of the four padding values. These *do not*
|
|
|
12
12
|
the text element. For instance, if you decrease the left padding, the left margin
|
|
13
13
|
will move, *not* the text element.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
_There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
|
|
16
16
|
These *do* move the element, but do not scale it. For instance, if you move the
|
|
17
17
|
leftmargin to the left, the right margin (and the text element with it) will move to
|
|
18
18
|
the left.
|
|
@@ -66,8 +66,9 @@ from __future__ import annotations
|
|
|
66
66
|
|
|
67
67
|
from typing import TYPE_CHECKING
|
|
68
68
|
|
|
69
|
-
from svg_ultralight.bounding_boxes.
|
|
69
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
70
70
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
71
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
71
72
|
|
|
72
73
|
if TYPE_CHECKING:
|
|
73
74
|
from lxml.etree import (
|
|
@@ -77,7 +78,7 @@ if TYPE_CHECKING:
|
|
|
77
78
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
78
79
|
|
|
79
80
|
|
|
80
|
-
class PaddedText(
|
|
81
|
+
class PaddedText(BoundElement):
|
|
81
82
|
"""A line of text with a bounding box and padding."""
|
|
82
83
|
|
|
83
84
|
def __init__(
|
|
@@ -99,14 +100,14 @@ class PaddedText(SupportsBounds):
|
|
|
99
100
|
:param lpad: Left padding.
|
|
100
101
|
"""
|
|
101
102
|
self.elem = elem
|
|
102
|
-
self.
|
|
103
|
+
self.unpadded_bbox = bbox
|
|
103
104
|
self.base_tpad = tpad
|
|
104
105
|
self.rpad = rpad
|
|
105
106
|
self.base_bpad = bpad
|
|
106
107
|
self.lpad = lpad
|
|
107
108
|
|
|
108
109
|
@property
|
|
109
|
-
def
|
|
110
|
+
def bbox(self) -> BoundingBox:
|
|
110
111
|
"""Return a BoundingBox around the margins and cap/baseline.
|
|
111
112
|
|
|
112
113
|
:return: A BoundingBox around the margins and cap/baseline.
|
|
@@ -117,22 +118,28 @@ class PaddedText(SupportsBounds):
|
|
|
117
118
|
instance around multiple text elements (a <g> elem).
|
|
118
119
|
"""
|
|
119
120
|
return BoundingBox(
|
|
120
|
-
self.
|
|
121
|
+
self.x,
|
|
122
|
+
self.y,
|
|
123
|
+
self.width,
|
|
124
|
+
self.height,
|
|
125
|
+
self.unpadded_bbox.transformation,
|
|
121
126
|
)
|
|
122
127
|
|
|
123
|
-
@
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
126
|
-
return self.bbox.transformation
|
|
128
|
+
@bbox.setter
|
|
129
|
+
def bbox(self, value: BoundingBox) -> None:
|
|
130
|
+
"""Set the bounding box of this PaddedText.
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
:param value: The new bounding box.
|
|
133
|
+
:effects: The text element is transformed to fit the new bounding box.
|
|
134
|
+
"""
|
|
135
|
+
msg = "Cannot set bbox of PaddedText, use transform() instead."
|
|
136
|
+
raise NotImplementedError(msg)
|
|
130
137
|
|
|
131
138
|
def transform(
|
|
132
139
|
self,
|
|
133
140
|
transformation: _Matrix | None = None,
|
|
134
141
|
*,
|
|
135
|
-
scale: float | None = None,
|
|
142
|
+
scale: tuple[float, float] | None = None,
|
|
136
143
|
dx: float | None = None,
|
|
137
144
|
dy: float | None = None,
|
|
138
145
|
):
|
|
@@ -143,8 +150,9 @@ class PaddedText(SupportsBounds):
|
|
|
143
150
|
:param dx: the x translation
|
|
144
151
|
:param dy: the y translation
|
|
145
152
|
"""
|
|
146
|
-
|
|
147
|
-
self.
|
|
153
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
154
|
+
self.unpadded_bbox.transform(tmat)
|
|
155
|
+
_ = transform_element(self.elem, tmat)
|
|
148
156
|
|
|
149
157
|
@property
|
|
150
158
|
def tpad(self) -> float:
|
|
@@ -152,7 +160,7 @@ class PaddedText(SupportsBounds):
|
|
|
152
160
|
|
|
153
161
|
:return: The scaled top padding of this line of text.
|
|
154
162
|
"""
|
|
155
|
-
return self.base_tpad * self.
|
|
163
|
+
return self.base_tpad * self.unpadded_bbox.scale[1]
|
|
156
164
|
|
|
157
165
|
@tpad.setter
|
|
158
166
|
def tpad(self, value: float) -> None:
|
|
@@ -160,7 +168,7 @@ class PaddedText(SupportsBounds):
|
|
|
160
168
|
|
|
161
169
|
:param value: The new top padding.
|
|
162
170
|
"""
|
|
163
|
-
self.base_tpad = value / self.
|
|
171
|
+
self.base_tpad = value / self.unpadded_bbox.scale[1]
|
|
164
172
|
|
|
165
173
|
@property
|
|
166
174
|
def bpad(self) -> float:
|
|
@@ -168,7 +176,7 @@ class PaddedText(SupportsBounds):
|
|
|
168
176
|
|
|
169
177
|
:return: The scaled bottom padding of this line of text.
|
|
170
178
|
"""
|
|
171
|
-
return self.base_bpad * self.
|
|
179
|
+
return self.base_bpad * self.unpadded_bbox.scale[1]
|
|
172
180
|
|
|
173
181
|
@bpad.setter
|
|
174
182
|
def bpad(self, value: float) -> None:
|
|
@@ -176,82 +184,18 @@ class PaddedText(SupportsBounds):
|
|
|
176
184
|
|
|
177
185
|
:param value: The new bottom padding.
|
|
178
186
|
"""
|
|
179
|
-
self.base_bpad = value / self.
|
|
180
|
-
|
|
181
|
-
@property
|
|
182
|
-
def lmargin(self) -> float:
|
|
183
|
-
"""The left margin of this line of text.
|
|
184
|
-
|
|
185
|
-
:return: The left margin of this line of text.
|
|
186
|
-
"""
|
|
187
|
-
return self.bbox.x - self.lpad
|
|
188
|
-
|
|
189
|
-
@lmargin.setter
|
|
190
|
-
def lmargin(self, value: float) -> None:
|
|
191
|
-
"""Set the left margin of this line of text.
|
|
192
|
-
|
|
193
|
-
:param value: The left margin of this line of text.
|
|
194
|
-
"""
|
|
195
|
-
self.transform(dx=value + self.lpad - self.bbox.x)
|
|
196
|
-
|
|
197
|
-
@property
|
|
198
|
-
def rmargin(self) -> float:
|
|
199
|
-
"""The right margin of this line of text.
|
|
200
|
-
|
|
201
|
-
:return: The right margin of this line of text.
|
|
202
|
-
"""
|
|
203
|
-
return self.bbox.x2 + self.rpad
|
|
204
|
-
|
|
205
|
-
@rmargin.setter
|
|
206
|
-
def rmargin(self, value: float) -> None:
|
|
207
|
-
"""Set the right margin of this line of text.
|
|
208
|
-
|
|
209
|
-
:param value: The right margin of this line of text.
|
|
210
|
-
"""
|
|
211
|
-
self.transform(dx=value - self.rpad - self.bbox.x2)
|
|
212
|
-
|
|
213
|
-
@property
|
|
214
|
-
def capline(self) -> float:
|
|
215
|
-
"""The top of this line of text.
|
|
216
|
-
|
|
217
|
-
:return: The top of this line of text.
|
|
218
|
-
"""
|
|
219
|
-
return self.bbox.y - self.tpad
|
|
220
|
-
|
|
221
|
-
@capline.setter
|
|
222
|
-
def capline(self, value: float) -> None:
|
|
223
|
-
"""Set the top of this line of text.
|
|
224
|
-
|
|
225
|
-
:param value: The top of this line of text.
|
|
226
|
-
"""
|
|
227
|
-
self.transform(dy=value + self.tpad - self.bbox.y)
|
|
187
|
+
self.base_bpad = value / self.unpadded_bbox.scale[1]
|
|
228
188
|
|
|
229
189
|
@property
|
|
230
|
-
def
|
|
231
|
-
"""The bottom of this line of text.
|
|
232
|
-
|
|
233
|
-
:return: The bottom of this line of text.
|
|
234
|
-
"""
|
|
235
|
-
return self.bbox.y2 + self.bpad
|
|
236
|
-
|
|
237
|
-
@baseline.setter
|
|
238
|
-
def baseline(self, value: float) -> None:
|
|
239
|
-
"""Set the bottom of this line of text.
|
|
240
|
-
|
|
241
|
-
:param value: The bottom of this line of text.
|
|
242
|
-
"""
|
|
243
|
-
self.transform(dy=value - self.bpad - self.bbox.y2)
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def padded_width(self) -> float:
|
|
190
|
+
def width(self) -> float:
|
|
247
191
|
"""The width of this line of text with padding.
|
|
248
192
|
|
|
249
193
|
:return: The scaled width of this line of text with padding.
|
|
250
194
|
"""
|
|
251
|
-
return self.
|
|
195
|
+
return self.unpadded_bbox.width + self.lpad + self.rpad
|
|
252
196
|
|
|
253
|
-
@
|
|
254
|
-
def
|
|
197
|
+
@width.setter
|
|
198
|
+
def width(self, value: float) -> None:
|
|
255
199
|
"""Scale to padded_width = width without scaling padding.
|
|
256
200
|
|
|
257
201
|
:param width: The new width of this line of text.
|
|
@@ -263,27 +207,26 @@ class PaddedText(SupportsBounds):
|
|
|
263
207
|
baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
|
|
264
208
|
*and* y2) when scaling.
|
|
265
209
|
"""
|
|
266
|
-
|
|
267
|
-
self.
|
|
268
|
-
self.
|
|
269
|
-
self._update_elem()
|
|
210
|
+
y2 = self.y2
|
|
211
|
+
self.unpadded_bbox.width = value - self.lpad - self.rpad
|
|
212
|
+
self.y2 = y2
|
|
270
213
|
|
|
271
214
|
@property
|
|
272
|
-
def
|
|
215
|
+
def height(self) -> float:
|
|
273
216
|
"""The height of this line of text with padding.
|
|
274
217
|
|
|
275
218
|
:return: The scaled height of this line of text with padding.
|
|
276
219
|
"""
|
|
277
|
-
return self.
|
|
220
|
+
return self.unpadded_bbox.height + self.tpad + self.bpad
|
|
278
221
|
|
|
279
|
-
@
|
|
280
|
-
def
|
|
281
|
-
"""Scale to
|
|
222
|
+
@height.setter
|
|
223
|
+
def height(self, value: float) -> None:
|
|
224
|
+
"""Scale to height without scaling padding.
|
|
282
225
|
|
|
283
226
|
:param height: The new height of this line of text.
|
|
284
227
|
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
285
228
|
"""
|
|
286
|
-
self.
|
|
229
|
+
self.width *= value / self.height
|
|
287
230
|
|
|
288
231
|
@property
|
|
289
232
|
def x(self) -> float:
|
|
@@ -291,15 +234,15 @@ class PaddedText(SupportsBounds):
|
|
|
291
234
|
|
|
292
235
|
:return: The left margin of this line of text.
|
|
293
236
|
"""
|
|
294
|
-
return self.
|
|
237
|
+
return self.unpadded_bbox.x - self.lpad
|
|
295
238
|
|
|
296
239
|
@x.setter
|
|
297
240
|
def x(self, value: float) -> None:
|
|
298
241
|
"""Set the left margin of this line of text.
|
|
299
242
|
|
|
300
|
-
:param value: The
|
|
243
|
+
:param value: The left margin of this line of text.
|
|
301
244
|
"""
|
|
302
|
-
self.
|
|
245
|
+
self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
|
|
303
246
|
|
|
304
247
|
@property
|
|
305
248
|
def x2(self) -> float:
|
|
@@ -307,134 +250,49 @@ class PaddedText(SupportsBounds):
|
|
|
307
250
|
|
|
308
251
|
:return: The right margin of this line of text.
|
|
309
252
|
"""
|
|
310
|
-
return self.
|
|
253
|
+
return self.unpadded_bbox.x2 + self.rpad
|
|
311
254
|
|
|
312
255
|
@x2.setter
|
|
313
256
|
def x2(self, value: float) -> None:
|
|
314
257
|
"""Set the right margin of this line of text.
|
|
315
258
|
|
|
316
|
-
:param value: The
|
|
259
|
+
:param value: The right margin of this line of text.
|
|
317
260
|
"""
|
|
318
|
-
self.
|
|
261
|
+
self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
|
|
319
262
|
|
|
320
263
|
@property
|
|
321
264
|
def y(self) -> float:
|
|
322
|
-
"""The
|
|
265
|
+
"""The top of this line of text.
|
|
323
266
|
|
|
324
|
-
:return: The
|
|
267
|
+
:return: The top of this line of text.
|
|
325
268
|
"""
|
|
326
|
-
return self.
|
|
269
|
+
return self.unpadded_bbox.y - self.tpad
|
|
327
270
|
|
|
328
271
|
@y.setter
|
|
329
272
|
def y(self, value: float) -> None:
|
|
330
|
-
"""Set the
|
|
273
|
+
"""Set the top of this line of text.
|
|
331
274
|
|
|
332
|
-
:param value: The
|
|
275
|
+
:param value: The top of this line of text.
|
|
333
276
|
"""
|
|
334
|
-
self.
|
|
277
|
+
self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
|
|
335
278
|
|
|
336
279
|
@property
|
|
337
280
|
def y2(self) -> float:
|
|
338
|
-
"""The
|
|
281
|
+
"""The bottom of this line of text.
|
|
339
282
|
|
|
340
|
-
:return: The
|
|
283
|
+
:return: The bottom of this line of text.
|
|
341
284
|
"""
|
|
342
|
-
return self.
|
|
285
|
+
return self.unpadded_bbox.y2 + self.bpad
|
|
343
286
|
|
|
344
287
|
@y2.setter
|
|
345
288
|
def y2(self, value: float) -> None:
|
|
346
|
-
"""Set the
|
|
347
|
-
|
|
348
|
-
:param value: The new baseline of this line of text.
|
|
349
|
-
"""
|
|
350
|
-
self.baseline = value
|
|
351
|
-
|
|
352
|
-
@property
|
|
353
|
-
def width(self) -> float:
|
|
354
|
-
"""The width of this line of text with padding.
|
|
355
|
-
|
|
356
|
-
:return: The scaled width of this line of text with padding.
|
|
357
|
-
"""
|
|
358
|
-
return self.padded_width
|
|
359
|
-
|
|
360
|
-
@width.setter
|
|
361
|
-
def width(self, value: float) -> None:
|
|
362
|
-
"""Scale to width without scaling padding.
|
|
363
|
-
|
|
364
|
-
:param value: The new width of this line of text.
|
|
365
|
-
:effects: the text_element bounding box is scaled to width - lpad - rpad.
|
|
366
|
-
|
|
367
|
-
Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
|
|
368
|
-
consistent with how rectangles, viewboxes, and anything else defined by x, y,
|
|
369
|
-
width, height behaves in SVG. This is unintuitive for text, because the
|
|
370
|
-
baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
|
|
371
|
-
*and* y2) when scaling.
|
|
372
|
-
"""
|
|
373
|
-
baseline = self.baseline
|
|
374
|
-
self.padded_width = value
|
|
375
|
-
self.baseline = baseline
|
|
376
|
-
|
|
377
|
-
@property
|
|
378
|
-
def height(self) -> float:
|
|
379
|
-
"""The height of this line of text with padding.
|
|
380
|
-
|
|
381
|
-
:return: The scaled height of this line of text with padding.
|
|
382
|
-
"""
|
|
383
|
-
return self.padded_height
|
|
384
|
-
|
|
385
|
-
@height.setter
|
|
386
|
-
def height(self, value: float) -> None:
|
|
387
|
-
"""Scale to height without scaling padding.
|
|
388
|
-
|
|
389
|
-
:param value: The new height of this line of text.
|
|
390
|
-
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
391
|
-
"""
|
|
392
|
-
self.padded_height = value
|
|
393
|
-
|
|
394
|
-
@property
|
|
395
|
-
def cx(self) -> float:
|
|
396
|
-
"""The x coordinate of the center between margins.
|
|
397
|
-
|
|
398
|
-
:return: the x coordinate of the center between margins
|
|
399
|
-
"""
|
|
400
|
-
return self.lmargin + self.padded_width / 2
|
|
401
|
-
|
|
402
|
-
@cx.setter
|
|
403
|
-
def cx(self, value: float):
|
|
404
|
-
"""Set the x coordinate of the center between margins.
|
|
405
|
-
|
|
406
|
-
:param value: the new x coordinate of the center between margins
|
|
407
|
-
"""
|
|
408
|
-
self.lmargin = value - self.padded_width / 2
|
|
409
|
-
|
|
410
|
-
@property
|
|
411
|
-
def cy(self) -> float:
|
|
412
|
-
"""The y coordinate of the center between baseline and capline.
|
|
413
|
-
|
|
414
|
-
:return: the y coordinate of the center between baseline and capline
|
|
415
|
-
"""
|
|
416
|
-
return self.capline + self.padded_height / 2
|
|
417
|
-
|
|
418
|
-
@cy.setter
|
|
419
|
-
def cy(self, value: float):
|
|
420
|
-
"""Set the y coordinate of the center between baseline and capline.
|
|
421
|
-
|
|
422
|
-
:param value: the new y coordinate of the center between baseline and capline
|
|
423
|
-
"""
|
|
424
|
-
self.capline = value - self.padded_height / 2
|
|
425
|
-
|
|
426
|
-
@property
|
|
427
|
-
def scale(self) -> float:
|
|
428
|
-
"""The scale of the text element.
|
|
289
|
+
"""Set the bottom of this line of text.
|
|
429
290
|
|
|
430
|
-
:
|
|
291
|
+
:param value: The bottom of this line of text.
|
|
431
292
|
"""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
@scale.setter
|
|
435
|
-
def scale(self, value: float):
|
|
436
|
-
"""Set the scale of the text element.
|
|
293
|
+
self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
|
|
437
294
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
295
|
+
lmargin = x
|
|
296
|
+
rmargin = x2
|
|
297
|
+
capline = y
|
|
298
|
+
baseline = y2
|
svg_ultralight/layout.py
CHANGED
|
@@ -144,11 +144,11 @@ def pad_and_scale(
|
|
|
144
144
|
"""Expand and scale the pad argument. If necessary, scale image.
|
|
145
145
|
|
|
146
146
|
:param viewbox: viewbox to pad (x, y, width height)
|
|
147
|
-
:param pad: padding to add around image, in user units or inches.
|
|
147
|
+
:param pad: padding to add around image, in user units or inches. If a
|
|
148
148
|
sequence, it should be (top, right, bottom, left). if a single float or
|
|
149
|
-
string, it will be applied to all sides.
|
|
150
|
-
then left and right.
|
|
151
|
-
|
|
149
|
+
string, it will be applied to all sides. If two floats, top and bottom
|
|
150
|
+
then left and right. If three floats, top, left and right, then bottom.
|
|
151
|
+
If four floats, top, right, bottom, left.
|
|
152
152
|
:param print_width: width of print area, in user units (float), a string
|
|
153
153
|
with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
|
|
154
154
|
"pt")
|
|
@@ -169,7 +169,7 @@ def pad_and_scale(
|
|
|
169
169
|
If the width and height *are* specified, the user units become whatever they
|
|
170
170
|
need to be to fit that requirement. For instance, if the viewbox width is 96
|
|
171
171
|
and the width argument is "1in", then the user units are *still* pixels,
|
|
172
|
-
because there are 96 pixels in an inch. If the viewbox
|
|
172
|
+
because there are 96 pixels in an inch. If the viewbox width is 2 and the
|
|
173
173
|
width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
|
|
174
174
|
pixels) each, because there are 2 user units in an inch. If the viewbox
|
|
175
175
|
width is 3 and the width argument is "1yd", the each user unit is 1 foot.
|
|
@@ -195,17 +195,26 @@ def pad_and_scale(
|
|
|
195
195
|
the unit designators without changing the scale.
|
|
196
196
|
|
|
197
197
|
Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
|
|
198
|
-
instance,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
instance, if you created two images
|
|
199
|
+
|
|
200
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
|
|
201
|
+
|
|
202
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
|
|
203
|
+
|
|
204
|
+
... (note that the images only vary in print_width_), the first image would be
|
|
205
|
+
rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
|
|
206
|
+
content in the viewbox would be exactly twice as wide in the larger image, but
|
|
207
|
+
the padding would remain 0.25 in both images. Despite setting `print_width_` to
|
|
208
|
+
exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
|
|
209
|
+
Despite a viewbox aspect ratio of 1:2, you would not get an output image of
|
|
210
|
+
exactly 1:2. If you want to use padding and need a specific output image size or
|
|
211
|
+
aspect ratio, remember to subtract the padding width from your print_width or
|
|
212
|
+
print_height.
|
|
204
213
|
|
|
205
214
|
Scaling attributes are returned as a dictonary that can be "exploded" into
|
|
206
215
|
the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
|
|
207
216
|
|
|
208
|
-
* If
|
|
217
|
+
* If neither a print_width nor print_height is specified, no scaling
|
|
209
218
|
attributes will be returned.
|
|
210
219
|
|
|
211
220
|
* If either is specified, both a width and height will be returned (even if
|
|
@@ -242,7 +251,7 @@ def pad_and_scale(
|
|
|
242
251
|
a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
|
|
243
252
|
... then scale the printout with dpu_=2 to get a 32" x 18" image with the
|
|
244
253
|
same viewbox. This means the padding will be 2" on all sides, but the image
|
|
245
|
-
will be identical (just twice as
|
|
254
|
+
will be identical (just twice as wide and twice as high) as the 16" x 9" image.
|
|
246
255
|
"""
|
|
247
256
|
pads = expand_pad_arg(pad)
|
|
248
257
|
|
svg_ultralight/query.py
CHANGED
|
@@ -63,7 +63,7 @@ def _fill_ids(*elem_args: EtreeElement) -> None:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _normalize_views(elem: EtreeElement) -> None:
|
|
66
|
-
"""Create a square
|
|
66
|
+
"""Create a square viewBox for any element with an svg tag.
|
|
67
67
|
|
|
68
68
|
:param elem: an etree element
|
|
69
69
|
|
|
@@ -110,51 +110,61 @@ def map_elems_to_bounding_boxes(
|
|
|
110
110
|
IMPORTANT: path cannot end with ``.exe``.
|
|
111
111
|
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
112
112
|
:param elem_args: xml element (written to a temporary file then queried)
|
|
113
|
-
:return: svg elements
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
:return: input svg elements and any descendents of those elements mapped
|
|
114
|
+
`BoundingBox(x, y, width, height)`
|
|
115
|
+
So return dict keys are the input elements themselves with one exception: a
|
|
116
|
+
string key, "svg", is mapped to a bounding box around all input elements.
|
|
117
|
+
:effects: temporarily adds an id attribute if any ids are missing. These are
|
|
118
|
+
removed if the function completes. Existing, non-unique ids will break this
|
|
119
|
+
function.
|
|
120
|
+
|
|
121
|
+
Bounding boxes are relative to svg viewBox. If, for instance, viewBox x == -10,
|
|
119
122
|
all bounding-box x values will be offset -10. So, everything is wrapped in a root
|
|
120
|
-
element with a "normalized"
|
|
121
|
-
elements ("child root elements" sounds wrong, but it works)
|
|
122
|
-
normalized as well. This works even with a root element around a
|
|
123
|
-
input elem_args can be root elements or "normal" elements like
|
|
124
|
-
or "text" or a mixture of both.
|
|
123
|
+
element, `envelope` with a "normalized" viewBox, `viewBox=(0, 0, 1, 1)`. That
|
|
124
|
+
way, any child root elements ("child root elements" sounds wrong, but it works)
|
|
125
|
+
viewBoxes are normalized as well. This works even with a root element around a
|
|
126
|
+
root element, so input elem_args can be root elements or "normal" elements like
|
|
127
|
+
"rect", "circle", or "text" or a mixture of both. Bounding boxes output here will
|
|
128
|
+
work as expected in any viewBox.
|
|
125
129
|
|
|
126
130
|
The ``inkscape --query-all svg`` call will return a tuple:
|
|
127
131
|
|
|
128
132
|
(b'svg1,x,y,width,height\\r\\elem1,x,y,width,height\\r\\n', None)
|
|
129
133
|
where x, y, width, and height are strings of numbers.
|
|
130
134
|
|
|
131
|
-
This calls the command and formats the output into a dictionary.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
a
|
|
136
|
-
|
|
135
|
+
This calls the command and formats the output into a dictionary. There is a
|
|
136
|
+
little extra complexity to handle cases with duplicate elements. Inkscape will
|
|
137
|
+
map bounding boxes to element ids *if* those ids are unique. If Inkscape
|
|
138
|
+
encounters a duplicate ID, Inkscape will map the bounding box of that element to
|
|
139
|
+
a string like "rect1". If you pass unequal elements with the same id, I can't
|
|
140
|
+
help you, but you might pass the same element multiple times. If you do this,
|
|
141
|
+
Inkscape will find a bounding box for each occurrence, map the first occurrence
|
|
142
|
+
to the id, then map subsequent occurrences to a string like "rect1". This
|
|
143
|
+
function will handle that.
|
|
137
144
|
"""
|
|
138
145
|
if not elem_args:
|
|
139
146
|
return {}
|
|
140
147
|
_fill_ids(*elem_args)
|
|
141
|
-
envelope = _envelop_copies(*elem_args)
|
|
142
148
|
|
|
149
|
+
envelope = _envelop_copies(*elem_args)
|
|
143
150
|
with NamedTemporaryFile(mode="wb", delete=False, suffix=".svg") as svg_file:
|
|
144
151
|
svg = write_svg(svg_file, envelope)
|
|
145
152
|
with Popen(f'"{inkscape}" --query-all {svg}', stdout=PIPE) as bb_process:
|
|
146
153
|
bb_data = str(bb_process.communicate()[0])[2:-1]
|
|
147
154
|
os.unlink(svg_file.name)
|
|
155
|
+
|
|
148
156
|
bb_strings = re.split(r"[\\r]*\\n", bb_data)[:-1]
|
|
149
157
|
id2bbox = dict(map(_split_bb_string, bb_strings))
|
|
150
158
|
|
|
151
159
|
elem2bbox: dict[EtreeElement | Literal["svg"], BoundingBox] = {}
|
|
152
160
|
for elem in _iter_elems(*elem_args):
|
|
153
|
-
|
|
154
|
-
if
|
|
161
|
+
elem_id = elem.attrib.get("id")
|
|
162
|
+
if not (elem_id): # id removed in a previous loop
|
|
163
|
+
continue
|
|
164
|
+
elem2bbox[elem] = id2bbox[elem_id]
|
|
165
|
+
if elem_id.startswith(_TEMP_ID_PREFIX):
|
|
155
166
|
del elem.attrib["id"]
|
|
156
|
-
|
|
157
|
-
elem2bbox["svg"] = scene_bbox
|
|
167
|
+
elem2bbox["svg"] = BoundingBox.merged(*id2bbox.values())
|
|
158
168
|
return elem2bbox
|
|
159
169
|
|
|
160
170
|
|
|
@@ -52,13 +52,19 @@ def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
|
|
|
52
52
|
return (aa, bb, cc, dd, ee, ff)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def mat_apply(
|
|
55
|
+
def mat_apply(matrix: _Matrix, point: tuple[float, float]) -> tuple[float, float]:
|
|
56
56
|
"""Apply an svg-style transformation matrix to a point.
|
|
57
57
|
|
|
58
|
-
:param mat1: transformation matrix (
|
|
58
|
+
:param mat1: transformation matrix (a, b, c, d, e, f) describing a 3x3 matrix
|
|
59
|
+
with an implied third row of (0, 0, 1)
|
|
60
|
+
[[a, c, e], [b, d, f], [0, 0, 1]]
|
|
59
61
|
:param mat2: point (x, y)
|
|
60
62
|
"""
|
|
61
|
-
|
|
63
|
+
a, b, c, d, e, f = matrix
|
|
64
|
+
x, y = point
|
|
65
|
+
result_x = a * x + c * y + e
|
|
66
|
+
result_y = b * x + d * y + f
|
|
67
|
+
return result_x, result_y
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
def mat_invert(tmat: _Matrix) -> _Matrix:
|
|
@@ -99,7 +105,7 @@ def get_transform_matrix(elem: EtreeElement) -> _Matrix:
|
|
|
99
105
|
def new_transformation_matrix(
|
|
100
106
|
transformation: _Matrix | None = None,
|
|
101
107
|
*,
|
|
102
|
-
scale: float | None = None,
|
|
108
|
+
scale: tuple[float, float] | None = None,
|
|
103
109
|
dx: float | None = None,
|
|
104
110
|
dy: float | None = None,
|
|
105
111
|
) -> _Matrix:
|
|
@@ -109,10 +115,10 @@ def new_transformation_matrix(
|
|
|
109
115
|
svg-style transformation matrix.
|
|
110
116
|
"""
|
|
111
117
|
transformation = transformation or (1, 0, 0, 1, 0, 0)
|
|
112
|
-
scale = scale or 1
|
|
118
|
+
scale = scale or (1, 1)
|
|
113
119
|
dx = dx or 0
|
|
114
120
|
dy = dy or 0
|
|
115
|
-
return mat_dot((scale, 0, 0, scale, dx, dy), transformation)
|
|
121
|
+
return mat_dot((scale[0], 0, 0, scale[1], dx, dy), transformation)
|
|
116
122
|
|
|
117
123
|
|
|
118
124
|
def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
|
|
@@ -2,28 +2,28 @@ svg_ultralight/__init__.py,sha256=wUc79mKsG6lGZ1xaYijyJ4Sm9lG5-5XgRArVsCI0niY,25
|
|
|
2
2
|
svg_ultralight/animate.py,sha256=JSrBm-59BcNXDF0cGgl4-C89eBunjevZnwZxIWt48TU,1112
|
|
3
3
|
svg_ultralight/image_ops.py,sha256=PXN_p5GX91UTvhnwwU-bPuj6WzM9wCx1SqfzR5icNnQ,4686
|
|
4
4
|
svg_ultralight/inkscape.py,sha256=tVFPtwsUcGKGODmYhC9IdGLyT7OAeNCVsDN0hPBSSgg,9194
|
|
5
|
-
svg_ultralight/layout.py,sha256=
|
|
5
|
+
svg_ultralight/layout.py,sha256=7LV2I3u4EhqSc6ASvgwDtTZyV-Y1qt2wtvRtH2uKVAE,12799
|
|
6
6
|
svg_ultralight/main.py,sha256=VA7tVMO7fiRI_JkEGaH7UFgzJ5YIbHKx4VHfMnT50hI,7446
|
|
7
7
|
svg_ultralight/metadata.py,sha256=xaIfqhKu52Dl4JOrRlpUsWkkE7Umw8j5Z4waFTli-kI,4234
|
|
8
8
|
svg_ultralight/nsmap.py,sha256=y63upO78Rr-JJT56RWWZuyrsILh6HPoY4GhbYnK1A0g,1244
|
|
9
9
|
svg_ultralight/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
svg_ultralight/query.py,sha256=
|
|
10
|
+
svg_ultralight/query.py,sha256=wR8oixRYA489k03RU5apXaoami2KvYl8BKmwypQGTi0,10972
|
|
11
11
|
svg_ultralight/root_elements.py,sha256=E_H7HXk0M5F3IyFVOxO8PQmhww1-sHTzJhx8hBJPZvg,2911
|
|
12
12
|
svg_ultralight/string_conversion.py,sha256=7gaUWEJptAVZawlvoAJ7JoP996nALoJrAByM3o219Tc,7462
|
|
13
|
-
svg_ultralight/transformations.py,sha256=
|
|
13
|
+
svg_ultralight/transformations.py,sha256=h1GUD4hvo10ugnSwShDmrwzF048yfug7lCWqCzOTxWs,4296
|
|
14
14
|
svg_ultralight/unit_conversion.py,sha256=g07nhzXdjPvGcJmkhLdFbeDLrSmbI8uFoVgPo7G62Bg,9258
|
|
15
15
|
svg_ultralight/bounding_boxes/__init__.py,sha256=qUEn3r4s-1QNHaguhWhhaNfdP4tl_B6YEqxtiTFuzhQ,78
|
|
16
|
-
svg_ultralight/bounding_boxes/bound_helpers.py,sha256
|
|
17
|
-
svg_ultralight/bounding_boxes/supports_bounds.py,sha256=
|
|
18
|
-
svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=
|
|
19
|
-
svg_ultralight/bounding_boxes/type_bound_element.py,sha256=
|
|
20
|
-
svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=
|
|
21
|
-
svg_ultralight/bounding_boxes/type_padded_text.py,sha256=
|
|
16
|
+
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=LFkVsdYFKYCnEL6vLvEa_5cfu8D44ZGYeEEb7_0MnC0,7146
|
|
17
|
+
svg_ultralight/bounding_boxes/supports_bounds.py,sha256=Yi696oJ393y29vql0U6kLMR3soSggLB3_wSACHppoJo,4505
|
|
18
|
+
svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=yW-gwehTrLhgG4Rk-wd6XWP0oLj7Yh_Njrudm8Dn1fk,2637
|
|
19
|
+
svg_ultralight/bounding_boxes/type_bound_element.py,sha256=v-XwW1bxA3tT0CzLWOrRO5EHtLA3ivds8DyA5KFju6w,2196
|
|
20
|
+
svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=XsjM6nR5bKRLFa8z1U84GIC-YbleKoXa0s-UScz9Ewo,13621
|
|
21
|
+
svg_ultralight/bounding_boxes/type_padded_text.py,sha256=fiDZoHIaQbGK8ETrPRa2wa5ykbdFBeeDHw9zGqKtD6E,10674
|
|
22
22
|
svg_ultralight/constructors/__init__.py,sha256=XLOInLhzMERWNnFAs-itMs-OZrBOpvQthZJ2T5duqBE,327
|
|
23
23
|
svg_ultralight/constructors/new_element.py,sha256=hRUW2hR_BTkthEqPClYV7-IeFe9iv2zwb6ehp1k1xDk,3475
|
|
24
24
|
svg_ultralight/strings/__init__.py,sha256=BMGhF1pulscIgkiYvZLr6kPRR0L4lW0jUNFxkul4_EM,295
|
|
25
25
|
svg_ultralight/strings/svg_strings.py,sha256=FQNxNmMkR2M-gCFo_woQKXLgCHi3ncUlRMiaRR_a9nQ,1978
|
|
26
|
-
svg_ultralight-0.
|
|
27
|
-
svg_ultralight-0.
|
|
28
|
-
svg_ultralight-0.
|
|
29
|
-
svg_ultralight-0.
|
|
26
|
+
svg_ultralight-0.39.0.dist-info/METADATA,sha256=--QD3_OuduOUZx2fsCx6FsgTaDAuPAFHkkeKxCKY5Dw,8971
|
|
27
|
+
svg_ultralight-0.39.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
28
|
+
svg_ultralight-0.39.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
|
|
29
|
+
svg_ultralight-0.39.0.dist-info/RECORD,,
|
|
File without changes
|