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

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

Potentially problematic release.


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

@@ -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] | 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] | 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] | None = None,
56
53
  dx: float | None = None,
57
54
  dy: float | None = None,
58
55
  ):
@@ -7,6 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import dataclasses
10
+ import math
10
11
 
11
12
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
12
13
  from svg_ultralight.string_conversion import format_number
@@ -15,85 +16,64 @@ from svg_ultralight.transformations import mat_apply, mat_dot, new_transformatio
15
16
  _Matrix = tuple[float, float, float, float, float, float]
16
17
 
17
18
 
18
- @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
@@ -12,7 +12,7 @@ There is a getter and setter for each of the four padding values. These *do not*
12
12
  the text element. For instance, if you decrease the left padding, the left margin
13
13
  will move, *not* the text element.
14
14
 
15
- There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
15
+ _There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
16
16
  These *do* move the element, but do not scale it. For instance, if you move the
17
17
  leftmargin to the left, the right margin (and the text element with it) will move to
18
18
  the left.
@@ -66,8 +66,9 @@ from __future__ import annotations
66
66
 
67
67
  from typing import TYPE_CHECKING
68
68
 
69
- from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
69
+ from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
70
70
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
71
+ from svg_ultralight.transformations import new_transformation_matrix, transform_element
71
72
 
72
73
  if TYPE_CHECKING:
