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.

Files changed (37) hide show
  1. svg_ultralight/__init__.py +108 -105
  2. svg_ultralight/animate.py +40 -40
  3. svg_ultralight/attrib_hints.py +14 -14
  4. svg_ultralight/bounding_boxes/__init__.py +5 -5
  5. svg_ultralight/bounding_boxes/bound_helpers.py +189 -189
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
  7. svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
  9. svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
  11. svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
  12. svg_ultralight/constructors/__init__.py +14 -14
  13. svg_ultralight/constructors/new_element.py +120 -115
  14. svg_ultralight/font_tools/__init__.py +5 -5
  15. svg_ultralight/font_tools/comp_results.py +295 -293
  16. svg_ultralight/font_tools/font_info.py +793 -792
  17. svg_ultralight/image_ops.py +156 -156
  18. svg_ultralight/inkscape.py +261 -261
  19. svg_ultralight/layout.py +291 -291
  20. svg_ultralight/main.py +183 -198
  21. svg_ultralight/metadata.py +122 -122
  22. svg_ultralight/nsmap.py +36 -36
  23. svg_ultralight/py.typed +5 -0
  24. svg_ultralight/query.py +254 -249
  25. svg_ultralight/read_svg.py +58 -0
  26. svg_ultralight/root_elements.py +87 -87
  27. svg_ultralight/string_conversion.py +244 -244
  28. svg_ultralight/strings/__init__.py +21 -13
  29. svg_ultralight/strings/svg_strings.py +106 -67
  30. svg_ultralight/transformations.py +140 -141
  31. svg_ultralight/unit_conversion.py +247 -248
  32. {svg_ultralight-0.48.0.dist-info → svg_ultralight-0.50.2.dist-info}/METADATA +208 -214
  33. svg_ultralight-0.50.2.dist-info/RECORD +34 -0
  34. svg_ultralight-0.50.2.dist-info/WHEEL +4 -0
  35. svg_ultralight-0.48.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
  37. 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.string_conversion import format_number
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 f"matrix({' '.join(map(format_number, 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)
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)