svg-ultralight 0.26.0__py3-none-any.whl → 0.28.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.

@@ -4,6 +4,11 @@
4
4
  :created: 12/22/2019.
5
5
  """
6
6
 
7
+ from svg_ultralight.bounding_boxes.bound_helpers import (
8
+ new_bbox_union,
9
+ new_bound_union,
10
+ new_element_union,
11
+ )
7
12
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
8
13
  from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
9
14
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
@@ -27,6 +32,7 @@ from svg_ultralight.nsmap import NSMAP, new_qname
27
32
  from svg_ultralight.query import pad_text
28
33
  from svg_ultralight.root_elements import new_svg_root_around_bounds
29
34
  from svg_ultralight.string_conversion import (
35
+ format_attr_dict,
30
36
  format_number,
31
37
  format_numbers,
32
38
  format_numbers_in_string,
@@ -39,10 +45,14 @@ __all__ = [
39
45
  "PaddedText",
40
46
  "SupportsBounds",
41
47
  "deepcopy_element",
48
+ "format_attr_dict",
42
49
  "format_number",
43
50
  "format_numbers",
44
51
  "format_numbers_in_string",
52
+ "new_bbox_union",
53
+ "new_bound_union",
45
54
  "new_element",
55
+ "new_element_union",
46
56
  "new_metadata",
47
57
  "new_qname",
48
58
  "new_sub_element",
@@ -0,0 +1,95 @@
1
+ """Helper functions for dealing with BoundElements.
2
+
3
+ :author: Shay Hill
4
+ :created: 2024-05-03
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from lxml.etree import _Element as EtreeElement # type: ignore
12
+
13
+ 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_padded_text import PaddedText
16
+ from svg_ultralight.constructors import new_element
17
+
18
+ if TYPE_CHECKING:
19
+ from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
20
+
21
+
22
+ def new_element_union(
23
+ *elems: EtreeElement | SupportsBounds, **attributes: float | str
24
+ ) -> EtreeElement:
25
+ """Get the union of any elements found in the given arguments.
26
+
27
+ :param elems: BoundElements, PaddedTexts, or EtreeElements.
28
+ Other arguments will be ignored.
29
+ :return: a new group element containing all elements.
30
+
31
+ This does not support consolidating attributes. E.g., if all elements have the
32
+ same fill color, this will not be recognized and consilidated into a single
33
+ attribute for the group. Too many attributes change their behavior when applied
34
+ to a group.
35
+ """
36
+ elements_found: list[EtreeElement] = []
37
+ for elem in elems:
38
+ if isinstance(elem, (BoundElement, PaddedText)):
39
+ elements_found.append(elem.elem)
40
+ elif isinstance(elem, EtreeElement):
41
+ elements_found.append(elem)
42
+
43
+ if not elements_found:
44
+ msg = (
45
+ "Cannot find any elements to union. "
46
+ + "At least one argument must be a "
47
+ + "BoundElement, PaddedText, or EtreeElement."
48
+ )
49
+ raise ValueError(msg)
50
+ group = new_element("g", **attributes)
51
+ group.extend(elements_found)
52
+ return group
53
+
54
+
55
+ def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
56
+ """Get the union of the bounding boxes of the given elements.
57
+
58
+ :param blems: BoundElements, BoundingBoxes, or PaddedTexts.
59
+ Other arguments will be ignored.
60
+ :return: the union of all bounding boxes as a BoundingBox instance.
61
+
62
+ Will used the padded_box attribute of PaddedText instances.
63
+ """
64
+ bboxes: list[BoundingBox] = []
65
+ for blem in blems:
66
+ if isinstance(blem, BoundingBox):
67
+ bboxes.append(blem)
68
+ elif isinstance(blem, BoundElement):
69
+ bboxes.append(blem.bbox)
70
+ elif isinstance(blem, PaddedText):
71
+ bboxes.append(blem.padded_bbox)
72
+
73
+ if not bboxes:
74
+ msg = (
75
+ "Cannot find any bounding boxes to union. "
76
+ + "At least one argument must be a "
77
+ + "BoundElement, BoundingBox, or PaddedText."
78
+ )
79
+ raise ValueError(msg)
80
+
81
+ return BoundingBox.merged(*bboxes)
82
+
83
+
84
+ def new_bound_union(*blems: SupportsBounds | EtreeElement) -> BoundElement:
85
+ """Get the union of the bounding boxes of the given elements.
86
+
87
+ :param blems: BoundElements or EtreeElements.
88
+ At least one argument must be a BoundElement, BoundingBox, or PaddedText.
89
+ :return: the union of all arguments as a BoundElement instance.
90
+
91
+ Will used the padded_box attribute of PaddedText instances.
92
+ """
93
+ group = new_element_union(*blems)
94
+ bbox = new_bbox_union(*blems)
95
+ return BoundElement(group, bbox)
@@ -6,13 +6,51 @@
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from dataclasses import dataclass
9
+ import dataclasses
10
10
 