73
74
  from lxml.etree import (
@@ -77,7 +78,7 @@ if TYPE_CHECKING:
77
78
  _Matrix = tuple[float, float, float, float, float, float]
78
79
 
79
80
 
80
- class PaddedText(SupportsBounds):
81
+ class PaddedText(BoundElement):
81
82
  """A line of text with a bounding box and padding."""
82
83
 
83
84
  def __init__(
@@ -99,14 +100,14 @@ class PaddedText(SupportsBounds):
99
100
  :param lpad: Left padding.
100
101
  """
101
102
  self.elem = elem
102
- self.bbox = bbox
103
+ self.unpadded_bbox = bbox
103
104
  self.base_tpad = tpad
104
105
  self.rpad = rpad
105
106
  self.base_bpad = bpad
106
107
  self.lpad = lpad
107
108
 
108
109
  @property
109
- def padded_bbox(self) -> BoundingBox:
110
+ def bbox(self) -> BoundingBox:
110
111
  """Return a BoundingBox around the margins and cap/baseline.
111
112
 
112
113
  :return: A BoundingBox around the margins and cap/baseline.
@@ -117,22 +118,28 @@ class PaddedText(SupportsBounds):
117
118
  instance around multiple text elements (a <g> elem).
118
119
  """
119
120
  return BoundingBox(
120
- self.lmargin, self.capline, self.padded_width, self.padded_height
121
+ self.x,
122
+ self.y,
123
+ self.width,
124
+ self.height,
125
+ self.unpadded_bbox.transformation,
121
126
  )
122
127
 
123
- @property
124
- def transformation(self) -> _Matrix:
125
- """The transformation matrix of the bounding box."""
126
- return self.bbox.transformation
128
+ @bbox.setter
129
+ def bbox(self, value: BoundingBox) -> None:
130
+ """Set the bounding box of this PaddedText.
127
131
 
128
- def _update_elem(self):
129
- self.elem.attrib["transform"] = self.bbox.transform_string
132
+ :param value: The new bounding box.
133
+ :effects: The text element is transformed to fit the new bounding box.
134
+ """
135
+ msg = "Cannot set bbox of PaddedText, use transform() instead."
136
+ raise NotImplementedError(msg)
130
137
 
131
138
  def transform(
132
139
  self,
133
140
  transformation: _Matrix | None = None,
134
141
  *,
135
- scale: float | None = None,
142
+ scale: tuple[float, float] | None = None,
136
143
  dx: float | None = None,
137
144
  dy: float | None = None,
138
145
  ):
@@ -143,8 +150,9 @@ class PaddedText(SupportsBounds):
143
150
  :param dx: the x translation
144
151
  :param dy: the y translation
145
152
  """
146
- self.bbox.transform(transformation, scale=scale, dx=dx, dy=dy)
147
- self._update_elem()
153
+ tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
154
+ self.unpadded_bbox.transform(tmat)
155
+ _ = transform_element(self.elem, tmat)
148
156
 
149
157
  @property
150
158
  def tpad(self) -> float:
@@ -152,7 +160,7 @@ class PaddedText(SupportsBounds):
152
160
 
153
161
  :return: The scaled top padding of this line of text.
154
162
  """
155
- return self.base_tpad * self.bbox.scale
163
+ return self.base_tpad * self.unpadded_bbox.scale[1]
156
164
 
157
165
  @tpad.setter
158
166
  def tpad(self, value: float) -> None:
@@ -160,7 +168,7 @@ class PaddedText(SupportsBounds):
160
168
 
161
169
  :param value: The new top padding.
162
170
  """
163
- self.base_tpad = value / self.bbox.scale
171
+ self.base_tpad = value / self.unpadded_bbox.scale[1]
164
172
 
165
173
  @property
166
174
  def bpad(self) -> float:
@@ -168,7 +176,7 @@ class PaddedText(SupportsBounds):
168
176
 
169
177
  :return: The scaled bottom padding of this line of text.
170
178
  """
171
- return self.base_bpad * self.bbox.scale
179
+ return self.base_bpad * self.unpadded_bbox.scale[1]
172
180
 
173
181
  @bpad.setter
174
182
  def bpad(self, value: float) -> None:
@@ -176,82 +184,18 @@ class PaddedText(SupportsBounds):
176
184
 
177
185
  :param value: The new bottom padding.
178
186
  """
179
- self.base_bpad = value / self.bbox.scale
180
-
181
- @property
182
- def lmargin(self) -> float:
183
- """The left margin of this line of text.
184
-
185
- :return: The left margin of this line of text.
186
- """
187
- return self.bbox.x - self.lpad
188
-
189
- @lmargin.setter
190
- def lmargin(self, value: float) -> None:
191
- """Set the left margin of this line of text.
192
-
193
- :param value: The left margin of this line of text.
194
- """
195
- self.transform(dx=value + self.lpad - self.bbox.x)
196
-
197
- @property
198
- def rmargin(self) -> float:
199
- """The right margin of this line of text.
200
-
201
- :return: The right margin of this line of text.
202
- """
203
- return self.bbox.x2 + self.rpad
204
-
205
- @rmargin.setter
206
- def rmargin(self, value: float) -> None:
207
- """Set the right margin of this line of text.
208
-
209
- :param value: The right margin of this line of text.
210
- """
211
- self.transform(dx=value - self.rpad - self.bbox.x2)
212
-
213
- @property
214
- def capline(self) -> float:
215
- """The top of this line of text.
216
-
217
- :return: The top of this line of text.
218
- """
219
- return self.bbox.y - self.tpad
220
-
221
- @capline.setter
222
- def capline(self, value: float) -> None:
223
- """Set the top of this line of text.
224
-
225
- :param value: The top of this line of text.
226
- """
227
- self.transform(dy=value + self.tpad - self.bbox.y)
187
+ self.base_bpad = value / self.unpadded_bbox.scale[1]
228
188
 
229
189
  @property
230
- def baseline(self) -> float:
231
- """The bottom of this line of text.
232
-
233
- :return: The bottom of this line of text.
234
- """
235
- return self.bbox.y2 + self.bpad
236
-
237
- @baseline.setter
238
- def baseline(self, value: float) -> None:
239
- """Set the bottom of this line of text.
240
-
241
- :param value: The bottom of this line of text.
242
- """
243
- self.transform(dy=value - self.bpad - self.bbox.y2)
244
-
245
- @property
246
- def padded_width(self) -> float:
190
+ def width(self) -> float:
247
191
  """The width of this line of text with padding.
248
192
 
249
193
  :return: The scaled width of this line of text with padding.
250
194
  """
251
- return self.bbox.width + self.lpad + self.rpad
195
+ return self.unpadded_bbox.width + self.lpad + self.rpad
252
196
 
253
- @padded_width.setter
254
- def padded_width(self, width: float) -> None:
197
+ @width.setter
198
+ def width(self, value: float) -> None:
255
199
  """Scale to padded_width = width without scaling padding.
256
200
 
257
201
  :param width: The new width of this line of text.
@@ -263,27 +207,26 @@ class PaddedText(SupportsBounds):
263
207
  baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
264
208
  *and* y2) when scaling.
265
209
  """
266
- baseline = self.baseline
267
- self.bbox.width = width - self.lpad - self.rpad
268
- self.baseline = baseline
269
- self._update_elem()
210
+ y2 = self.y2
211
+ self.unpadded_bbox.width = value - self.lpad - self.rpad
212
+ self.y2 = y2
270
213
 
271
214
  @property
272
- def padded_height(self) -> float:
215
+ def height(self) -> float:
273
216
  """The height of this line of text with padding.
274
217
 
275
218
  :return: The scaled height of this line of text with padding.
276
219
  """
277
- return self.bbox.height + self.tpad + self.bpad
220
+ return self.unpadded_bbox.height + self.tpad + self.bpad
278
221
 
279
- @padded_height.setter
280
- def padded_height(self, height: float) -> None:
281
- """Scale to padded_height = height without scaling padding.
222
+ @height.setter
223
+ def height(self, value: float) -> None:
224
+ """Scale to height without scaling padding.
282
225
 
283
226
  :param height: The new height of this line of text.
284
227
  :effects: the text_element bounding box is scaled to height - tpad - bpad.
285
228
  """
286
- self.padded_width *= height / self.padded_height
229
+ self.width *= value / self.height
287
230
 
288
231
  @property
289
232
  def x(self) -> float:
@@ -291,15 +234,15 @@ class PaddedText(SupportsBounds):
291
234
 
292
235
  :return: The left margin of this line of text.
293
236
  """
294
- return self.lmargin
237
+ return self.unpadded_bbox.x - self.lpad
295
238
 
296
239
  @x.setter
297
240
  def x(self, value: float) -> None:
298
241
  """Set the left margin of this line of text.
299
242
 
300
- :param value: The new left margin of this line of text.
243
+ :param value: The left margin of this line of text.
301
244
  """
302
- self.lmargin = value
245
+ self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
303
246
 
304
247
  @property
305
248
  def x2(self) -> float:
@@ -307,134 +250,49 @@ class PaddedText(SupportsBounds):
307
250
 
308
251
  :return: The right margin of this line of text.
309
252
  """
310
- return self.rmargin
253
+ return self.unpadded_bbox.x2 + self.rpad
311
254
 
312
255
  @x2.setter
313
256
  def x2(self, value: float) -> None:
314
257
  """Set the right margin of this line of text.
315
258
 
316
- :param value: The new right margin of this line of this text.
259
+ :param value: The right margin of this line of text.
317
260
  """
318
- self.rmargin = value
261
+ self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
319
262
 
320
263
  @property
321
264
  def y(self) -> float:
322
- """The capline of this line of text.
265
+ """The top of this line of text.
323
266
 
324
- :return: The capline of this line of text.
267
+ :return: The top of this line of text.
325
268
  """
326
- return self.capline
269
+ return self.unpadded_bbox.y - self.tpad
327
270
 
328
271
  @y.setter
329
272
  def y(self, value: float) -> None:
330
- """Set the capline of this line of text.
273
+ """Set the top of this line of text.
331
274
 
332
- :param value: The new capline of this line of text.
275
+ :param value: The top of this line of text.
333
276
  """
334
- self.capline = value
277
+ self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
335
278
 
336
279
  @property
337
280
  def y2(self) -> float:
338
- """The baseline of this line of text.
281
+ """The bottom of this line of text.
339
282
 
340
- :return: The baseline of this line of text.
283
+ :return: The bottom of this line of text.
341
284
  """
342
- return self.baseline
285
+ return self.unpadded_bbox.y2 + self.bpad
343
286
 
344
287
  @y2.setter
345
288
  def y2(self, value: float) -> None:
346
- """Set the baseline of this line of text.
347
-
348
- :param value: The new baseline of this line of text.
349
- """
350
- self.baseline = value
351
-
352
- @property
353
- def width(self) -> float:
354
- """The width of this line of text with padding.
355
-
356
- :return: The scaled width of this line of text with padding.
357
- """
358
- return self.padded_width
359
-
360
- @width.setter
361
- def width(self, value: float) -> None:
362
- """Scale to width without scaling padding.
363
-
364
- :param value: The new width of this line of text.
365
- :effects: the text_element bounding box is scaled to width - lpad - rpad.
366
-
367
- Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
368
- consistent with how rectangles, viewboxes, and anything else defined by x, y,
369
- width, height behaves in SVG. This is unintuitive for text, because the
370
- baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
371
- *and* y2) when scaling.
372
- """
373
- baseline = self.baseline
374
- self.padded_width = value
375
- self.baseline = baseline
376
-
377
- @property
378
- def height(self) -> float:
379
- """The height of this line of text with padding.
380
-
381
- :return: The scaled height of this line of text with padding.
382
- """
383
- return self.padded_height
384
-
385
- @height.setter
386
- def height(self, value: float) -> None:
387
- """Scale to height without scaling padding.
388
-
389
- :param value: The new height of this line of text.
390
- :effects: the text_element bounding box is scaled to height - tpad - bpad.
391
- """
392
- self.padded_height = value
393
-
394
- @property
395
- def cx(self) -> float:
396
- """The x coordinate of the center between margins.
397
-
398
- :return: the x coordinate of the center between margins
399
- """
400
- return self.lmargin + self.padded_width / 2
401
-
402
- @cx.setter
403
- def cx(self, value: float):
404
- """Set the x coordinate of the center between margins.
405
-
406
- :param value: the new x coordinate of the center between margins
407
- """
408
- self.lmargin = value - self.padded_width / 2
409
-
410
- @property
411
- def cy(self) -> float:
412
- """The y coordinate of the center between baseline and capline.
413
-
414
- :return: the y coordinate of the center between baseline and capline
415
- """
416
- return self.capline + self.padded_height / 2
417
-
418
- @cy.setter
419
- def cy(self, value: float):
420
- """Set the y coordinate of the center between baseline and capline.
421
-
422
- :param value: the new y coordinate of the center between baseline and capline
423
- """
424
- self.capline = value - self.padded_height / 2
425
-
426
- @property
427
- def scale(self) -> float:
428
- """The scale of the text element.
289
+ """Set the bottom of this line of text.
429
290
 
430
- :return: the scale of the text element
291
+ :param value: The bottom of this line of text.
431
292
  """
432
- return self.bbox.scale
433
-
434
- @scale.setter
435
- def scale(self, value: float):
436
- """Set the scale of the text element.
293
+ self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
437
294
 
438
- :param value: the new scale of the text element
439
- """
440
- self.bbox.scale = value
295
+ lmargin = x
296
+ rmargin = x2
297
+ capline = y
298
+ baseline = y2
svg_ultralight/layout.py CHANGED
@@ -144,11 +144,11 @@ def pad_and_scale(
144
144
  """Expand and scale the pad argument. If necessary, scale image.
145
145
 
146
146
  :param viewbox: viewbox to pad (x, y, width height)
147
- :param pad: padding to add around image, in user units or inches. if a
147
+ :param pad: padding to add around image, in user units or inches. If a
148
148
  sequence, it should be (top, right, bottom, left). if a single float or
149
- string, it will be applied to all sides. if two floats, top and bottom
150
- then left and right. if three floats, top, left and right, then bottom.
151
- if four floats, top, right, bottom, left.
149
+ string, it will be applied to all sides. If two floats, top and bottom
150
+ then left and right. If three floats, top, left and right, then bottom.
151
+ If four floats, top, right, bottom, left.
152
152
  :param print_width: width of print area, in user units (float), a string
153
153
  with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
154
154
  "pt")
@@ -169,7 +169,7 @@ def pad_and_scale(
169
169
  If the width and height *are* specified, the user units become whatever they
170
170
  need to be to fit that requirement. For instance, if the viewbox width is 96
171
171
  and the width argument is "1in", then the user units are *still* pixels,
172
- because there are 96 pixels in an inch. If the viewbox with is 2 and the
172
+ because there are 96 pixels in an inch. If the viewbox width is 2 and the
173
173
  width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
174
174
  pixels) each, because there are 2 user units in an inch. If the viewbox
175
175
  width is 3 and the width argument is "1yd", the each user unit is 1 foot.
@@ -195,17 +195,26 @@ def pad_and_scale(
195
195
  the unit designators without changing the scale.
196
196
 
197
197
  Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
198
- instance, If you take a 100x100 unit image then pass pad="0.25in" and
199
- print_width="12in", the output image will be 12.5 inches across. Whatever
200
- geometry was visible in the original viewbox will be much larger, but the
201
- padding will still be 0.25 inches. If you want to use padding and need a
202
- specific output image size, remember to subtract the padding width from your
203
- print_width or print_height.
198
+ instance, if you created two images
199
+
200
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
201
+
202
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
203
+
204
+ ... (note that the images only vary in print_width_), the first image would be
205
+ rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
206
+ content in the viewbox would be exactly twice as wide in the larger image, but
207
+ the padding would remain 0.25 in both images. Despite setting `print_width_` to
208
+ exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
209
+ Despite a viewbox aspect ratio of 1:2, you would not get an output image of
210
+ exactly 1:2. If you want to use padding and need a specific output image size or
211
+ aspect ratio, remember to subtract the padding width from your print_width or
212
+ print_height.
204
213
 
205
214
  Scaling attributes are returned as a dictonary that can be "exploded" into
206
215
  the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
207
216
 
208
- * If neighther a print_width nor print_height is specified, no scaling
217
+ * If neither a print_width nor print_height is specified, no scaling
209
218
  attributes will be returned.
210
219
 
211
220
  * If either is specified, both a width and height will be returned (even if
@@ -242,7 +251,7 @@ def pad_and_scale(
242
251
  a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
243
252
  ... then scale the printout with dpu_=2 to get a 32" x 18" image with the
244
253
  same viewbox. This means the padding will be 2" on all sides, but the image
245
- will be identical (just twice as large) as the 16" x 9" image.
254
+ will be identical (just twice as wide and twice as high) as the 16" x 9" image.
246
255
  """
247
256
  pads = expand_pad_arg(pad)
248
257
 
svg_ultralight/query.py CHANGED
@@ -63,7 +63,7 @@ def _fill_ids(*elem_args: EtreeElement) -> None:
63
63
 
64
64
 
65
65
  def _normalize_views(elem: EtreeElement) -> None:
66
- """Create a square viewbox for any element with an svg tag.
66
+ """Create a square viewBox for any element with an svg tag.
67
67
 
68
68
  :param elem: an etree element
69
69
 
@@ -110,51 +110,61 @@ def map_elems_to_bounding_boxes(
110
110
  IMPORTANT: path cannot end with ``.exe``.
111
111
  Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
112
112
  :param elem_args: xml element (written to a temporary file then queried)
113
- :return: svg elements (and a bounding box for the entire svg file as ``svg``)
114
- mapped to BoundingBox(x, y, width, height)
115
- :effects: temporarily adds an id attribute if any ids are missing. Non-unique ids
116
- will break this function.
117
-
118
- Bounding boxes are relative to svg viewbox. If viewbox x == -10,
113
+ :return: input svg elements and any descendents of those elements mapped
114
+ `BoundingBox(x, y, width, height)`
115
+ So return dict keys are the input elements themselves with one exception: a
116
+ string key, "svg", is mapped to a bounding box around all input elements.
117
+ :effects: temporarily adds an id attribute if any ids are missing. These are
118
+ removed if the function completes. Existing, non-unique ids will break this
119
+ function.
120
+
121
+ Bounding boxes are relative to svg viewBox. If, for instance, viewBox x == -10,
119
122
  all bounding-box x values will be offset -10. So, everything is wrapped in a root
120
- element with a "normalized" viewbox, (viewbox=(0, 0, 1, 1)) then any child root
121
- elements ("child root elements" sounds wrong, but it works) viewboxes are
122
- normalized as well. This works even with a root element around a root element, so
123
- input elem_args can be root elements or "normal" elements like "rect", "circle",
124
- or "text" or a mixture of both.
123
+ element, `envelope` with a "normalized" viewBox, `viewBox=(0, 0, 1, 1)`. That
124
+ way, any child root elements ("child root elements" sounds wrong, but it works)
125
+ viewBoxes are normalized as well. This works even with a root element around a
126
+ root element, so input elem_args can be root elements or "normal" elements like
127
+ "rect", "circle", or "text" or a mixture of both. Bounding boxes output here will
128
+ work as expected in any viewBox.
125
129
 
126
130
  The ``inkscape --query-all svg`` call will return a tuple:
127
131
 
128
132
  (b'svg1,x,y,width,height\\r\\elem1,x,y,width,height\\r\\n', None)
129
133
  where x, y, width, and height are strings of numbers.
130
134
 
131
- This calls the command and formats the output into a dictionary.
132
-
133
- Scaling arguments ("width", "height") to new_svg_root transform the bounding
134
- boxes in non-useful ways. This copies all elements except the root element in to
135
- a (0, 0, 1, 1) root. This will put the boxes where you'd expect them to be, no
136
- matter what root you use.
135
+ This calls the command and formats the output into a dictionary. There is a
136
+ little extra complexity to handle cases with duplicate elements. Inkscape will
137
+ map bounding boxes to element ids *if* those ids are unique. If Inkscape
138
+ encounters a duplicate ID, Inkscape will map the bounding box of that element to
139
+ a string like "rect1". If you pass unequal elements with the same id, I can't
140
+ help you, but you might pass the same element multiple times. If you do this,
141
+ Inkscape will find a bounding box for each occurrence, map the first occurrence
142
+ to the id, then map subsequent occurrences to a string like "rect1". This
143
+ function will handle that.
137
144
  """
138
145
  if not elem_args:
139
146
  return {}
140
147
  _fill_ids(*elem_args)
141
- envelope = _envelop_copies(*elem_args)
142
148
 
149
+ envelope = _envelop_copies(*elem_args)
143
150
  with NamedTemporaryFile(mode="wb", delete=False, suffix=".svg") as svg_file:
144
151
  svg = write_svg(svg_file, envelope)
145
152
  with Popen(f'"{inkscape}" --query-all {svg}', stdout=PIPE) as bb_process:
146
153
  bb_data = str(bb_process.communicate()[0])[2:-1]
147
154
  os.unlink(svg_file.name)
155
+
148
156
  bb_strings = re.split(r"[\\r]*\\n", bb_data)[:-1]
149
157
  id2bbox = dict(map(_split_bb_string, bb_strings))
150
158
 
151
159
  elem2bbox: dict[EtreeElement | Literal["svg"], BoundingBox] = {}
152
160
  for elem in _iter_elems(*elem_args):
153
- elem2bbox[elem] = id2bbox.pop(elem.attrib["id"])
154
- if elem.attrib["id"].startswith(_TEMP_ID_PREFIX):
161
+ elem_id = elem.attrib.get("id")
162
+ if not (elem_id): # id removed in a previous loop
163
+ continue
164
+ elem2bbox[elem] = id2bbox[elem_id]
165
+ if elem_id.startswith(_TEMP_ID_PREFIX):
155
166
  del elem.attrib["id"]
156
- ((_, scene_bbox),) = id2bbox.items()
157
- elem2bbox["svg"] = scene_bbox
167
+ elem2bbox["svg"] = BoundingBox.merged(*id2bbox.values())
158
168
  return elem2bbox
159
169
 
160
170
 
@@ -52,13 +52,19 @@ def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
52
52
  return (aa, bb, cc, dd, ee, ff)
53
53
 
54
54
 
55
- def mat_apply(mat1: _Matrix, mat2: tuple[float, float]) -> tuple[float, float]:
55
+ def mat_apply(matrix: _Matrix, point: tuple[float, float]) -> tuple[float, float]:
56
56
  """Apply an svg-style transformation matrix to a point.
57
57
 
58
- :param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
58
+ :param mat1: transformation matrix (a, b, c, d, e, f) describing a 3x3 matrix
59
+ with an implied third row of (0, 0, 1)
60
+ [[a, c, e], [b, d, f], [0, 0, 1]]
59
61
  :param mat2: point (x, y)
60
62
  """
61
- return mat1[0] * mat2[0] + mat1[4], mat1[3] * mat2[1] + mat1[5]
63
+ a, b, c, d, e, f = matrix
64
+ x, y = point
65
+ result_x = a * x + c * y + e
66
+ result_y = b * x + d * y + f
67
+ return result_x, result_y
62
68
 
63
69
 
64
70
  def mat_invert(tmat: _Matrix) -> _Matrix:
@@ -99,7 +105,7 @@ def get_transform_matrix(elem: EtreeElement) -> _Matrix:
99
105
  def new_transformation_matrix(
100
106
  transformation: _Matrix | None = None,
101
107
  *,
102
- scale: float | None = None,
108
+ scale: tuple[float, float] | None = None,
103
109
  dx: float | None = None,
104
110
  dy: float | None = None,
105
111
  ) -> _Matrix:
@@ -109,10 +115,10 @@ def new_transformation_matrix(
109
115
  svg-style transformation matrix.
110
116
  """
111
117
  transformation = transformation or (1, 0, 0, 1, 0, 0)
112
- scale = scale or 1
118
+ scale = scale or (1, 1)
113
119
  dx = dx or 0
114
120
  dy = dy or 0
115
- return mat_dot((scale, 0, 0, scale, dx, dy), transformation)
121
+ return mat_dot((scale[0], 0, 0, scale[1], dx, dy), transformation)
116
122
 
117
123
 
118
124
  def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svg-ultralight
3
- Version: 0.38.0
3
+ Version: 0.39.0
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -2,28 +2,28 @@ svg_ultralight/__init__.py,sha256=wUc79mKsG6lGZ1xaYijyJ4Sm9lG5-5XgRArVsCI0niY,25
2
2
  svg_ultralight/animate.py,sha256=JSrBm-59BcNXDF0cGgl4-C89eBunjevZnwZxIWt48TU,1112
3
3
  svg_ultralight/image_ops.py,sha256=PXN_p5GX91UTvhnwwU-bPuj6WzM9wCx1SqfzR5icNnQ,4686
4
4
  svg_ultralight/inkscape.py,sha256=tVFPtwsUcGKGODmYhC9IdGLyT7OAeNCVsDN0hPBSSgg,9194
5
- svg_ultralight/layout.py,sha256=FgR45FsHax4xDjGkk9HEVW4OcwhtM8aqw2JUdZs_m7Q,12326
5
+ svg_ultralight/layout.py,sha256=7LV2I3u4EhqSc6ASvgwDtTZyV-Y1qt2wtvRtH2uKVAE,12799
6
6
  svg_ultralight/main.py,sha256=VA7tVMO7fiRI_JkEGaH7UFgzJ5YIbHKx4VHfMnT50hI,7446
7
7
  svg_ultralight/metadata.py,sha256=xaIfqhKu52Dl4JOrRlpUsWkkE7Umw8j5Z4waFTli-kI,4234
8
8
  svg_ultralight/nsmap.py,sha256=y63upO78Rr-JJT56RWWZuyrsILh6HPoY4GhbYnK1A0g,1244
9
9
  svg_ultralight/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- svg_ultralight/query.py,sha256=SC8E-7UejzbFvFaYryWVM9uups0rI1AgBY7L8-421Zw,10229
10
+ svg_ultralight/query.py,sha256=wR8oixRYA489k03RU5apXaoami2KvYl8BKmwypQGTi0,10972
11
11
  svg_ultralight/root_elements.py,sha256=E_H7HXk0M5F3IyFVOxO8PQmhww1-sHTzJhx8hBJPZvg,2911
12
12
  svg_ultralight/string_conversion.py,sha256=7gaUWEJptAVZawlvoAJ7JoP996nALoJrAByM3o219Tc,7462
13
- svg_ultralight/transformations.py,sha256=tqexXWmnjS2uRdxfedYIHVAP3RPgGHhFJ62OBY1fMjk,4078
13
+ svg_ultralight/transformations.py,sha256=h1GUD4hvo10ugnSwShDmrwzF048yfug7lCWqCzOTxWs,4296
14
14
  svg_ultralight/unit_conversion.py,sha256=g07nhzXdjPvGcJmkhLdFbeDLrSmbI8uFoVgPo7G62Bg,9258
15
15
  svg_ultralight/bounding_boxes/__init__.py,sha256=qUEn3r4s-1QNHaguhWhhaNfdP4tl_B6YEqxtiTFuzhQ,78
16
- svg_ultralight/bounding_boxes/bound_helpers.py,sha256=-0PrWaFVh9BElL7_FBATwAe1w4WU-ReUbNk5gmbgZfI,7296
17
- svg_ultralight/bounding_boxes/supports_bounds.py,sha256=fbHV6mGdeIVV3lS15vBKSrHiHKR7DMg4K5X__4LLmCE,4569
18
- svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=nsgzLlOb_X_OX6uw6RpbTBLuFtma-GY832vJXGTFEG4,2623
19
- svg_ultralight/bounding_boxes/type_bound_element.py,sha256=irJlwy9Xy6OfJkyVVGq1t2Mv2JIggRyr7972KE3sfL4,2281
20
- svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=cDrMp6uwaA--KJIQS2puG10qh8n3TBmiscg-cfk1f3w,16149
21
- svg_ultralight/bounding_boxes/type_padded_text.py,sha256=YEn8aV6urhXl2Pl4DA3W90cWdXiX7abDweQEohf3ve4,14890
16
+ svg_ultralight/bounding_boxes/bound_helpers.py,sha256=LFkVsdYFKYCnEL6vLvEa_5cfu8D44ZGYeEEb7_0MnC0,7146
17
+ svg_ultralight/bounding_boxes/supports_bounds.py,sha256=Yi696oJ393y29vql0U6kLMR3soSggLB3_wSACHppoJo,4505
18
+ svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=yW-gwehTrLhgG4Rk-wd6XWP0oLj7Yh_Njrudm8Dn1fk,2637
19
+ svg_ultralight/bounding_boxes/type_bound_element.py,sha256=v-XwW1bxA3tT0CzLWOrRO5EHtLA3ivds8DyA5KFju6w,2196
20
+ svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=XsjM6nR5bKRLFa8z1U84GIC-YbleKoXa0s-UScz9Ewo,13621
21
+ svg_ultralight/bounding_boxes/type_padded_text.py,sha256=fiDZoHIaQbGK8ETrPRa2wa5ykbdFBeeDHw9zGqKtD6E,10674
22
22
  svg_ultralight/constructors/__init__.py,sha256=XLOInLhzMERWNnFAs-itMs-OZrBOpvQthZJ2T5duqBE,327
23
23
  svg_ultralight/constructors/new_element.py,sha256=hRUW2hR_BTkthEqPClYV7-IeFe9iv2zwb6ehp1k1xDk,3475
24
24
  svg_ultralight/strings/__init__.py,sha256=BMGhF1pulscIgkiYvZLr6kPRR0L4lW0jUNFxkul4_EM,295
25
25
  svg_ultralight/strings/svg_strings.py,sha256=FQNxNmMkR2M-gCFo_woQKXLgCHi3ncUlRMiaRR_a9nQ,1978
26
- svg_ultralight-0.38.0.dist-info/METADATA,sha256=pq9i97xHJDcgb_Q4H8-bcwEmwkVtrKj8zntmBQeOHwM,8971
27
- svg_ultralight-0.38.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
28
- svg_ultralight-0.38.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
29
- svg_ultralight-0.38.0.dist-info/RECORD,,
26
+ svg_ultralight-0.39.0.dist-info/METADATA,sha256=--QD3_OuduOUZx2fsCx6FsgTaDAuPAFHkkeKxCKY5Dw,8971
27
+ svg_ultralight-0.39.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
28
+ svg_ultralight-0.39.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
29
+ svg_ultralight-0.39.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5