svg-ultralight 0.64.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.
Files changed (35) hide show
  1. svg_ultralight/__init__.py +112 -0
  2. svg_ultralight/animate.py +40 -0
  3. svg_ultralight/attrib_hints.py +14 -0
  4. svg_ultralight/bounding_boxes/__init__.py +5 -0
  5. svg_ultralight/bounding_boxes/bound_helpers.py +200 -0
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +442 -0
  7. svg_ultralight/bounding_boxes/supports_bounds.py +167 -0
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +74 -0
  9. svg_ultralight/bounding_boxes/type_bound_element.py +68 -0
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +432 -0
  11. svg_ultralight/bounding_boxes/type_padded_list.py +208 -0
  12. svg_ultralight/bounding_boxes/type_padded_text.py +502 -0
  13. svg_ultralight/constructors/__init__.py +14 -0
  14. svg_ultralight/constructors/new_element.py +117 -0
  15. svg_ultralight/font_tools/__init__.py +5 -0
  16. svg_ultralight/font_tools/comp_results.py +291 -0
  17. svg_ultralight/font_tools/font_info.py +849 -0
  18. svg_ultralight/image_ops.py +156 -0
  19. svg_ultralight/inkscape.py +261 -0
  20. svg_ultralight/layout.py +291 -0
  21. svg_ultralight/main.py +183 -0
  22. svg_ultralight/metadata.py +122 -0
  23. svg_ultralight/nsmap.py +36 -0
  24. svg_ultralight/py.typed +5 -0
  25. svg_ultralight/query.py +254 -0
  26. svg_ultralight/read_svg.py +58 -0
  27. svg_ultralight/root_elements.py +96 -0
  28. svg_ultralight/string_conversion.py +244 -0
  29. svg_ultralight/strings/__init__.py +21 -0
  30. svg_ultralight/strings/svg_strings.py +106 -0
  31. svg_ultralight/transformations.py +152 -0
  32. svg_ultralight/unit_conversion.py +247 -0
  33. svg_ultralight-0.64.0.dist-info/METADATA +208 -0
  34. svg_ultralight-0.64.0.dist-info/RECORD +35 -0
  35. svg_ultralight-0.64.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,68 @@