11
11
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
12
12
  from svg_ultralight.string_conversion import format_number
13
13
 
14
+ Matrix = tuple[float, float, float, float, float, float]
14
15
 
15
- @dataclass
16
+
17
+ def mat_dot(mat1: Matrix, mat2: Matrix) -> Matrix:
18
+ """Matrix multiplication for svg-style matrices.
19
+
20
+ :param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
21
+ :param mat2: transformation matrix (sx, 0, 0, sy, tx, ty)
22
+
23
+ Svg uses an unusual matrix format. For 3x3 transformation matrix
24
+
25
+ [[00, 01, 02],
26
+ [10, 11, 12],
27
+ [20, 21, 22]]
28
+
29
+ The svg matrix is
30
+ (00, 10, 01, 11, 02, 12)
31
+
32
+ Values 10 and 01 are only used for skewing, which is not supported by a bounding
33
+ box, but they're here if this function is used in other ways.
34
+ """
35
+ aa = sum(mat1[x] * mat2[y] for x, y in ((0, 0), (2, 1)))
36
+ bb = sum(mat1[x] * mat2[y] for x, y in ((1, 0), (3, 1)))
37
+ cc = sum(mat1[x] * mat2[y] for x, y in ((0, 2), (2, 3)))
38
+ dd = sum(mat1[x] * mat2[y] for x, y in ((1, 2), (3, 3)))
39
+ ee = sum(mat1[x] * mat2[y] for x, y in ((0, 4), (2, 5))) + mat1[4]
40
+ ff = sum(mat1[x] * mat2[y] for x, y in ((1, 4), (3, 5))) + mat1[5]
41
+ return (aa, bb, cc, dd, ee, ff)
42
+
43
+
44
+ def mat_apply(mat1: Matrix, mat2: tuple[float, float]) -> tuple[float, float]:
45
+ """Apply an svg-style transformation matrix to a point.
46
+
47
+ :param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
48
+ :param mat2: point (x, y)
49
+ """
50
+ return mat1[0] * mat2[0] + mat1[4], mat1[3] * mat2[1] + mat1[5]
51
+
52
+
53
+ @dataclasses.dataclass
16
54
  class BoundingBox(SupportsBounds):
17
55
  """Mutable bounding box object for svg_ultralight.
18
56
 
@@ -21,14 +59,12 @@ class BoundingBox(SupportsBounds):
21
59
  :param width: width of the bounding box
22
60
  :param height: height of the bounding box
23
61
 
24
- The below optional parameters, in addition to the required parameters, capture
25
- the entire state of a BoundingBox instance. They could be used to make a copy or
62
+ The below optional parameter, in addition to the required parameters, captures
63
+ the entire state of a BoundingBox instance. It could be used to make a copy or
26
64
  to initialize a transformed box with the same transform_string as another box.
27
- Under most circumstances, they will not be used.
65
+ Under most circumstances, it will not be used.
28
66
 
29
- :param scale: scale of the bounding box
30
- :param translation_x: x translation of the bounding box
31
- :param translation_y: y translation of the bounding box
67
+ :param _transform: transformation matrix
32
68
 
33
69
  Functions that return a bounding box will return a BoundingBox instance. This
34
70
  instance can be transformed (uniform scale and translate only). Transformations
@@ -37,8 +73,8 @@ class BoundingBox(SupportsBounds):
37
73
  Define the bbox with x=, y=, width=, height=
38
74
 
39
75
  Transform the BoundingBox by setting these variables. Each time you set x, cx,
40
- x2, y, cy, y2, width, or height, private transformation values (_scale,
41
- _transform_x, and _transform_y) will be updated.
76
+ x2, y, cy, y2, width, or height, private transformation value _transform will be
77
+ updated.
42
78
 
43
79
  The ultimate transformation can be accessed through ``.transformation_string``.
44
80
  So the workflow will look like :
@@ -78,13 +114,19 @@ class BoundingBox(SupportsBounds):
78
114
  _y: float
79
115
  _width: float
80
116
  _height: float
81
- _scale: float = 1.0
82
- _translation_x: float = 0.0
83
- _translation_y: float = 0.0
117
+ _transform: Matrix = (1, 0, 0, 1, 0, 0)
118
+
119
+ @property
120
+ def transform(self) -> Matrix:
121
+ """Get read only tranformation matrix.
122
+
123
+ :return: transformation matrix of the bounding box
124
+ """
125
+ return self._transform
84
126
 
85
127
  @property
86
128
  def scale(self) -> float:
87
- """Read-only scale.
129
+ """Get scale of the bounding box.
88
130
 
89
131
  :return: uniform scale of the bounding box
90
132
 
@@ -95,7 +137,7 @@ class BoundingBox(SupportsBounds):
95
137
  width*scale, height => height*scale, scale => scale*scale. This matches how
96
138
  scale works in almost every other context.
97
139
  """
