svg-ultralight 0.37.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.

@@ -8,10 +8,11 @@ from __future__ import annotations
8
8
 
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- from lxml.etree import _Element as EtreeElement # type: ignore
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] | 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.
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import dataclasses
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- from lxml.etree import _Element as EtreeElement # type: ignore
12
+ from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
13
13
 
14
14
  from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_union
15
15
  from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
@@ -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] | None = None,
48
48
  dx: float | None = None,
49
49
  dy: float | None = None,
50
50
  ):
@@ -19,7 +19,9 @@ from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
19
19
  from svg_ultralight.transformations import new_transformation_matrix, transform_element
20
20
 
21
21
  if TYPE_CHECKING:
22
- from lxml.etree import _Element as EtreeElement # type: ignore
22
+ from lxml.etree import (
23
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
24
+ )
23
25
 
24
26
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
25
27
 
@@ -43,14 +45,11 @@ class BoundElement(HasBoundingBox):
43
45
  self.elem = element
44
46
  self.bbox = bounding_box
45
47
 
46
- def _update_elem(self):
47
- self.elem.attrib["transform"] = self.bbox.transform_string
48
-
49
48
  def transform(
50
49
  self,
51
50
  transformation: _Matrix | None = None,
52
51
  *,
53
- scale: float | None = None,
52
+ scale: tuple[float, float] | None = None,
54
53
  dx: float | None = None,
55
54
  dy: float | None = None,
56
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
- @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
19
+ class HasBoundingBox(SupportsBounds):
20
+ """A parent class for BoundElement and others that have a bbox attribute."""
70
21
 
71
- # stack a on top of b
72
- bbox_a.y2 = bbox_b.y
22
+ def __init__(self, bbox: BoundingBox) -> None:
23
+ """Initialize the HasBoundingBox instance."""
24
+ self.bbox = bbox
73
25
 
74
- update_element(elem_a, transform=bbox_a.transform_string)
75
- update_element(elem_b, transform=bbox_b.transform_string)
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
- _x: float
79
- _y: float
80
- _width: float
81
- _height: float
82
- _transformation: _Matrix = (1, 0, 0, 1, 0, 0)
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
- @property
85
- def transformation(self) -> _Matrix:
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
- :return: transformation matrix
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
- return self._transformation
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._transformation = mat_dot(tmat, self.transformation)
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
- return self.transformation[0]
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.transform(scale=value / self.scale)
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 mat_apply(self.transformation, (self._x, 0))[0]
135
+ return min(x for x, _ in self._get_transformed_corners())
154
136
 
155
137
  @x.setter
156
- def x(self, value: float) -> None:
157
- """Update transformation values (do not alter self._x).
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 value after transformation
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 self.x + self.width
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 mat_apply(self.transformation, (0, self._y))[1]
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 self.y + self.height
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._width * self.scale
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.scale *= value / self.width
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._height * self.scale
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