svg-ultralight 0.48.0__py3-none-any.whl → 0.50.2__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/__init__.py +108 -105
- svg_ultralight/animate.py +40 -40
- svg_ultralight/attrib_hints.py +14 -14
- svg_ultralight/bounding_boxes/__init__.py +5 -5
- svg_ultralight/bounding_boxes/bound_helpers.py +189 -189
- svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
- svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
- svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
- svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
- svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
- svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
- svg_ultralight/constructors/__init__.py +14 -14
- svg_ultralight/constructors/new_element.py +120 -115
- svg_ultralight/font_tools/__init__.py +5 -5
- svg_ultralight/font_tools/comp_results.py +295 -293
- svg_ultralight/font_tools/font_info.py +793 -792
- svg_ultralight/image_ops.py +156 -156
- svg_ultralight/inkscape.py +261 -261
- svg_ultralight/layout.py +291 -291
- svg_ultralight/main.py +183 -198
- svg_ultralight/metadata.py +122 -122
- svg_ultralight/nsmap.py +36 -36
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -249
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +87 -87
- svg_ultralight/string_conversion.py +244 -244
- svg_ultralight/strings/__init__.py +21 -13
- svg_ultralight/strings/svg_strings.py +106 -67
- svg_ultralight/transformations.py +140 -141
- svg_ultralight/unit_conversion.py +247 -248
- {svg_ultralight-0.48.0.dist-info → svg_ultralight-0.50.2.dist-info}/METADATA +208 -214
- svg_ultralight-0.50.2.dist-info/RECORD +34 -0
- svg_ultralight-0.50.2.dist-info/WHEEL +4 -0
- svg_ultralight-0.48.0.dist-info/RECORD +0 -34
- svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
- svg_ultralight-0.48.0.dist-info/top_level.txt +0 -1
|
@@ -1,396 +1,396 @@
|
|
|
1
|
-
"""Bounding box classes for SVG elements.
|
|
2
|
-
|
|
3
|
-
:author: Shay Hill
|
|
4
|
-
:created: 2022-12-09
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import dataclasses
|
|
10
|
-
import math
|
|
11
|
-
|
|
12
|
-
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
13
|
-
from svg_ultralight.
|
|
14
|
-
from svg_ultralight.transformations import mat_apply, mat_dot, new_transformation_matrix
|
|
15
|
-
|
|
16
|
-
_Matrix = tuple[float, float, float, float, float, float]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class HasBoundingBox(SupportsBounds):
|
|
20
|
-
"""A parent class for BoundElement and others that have a bbox attribute."""
|
|
21
|
-
|
|
22
|
-
def __init__(self, bbox: BoundingBox) -> None:
|
|
23
|
-
"""Initialize the HasBoundingBox instance."""
|
|
24
|
-
self.bbox = bbox
|
|
25
|
-
|
|
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 values(self) -> tuple[float, float, float, float]:
|
|
45
|
-
"""Get the values of the bounding box.
|
|
46
|
-
|
|
47
|
-
:return: x, y, width, height of the bounding box
|
|
48
|
-
"""
|
|
49
|
-
return (
|
|
50
|
-
self.bbox.x,
|
|
51
|
-
self.bbox.y,
|
|
52
|
-
self.bbox.width,
|
|
53
|
-
self.bbox.height,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
def _get_transformed_corners(
|
|
57
|
-
self,
|
|
58
|
-
) -> tuple[
|
|
59
|
-
tuple[float, float],
|
|
60
|
-
tuple[float, float],
|
|
61
|
-
tuple[float, float],
|
|
62
|
-
tuple[float, float],
|
|
63
|
-
]:
|
|
64
|
-
"""Get the transformed corners of the bounding box.
|
|
65
|
-
|
|
66
|
-
:return: four corners counter-clockwise starting at top left, transformed by
|
|
67
|
-
self.transformation
|
|
68
|
-
"""
|
|
69
|
-
c0, c1, c2, c3 = (
|
|
70
|
-
mat_apply(self.bbox.transformation, c) for c in self._get_input_corners()
|
|
71
|
-
)
|
|
72
|
-
return c0, c1, c2, c3
|
|
73
|
-
|
|
74
|
-
def transform(
|
|
75
|
-
self,
|
|
76
|
-
transformation: _Matrix | None = None,
|
|
77
|
-
*,
|
|
78
|
-
scale: tuple[float, float] | float | None = None,
|
|
79
|
-
dx: float | None = None,
|
|
80
|
-
dy: float | None = None,
|
|
81
|
-
):
|
|
82
|
-
"""Transform the bounding box by updating the transformation attribute.
|
|
83
|
-
|
|
84
|
-
:param transformation: 2D transformation matrix
|
|
85
|
-
:param scale: scale factor
|
|
86
|
-
:param dx: x translation
|
|
87
|
-
:param dy: y translation
|
|
88
|
-
|
|
89
|
-
All parameters are optional. Scale, dx, and dy are optional and applied after
|
|
90
|
-
the transformation matrix if both are given. This shouldn't be necessary in
|
|
91
|
-
most cases, the four parameters are there to allow transformation arguments
|
|
92
|
-
to be passed in a variety of ways. Scale, dx, and dy are the sensible values
|
|
93
|
-
to pass "by hand". The transformation matrix is the sensible argument to pass
|
|
94
|
-
when applying a transformation from another bounding box instance.
|
|
95
|
-
"""
|
|
96
|
-
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
97
|
-
self.bbox.transformation = mat_dot(tmat, self.bbox.transformation)
|
|
98
|
-
|
|
99
|
-
@property
|
|
100
|
-
def scale(self) -> tuple[float, float]:
|
|
101
|
-
"""Get scale of the bounding box.
|
|
102
|
-
|
|
103
|
-
:return: uniform scale of the bounding box
|
|
104
|
-
|
|
105
|
-
Use caution, the scale attribute can cause errors in intuition. Changing
|
|
106
|
-
width or height will change the scale attribute, but not the x or y values.
|
|
107
|
-
The scale setter, on the other hand, will work in the tradational manner.
|
|
108
|
-
I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
|
|
109
|
-
width*scale, height => height*scale, scale => scale*scale. This matches how
|
|
110
|
-
scale works in almost every other context.
|
|
111
|
-
"""
|
|
112
|
-
xx, xy, yx, yy, *_ = self.bbox.transformation
|
|
113
|
-
return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
|
|
114
|
-
|
|
115
|
-
@scale.setter
|
|
116
|
-
def scale(self, value: tuple[float, float]) -> None:
|
|
117
|
-
"""Scale the bounding box by a uniform factor.
|
|
118
|
-
|
|
119
|
-
:param value: new scale value
|
|
120
|
-
|
|
121
|
-
Don't miss this! You are setting the scale, not scaling the scale! If you
|
|
122
|
-
have a previously defined scale other than 1, this is probably not what you
|
|
123
|
-
want. Most of the time, you will want to use the *= operator.
|
|
124
|
-
|
|
125
|
-
`scale = 2` -> ignore whatever scale was previously defined and set scale to 2
|
|
126
|
-
`scale *= 2` -> make it twice as big as it was.
|
|
127
|
-
"""
|
|
128
|
-
new_scale = value[0] / self.scale[0], value[1] / self.scale[1]
|
|
129
|
-
self.transform(scale=new_scale)
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def x(self) -> float:
|
|
133
|
-
"""Return x left value of bounding box.
|
|
134
|
-
|
|
135
|
-
:return: internal _x value transformed by scale and translation
|
|
136
|
-
"""
|
|
137
|
-
return min(x for x, _ in self._get_transformed_corners())
|
|
138
|
-
|
|
139
|
-
@x.setter
|
|
140
|
-
def x(self, value: float):
|
|
141
|
-
"""Set the x coordinate of the left edge of the bounding box.
|
|
142
|
-
|
|
143
|
-
:param value: the new x coordinate of the left edge of the bounding box
|
|
144
|
-
"""
|
|
145
|
-
self.transform(dx=value - self.x)
|
|
146
|
-
|
|
147
|
-
@property
|
|
148
|
-
def cx(self) -> float:
|
|
149
|
-
"""Center x value.
|
|
150
|
-
|
|
151
|
-
:return: midpoint of transformed x and x2
|
|
152
|
-
"""
|
|
153
|
-
return self.x + self.width / 2
|
|
154
|
-
|
|
155
|
-
@cx.setter
|
|
156
|
-
def cx(self, value: float):
|
|
157
|
-
"""Center x value.
|
|
158
|
-
|
|
159
|
-
:param value: new center x value after transformation
|
|
160
|
-
"""
|
|
161
|
-
self.x += value - self.cx
|
|
162
|
-
|
|
163
|
-
@property
|
|
164
|
-
def x2(self) -> float:
|
|
165
|
-
"""Return x right value of bounding box.
|
|
166
|
-
|
|
167
|
-
:return: transformed x + transformed width
|
|
168
|
-
"""
|
|
169
|
-
return max(x for x, _ in self._get_transformed_corners())
|
|
170
|
-
|
|
171
|
-
@x2.setter
|
|
172
|
-
def x2(self, value: float) -> None:
|
|
173
|
-
"""Update transformation values (do not alter self._x2).
|
|
174
|
-
|
|
175
|
-
:param value: new x2 value after transformation
|
|
176
|
-
"""
|
|
177
|
-
self.x += value - self.x2
|
|
178
|
-
|
|
179
|
-
@property
|
|
180
|
-
def y(self) -> float:
|
|
181
|
-
"""Return y top value of bounding box.
|
|
182
|
-
|
|
183
|
-
:return: internal _y value transformed by scale and translation
|
|
184
|
-
"""
|
|
185
|
-
return min(y for _, y in self._get_transformed_corners())
|
|
186
|
-
|
|
187
|
-
@y.setter
|
|
188
|
-
def y(self, value: float) -> None:
|
|
189
|
-
"""Update transformation values (do not alter self._y).
|
|
190
|
-
|
|
191
|
-
:param value: new y value after transformation
|
|
192
|
-
"""
|
|
193
|
-
self.transform(dy=value - self.y)
|
|
194
|
-
|
|
195
|
-
@property
|
|
196
|
-
def cy(self) -> float:
|
|
197
|
-
"""Center y value.
|
|
198
|
-
|
|
199
|
-
:return: midpoint of transformed y and y2
|
|
200
|
-
"""
|
|
201
|
-
return self.y + self.height / 2
|
|
202
|
-
|
|
203
|
-
@cy.setter
|
|
204
|
-
def cy(self, value: float):
|
|
205
|
-
"""Center y value.
|
|
206
|
-
|
|
207
|
-
:param value: new center y value after transformation
|
|
208
|
-
"""
|
|
209
|
-
self.y += value - self.cy
|
|
210
|
-
|
|
211
|
-
@property
|
|
212
|
-
def y2(self) -> float:
|
|
213
|
-
"""Return y bottom value of bounding box.
|
|
214
|
-
|
|
215
|
-
:return: transformed y + transformed height
|
|
216
|
-
"""
|
|
217
|
-
return max(y for _, y in self._get_transformed_corners())
|
|
218
|
-
|
|
219
|
-
@y2.setter
|
|
220
|
-
def y2(self, value: float) -> None:
|
|
221
|
-
"""Update transformation values (do not alter self._y).
|
|
222
|
-
|
|
223
|
-
:param value: new y2 value after transformation
|
|
224
|
-
"""
|
|
225
|
-
self.y += value - self.y2
|
|
226
|
-
|
|
227
|
-
@property
|
|
228
|
-
def width(self) -> float:
|
|
229
|
-
"""Width of transformed bounding box.
|
|
230
|
-
|
|
231
|
-
:return: internal _width value transformed by scale
|
|
232
|
-
"""
|
|
233
|
-
return self.x2 - self.x
|
|
234
|
-
|
|
235
|
-
@width.setter
|
|
236
|
-
def width(self, value: float) -> None:
|
|
237
|
-
"""Update transformation values, Do not alter self._width.
|
|
238
|
-
|
|
239
|
-
:param value: new width value after transformation
|
|
240
|
-
|
|
241
|
-
Here transformed x and y value will be preserved. That is, the bounding box
|
|
242
|
-
is scaled, but still anchored at (transformed) self.x and self.y
|
|
243
|
-
"""
|
|
244
|
-
current_x = self.x
|
|
245
|
-
current_y = self.y
|
|
246
|
-
self.transform(scale=value / self.width)
|
|
247
|
-
self.x = current_x
|
|
248
|
-
self.y = current_y
|
|
249
|
-
|
|
250
|
-
@property
|
|
251
|
-
def height(self) -> float:
|
|
252
|
-
"""Height of transformed bounding box.
|
|
253
|
-
|
|
254
|
-
:return: internal _height value transformed by scale
|
|
255
|
-
"""
|
|
256
|
-
return self.y2 - self.y
|
|
257
|
-
|
|
258
|
-
@height.setter
|
|
259
|
-
def height(self, value: float) -> None:
|
|
260
|
-
"""Update transformation values, Do not alter self._height.
|
|
261
|
-
|
|
262
|
-
:param value: new height value after transformation
|
|
263
|
-
|
|
264
|
-
Here transformed x and y value will be preserved. That is, the bounding box
|
|
265
|
-
is scaled, but still anchored at (transformed) self.x and self.y
|
|
266
|
-
"""
|
|
267
|
-
self.width = value * self.width / self.height
|
|
268
|
-
|
|
269
|
-
@property
|
|
270
|
-
def transform_string(self) -> str:
|
|
271
|
-
"""Transformation property string value for svg element.
|
|
272
|
-
|
|
273
|
-
:return: string value for an svg transformation attribute.
|
|
274
|
-
|
|
275
|
-
Use with
|
|
276
|
-
``update_element(elem, transform=bbox.transform_string)``
|
|
277
|
-
"""
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@dataclasses.dataclass
|
|
282
|
-
class BoundingBox(HasBoundingBox):
|
|
283
|
-
"""Mutable bounding box object for svg_ultralight.
|
|
284
|
-
|
|
285
|
-
:param x: left x value
|
|
286
|
-
:param y: top y value
|
|
287
|
-
:param width: width of the bounding box
|
|
288
|
-
:param height: height of the bounding box
|
|
289
|
-
|
|
290
|
-
The below optional parameter, in addition to the required parameters, captures
|
|
291
|
-
the entire state of a BoundingBox instance. It could be used to make a copy or
|
|
292
|
-
to initialize a transformed box with the same transform_string as another box.
|
|
293
|
-
Under most circumstances, it will not be used.
|
|
294
|
-
|
|
295
|
-
:param transformation: transformation matrix
|
|
296
|
-
|
|
297
|
-
Functions that return a bounding box will return a BoundingBox instance. This
|
|
298
|
-
instance can be transformed (uniform scale and translate only). Transformations
|
|
299
|
-
will be combined and scored to be passed to new_element as a transform value.
|
|
300
|
-
|
|
301
|
-
Define the bbox with x=, y=, width=, height=
|
|
302
|
-
|
|
303
|
-
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
304
|
-
x2, y, cy, y2, width, or height, private transformation value transformation
|
|
305
|
-
will be updated.
|
|
306
|
-
|
|
307
|
-
The ultimate transformation can be accessed through ``.transform_string``.
|
|
308
|
-
So the workflow will look like :
|
|
309
|
-
|
|
310
|
-
1. Get the bounding box of an svg element
|
|
311
|
-
2. Update the bounding box x, y, width, and height
|
|
312
|
-
3. Transform the original svg element with
|
|
313
|
-
update_element(elem, transform=bbox.transform_string)
|
|
314
|
-
4. The transformed element will lie in the transformed BoundingBox
|
|
315
|
-
|
|
316
|
-
In addition to x, y, width, and height, x2 and y2 can be set to establish the
|
|
317
|
-
right x value or bottom y value.
|
|
318
|
-
|
|
319
|
-
The point of all of this is to simplify stacking and aligning elements. To stack:
|
|
320
|
-
|
|
321
|
-
```
|
|
322
|
-
elem_a = new_element(*args)
|
|
323
|
-
bbox_a = get_bounding_box(elem_a)
|
|
324
|
-
|
|
325
|
-
elem_b = new_element(*args)
|
|
326
|
-
bbox_b = get_bounding_box(elem_b)
|
|
327
|
-
|
|
328
|
-
# align at same x
|
|
329
|
-
bbox_b.x = bbox_a.x
|
|
330
|
-
|
|
331
|
-
# make the same width
|
|
332
|
-
bbox_b.width = bbox_a.width
|
|
333
|
-
|
|
334
|
-
# stack a on top of b
|
|
335
|
-
bbox_a.y2 = bbox_b.y
|
|
336
|
-
|
|
337
|
-
update_element(elem_a, transform=bbox_a.transform_string)
|
|
338
|
-
update_element(elem_b, transform=bbox_b.transform_string)
|
|
339
|
-
"""
|
|
340
|
-
|
|
341
|
-
base_x: float = dataclasses.field(init=False)
|
|
342
|
-
base_y: float = dataclasses.field(init=False)
|
|
343
|
-
base_width: float = dataclasses.field(init=False)
|
|
344
|
-
base_height: float = dataclasses.field(init=False)
|
|
345
|
-
transformation: _Matrix = dataclasses.field(init=False)
|
|
346
|
-
|
|
347
|
-
def __init__(
|
|
348
|
-
self,
|
|
349
|
-
x: float,
|
|
350
|
-
y: float,
|
|
351
|
-
width: float,
|
|
352
|
-
height: float,
|
|
353
|
-
transformation: _Matrix = (1, 0, 0, 1, 0, 0),
|
|
354
|
-
) -> None:
|
|
355
|
-
"""Initialize a BoundingBox instance.
|
|
356
|
-
|
|
357
|
-
:param x: left x value
|
|
358
|
-
:param y: top y value
|
|
359
|
-
:param width: width of the bounding box
|
|
360
|
-
:param height: height of the bounding box
|
|
361
|
-
"""
|
|
362
|
-
self.base_x = x
|
|
363
|
-
self.base_y = y
|
|
364
|
-
self.base_width = width
|
|
365
|
-
self.base_height = height
|
|
366
|
-
self.transformation = transformation
|
|
367
|
-
self.bbox = self
|
|
368
|
-
|
|
369
|
-
def merge(self, *others: BoundingBox) -> BoundingBox:
|
|
370
|
-
"""Create a bounding box around all other bounding boxes.
|
|
371
|
-
|
|
372
|
-
:param others: one or more bounding boxes to merge with self
|
|
373
|
-
:return: a bounding box around self and other bounding boxes
|
|
374
|
-
:raises DeprecationWarning:
|
|
375
|
-
"""
|
|
376
|
-
return BoundingBox.merged(self, *others)
|
|
377
|
-
|
|
378
|
-
@classmethod
|
|
379
|
-
def merged(cls, *bboxes: SupportsBounds) -> BoundingBox:
|
|
380
|
-
"""Create a bounding box around all other bounding boxes.
|
|
381
|
-
|
|
382
|
-
:param bboxes: one or more bounding boxes
|
|
383
|
-
:return: a bounding box encompasing all bboxes args
|
|
384
|
-
:raises ValueError: if no bboxes are given
|
|
385
|
-
|
|
386
|
-
This can be used to repace a bounding box after the element it bounds has
|
|
387
|
-
been transformed with instance.transform_string.
|
|
388
|
-
"""
|
|
389
|
-
if not bboxes:
|
|
390
|
-
msg = "At least one bounding box is required"
|
|
391
|
-
raise ValueError(msg)
|
|
392
|
-
min_x = min(x.x for x in bboxes)
|
|
393
|
-
max_x = max(x.x + x.width for x in bboxes)
|
|
394
|
-
min_y = min(x.y for x in bboxes)
|
|
395
|
-
max_y = max(x.y + x.height for x in bboxes)
|
|
396
|
-
return BoundingBox(min_x, min_y, max_x - min_x, max_y - min_y)
|
|
1
|
+
"""Bounding box classes for SVG elements.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2022-12-09
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
13
|
+
from svg_ultralight.strings import svg_matrix
|
|
14
|
+
from svg_ultralight.transformations import mat_apply, mat_dot, new_transformation_matrix
|
|
15
|
+
|
|
16
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HasBoundingBox(SupportsBounds):
|
|
20
|
+
"""A parent class for BoundElement and others that have a bbox attribute."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, bbox: BoundingBox) -> None:
|
|
23
|
+
"""Initialize the HasBoundingBox instance."""
|
|
24
|
+
self.bbox = bbox
|
|
25
|
+
|
|
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 values(self) -> tuple[float, float, float, float]:
|
|
45
|
+
"""Get the values of the bounding box.
|
|
46
|
+
|
|
47
|
+
:return: x, y, width, height of the bounding box
|
|
48
|
+
"""
|
|
49
|
+
return (
|
|
50
|
+
self.bbox.x,
|
|
51
|
+
self.bbox.y,
|
|
52
|
+
self.bbox.width,
|
|
53
|
+
self.bbox.height,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _get_transformed_corners(
|
|
57
|
+
self,
|
|
58
|
+
) -> tuple[
|
|
59
|
+
tuple[float, float],
|
|
60
|
+
tuple[float, float],
|
|
61
|
+
tuple[float, float],
|
|
62
|
+
tuple[float, float],
|
|
63
|
+
]:
|
|
64
|
+
"""Get the transformed corners of the bounding box.
|
|
65
|
+
|
|
66
|
+
:return: four corners counter-clockwise starting at top left, transformed by
|
|
67
|
+
self.transformation
|
|
68
|
+
"""
|
|
69
|
+
c0, c1, c2, c3 = (
|
|
70
|
+
mat_apply(self.bbox.transformation, c) for c in self._get_input_corners()
|
|
71
|
+
)
|
|
72
|
+
return c0, c1, c2, c3
|
|
73
|
+
|
|
74
|
+
def transform(
|
|
75
|
+
self,
|
|
76
|
+
transformation: _Matrix | None = None,
|
|
77
|
+
*,
|
|
78
|
+
scale: tuple[float, float] | float | None = None,
|
|
79
|
+
dx: float | None = None,
|
|
80
|
+
dy: float | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Transform the bounding box by updating the transformation attribute.
|
|
83
|
+
|
|
84
|
+
:param transformation: 2D transformation matrix
|
|
85
|
+
:param scale: scale factor
|
|
86
|
+
:param dx: x translation
|
|
87
|
+
:param dy: y translation
|
|
88
|
+
|
|
89
|
+
All parameters are optional. Scale, dx, and dy are optional and applied after
|
|
90
|
+
the transformation matrix if both are given. This shouldn't be necessary in
|
|
91
|
+
most cases, the four parameters are there to allow transformation arguments
|
|
92
|
+
to be passed in a variety of ways. Scale, dx, and dy are the sensible values
|
|
93
|
+
to pass "by hand". The transformation matrix is the sensible argument to pass
|
|
94
|
+
when applying a transformation from another bounding box instance.
|
|
95
|
+
"""
|
|
96
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
97
|
+
self.bbox.transformation = mat_dot(tmat, self.bbox.transformation)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def scale(self) -> tuple[float, float]:
|
|
101
|
+
"""Get scale of the bounding box.
|
|
102
|
+
|
|
103
|
+
:return: uniform scale of the bounding box
|
|
104
|
+
|
|
105
|
+
Use caution, the scale attribute can cause errors in intuition. Changing
|
|
106
|
+
width or height will change the scale attribute, but not the x or y values.
|
|
107
|
+
The scale setter, on the other hand, will work in the tradational manner.
|
|
108
|
+
I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
|
|
109
|
+
width*scale, height => height*scale, scale => scale*scale. This matches how
|
|
110
|
+
scale works in almost every other context.
|
|
111
|
+
"""
|
|
112
|
+
xx, xy, yx, yy, *_ = self.bbox.transformation
|
|
113
|
+
return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
|
|
114
|
+
|
|
115
|
+
@scale.setter
|
|
116
|
+
def scale(self, value: tuple[float, float]) -> None:
|
|
117
|
+
"""Scale the bounding box by a uniform factor.
|
|
118
|
+
|
|
119
|
+
:param value: new scale value
|
|
120
|
+
|
|
121
|
+
Don't miss this! You are setting the scale, not scaling the scale! If you
|
|
122
|
+
have a previously defined scale other than 1, this is probably not what you
|
|
123
|
+
want. Most of the time, you will want to use the *= operator.
|
|
124
|
+
|
|
125
|
+
`scale = 2` -> ignore whatever scale was previously defined and set scale to 2
|
|
126
|
+
`scale *= 2` -> make it twice as big as it was.
|
|
127
|
+
"""
|
|
128
|
+
new_scale = value[0] / self.scale[0], value[1] / self.scale[1]
|
|
129
|
+
self.transform(scale=new_scale)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def x(self) -> float:
|
|
133
|
+
"""Return x left value of bounding box.
|
|
134
|
+
|
|
135
|
+
:return: internal _x value transformed by scale and translation
|
|
136
|
+
"""
|
|
137
|
+
return min(x for x, _ in self._get_transformed_corners())
|
|
138
|
+
|
|
139
|
+
@x.setter
|
|
140
|
+
def x(self, value: float) -> None:
|
|
141
|
+
"""Set the x coordinate of the left edge of the bounding box.
|
|
142
|
+
|
|
143
|
+
:param value: the new x coordinate of the left edge of the bounding box
|
|
144
|
+
"""
|
|
145
|
+
self.transform(dx=value - self.x)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def cx(self) -> float:
|
|
149
|
+
"""Center x value.
|
|
150
|
+
|
|
151
|
+
:return: midpoint of transformed x and x2
|
|
152
|
+
"""
|
|
153
|
+
return self.x + self.width / 2
|
|
154
|
+
|
|
155
|
+
@cx.setter
|
|
156
|
+
def cx(self, value: float) -> None:
|
|
157
|
+
"""Center x value.
|
|
158
|
+
|
|
159
|
+
:param value: new center x value after transformation
|
|
160
|
+
"""
|
|
161
|
+
self.x += value - self.cx
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def x2(self) -> float:
|
|
165
|
+
"""Return x right value of bounding box.
|
|
166
|
+
|
|
167
|
+
:return: transformed x + transformed width
|
|
168
|
+
"""
|
|
169
|
+
return max(x for x, _ in self._get_transformed_corners())
|
|
170
|
+
|
|
171
|
+
@x2.setter
|
|
172
|
+
def x2(self, value: float) -> None:
|
|
173
|
+
"""Update transformation values (do not alter self._x2).
|
|
174
|
+
|
|
175
|
+
:param value: new x2 value after transformation
|
|
176
|
+
"""
|
|
177
|
+
self.x += value - self.x2
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def y(self) -> float:
|
|
181
|
+
"""Return y top value of bounding box.
|
|
182
|
+
|
|
183
|
+
:return: internal _y value transformed by scale and translation
|
|
184
|
+
"""
|
|
185
|
+
return min(y for _, y in self._get_transformed_corners())
|
|
186
|
+
|
|
187
|
+
@y.setter
|
|
188
|
+
def y(self, value: float) -> None:
|
|
189
|
+
"""Update transformation values (do not alter self._y).
|
|
190
|
+
|
|
191
|
+
:param value: new y value after transformation
|
|
192
|
+
"""
|
|
193
|
+
self.transform(dy=value - self.y)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def cy(self) -> float:
|
|
197
|
+
"""Center y value.
|
|
198
|
+
|
|
199
|
+
:return: midpoint of transformed y and y2
|
|
200
|
+
"""
|
|
201
|
+
return self.y + self.height / 2
|
|
202
|
+
|
|
203
|
+
@cy.setter
|
|
204
|
+
def cy(self, value: float) -> None:
|
|
205
|
+
"""Center y value.
|
|
206
|
+
|
|
207
|
+
:param value: new center y value after transformation
|
|
208
|
+
"""
|
|
209
|
+
self.y += value - self.cy
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def y2(self) -> float:
|
|
213
|
+
"""Return y bottom value of bounding box.
|
|
214
|
+
|
|
215
|
+
:return: transformed y + transformed height
|
|
216
|
+
"""
|
|
217
|
+
return max(y for _, y in self._get_transformed_corners())
|
|
218
|
+
|
|
219
|
+
@y2.setter
|
|
220
|
+
def y2(self, value: float) -> None:
|
|
221
|
+
"""Update transformation values (do not alter self._y).
|
|
222
|
+
|
|
223
|
+
:param value: new y2 value after transformation
|
|
224
|
+
"""
|
|
225
|
+
self.y += value - self.y2
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def width(self) -> float:
|
|
229
|
+
"""Width of transformed bounding box.
|
|
230
|
+
|
|
231
|
+
:return: internal _width value transformed by scale
|
|
232
|
+
"""
|
|
233
|
+
return self.x2 - self.x
|
|
234
|
+
|
|
235
|
+
@width.setter
|
|
236
|
+
def width(self, value: float) -> None:
|
|
237
|
+
"""Update transformation values, Do not alter self._width.
|
|
238
|
+
|
|
239
|
+
:param value: new width value after transformation
|
|
240
|
+
|
|
241
|
+
Here transformed x and y value will be preserved. That is, the bounding box
|
|
242
|
+
is scaled, but still anchored at (transformed) self.x and self.y
|
|
243
|
+
"""
|
|
244
|
+
current_x = self.x
|
|
245
|
+
current_y = self.y
|
|
246
|
+
self.transform(scale=value / self.width)
|
|
247
|
+
self.x = current_x
|
|
248
|
+
self.y = current_y
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def height(self) -> float:
|
|
252
|
+
"""Height of transformed bounding box.
|
|
253
|
+
|
|
254
|
+
:return: internal _height value transformed by scale
|
|
255
|
+
"""
|
|
256
|
+
return self.y2 - self.y
|
|
257
|
+
|
|
258
|
+
@height.setter
|
|
259
|
+
def height(self, value: float) -> None:
|
|
260
|
+
"""Update transformation values, Do not alter self._height.
|
|
261
|
+
|
|
262
|
+
:param value: new height value after transformation
|
|
263
|
+
|
|
264
|
+
Here transformed x and y value will be preserved. That is, the bounding box
|
|
265
|
+
is scaled, but still anchored at (transformed) self.x and self.y
|
|
266
|
+
"""
|
|
267
|
+
self.width = value * self.width / self.height
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def transform_string(self) -> str:
|
|
271
|
+
"""Transformation property string value for svg element.
|
|
272
|
+
|
|
273
|
+
:return: string value for an svg transformation attribute.
|
|
274
|
+
|
|
275
|
+
Use with
|
|
276
|
+
``update_element(elem, transform=bbox.transform_string)``
|
|
277
|
+
"""
|
|
278
|
+
return svg_matrix(self.bbox.transformation)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclasses.dataclass
|
|
282
|
+
class BoundingBox(HasBoundingBox):
|
|
283
|
+
"""Mutable bounding box object for svg_ultralight.
|
|
284
|
+
|
|
285
|
+
:param x: left x value
|
|
286
|
+
:param y: top y value
|
|
287
|
+
:param width: width of the bounding box
|
|
288
|
+
:param height: height of the bounding box
|
|
289
|
+
|
|
290
|
+
The below optional parameter, in addition to the required parameters, captures
|
|
291
|
+
the entire state of a BoundingBox instance. It could be used to make a copy or
|
|
292
|
+
to initialize a transformed box with the same transform_string as another box.
|
|
293
|
+
Under most circumstances, it will not be used.
|
|
294
|
+
|
|
295
|
+
:param transformation: transformation matrix
|
|
296
|
+
|
|
297
|
+
Functions that return a bounding box will return a BoundingBox instance. This
|
|
298
|
+
instance can be transformed (uniform scale and translate only). Transformations
|
|
299
|
+
will be combined and scored to be passed to new_element as a transform value.
|
|
300
|
+
|
|
301
|
+
Define the bbox with x=, y=, width=, height=
|
|
302
|
+
|
|
303
|
+
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
304
|
+
x2, y, cy, y2, width, or height, private transformation value transformation
|
|
305
|
+
will be updated.
|
|
306
|
+
|
|
307
|
+
The ultimate transformation can be accessed through ``.transform_string``.
|
|
308
|
+
So the workflow will look like :
|
|
309
|
+
|
|
310
|
+
1. Get the bounding box of an svg element
|
|
311
|
+
2. Update the bounding box x, y, width, and height
|
|
312
|
+
3. Transform the original svg element with
|
|
313
|
+
update_element(elem, transform=bbox.transform_string)
|
|
314
|
+
4. The transformed element will lie in the transformed BoundingBox
|
|
315
|
+
|
|
316
|
+
In addition to x, y, width, and height, x2 and y2 can be set to establish the
|
|
317
|
+
right x value or bottom y value.
|
|
318
|
+
|
|
319
|
+
The point of all of this is to simplify stacking and aligning elements. To stack:
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
elem_a = new_element(*args)
|
|
323
|
+
bbox_a = get_bounding_box(elem_a)
|
|
324
|
+
|
|
325
|
+
elem_b = new_element(*args)
|
|
326
|
+
bbox_b = get_bounding_box(elem_b)
|
|
327
|
+
|
|
328
|
+
# align at same x
|
|
329
|
+
bbox_b.x = bbox_a.x
|
|
330
|
+
|
|
331
|
+
# make the same width
|
|
332
|
+
bbox_b.width = bbox_a.width
|
|
333
|
+
|
|
334
|
+
# stack a on top of b
|
|
335
|
+
bbox_a.y2 = bbox_b.y
|
|
336
|
+
|
|
337
|
+
update_element(elem_a, transform=bbox_a.transform_string)
|
|
338
|
+
update_element(elem_b, transform=bbox_b.transform_string)
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
base_x: float = dataclasses.field(init=False)
|
|
342
|
+
base_y: float = dataclasses.field(init=False)
|
|
343
|
+
base_width: float = dataclasses.field(init=False)
|
|
344
|
+
base_height: float = dataclasses.field(init=False)
|
|
345
|
+
transformation: _Matrix = dataclasses.field(init=False)
|
|
346
|
+
|
|
347
|
+
def __init__(
|
|
348
|
+
self,
|
|
349
|
+
x: float,
|
|
350
|
+
y: float,
|
|
351
|
+
width: float,
|
|
352
|
+
height: float,
|
|
353
|
+
transformation: _Matrix = (1, 0, 0, 1, 0, 0),
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Initialize a BoundingBox instance.
|
|
356
|
+
|
|
357
|
+
:param x: left x value
|
|
358
|
+
:param y: top y value
|
|
359
|
+
:param width: width of the bounding box
|
|
360
|
+
:param height: height of the bounding box
|
|
361
|
+
"""
|
|
362
|
+
self.base_x = x
|
|
363
|
+
self.base_y = y
|
|
364
|
+
self.base_width = width
|
|
365
|
+
self.base_height = height
|
|
366
|
+
self.transformation = transformation
|
|
367
|
+
self.bbox = self
|
|
368
|
+
|
|
369
|
+
def merge(self, *others: BoundingBox) -> BoundingBox:
|
|
370
|
+
"""Create a bounding box around all other bounding boxes.
|
|
371
|
+
|
|
372
|
+
:param others: one or more bounding boxes to merge with self
|
|
373
|
+
:return: a bounding box around self and other bounding boxes
|
|
374
|
+
:raises DeprecationWarning:
|
|
375
|
+
"""
|
|
376
|
+
return BoundingBox.merged(self, *others)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def merged(cls, *bboxes: SupportsBounds) -> BoundingBox:
|
|
380
|
+
"""Create a bounding box around all other bounding boxes.
|
|
381
|
+
|
|
382
|
+
:param bboxes: one or more bounding boxes
|
|
383
|
+
:return: a bounding box encompasing all bboxes args
|
|
384
|
+
:raises ValueError: if no bboxes are given
|
|
385
|
+
|
|
386
|
+
This can be used to repace a bounding box after the element it bounds has
|
|
387
|
+
been transformed with instance.transform_string.
|
|
388
|
+
"""
|
|
389
|
+
if not bboxes:
|
|
390
|
+
msg = "At least one bounding box is required"
|
|
391
|
+
raise ValueError(msg)
|
|
392
|
+
min_x = min(x.x for x in bboxes)
|
|
393
|
+
max_x = max(x.x + x.width for x in bboxes)
|
|
394
|
+
min_y = min(x.y for x in bboxes)
|
|
395
|
+
max_y = max(x.y + x.height for x in bboxes)
|
|
396
|
+
return BoundingBox(min_x, min_y, max_x - min_x, max_y - min_y)
|