98
- return self._scale
140
+ return self._transform[0]
99
141
 
100
142
  @scale.setter
101
143
  def scale(self, value: float) -> None:
@@ -110,7 +152,7 @@ class BoundingBox(SupportsBounds):
110
152
  `scale = 2` -> ignore whatever scale was previously defined and set scale to 2
111
153
  `scale *= 2` -> make it twice as big as it was.
112
154
  """
113
- self._scale = value
155
+ self._add_transform(value / self.scale, 0, 0)
114
156
 
115
157
  @property
116
158
  def x(self) -> float:
@@ -118,7 +160,7 @@ class BoundingBox(SupportsBounds):
118
160
 
119
161
  :return: internal _x value transformed by scale and translation
120
162
  """
121
- return (self._translation_x + self._x) * self._scale
163
+ return mat_apply(self._transform, (self._x, 0))[0]
122
164
 
123
165
  @x.setter
124
166
  def x(self, value: float) -> None:
@@ -142,7 +184,7 @@ class BoundingBox(SupportsBounds):
142
184
 
143
185
  :param value: new center x value after transformation
144
186
  """
145
- self._add_transform(1, value - self.cx, 0)
187
+ self.x += value - self.cx
146
188
 
147
189
  @property
148
190
  def x2(self) -> float:
@@ -158,7 +200,7 @@ class BoundingBox(SupportsBounds):
158
200
 
159
201
  :param value: new x2 value after transformation
160
202
  """
161
- self._add_transform(1, value - self.x2, 0)
203
+ self.x += value - self.x2
162
204
 
163
205
  @property
164
206
  def y(self) -> float:
@@ -166,7 +208,7 @@ class BoundingBox(SupportsBounds):
166
208
 
167
209
  :return: internal _y value transformed by scale and translation
168
210
  """
169
- return (self._translation_y + self._y) * self._scale
211
+ return mat_apply(self._transform, (0, self._y))[1]
170
212
 
171
213
  @y.setter
172
214
  def y(self, value: float) -> None:
@@ -190,7 +232,7 @@ class BoundingBox(SupportsBounds):
190
232
 
191
233
  :param value: new center y value after transformation
192
234
  """
193
- self._add_transform(1, 0, value - self.cy)
235
+ self.y += value - self.cy
194
236
 
195
237
  @property
196
238
  def y2(self) -> float:
@@ -206,7 +248,7 @@ class BoundingBox(SupportsBounds):
206
248
 
207
249
  :param value: new y2 value after transformation
208
250
  """
209
- self.y = value - self.height
251
+ self.y += value - self.y2
210
252
 
211
253
  @property
212
254
  def width(self) -> float:
@@ -214,7 +256,7 @@ class BoundingBox(SupportsBounds):
214
256
 
215
257
  :return: internal _width value transformed by scale
216
258
  """
217
- return self._width * self._scale
259
+ return self._width * self.scale
218
260
 
219
261
  @width.setter
220
262
  def width(self, value: float) -> None:
@@ -227,7 +269,7 @@ class BoundingBox(SupportsBounds):
227
269
  """
228
270
  current_x = self.x
229
271
  current_y = self.y
230
- self._scale *= value / self.width
272
+ self.scale *= value / self.width
231
273
  self.x = current_x
232
274
  self.y = current_y
233
275
 
@@ -237,7 +279,7 @@ class BoundingBox(SupportsBounds):
237
279
 
238
280
  :return: internal _height value transformed by scale
239
281
  """
