svg-ultralight 0.38.0__py3-none-any.whl → 0.39.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of svg-ultralight might be problematic. Click here for more details.

@@ -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: list[BoundingBox] = []
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] | 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.
@@ -44,7 +44,7 @@ class BoundCollection(HasBoundingBox):
44
44
  self,
45
45
  transformation: _Matrix | None = None,
46
46
  *,
47
- scale: float | None = None,
47
+ scale: tuple[float, float] | float | None = None,
48
48
  dx: float | None = None,
49
49
  dy: float | None = None,
50
50
  ):
@@ -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] | 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,54 @@ 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
- @dataclasses.dataclass
19
- class BoundingBox(SupportsBounds):
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
70
-
71
- # stack a on top of b
72
- bbox_a.y2 = bbox_b.y
73
-
74
- update_element(elem_a, transform=bbox_a.transform_string)
75
- update_element(elem_b, transform=bbox_b.transform_string)
76
- """
77
-
78
- _x: float
79
- _y: float
80
- _width: float
81
- _height: float
82
- _transformation: _Matrix = (1, 0, 0, 1, 0, 0)
19
+ class HasBoundingBox(SupportsBounds):
20
+ """A parent class for BoundElement and others that have a bbox attribute."""
83
21
 
84
- @property
85
- def transformation(self) -> _Matrix:
86
- """Return transformation matrix.
22
+ def __init__(self, bbox: BoundingBox) -> None:
23
+ """Initialize the HasBoundingBox instance."""
24
+ self.bbox = bbox
87
25
 
88
- :return: transformation matrix
89
- """
90
- return self._transformation
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.
53
+
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
91
61
 
92
62
  def transform(
93
63
  self,
94
64
  transformation: _Matrix | None = None,
95
65
  *,
96
- scale: float | None = None,
66
+ scale: tuple[float, float] | float | None = None,
97
67
  dx: float | None = None,
98
68
  dy: float | None = None,
99
69
  ):
@@ -112,10 +82,10 @@ class BoundingBox(SupportsBounds):
112
82
  when applying a transformation from another bounding box instance.
113
83
  """
114
84
  tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
115
- self._transformation = mat_dot(tmat, self.transformation)
85
+ self.bbox.transformation = mat_dot(tmat, self.bbox.transformation)
116
86
 
117
87
  @property
118
- def scale(self) -> float:
88
+ def scale(self) -> tuple[float, float]:
119
89
  """Get scale of the bounding box.
120
90
 
121
91
  :return: uniform scale of the bounding box
@@ -127,10 +97,11 @@ class BoundingBox(SupportsBounds):
127
97
  width*scale, height => height*scale, scale => scale*scale. This matches how
128
98
  scale works in almost every other context.
129
99
  """
130
- return self.transformation[0]
100
+ xx, xy, yx, yy, *_ = self.bbox.transformation
101
+ return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
131
102
 
132
103
  @scale.setter
133
- def scale(self, value: float) -> None:
104
+ def scale(self, value: tuple[float, float]) -> None:
134
105
  """Scale the bounding box by a uniform factor.
135
106
 
136
107
  :param value: new scale value
@@ -142,7 +113,8 @@ class BoundingBox(SupportsBounds):
142
113
  `scale = 2` -> ignore whatever scale was previously defined and set scale to 2
143
114
  `scale *= 2` -> make it twice as big as it was.
144
115
  """
145
- self.transform(scale=value / self.scale)
116
+ new_scale = value[0] / self.scale[0], value[1] / self.scale[1]
117
+ self.transform(scale=new_scale)
146
118
 
147
119
  @property
148
120
  def x(self) -> float:
@@ -150,13 +122,13 @@ class BoundingBox(SupportsBounds):
150
122
 
151
123
  :return: internal _x value transformed by scale and translation
152
124
  """
153
- return mat_apply(self.transformation, (self._x, 0))[0]
125
+ return min(x for x, _ in self._get_transformed_corners())
154
126
 
155
127
  @x.setter
156
- def x(self, value: float) -> None:
157
- """Update transformation values (do not alter self._x).
128
+ def x(self, value: float):
129
+ """Set the x coordinate of the left edge of the bounding box.
158
130
 
159
- :param value: new x value after transformation
131
+ :param value: the new x coordinate of the left edge of the bounding box
160
132
  """
161
133
  self.transform(dx=value - self.x)
162
134
 
@@ -182,7 +154,7 @@ class BoundingBox(SupportsBounds):
182
154
 
183
155
  :return: transformed x + transformed width
184
156
  """
185
- return self.x + self.width
157
+ return max(x for x, _ in self._get_transformed_corners())
186
158
 
187
159
  @x2.setter
188
160
  def x2(self, value: float) -> None:
@@ -198,7 +170,7 @@ class BoundingBox(SupportsBounds):
198
170
 
199
171
  :return: internal _y value transformed by scale and translation
200
172
  """
201
- return mat_apply(self.transformation, (0, self._y))[1]
173
+ return min(y for _, y in self._get_transformed_corners())
202
174
 
203
175
  @y.setter
204
176
  def y(self, value: float) -> None:
@@ -230,7 +202,7 @@ class BoundingBox(SupportsBounds):
230
202
 
231
203
  :return: transformed y + transformed height