1
+ """An element tied to a BoundingBox instance.
2
+
3
+ Take an element, associate it to a BoundingBox instance, transform the BoundingBox
4
+ instance. The element will be transformed accordingly.
5
+
6
+ It is critical to remember that self.elem is a reference. It is not necessary to
7
+ access self.elem through the BoundElement instance. Earlier and later references will
8
+ all be updated as the BoundElement instance is updated.
9
+
10
+ :author: Shay Hill
11
+ :created: 2022-12-09
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
19
+ from svg_ultralight.transformations import new_transformation_matrix, transform_element
20
+
21
+ if TYPE_CHECKING:
22
+ from lxml.etree import (
23
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
24
+ )
25
+
26
+ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
27
+
28
+ _Matrix = tuple[float, float, float, float, float, float]
29
+
30
+
31
+ class BoundElement(HasBoundingBox):
32
+ """An element with a bounding box.
33
+
34
+ Updates the element when x, y, x2, y2, width, or height are set.
35
+
36
+ Can access these BoundingBox attributes (plus scale) as attributes of this object.
37
+ """
38
+
39
+ def __init__(self, element: EtreeElement, bounding_box: BoundingBox) -> None:
40
+ """Initialize a BoundElement instance.
41
+
42
+ :param element: the element to be bound
43
+ :param bounding_box: the bounding box around the element
44
+ """
45
+ self.elem = element
46
+ self.bbox = bounding_box
47
+
48
+ def transform(
49
+ self,
50
+ transformation: _Matrix | None = None,
51
+ *,
52
+ scale: tuple[float, float] | float | None = None,
53
+ dx: float | None = None,
54
+ dy: float | None = None,
55
+ reverse: bool = False,
56
+ ) -> None:
57
+ """Transform the element and bounding box.
58
+
59
+ :param transformation: a 6-tuple transformation matrix
60
+ :param scale: a scaling factor
61
+ :param dx: the x translation
62
+ :param dy: the y translation
63
+ :param reverse: Transform the element as if it were in a <g> element
64
+ transformed by tmat.
65
+ """
66
+ tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
67
+ self.bbox.transform(tmat, reverse=reverse)
68
+ _ = transform_element(self.elem, tmat, reverse=reverse)
@@ -0,0 +1,432 @@
1
+ """Bounding box classes for SVG elements.
2
+
3
+ :author: Shay Hill
4
+ :created: 2022-12-09
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import dataclasses
10
+ import math
11
+
12
+ from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
13
+ from svg_ultralight.strings import svg_matrix
14
+ from svg_ultralight.transformations import mat_apply, mat_dot, new_transformation_matrix
15
+
16
+ _Matrix = tuple[float, float, float, float, float, float]
17
+
18
+
19
+ class HasBoundingBox(SupportsBounds):
20
+ """A parent class for BoundElement and others that have a bbox attribute."""
21
+
22
+ def __init__(self, bbox: BoundingBox) -> None:
23
+ """Initialize the HasBoundingBox instance."""
24
+ self.bbox = bbox
25
+
26
+ def _get_input_corners(
27
+ self,
28
+ ) -> tuple[
29
+ tuple[float, float],
30
+ tuple[float, float],
31
+ tuple[float, float],
32
+ tuple[float, float],
33
+ ]:
34
+ """Get the input corners of the bounding box.
35
+
36
+ :return: four corners counter-clockwise starting at top left
37
+ """
38
+ x = self.bbox.base_x
39
+ y = self.bbox.base_y
40
+ x2 = x + self.bbox.base_width
41
+ y2 = y + self.bbox.base_height
42
+ return (x, y), (x2, y), (x2, y2), (x, y2)
43
+
44
+ def values(self) -> tuple[float, float, float, float]:
45
+ """Get the values of the bounding box.
46
+
47
+ :return: x, y, width, height of the bounding box
48
+ """
49
+ return (
50
+ self.bbox.x,
51
+ self.bbox.y,
52
+ self.bbox.width,
53
+ self.bbox.height,
54
+ )
55
+
56
+ def _get_transformed_corners(
57
+ self,
58
+ ) -> tuple[
59
+ tuple[float, float],
60
+ tuple[float, float],
61
+ tuple[float, float],
62
+ tuple[float, float],
63
+ ]:
64
+ """Get the transformed corners of the bounding box.
65
+
66
+ :return: four corners counter-clockwise starting at top left, transformed by
67
+ self.transformation
68
+ """
69
+ c0, c1, c2, c3 = (
70
+ mat_apply(self.bbox.transformation, c) for c in self._get_input_corners()
71
+ )
72
+ return c0, c1, c2, c3
73
+
74
+ def transform(
75
+ self,
76
+ transformation: _Matrix | None = None,
77
+ *,
78
+ scale: tuple[float, float] | float | None = None,
79
+ dx: float | None = None,
80
+ dy: float | None = None,
81
+ reverse: bool = False,
82
+ ) -> None:
83
+ """Transform the bounding box by updating the transformation attribute.
84
+
85
+ :param transformation: 2D transformation matrix
86
+ :param scale: scale factor
87
+ :param dx: x translation
88
+ :param dy: y translation
89
+ :param reverse: Transform the element as if it were in a <g> element
90
+ transformed by tmat.
91
+
92
+ All parameters are optional. Scale, dx, and dy are optional and applied after
93
+ the transformation matrix if both are given. This shouldn't be necessary in
94
+ most cases, the four parameters are there to allow transformation arguments
95
+ to be passed in a variety of ways. Scale, dx, and dy are the sensible values
96
+ to pass "by hand". The transformation matrix is the sensible argument to pass
97
+ when applying a transformation from another bounding box instance.
98
+ """
99
+ tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
100
+ if reverse:
101
+ self.bbox.transformation = mat_dot(self.bbox.transformation, tmat)
102
+ else:
103
+ self.bbox.transformation = mat_dot(tmat, self.bbox.transformation)
104
+
105
+ @property
106
+ def scale(self) -> tuple[float, float]:
107
+ """Get scale of the bounding box.
108
+
109
+ :return: uniform scale of the bounding box
110
+
111
+ Use caution, the scale attribute can cause errors in intuition. Changing
112
+ width or height will change the scale attribute, but not the x or y values.
113
+ The scale setter, on the other hand, will work in the tradational manner.
114
+ I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
115
+ width*scale, height => height*scale, scale => scale*scale. This matches how
116
+ scale works in almost every other context.
117
+ """
118
+ xx, xy, yx, yy, *_ = self.bbox.transformation
119
+ return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
120
+
121
+ @scale.setter
122
+ def scale(self, value: tuple[float, float]) -> None:
123
+ """Scale the bounding box by a uniform factor.
124
+
125
+ :param value: new scale value
126
+
127
+ Don't miss this! You are setting the scale, not scaling the scale! If you
128
+ have a previously defined scale other than 1, this is probably not what you
129
+ want. Most of the time, you will want to use the *= operator.
130
+
131
+ `scale = 2` -> ignore whatever scale was previously defined and set scale to 2
132
+ `scale *= 2` -> make it twice as big as it was.
133
+ """
134
+ new_scale = value[0] / self.scale[0], value[1] / self.scale[1]
135
+ self.transform(scale=new_scale)
136
+
137
+ @property
138
+ def x(self) -> float:
139
+ """Return x left value of bounding box.
140
+
141
+ :return: internal _x value transformed by scale and translation
142
+ """
143
+ return min(x for x, _ in self._get_transformed_corners())
144
+
145
+ @x.setter
146
+ def x(self, value: float) -> None:
147
+ """Set the x coordinate of the left edge of the bounding box.
148
+
149
+ :param value: the new x coordinate of the left edge of the bounding box
150
+ """
151
+ self.transform(dx=value - self.x)
152
+
153
+ @property
154
+ def cx(self) -> float:
155
+ """Center x value.
156
+
157
+ :return: midpoint of transformed x and x2
158
+ """
159
+ return self.x + self.width / 2
160
+
161
+ @cx.setter
162
+ def cx(self, value: float) -> None:
163
+ """Center x value.
164
+
165
+ :param value: new center x value after transformation
166
+ """
167
+ self.x += value - self.cx
168
+
169
+ @property
170
+ def x2(self) -> float:
171
+ """Return x right value of bounding box.
172
+
173
+ :return: transformed x + transformed width
174
+ """
175
+ return max(x for x, _ in self._get_transformed_corners())
176
+
177
+ @x2.setter
178
+ def x2(self, value: float) -> None:
179
+ """Update transformation values (do not alter self._x2).
180
+
181
+ :param value: new x2 value after transformation
182
+ """
183
+ self.x += value - self.x2
184
+
185
+ @property
186
+ def y(self) -> float:
187
+ """Return y top value of bounding box.
188
+
189
+ :return: internal _y value transformed by scale and translation
190
+ """
191
+ return min(y for _, y in self._get_transformed_corners())
192
+
193
+ @y.setter
194
+ def y(self, value: float) -> None:
195
+ """Update transformation values (do not alter self._y).
196
+
197
+ :param value: new y value after transformation
198
+ """
199
+ self.transform(dy=value - self.y)
200
+
201
+ @property
202
+ def cy(self) -> float:
203
+ """Center y value.
204
+
205
+ :return: midpoint of transformed y and y2
206
+ """
207
+ return self.y + self.height / 2
208
+
209
+ @cy.setter
210
+ def cy(self, value: float) -> None:
211
+ """Center y value.
212
+
213
+ :param value: new center y value after transformation
214
+ """
215
+ self.y += value - self.cy
216
+
217
+ @property
218
+ def y2(self) -> float:
219
+ """Return y bottom value of bounding box.
220
+
221
+ :return: transformed y + transformed height
222
+ """
223
+ return max(y for _, y in self._get_transformed_corners())
224
+
225
+ @y2.setter
226
+ def y2(self, value: float) -> None:
227
+ """Update transformation values (do not alter self._y).
228
+
229
+ :param value: new y2 value after transformation
230
+ """
231
+ self.y += value - self.y2
232
+
233
+ @property
234
+ def width(self) -> float:
235
+ """Width of transformed bounding box.
236
+
237
+ :return: internal _width value transformed by scale
238
+ """
239
+ return self.x2 - self.x
240
+
241
+ @width.setter
242
+ def width(self, value: float) -> None:
243
+ """Update transformation values, Do not alter self._width.
244
+
245
+ :param value: new width value after transformation
246
+
247
+ Here transformed x and y value will be preserved. That is, the bounding box
248
+ is scaled, but still anchored at (transformed) self.x and self.y
249
+ """
250
+ current_x = self.x
251
+ current_y = self.y
252
+ self.transform(scale=value / self.width)
253
+ self.x = current_x
254
+ self.y = current_y
255
+
256
+ @property
257
+ def height(self) -> float:
258
+ """Height of transformed bounding box.
259
+
260
+ :return: internal _height value transformed by scale
261
+ """
262
+ return self.y2 - self.y
263
+
264
+ @height.setter
265
+ def height(self, value: float) -> None:
266
+ """Update transformation values, Do not alter self._height.
267
+
268
+ :param value: new height value after transformation
269
+
270
+ Here transformed x and y value will be preserved. That is, the bounding box
271
+ is scaled, but still anchored at (transformed) self.x and self.y
272
+ """
273
+ self.width = value * self.width / self.height
274
+
275
+ @property
276
+ def transform_string(self) -> str:
277
+ """Transformation property string value for svg element.
278
+
279
+ :return: string value for an svg transformation attribute.
280
+
281
+ Use with
282
+ ``update_element(elem, transform=bbox.transform_string)``
283
+ """
284
+ return svg_matrix(self.bbox.transformation)
285
+
286
+
287
+ @dataclasses.dataclass
288
+ class BoundingBox(HasBoundingBox):
289
+ """Mutable bounding box object for svg_ultralight.
290
+
291
+ :param x: left x value
292
+ :param y: top y value
293
+ :param width: width of the bounding box
294
+ :param height: height of the bounding box
295
+
296
+ The below optional parameter, in addition to the required parameters, captures
297
+ the entire state of a BoundingBox instance. It could be used to make a copy or
298
+ to initialize a transformed box with the same transform_string as another box.
299
+ Under most circumstances, it will not be used.
300
+
301
+ :param transformation: transformation matrix
302
+
303
+ Functions that return a bounding box will return a BoundingBox instance. This
304
+ instance can be transformed (uniform scale and translate only). Transformations
305
+ will be combined and scored to be passed to new_element as a transform value.
306
+
307
+ Define the bbox with x=, y=, width=, height=
308
+
309
+ Transform the BoundingBox by setting these variables. Each time you set x, cx,
310
+ x2, y, cy, y2, width, or height, private transformation value transformation
311
+ will be updated.
312
+
313
+ The ultimate transformation can be accessed through ``.transform_string``.
314
+ So the workflow will look like :
315
+
316
+ 1. Get the bounding box of an svg element
317
+ 2. Update the bounding box x, y, width, and height
318
+ 3. Transform the original svg element with
319
+ update_element(elem, transform=bbox.transform_string)
320
+ 4. The transformed element will lie in the transformed BoundingBox
321
+
322
+ In addition to x, y, width, and height, x2 and y2 can be set to establish the
323
+ right x value or bottom y value.
324
+
325
+ The point of all of this is to simplify stacking and aligning elements. To stack:
326
+
327
+ ```
328
+ elem_a = new_element(*args)
329
+ bbox_a = get_bounding_box(elem_a)
330
+
331
+ elem_b = new_element(*args)
332
+ bbox_b = get_bounding_box(elem_b)
333
+
334
+ # align at same x
335
+ bbox_b.x = bbox_a.x
336
+
337
+ # make the same width
338
+ bbox_b.width = bbox_a.width
339
+
340
+ # stack a on top of b
341
+ bbox_a.y2 = bbox_b.y
342
+
343
+ update_element(elem_a, transform=bbox_a.transform_string)
344
+ update_element(elem_b, transform=bbox_b.transform_string)
345
+ """
346
+
347
+ base_x: float = dataclasses.field(init=False)
348
+ base_y: float = dataclasses.field(init=False)
349
+ base_width: float = dataclasses.field(init=False)
350
+ base_height: float = dataclasses.field(init=False)
351
+ transformation: _Matrix = dataclasses.field(init=False)
352
+
353
+ def __init__(
354
+ self,
355
+ x: float,
356
+ y: float,
357
+ width: float,
358
+ height: float,
359
+ transformation: _Matrix = (1, 0, 0, 1, 0, 0),
360
+ ) -> None:
361
+ """Initialize a BoundingBox instance.
362
+
363
+ :param x: left x value
364
+ :param y: top y value
365
+ :param width: width of the bounding box
366
+ :param height: height of the bounding box
367
+ """
368
+ self.base_x = x
369
+ self.base_y = y
370
+ self.base_width = width
371
+ self.base_height = height
372
+ self.transformation = transformation
373
+ self.bbox = self
374
+
375
+ def join(self, *others: BoundingBox) -> BoundingBox:
376
+ """Create a bounding box around all other bounding boxes.
377
+
378
+ :param others: one or more bounding boxes to merge with self
379
+ :return: a bounding box around self and other bounding boxes
380
+ :raises DeprecationWarning:
381
+ """
382
+ return BoundingBox.union(self, *others)
383
+
384
+ def intersect(self, *bboxes: SupportsBounds) -> BoundingBox | None:
385
+ """Create a bounding box around the intersection of all other bounding boxes.
386
+
387
+ :param bboxes: one or more bounding boxes to intersect with self
388
+ :return: a bounding box around the intersection of self and other bounding
389
+ boxes, or None if there is no intersection
390
+ """
391
+ return BoundingBox.intersection(self, *bboxes)
392
+
393
+ @classmethod
394
+ def union(cls, *bboxes: SupportsBounds) -> BoundingBox:
395
+ """Create a bounding box around all other bounding boxes.
396
+
397
+ :param bboxes: one or more bounding boxes
398
+ :return: a bounding box encompasing all bboxes args
399
+ :raises ValueError: if no bboxes are given
400
+ """
401
+ if not bboxes:
402
+ msg = "At least one bounding box is required"
403
+ raise ValueError(msg)
404
+ xs = [*(x.x for x in bboxes), *(x.x2 for x in bboxes)]
405
+ ys = [*(x.y for x in bboxes), *(x.y2 for x in bboxes)]
406
+ min_x, max_x = min(xs), max(xs)
407
+ min_y, max_y = min(ys), max(ys)
408
+ return BoundingBox(min_x, min_y, max_x - min_x, max_y - min_y)
409
+
410
+ @classmethod
411
+ def intersection(cls, *bboxes: SupportsBounds) -> BoundingBox | None:
412
+ """Create a bounding box around the intersection of all other bounding boxes.
413
+
414
+ :param bboxes: one or more bounding boxes
415
+ :return: a bounding box around the intersection of all bboxes, or None if
416
+ there is no intersection
417
+ :raises ValueError: if no bboxes are given
418
+ """
419
+ if not bboxes:
420
+ return None
421
+ valid_bboxes: list[BoundingBox] = []
422
+ for bbox in bboxes:
423
+ x, x2 = sorted([bbox.x, bbox.x2])
424
+ y, y2 = sorted([bbox.y, bbox.y2])
425
+ valid_bboxes.append(BoundingBox(x, y, x2 - x, y2 - y))
426
+ x = max(x.x for x in valid_bboxes)
427
+ x2 = min(x.x2 for x in valid_bboxes)
428
+ y = max(x.y for x in valid_bboxes)
429
+ y2 = min(x.y2 for x in valid_bboxes)
430
+ if x > x2 or y > y2:
431
+ return None
432
+ return BoundingBox(x, y, x2 - x, y2 - y)