240
- return self._height * self._scale
282
+ return self._height * self.scale
241
283
 
242
284
  @height.setter
243
285
  def height(self, value: float) -> None:
@@ -250,18 +292,15 @@ class BoundingBox(SupportsBounds):
250
292
  """
251
293
  self.width = value * self.width / self.height
252
294
 
253
- def _add_transform(self, scale: float, translation_x: float, translation_y: float):
295
+ def _add_transform(self, scale: float, dx: float, dy: float):
254
296
  """Transform the bounding box by updating the transformation attributes.
255
297
 
256
298
  :param scale: scale factor
257
- :param translation_x: x translation
258
- :param translation_y: y translation
259
-
260
- Transformation attributes are _translation_x, _translation_y, and _scale
299
+ :param dx: x translation
300
+ :param dy: y translation
261
301
  """
262
- self._translation_x += translation_x / self._scale
263
- self._translation_y += translation_y / self._scale
264
- self._scale *= scale
302
+ tmat = (scale, 0, 0, scale, dx, dy)
303
+ self._transform = mat_dot(tmat, self._transform)
265
304
 
266
305
  @property
267
306
  def transform_string(self) -> str:
@@ -272,11 +311,7 @@ class BoundingBox(SupportsBounds):
272
311
  Use with
273
312
  ``update_element(elem, transform=bbox.transform_string)``
274
313
  """
275
- scale, trans_x, trans_y = (
276
- format_number(x)
277
- for x in (self._scale, self._translation_x, self._translation_y)
278
- )
279
- return f"scale({scale}) translate({trans_x} {trans_y})"
314
+ return f"matrix({' '.join(map(format_number, self._transform))})"
280
315
 
281
316
  def merge(self, *others: BoundingBox) -> BoundingBox:
282
317
  """Create a bounding box around all other bounding boxes.
svg_ultralight/query.py CHANGED
@@ -118,7 +118,8 @@ def map_ids_to_bounding_boxes(
118
118
 
119
119
  id2bbox: dict[str, BoundingBox] = {}
120
120
  for id_, *bounds in (x.split(",") for x in bb_strings):
121
- id2bbox[id_] = BoundingBox(*(float(x) for x in bounds))
121
+ x, y, width, height = (float(x) for x in bounds)
122
+ id2bbox[id_] = BoundingBox(x, y, width, height)
122
123
  return id2bbox
123
124
 
124
125
 
@@ -8,9 +8,8 @@ from __future__ import annotations
8
8
 
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
11
+ from svg_ultralight.bounding_boxes import bound_helpers as bound
12
12
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
13
- from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
14
13
  from svg_ultralight.main import new_svg_root
15
14
 
16
15
  if TYPE_CHECKING:
@@ -64,15 +63,8 @@ def new_svg_root_around_bounds(
64
63
  :return: root svg element
65
64
  :raise ValueError: if no bounding boxes are found in bounded
66
65
  """