232
204
  """
233
- return self.y + self.height
205
+ return max(y for _, y in self._get_transformed_corners())
234
206
 
235
207
  @y2.setter
236
208
  def y2(self, value: float) -> None:
@@ -246,7 +218,7 @@ class BoundingBox(SupportsBounds):
246
218
 
247
219
  :return: internal _width value transformed by scale
248
220
  """
249
- return self._width * self.scale
221
+ return self.x2 - self.x
250
222
 
251
223
  @width.setter
252
224
  def width(self, value: float) -> None:
@@ -259,7 +231,7 @@ class BoundingBox(SupportsBounds):
259
231
  """
260
232
  current_x = self.x
261
233
  current_y = self.y
262
- self.scale *= value / self.width
234
+ self.transform(scale=value / self.width)
263
235
  self.x = current_x
264
236
  self.y = current_y
265
237
 
@@ -269,7 +241,7 @@ class BoundingBox(SupportsBounds):
269
241
 
270
242
  :return: internal _height value transformed by scale
271
243
  """
272
- return self._height * self.scale
244
+ return self.y2 - self.y
273
245
 
274
246
  @height.setter
275
247
  def height(self, value: float) -> None:
@@ -291,7 +263,96 @@ class BoundingBox(SupportsBounds):
291
263
  Use with
292
264
  ``update_element(elem, transform=bbox.transform_string)``
293
265
  """
294
- return f"matrix({' '.join(map(format_number, self.transformation))})"
266
+ return f"matrix({' '.join(map(format_number, self.bbox.transformation))})"
267
+
268
+
269
+ @dataclasses.dataclass
270
+ class BoundingBox(HasBoundingBox):
271
+ """Mutable bounding box object for svg_ultralight.
272
+
273
+ :param x: left x value
274
+ :param y: top y value
275
+ :param width: width of the bounding box
276
+ :param height: height of the bounding box
277
+
278
+ The below optional parameter, in addition to the required parameters, captures
279
+ the entire state of a BoundingBox instance. It could be used to make a copy or
280
+ to initialize a transformed box with the same transform_string as another box.
281
+ Under most circumstances, it will not be used.
282
+
283
+ :param transformation: transformation matrix
284
+
285
+ Functions that return a bounding box will return a BoundingBox instance. This
286
+ instance can be transformed (uniform scale and translate only). Transformations
287
+ will be combined and scored to be passed to new_element as a transform value.
288
+
289
+ Define the bbox with x=, y=, width=, height=
290
+
291
+ Transform the BoundingBox by setting these variables. Each time you set x, cx,
292
+ x2, y, cy, y2, width, or height, private transformation value transformation
293
+ will be updated.
294
+
295
+ The ultimate transformation can be accessed through ``.transform_string``.
296
+ So the workflow will look like :
297
+
298
+ 1. Get the bounding box of an svg element
299
+ 2. Update the bounding box x, y, width, and height
300
+ 3. Transform the original svg element with
301
+ update_element(elem, transform=bbox.transform_string)
302
+ 4. The transformed element will lie in the transformed BoundingBox
303
+
304
+ In addition to x, y, width, and height, x2 and y2 can be set to establish the
305
+ right x value or bottom y value.
306
+
307
+ The point of all of this is to simplify stacking and aligning elements. To stack:
308
+
309
+ ```
310
+ elem_a = new_element(*args)
311
+ bbox_a = get_bounding_box(elem_a)
312
+
313
+ elem_b = new_element(*args)
314
+ bbox_b = get_bounding_box(elem_b)
315
+
316
+ # align at same x
317
+ bbox_b.x = bbox_a.x
318
+
319
+ # make the same width
320
+ bbox_b.width = bbox_a.width
321
+
322
+ # stack a on top of b
323
+ bbox_a.y2 = bbox_b.y
324
+
325
+ update_element(elem_a, transform=bbox_a.transform_string)
326
+ update_element(elem_b, transform=bbox_b.transform_string)
327
+ """
328
+
329
+ base_x: float = dataclasses.field(init=False)
330
+ base_y: float = dataclasses.field(init=False)
331
+ base_width: float = dataclasses.field(init=False)
332
+ base_height: float = dataclasses.field(init=False)
333
+ transformation: _Matrix = dataclasses.field(init=False)
334
+
335
+ def __init__(
336
+ self,
337
+ x: float,
338
+ y: float,
339
+ width: float,
340
+ height: float,
341
+ transformation: _Matrix = (1, 0, 0, 1, 0, 0),
342
+ ) -> None:
343
+ """Initialize a BoundingBox instance.
344
+
345
+ :param x: left x value
346
+ :param y: top y value
347
+ :param width: width of the bounding box
348
+ :param height: height of the bounding box
349
+ """
350
+ self.base_x = x
351
+ self.base_y = y
352
+ self.base_width = width
353
+ self.base_height = height
354
+ self.transformation = transformation
355
+ self.bbox = self
295
356
 
296
357
  def merge(self, *others: BoundingBox) -> BoundingBox:
297
358
  """Create a bounding box around all other bounding boxes.
@@ -321,181 +382,3 @@ class BoundingBox(SupportsBounds):
321
382
  min_y = min(x.y for x in bboxes)
322
383
  max_y = max(x.y + x.height for x in bboxes)
323
384
  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