67
- bboxes = [x for x in bounded if isinstance(x, BoundingBox)]
68
- bboxes += [x.bbox for x in bounded if isinstance(x, BoundElement)]
69
- bboxes += [x.padded_bbox for x in bounded if isinstance(x, PaddedText)]
70
-
71
- if not bboxes:
72
- msg = "no bounding boxes found"
73
- raise ValueError(msg)
74
-
75
- viewbox = _viewbox_args_from_bboxes(*bboxes)
66
+ bbox = bound.new_bbox_union(*bounded)
67
+ viewbox = _viewbox_args_from_bboxes(bbox)
76
68
  return new_svg_root(
77
69
  x_=viewbox["x_"],
78
70
  y_=viewbox["y_"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: svg-ultralight
3
- Version: 0.26.0
3
+ Version: 0.28.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
@@ -1,4 +1,4 @@
1
- svg_ultralight/__init__.py,sha256=5n-7C9idISTR56-AqZ834DpmYGgPUBdtFK7iIekiCqc,1627
1
+ svg_ultralight/__init__.py,sha256=Xyi-aBOAfVNoUg9rHXYVMELehe1kReEyQYL682AcQ1A,1877
2
2
  svg_ultralight/animate.py,sha256=JSrBm-59BcNXDF0cGgl4-C89eBunjevZnwZxIWt48TU,1112
3
3
  svg_ultralight/inkscape.py,sha256=M8yTxXOu4NlXnhsMycvEJiIDpnDeiZ_bZakJBM38ZoU,9152
4
4
  svg_ultralight/layout.py,sha256=TTETT_8WLBXnQxDGXdAeczCFN5pFo5kKY3Q6zv4FPX4,12238
@@ -6,20 +6,21 @@ svg_ultralight/main.py,sha256=6oNkZfD27UMdP-oYqp5agS_IGcYb8NkUZwM9Zdyb3SA,7287
6
6
  svg_ultralight/metadata.py,sha256=Mxgxrxe1Ar4kp2wTT29aadxgHNFaaNLABo29jStiWDg,4201
7
7
  svg_ultralight/nsmap.py,sha256=y63upO78Rr-JJT56RWWZuyrsILh6HPoY4GhbYnK1A0g,1244
8
8
  svg_ultralight/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- svg_ultralight/query.py,sha256=PFyhR9v60JkuksRb0LVkml67nmOxfZW9eCtL-JoH52Y,7270
10
- svg_ultralight/root_elements.py,sha256=rUc26ec-FLAUgO5iOF3aOeKZC7CjiTw7TGyGAj33yNU,3219
9
+ svg_ultralight/query.py,sha256=_KQuk4IwhVVDgTT0GZm_gbqNUGp7lMEDM32b_CTTSUA,7320
10
+ svg_ultralight/root_elements.py,sha256=pt9J6mPrnoTAZVF6vKTZoM_o947I8UCj6MbGcD2JUCk,2869
11
11
  svg_ultralight/string_conversion.py,sha256=WEmpf75RJmJ2lfJluagAz2wPsz6wM8XvTEwkq4U0vEc,7353
12
12
  svg_ultralight/unit_conversion.py,sha256=g07nhzXdjPvGcJmkhLdFbeDLrSmbI8uFoVgPo7G62Bg,9258
13
13
  svg_ultralight/bounding_boxes/__init__.py,sha256=qUEn3r4s-1QNHaguhWhhaNfdP4tl_B6YEqxtiTFuzhQ,78
14
+ svg_ultralight/bounding_boxes/bound_helpers.py,sha256=D-Qp8yDmn5vIBdaSlBvaWR0F_iRCdotgQqjaRHXBq-8,3397
14
15
  svg_ultralight/bounding_boxes/supports_bounds.py,sha256=1yqmZ7PH1bBH-LTIUDzSvKFXkPLXfJM7jJNz0bXurZ4,3926
15
16
  svg_ultralight/bounding_boxes/type_bound_element.py,sha256=VKiN4UnC2XlPKapWRHxgtqhO4BuVoe6YkLrirlEP09w,5714
16
- svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=OqvS6LhfMwnoE4PUwAmHlY0tjY-2YQZQiLY6O2U3vfs,10378
17
+ svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=8c9aNTVCjyZpHQMW5mFsxTkgOi_2yH7TyGxlmJPxUR0,11444
17
18
  svg_ultralight/bounding_boxes/type_padded_text.py,sha256=FnwHD54qPMlyOB0yhRgZShQWA_riaaTS9GwD9HnbPm4,14119
18
19
  svg_ultralight/constructors/__init__.py,sha256=YcnO0iBQc19aL8Iemw0Y452MBMBIT2AN5nZCnoGxpn0,327
19
20
  svg_ultralight/constructors/new_element.py,sha256=VtMz9sPn9rMk6rui5Poysy3vezlOaS-tGIcGbu-SXmY,3406
20
21
  svg_ultralight/strings/__init__.py,sha256=Zalrf-ThFz7b7xKELx5lb2gOlBgV-6jk_k_EeSdVCVk,295
21
22
  svg_ultralight/strings/svg_strings.py,sha256=RYKMxOHq9abbZyGcFqsElBGLrBX-EjjNxln3s_ibi30,1296
22
- svg_ultralight-0.26.0.dist-info/METADATA,sha256=5SIJniz8QdUQVaARRpewHQv6lkmLnetygXSSnVCNmUg,8871
23
- svg_ultralight-0.26.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
24
- svg_ultralight-0.26.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
25
- svg_ultralight-0.26.0.dist-info/RECORD,,
23
+ svg_ultralight-0.28.0.dist-info/METADATA,sha256=ybepa8hEhsBKXUT_CtnqUoKBh5Eg5nQReYFyQwdNW60,8871
24
+ svg_ultralight-0.28.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
25
+ svg_ultralight-0.28.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
26
+ svg_ultralight-0.28.0.dist-info/RECORD,,