svg-ultralight 0.28.0__py3-none-any.whl → 0.30.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.
- svg_ultralight/__init__.py +12 -0
- svg_ultralight/animate.py +2 -2
- svg_ultralight/bounding_boxes/bound_helpers.py +2 -0
- svg_ultralight/bounding_boxes/supports_bounds.py +22 -0
- svg_ultralight/bounding_boxes/type_bound_collection.py +71 -0
- svg_ultralight/bounding_boxes/type_bound_confederation.py +71 -0
- svg_ultralight/bounding_boxes/type_bound_element.py +23 -153
- svg_ultralight/bounding_boxes/type_bounding_box.py +228 -70
- svg_ultralight/bounding_boxes/type_padded_text.py +32 -8
- svg_ultralight/transformations.py +108 -0
- {svg_ultralight-0.28.0.dist-info → svg_ultralight-0.30.0.dist-info}/METADATA +1 -1
- {svg_ultralight-0.28.0.dist-info → svg_ultralight-0.30.0.dist-info}/RECORD +14 -11
- {svg_ultralight-0.28.0.dist-info → svg_ultralight-0.30.0.dist-info}/WHEEL +0 -0
- {svg_ultralight-0.28.0.dist-info → svg_ultralight-0.30.0.dist-info}/top_level.txt +0 -0
svg_ultralight/__init__.py
CHANGED
|
@@ -10,6 +10,11 @@ from svg_ultralight.bounding_boxes.bound_helpers import (
|
|
|
10
10
|
new_element_union,
|
|
11
11
|
)
|
|
12
12
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
13
|
+
<<<<<<< HEAD
|
|
14
|
+
from svg_ultralight.bounding_boxes.type_bound_confederation import BoundConfederation
|
|
15
|
+
=======
|
|
16
|
+
from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
|
|
17
|
+
>>>>>>> dev
|
|
13
18
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
14
19
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
15
20
|
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
@@ -37,8 +42,14 @@ from svg_ultralight.string_conversion import (
|
|
|
37
42
|
format_numbers,
|
|
38
43
|
format_numbers_in_string,
|
|
39
44
|
)
|
|
45
|
+
from svg_ultralight.transformations import transform_element
|
|
40
46
|
|
|
41
47
|
__all__ = [
|
|
48
|
+
<<<<<<< HEAD
|
|
49
|
+
"BoundConfederation",
|
|
50
|
+
=======
|
|
51
|
+
"BoundCollection",
|
|
52
|
+
>>>>>>> dev
|
|
42
53
|
"BoundElement",
|
|
43
54
|
"BoundingBox",
|
|
44
55
|
"NSMAP",
|
|
@@ -59,6 +70,7 @@ __all__ = [
|
|
|
59
70
|
"new_svg_root",
|
|
60
71
|
"new_svg_root_around_bounds",
|
|
61
72
|
"pad_text",
|
|
73
|
+
"transform_element",
|
|
62
74
|
"update_element",
|
|
63
75
|
"write_pdf",
|
|
64
76
|
"write_pdf_from_svg",
|
svg_ultralight/animate.py
CHANGED
|
@@ -34,7 +34,7 @@ def write_gif(
|
|
|
34
34
|
:param loop: how many times to loop gif. 0 -> forever
|
|
35
35
|
:effects: write file to gif
|
|
36
36
|
"""
|
|
37
|
-
images = [Image.open(x) for x in pngs]
|
|
38
|
-
images[0].save(
|
|
37
|
+
images = [Image.open(x) for x in pngs] # type: ignore
|
|
38
|
+
images[0].save( # type: ignore
|
|
39
39
|
gif, save_all=True, append_images=images[1:], duration=duration, loop=loop
|
|
40
40
|
)
|
|
@@ -18,6 +18,8 @@ from svg_ultralight.constructors import new_element
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
20
20
|
|
|
21
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def new_element_union(
|
|
23
25
|
*elems: EtreeElement | SupportsBounds, **attributes: float | str
|
|
@@ -16,13 +16,19 @@ Attributes:
|
|
|
16
16
|
:created: 2023-02-15
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
19
21
|
from typing import Protocol
|
|
20
22
|
|
|
23
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
class SupportsBounds(Protocol):
|
|
23
27
|
"""Protocol for objects that can have bounds.
|
|
24
28
|
|
|
25
29
|
Attributes:
|
|
30
|
+
transformation (_Matrix): An svg-style transformation matrix.
|
|
31
|
+
transform (method): Apply a transformation to the object.
|
|
26
32
|
x (float): The minimum x coordinate.
|
|
27
33
|
x2 (float): The maximum x coordinate.
|
|
28
34
|
cx (float): The center x coordinate.
|
|
@@ -39,6 +45,22 @@ class SupportsBounds(Protocol):
|
|
|
39
45
|
set width and height.
|
|
40
46
|
"""
|
|
41
47
|
|
|
48
|
+
@property
|
|
49
|
+
def transformation(self) -> _Matrix:
|
|
50
|
+
"""Return an svg-style transformation matrix."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def transform(
|
|
54
|
+
self,
|
|
55
|
+
transformation: _Matrix | None = None,
|
|
56
|
+
*,
|
|
57
|
+
scale: float | None = None,
|
|
58
|
+
dx: float | None = None,
|
|
59
|
+
dy: float | None = None,
|
|
60
|
+
):
|
|
61
|
+
"""Apply a transformation to the object."""
|
|
62
|
+
...
|
|
63
|
+
|
|
42
64
|
@property
|
|
43
65
|
def x(self) -> float:
|
|
44
66
|
"""Return minimum x coordinate."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2024-05-05
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
13
|
+
|
|
14
|
+
from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_union
|
|
15
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
|
|
16
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
20
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
21
|
+
|
|
22
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class BoundCollection(HasBoundingBox):
|
|
27
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
28
|
+
|
|
29
|
+
This will transform the individual elements in place.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
blems: list[SupportsBounds | EtreeElement] = dataclasses.field(init=False)
|
|
33
|
+
bbox: BoundingBox = dataclasses.field(init=False)
|
|
34
|
+
|
|
35
|
+
def __init__(self, *blems: SupportsBounds | EtreeElement) -> None:
|
|
36
|
+
"""Initialize the bound collection.
|
|
37
|
+
|
|
38
|
+
:param blems: bound elements to be transformed together
|
|
39
|
+
"""
|
|
40
|
+
self.blems = list(blems)
|
|
41
|
+
self.bbox = new_bbox_union(*self.blems)
|
|
42
|
+
|
|
43
|
+
def transform(
|
|
44
|
+
self,
|
|
45
|
+
transformation: _Matrix | None = None,
|
|
46
|
+
*,
|
|
47
|
+
scale: float | None = None,
|
|
48
|
+
dx: float | None = None,
|
|
49
|
+
dy: float | None = None,
|
|
50
|
+
):
|
|
51
|
+
"""Transform each bound element in self.blems.
|
|
52
|
+
|
|
53
|
+
:param transformation: 2D transformation matrix
|
|
54
|
+
:param scale: optional scale factor
|
|
55
|
+
:param dx: optional x translation
|
|
56
|
+
:param dy: optional y translation
|
|
57
|
+
|
|
58
|
+
Keep track of all compounding transformations in order to have a value for
|
|
59
|
+
self.scale (required for membersh and to provide access to cumulative
|
|
60
|
+
transforms should this be useful for any reason. This means all
|
|
61
|
+
transformations must be applied to two bounding boxes: a persistant bbox to
|
|
62
|
+
keep track of the scale property and a temporary bbox to isolate each
|
|
63
|
+
transformation.
|
|
64
|
+
"""
|
|
65
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
66
|
+
self.bbox.transform(tmat)
|
|
67
|
+
for blem in self.blems:
|
|
68
|
+
if isinstance(blem, EtreeElement):
|
|
69
|
+
_ = transform_element(blem, tmat)
|
|
70
|
+
else:
|
|
71
|
+
blem.transform(tmat)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2024-05-05
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
13
|
+
|
|
14
|
+
from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_union
|
|
15
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
16
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
|
|
17
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
21
|
+
|
|
22
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class BoundConfederation(HasBoundingBox):
|
|
27
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
28
|
+
|
|
29
|
+
This will transform the individual elements in place.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
blems: list[SupportsBounds | EtreeElement] = dataclasses.field(init=False)
|
|
33
|
+
bbox: BoundingBox = dataclasses.field(init=False)
|
|
34
|
+
|
|
35
|
+
def __init__(self, *blems: SupportsBounds | EtreeElement) -> None:
|
|
36
|
+
"""Initialize the bound confederation.
|
|
37
|
+
|
|
38
|
+
:param blems: bound elements to be transformed together
|
|
39
|
+
"""
|
|
40
|
+
self.blems = list(blems)
|
|
41
|
+
self.bbox = new_bbox_union(*self.blems)
|
|
42
|
+
|
|
43
|
+
def transform(
|
|
44
|
+
self,
|
|
45
|
+
transformation: _Matrix | None = None,
|
|
46
|
+
*,
|
|
47
|
+
scale: float | None = None,
|
|
48
|
+
dx: float | None = None,
|
|
49
|
+
dy: float | None = None,
|
|
50
|
+
):
|
|
51
|
+
"""Transform each bound element in self.blems.
|
|
52
|
+
|
|
53
|
+
:param transformation: 2D transformation matrix
|
|
54
|
+
:param scale: optional scale factor
|
|
55
|
+
:param dx: optional x translation
|
|
56
|
+
:param dy: optional y translation
|
|
57
|
+
|
|
58
|
+
Keep track of all compounding transformations in order to have a value for
|
|
59
|
+
self.scale (required for membersh and to provide access to cumulative
|
|
60
|
+
transforms should this be useful for any reason. This means all
|
|
61
|
+
transformations must be applied to two bounding boxes: a persistant bbox to
|
|
62
|
+
keep track of the scale property and a temporary bbox to isolate each
|
|
63
|
+
transformation.
|
|
64
|
+
"""
|
|
65
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
66
|
+
self.bbox.transform(tmat)
|
|
67
|
+
for blem in self.blems:
|
|
68
|
+
if isinstance(blem, EtreeElement):
|
|
69
|
+
_ = transform_element(blem, tmat)
|
|
70
|
+
else:
|
|
71
|
+
blem.transform(tmat)
|
|
@@ -15,15 +15,18 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
from typing import TYPE_CHECKING
|
|
17
17
|
|
|
18
|
-
from svg_ultralight.bounding_boxes.
|
|
18
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
|
|
19
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
22
23
|
|
|
23
24
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
24
25
|
|
|
26
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
class BoundElement(HasBoundingBox):
|
|
27
30
|
"""An element with a bounding box.
|
|
28
31
|
|
|
29
32
|
Updates the element when x, y, x2, y2, width, or height are set.
|
|
@@ -43,154 +46,21 @@ class BoundElement(SupportsBounds):
|
|
|
43
46
|
def _update_elem(self):
|
|
44
47
|
self.elem.attrib["transform"] = self.bbox.transform_string
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
:param
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def x2(self) -> float:
|
|
65
|
-
"""The x coordinate of the right edge of the bounding box.
|
|
66
|
-
|
|
67
|
-
:return: the x coordinate of the right edge of the bounding box
|
|
68
|
-
"""
|
|
69
|
-
return self.bbox.x2
|
|
70
|
-
|
|
71
|
-
@x2.setter
|
|
72
|
-
def x2(self, value: float):
|
|
73
|
-
"""Set the x coordinate of the right edge of the bounding box.
|
|
74
|
-
|
|
75
|
-
:param value: the new x coordinate of the right edge of the bounding box
|
|
76
|
-
"""
|
|
77
|
-
self.bbox.x2 = value
|
|
78
|
-
self._update_elem()
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def cx(self) -> float:
|
|
82
|
-
"""The x coordinate of the center of the bounding box.
|
|
83
|
-
|
|
84
|
-
:return: the x coordinate of the center of the bounding box
|
|
85
|
-
"""
|
|
86
|
-
return self.bbox.cx
|
|
87
|
-
|
|
88
|
-
@cx.setter
|
|
89
|
-
def cx(self, value: float):
|
|
90
|
-
"""Set the x coordinate of the center of the bounding box.
|
|
91
|
-
|
|
92
|
-
:param value: the new x coordinate of the center of the bounding box
|
|
93
|
-
"""
|
|
94
|
-
self.bbox.cx = value
|
|
95
|
-
self._update_elem()
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def y(self) -> float:
|
|
99
|
-
"""The y coordinate of the top edge of the bounding box.
|
|
100
|
-
|
|
101
|
-
:return: the y coordinate of the top edge of the bounding box
|
|
102
|
-
"""
|
|
103
|
-
return self.bbox.y
|
|
104
|
-
|
|
105
|
-
@y.setter
|
|
106
|
-
def y(self, value: float):
|
|
107
|
-
"""Set the y coordinate of the top edge of the bounding box.
|
|
108
|
-
|
|
109
|
-
:param value: the new y coordinate of the top edge of the bounding box
|
|
110
|
-
"""
|
|
111
|
-
self.bbox.y = value
|
|
112
|
-
self._update_elem()
|
|
113
|
-
|
|
114
|
-
@property
|
|
115
|
-
def y2(self) -> float:
|
|
116
|
-
"""The y coordinate of the bottom edge of the bounding box.
|
|
117
|
-
|
|
118
|
-
:return: the y coordinate of the bottom edge of the bounding box
|
|
119
|
-
"""
|
|
120
|
-
return self.bbox.y2
|
|
121
|
-
|
|
122
|
-
@y2.setter
|
|
123
|
-
def y2(self, value: float):
|
|
124
|
-
"""Set the y coordinate of the bottom edge of the bounding box.
|
|
125
|
-
|
|
126
|
-
:param value: the new y coordinate of the bottom edge of the bounding box
|
|
127
|
-
"""
|
|
128
|
-
self.bbox.y2 = value
|
|
129
|
-
self._update_elem()
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def cy(self) -> float:
|
|
133
|
-
"""The y coordinate of the center of the bounding box.
|
|
134
|
-
|
|
135
|
-
:return: the y coordinate of the center of the bounding box
|
|
136
|
-
"""
|
|
137
|
-
return self.bbox.cy
|
|
138
|
-
|
|
139
|
-
@cy.setter
|
|
140
|
-
def cy(self, value: float):
|
|
141
|
-
"""Set the y coordinate of the center of the bounding box.
|
|
142
|
-
|
|
143
|
-
:param value: the new y coordinate of the center of the bounding box
|
|
144
|
-
"""
|
|
145
|
-
self.bbox.cy = value
|
|
146
|
-
self._update_elem()
|
|
147
|
-
|
|
148
|
-
@property
|
|
149
|
-
def width(self) -> float:
|
|
150
|
-
"""The width of the bounding box.
|
|
151
|
-
|
|
152
|
-
:return: the width of the bounding box
|
|
153
|
-
"""
|
|
154
|
-
return self.bbox.width
|
|
155
|
-
|
|
156
|
-
@width.setter
|
|
157
|
-
def width(self, value: float):
|
|
158
|
-
"""Set the width of the bounding box.
|
|
159
|
-
|
|
160
|
-
:param value: the new width of the bounding box
|
|
161
|
-
"""
|
|
162
|
-
self.bbox.width = value
|
|
163
|
-
self._update_elem()
|
|
164
|
-
|
|
165
|
-
@property
|
|
166
|
-
def height(self) -> float:
|
|
167
|
-
"""The height of the bounding box.
|
|
168
|
-
|
|
169
|
-
:return: the height of the bounding box
|
|
170
|
-
"""
|
|
171
|
-
return self.bbox.height
|
|
172
|
-
|
|
173
|
-
@height.setter
|
|
174
|
-
def height(self, value: float):
|
|
175
|
-
"""Set the height of the bounding box.
|
|
176
|
-
|
|
177
|
-
:param value: the new height of the bounding box
|
|
178
|
-
"""
|
|
179
|
-
self.bbox.height = value
|
|
180
|
-
self._update_elem()
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def scale(self) -> float:
|
|
184
|
-
"""The scale of the bounding box.
|
|
185
|
-
|
|
186
|
-
:return: the scale of the bounding box
|
|
187
|
-
"""
|
|
188
|
-
return self.bbox.scale
|
|
189
|
-
|
|
190
|
-
@scale.setter
|
|
191
|
-
def scale(self, value: float):
|
|
192
|
-
"""Set the scale of the bounding box.
|
|
193
|
-
|
|
194
|
-
:param value: the scale of the bounding box
|
|
195
|
-
"""
|
|
196
|
-
self.bbox.scale = value
|
|
49
|
+
def transform(
|
|
50
|
+
self,
|
|
51
|
+
transformation: _Matrix | None = None,
|
|
52
|
+
*,
|
|
53
|
+
scale: float | None = None,
|
|
54
|
+
dx: float | None = None,
|
|
55
|
+
dy: float | None = None,
|
|
56
|
+
):
|
|
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
|
+
"""
|
|
64
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
65
|
+
self.bbox.transform(tmat)
|
|
66
|
+
_ = transform_element(self.elem, tmat)
|
|
@@ -10,44 +10,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
|
+
from svg_ultralight.transformations import mat_apply, mat_dot, new_transformation_matrix
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
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]
|
|
15
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
51
16
|
|
|
52
17
|
|
|
53
18
|
@dataclasses.dataclass
|
|
@@ -64,7 +29,7 @@ class BoundingBox(SupportsBounds):
|
|
|
64
29
|
to initialize a transformed box with the same transform_string as another box.
|
|
65
30
|
Under most circumstances, it will not be used.
|
|
66
31
|
|
|
67
|
-
:param
|
|
32
|
+
:param _transformation: transformation matrix
|
|
68
33
|
|
|
69
34
|
Functions that return a bounding box will return a BoundingBox instance. This
|
|
70
35
|
instance can be transformed (uniform scale and translate only). Transformations
|
|
@@ -73,10 +38,10 @@ class BoundingBox(SupportsBounds):
|
|
|
73
38
|
Define the bbox with x=, y=, width=, height=
|
|
74
39
|
|
|
75
40
|
Transform the BoundingBox by setting these variables. Each time you set x, cx,
|
|
76
|
-
x2, y, cy, y2, width, or height, private transformation value
|
|
77
|
-
updated.
|
|
41
|
+
x2, y, cy, y2, width, or height, private transformation value _transformation
|
|
42
|
+
will be updated.
|
|
78
43
|
|
|
79
|
-
The ultimate transformation can be accessed through ``.
|
|
44
|
+
The ultimate transformation can be accessed through ``.transform_string``.
|
|
80
45
|
So the workflow will look like :
|
|
81
46
|
|
|
82
47
|
1. Get the bounding box of an svg element
|
|
@@ -114,15 +79,40 @@ class BoundingBox(SupportsBounds):
|
|
|
114
79
|
_y: float
|
|
115
80
|
_width: float
|
|
116
81
|
_height: float
|
|
117
|
-
|
|
82
|
+
_transformation: _Matrix = (1, 0, 0, 1, 0, 0)
|
|
118
83
|
|
|
119
84
|
@property
|
|
120
|
-
def
|
|
121
|
-
"""
|
|
85
|
+
def transformation(self) -> _Matrix:
|
|
86
|
+
"""Return transformation matrix.
|
|
122
87
|
|
|
123
|
-
:return: transformation matrix
|
|
88
|
+
:return: transformation matrix
|
|
124
89
|
"""
|
|
125
|
-
return self.
|
|
90
|
+
return self._transformation
|
|
91
|
+
|
|
92
|
+
def transform(
|
|
93
|
+
self,
|
|
94
|
+
transformation: _Matrix | None = None,
|
|
95
|
+
*,
|
|
96
|
+
scale: float | None = None,
|
|
97
|
+
dx: float | None = None,
|
|
98
|
+
dy: float | None = None,
|
|
99
|
+
):
|
|
100
|
+
"""Transform the bounding box by updating the transformation attribute.
|
|
101
|
+
|
|
102
|
+
:param transformation: 2D transformation matrix
|
|
103
|
+
:param scale: scale factor
|
|
104
|
+
:param dx: x translation
|
|
105
|
+
:param dy: y translation
|
|
106
|
+
|
|
107
|
+
All parameters are optional. Scale, dx, and dy are optional and applied after
|
|
108
|
+
the transformation matrix if both are given. This shouldn't be necessary in
|
|
109
|
+
most cases, the four parameters are there to allow transformation arguments
|
|
110
|
+
to be passed in a variety of ways. Scale, dx, and dy are the sensible values
|
|
111
|
+
to pass "by hand". The transformation matrix is the sensible argument to pass
|
|
112
|
+
when applying a transformation from another bounding box instance.
|
|
113
|
+
"""
|
|
114
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
115
|
+
self._transformation = mat_dot(tmat, self.transformation)
|
|
126
116
|
|
|
127
117
|
@property
|
|
128
118
|
def scale(self) -> float:
|
|
@@ -137,7 +127,7 @@ class BoundingBox(SupportsBounds):
|
|
|
137
127
|
width*scale, height => height*scale, scale => scale*scale. This matches how
|
|
138
128
|
scale works in almost every other context.
|
|
139
129
|
"""
|
|
140
|
-
return self.
|
|
130
|
+
return self.transformation[0]
|
|
141
131
|
|
|
142
132
|
@scale.setter
|
|
143
133
|
def scale(self, value: float) -> None:
|
|
@@ -152,7 +142,7 @@ class BoundingBox(SupportsBounds):
|
|
|
152
142
|
`scale = 2` -> ignore whatever scale was previously defined and set scale to 2
|
|
153
143
|
`scale *= 2` -> make it twice as big as it was.
|
|
154
144
|
"""
|
|
155
|
-
self.
|
|
145
|
+
self.transform(scale=value / self.scale)
|
|
156
146
|
|
|
157
147
|
@property
|
|
158
148
|
def x(self) -> float:
|
|
@@ -160,15 +150,15 @@ class BoundingBox(SupportsBounds):
|
|
|
160
150
|
|
|
161
151
|
:return: internal _x value transformed by scale and translation
|
|
162
152
|
"""
|
|
163
|
-
return mat_apply(self.
|
|
153
|
+
return mat_apply(self.transformation, (self._x, 0))[0]
|
|
164
154
|
|
|
165
155
|
@x.setter
|
|
166
156
|
def x(self, value: float) -> None:
|
|
167
|
-
"""Update
|
|
157
|
+
"""Update transformation values (do not alter self._x).
|
|
168
158
|
|
|
169
159
|
:param value: new x value after transformation
|
|
170
160
|
"""
|
|
171
|
-
self.
|
|
161
|
+
self.transform(dx=value - self.x)
|
|
172
162
|
|
|
173
163
|
@property
|
|
174
164
|
def cx(self) -> float:
|
|
@@ -196,7 +186,7 @@ class BoundingBox(SupportsBounds):
|
|
|
196
186
|
|
|
197
187
|
@x2.setter
|
|
198
188
|
def x2(self, value: float) -> None:
|
|
199
|
-
"""Update
|
|
189
|
+
"""Update transformation values (do not alter self._x2).
|
|
200
190
|
|
|
201
191
|
:param value: new x2 value after transformation
|
|
202
192
|
"""
|
|
@@ -208,15 +198,15 @@ class BoundingBox(SupportsBounds):
|
|
|
208
198
|
|
|
209
199
|
:return: internal _y value transformed by scale and translation
|
|
210
200
|
"""
|
|
211
|
-
return mat_apply(self.
|
|
201
|
+
return mat_apply(self.transformation, (0, self._y))[1]
|
|
212
202
|
|
|
213
203
|
@y.setter
|
|
214
204
|
def y(self, value: float) -> None:
|
|
215
|
-
"""Update
|
|
205
|
+
"""Update transformation values (do not alter self._y).
|
|
216
206
|
|
|
217
207
|
:param value: new y value after transformation
|
|
218
208
|
"""
|
|
219
|
-
self.
|
|
209
|
+
self.transform(dy=value - self.y)
|
|
220
210
|
|
|
221
211
|
@property
|
|
222
212
|
def cy(self) -> float:
|
|
@@ -244,7 +234,7 @@ class BoundingBox(SupportsBounds):
|
|
|
244
234
|
|
|
245
235
|
@y2.setter
|
|
246
236
|
def y2(self, value: float) -> None:
|
|
247
|
-
"""Update
|
|
237
|
+
"""Update transformation values (do not alter self._y).
|
|
248
238
|
|
|
249
239
|
:param value: new y2 value after transformation
|
|
250
240
|
"""
|
|
@@ -260,7 +250,7 @@ class BoundingBox(SupportsBounds):
|
|
|
260
250
|
|
|
261
251
|
@width.setter
|
|
262
252
|
def width(self, value: float) -> None:
|
|
263
|
-
"""Update
|
|
253
|
+
"""Update transformation values, Do not alter self._width.
|
|
264
254
|
|
|
265
255
|
:param value: new width value after transformation
|
|
266
256
|
|
|
@@ -283,7 +273,7 @@ class BoundingBox(SupportsBounds):
|
|
|
283
273
|
|
|
284
274
|
@height.setter
|
|
285
275
|
def height(self, value: float) -> None:
|
|
286
|
-
"""Update
|
|
276
|
+
"""Update transformation values, Do not alter self._height.
|
|
287
277
|
|
|
288
278
|
:param value: new height value after transformation
|
|
289
279
|
|
|
@@ -292,26 +282,16 @@ class BoundingBox(SupportsBounds):
|
|
|
292
282
|
"""
|
|
293
283
|
self.width = value * self.width / self.height
|
|
294
284
|
|
|
295
|
-
def _add_transform(self, scale: float, dx: float, dy: float):
|
|
296
|
-
"""Transform the bounding box by updating the transformation attributes.
|
|
297
|
-
|
|
298
|
-
:param scale: scale factor
|
|
299
|
-
:param dx: x translation
|
|
300
|
-
:param dy: y translation
|
|
301
|
-
"""
|
|
302
|
-
tmat = (scale, 0, 0, scale, dx, dy)
|
|
303
|
-
self._transform = mat_dot(tmat, self._transform)
|
|
304
|
-
|
|
305
285
|
@property
|
|
306
286
|
def transform_string(self) -> str:
|
|
307
287
|
"""Transformation property string value for svg element.
|
|
308
288
|
|
|
309
|
-
:return: string value for an svg
|
|
289
|
+
:return: string value for an svg transformation attribute.
|
|
310
290
|
|
|
311
291
|
Use with
|
|
312
292
|
``update_element(elem, transform=bbox.transform_string)``
|
|
313
293
|
"""
|
|
314
|
-
return f"matrix({' '.join(map(format_number, self.
|
|
294
|
+
return f"matrix({' '.join(map(format_number, self.transformation))})"
|
|
315
295
|
|
|
316
296
|
def merge(self, *others: BoundingBox) -> BoundingBox:
|
|
317
297
|
"""Create a bounding box around all other bounding boxes.
|
|
@@ -341,3 +321,181 @@ class BoundingBox(SupportsBounds):
|
|
|
341
321
|
min_y = min(x.y for x in bboxes)
|
|
342
322
|
max_y = max(x.y + x.height for x in bboxes)
|
|
343
323
|
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
|
|
@@ -72,6 +72,8 @@ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
|
72
72
|
if TYPE_CHECKING:
|
|
73
73
|
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
74
74
|
|
|
75
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
76
|
+
|
|
75
77
|
|
|
76
78
|
class PaddedText(SupportsBounds):
|
|
77
79
|
"""A line of text with a bounding box and padding."""
|
|
@@ -116,11 +118,32 @@ class PaddedText(SupportsBounds):
|
|
|
116
118
|
self.lmargin, self.capline, self.padded_width, self.padded_height
|
|
117
119
|
)
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
@property
|
|
122
|
+
def transformation(self) -> _Matrix:
|
|
123
|
+
"""The transformation matrix of the bounding box."""
|
|
124
|
+
return self.bbox.transformation
|
|
125
|
+
|
|
126
|
+
def _update_elem(self):
|
|
122
127
|
self.elem.attrib["transform"] = self.bbox.transform_string
|
|
123
128
|
|
|
129
|
+
def transform(
|
|
130
|
+
self,
|
|
131
|
+
transformation: _Matrix | None = None,
|
|
132
|
+
*,
|
|
133
|
+
scale: float | None = None,
|
|
134
|
+
dx: float | None = None,
|
|
135
|
+
dy: float | None = None,
|
|
136
|
+
):
|
|
137
|
+
"""Transform the element and bounding box.
|
|
138
|
+
|
|
139
|
+
:param transformation: a 6-tuple transformation matrix
|
|
140
|
+
:param scale: a scaling factor
|
|
141
|
+
:param dx: the x translation
|
|
142
|
+
:param dy: the y translation
|
|
143
|
+
"""
|
|
144
|
+
self.bbox.transform(transformation, scale=scale, dx=dx, dy=dy)
|
|
145
|
+
self._update_elem()
|
|
146
|
+
|
|
124
147
|
@property
|
|
125
148
|
def tpad(self) -> float:
|
|
126
149
|
"""The top padding of this line of text.
|
|
@@ -167,7 +190,7 @@ class PaddedText(SupportsBounds):
|
|
|
167
190
|
|
|
168
191
|
:param value: The left margin of this line of text.
|
|
169
192
|
"""
|
|
170
|
-
self.
|
|
193
|
+
self.transform(dx=value + self.lpad - self.bbox.x)
|
|
171
194
|
|
|
172
195
|
@property
|
|
173
196
|
def rmargin(self) -> float:
|
|
@@ -183,7 +206,7 @@ class PaddedText(SupportsBounds):
|
|
|
183
206
|
|
|
184
207
|
:param value: The right margin of this line of text.
|
|
185
208
|
"""
|
|
186
|
-
self.
|
|
209
|
+
self.transform(dx=value - self.rpad - self.bbox.x2)
|
|
187
210
|
|
|
188
211
|
@property
|
|
189
212
|
def capline(self) -> float:
|
|
@@ -199,7 +222,7 @@ class PaddedText(SupportsBounds):
|
|
|
199
222
|
|
|
200
223
|
:param value: The top of this line of text.
|
|
201
224
|
"""
|
|
202
|
-
self.
|
|
225
|
+
self.transform(dy=value + self.tpad - self.bbox.y)
|
|
203
226
|
|
|
204
227
|
@property
|
|
205
228
|
def baseline(self) -> float:
|
|
@@ -215,7 +238,7 @@ class PaddedText(SupportsBounds):
|
|
|
215
238
|
|
|
216
239
|
:param value: The bottom of this line of text.
|
|
217
240
|
"""
|
|
218
|
-
self.
|
|
241
|
+
self.transform(dy=value - self.bpad - self.bbox.y2)
|
|
219
242
|
|
|
220
243
|
@property
|
|
221
244
|
def padded_width(self) -> float:
|
|
@@ -239,8 +262,9 @@ class PaddedText(SupportsBounds):
|
|
|
239
262
|
*and* y2) when scaling.
|
|
240
263
|
"""
|
|
241
264
|
baseline = self.baseline
|
|
242
|
-
self.
|
|
265
|
+
self.bbox.width = width - self.lpad - self.rpad
|
|
243
266
|
self.baseline = baseline
|
|
267
|
+
self._update_elem()
|
|
244
268
|
|
|
245
269
|
@property
|
|
246
270
|
def padded_height(self) -> float:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Math and conversion for svg-style transformation matrices.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2024-05-05
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from contextlib import suppress
|
|
11
|
+
from typing import TYPE_CHECKING, cast
|
|
12
|
+
|
|
13
|
+
from svg_ultralight.string_conversion import format_number
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_RE_MATRIX = re.compile(r"matrix\(([^)]+)\)")
|
|
20
|
+
|
|
21
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
|
|
25
|
+
"""Matrix multiplication for svg-style matrices.
|
|
26
|
+
|
|
27
|
+
:param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
|
|
28
|
+
:param mat2: transformation matrix (sx, 0, 0, sy, tx, ty)
|
|
29
|
+
|
|
30
|
+
Svg uses an unusual matrix format. For 3x3 transformation matrix
|
|
31
|
+
|
|
32
|
+
[[00, 01, 02],
|
|
33
|
+
[10, 11, 12],
|
|
34
|
+
[20, 21, 22]]
|
|
35
|
+
|
|
36
|
+
The svg matrix is
|
|
37
|
+
(00, 10, 01, 11, 02, 12)
|
|
38
|
+
|
|
39
|
+
Values 10 and 01 are only used for skewing, which is not supported by a bounding
|
|
40
|
+
box. Values 00 and 11 will always be identical for symmetric scaling, which is
|
|
41
|
+
the only scaling implemented in my BoundingBox classes. However, all six values
|
|
42
|
+
are implemented in case this function is used in other contexts.
|
|
43
|
+
"""
|
|
44
|
+
aa = sum(mat1[x] * mat2[y] for x, y in ((0, 0), (2, 1)))
|
|
45
|
+
bb = sum(mat1[x] * mat2[y] for x, y in ((1, 0), (3, 1)))
|
|
46
|
+
cc = sum(mat1[x] * mat2[y] for x, y in ((0, 2), (2, 3)))
|
|
47
|
+
dd = sum(mat1[x] * mat2[y] for x, y in ((1, 2), (3, 3)))
|
|
48
|
+
ee = sum(mat1[x] * mat2[y] for x, y in ((0, 4), (2, 5))) + mat1[4]
|
|
49
|
+
ff = sum(mat1[x] * mat2[y] for x, y in ((1, 4), (3, 5))) + mat1[5]
|
|
50
|
+
return (aa, bb, cc, dd, ee, ff)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def mat_apply(mat1: _Matrix, mat2: tuple[float, float]) -> tuple[float, float]:
|
|
54
|
+
"""Apply an svg-style transformation matrix to a point.
|
|
55
|
+
|
|
56
|
+
:param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
|
|
57
|
+
:param mat2: point (x, y)
|
|
58
|
+
"""
|
|
59
|
+
return mat1[0] * mat2[0] + mat1[4], mat1[3] * mat2[1] + mat1[5]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_transform_matrix(elem: EtreeElement) -> _Matrix:
|
|
63
|
+
"""Get the transformation matrix from an svg element.
|
|
64
|
+
|
|
65
|
+
:param element: svg element
|
|
66
|
+
"""
|
|
67
|
+
transform = elem.attrib.get("transform")
|
|
68
|
+
if not transform:
|
|
69
|
+
return (1, 0, 0, 1, 0, 0)
|
|
70
|
+
values_str = ""
|
|
71
|
+
with suppress(AttributeError):
|
|
72
|
+
values_str = cast(re.Match[str], _RE_MATRIX.match(transform)).group(1)
|
|
73
|
+
with suppress(ValueError):
|
|
74
|
+
aa, bb, cc, dd, ee, ff = (float(val) for val in values_str.split())
|
|
75
|
+
return (aa, bb, cc, dd, ee, ff)
|
|
76
|
+
msg = f"Could not parse transformation matrix from {transform}"
|
|
77
|
+
raise ValueError(msg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def new_transformation_matrix(
|
|
81
|
+
transformation: _Matrix | None = None,
|
|
82
|
+
*,
|
|
83
|
+
scale: float | None = None,
|
|
84
|
+
dx: float | None = None,
|
|
85
|
+
dy: float | None = None,
|
|
86
|
+
) -> _Matrix:
|
|
87
|
+
"""Create a new transformation matrix.
|
|
88
|
+
|
|
89
|
+
This takes the standard arguments in the BoundingBox classes and returns an
|
|
90
|
+
svg-style transformation matrix.
|
|
91
|
+
"""
|
|
92
|
+
transformation = transformation or (1, 0, 0, 1, 0, 0)
|
|
93
|
+
scale = scale or 1
|
|
94
|
+
dx = dx or 0
|
|
95
|
+
dy = dy or 0
|
|
96
|
+
return mat_dot((scale, 0, 0, scale, dx, dy), transformation)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
|
|
100
|
+
"""Apply a transformation matrix to an svg element.
|
|
101
|
+
|
|
102
|
+
:param elem: svg element
|
|
103
|
+
:param matrix: transformation matrix
|
|
104
|
+
"""
|
|
105
|
+
current = get_transform_matrix(elem)
|
|
106
|
+
updated = map(format_number, mat_dot(matrix, current))
|
|
107
|
+
elem.attrib["transform"] = f"matrix({' '.join(updated)})"
|
|
108
|
+
return elem
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
svg_ultralight/__init__.py,sha256=
|
|
2
|
-
svg_ultralight/animate.py,sha256=
|
|
1
|
+
svg_ultralight/__init__.py,sha256=KkTwZoRU2fOtStK38grA5Tjug5bJpUw78FZ9TLN-170,2256
|
|
2
|
+
svg_ultralight/animate.py,sha256=fE-zRU_uFrZIL9W78fcGz7qmrts8fz5UoWEy7b4xb44,1144
|
|
3
3
|
svg_ultralight/inkscape.py,sha256=M8yTxXOu4NlXnhsMycvEJiIDpnDeiZ_bZakJBM38ZoU,9152
|
|
4
4
|
svg_ultralight/layout.py,sha256=TTETT_8WLBXnQxDGXdAeczCFN5pFo5kKY3Q6zv4FPX4,12238
|
|
5
5
|
svg_ultralight/main.py,sha256=6oNkZfD27UMdP-oYqp5agS_IGcYb8NkUZwM9Zdyb3SA,7287
|
|
@@ -9,18 +9,21 @@ svg_ultralight/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
9
9
|
svg_ultralight/query.py,sha256=_KQuk4IwhVVDgTT0GZm_gbqNUGp7lMEDM32b_CTTSUA,7320
|
|
10
10
|
svg_ultralight/root_elements.py,sha256=pt9J6mPrnoTAZVF6vKTZoM_o947I8UCj6MbGcD2JUCk,2869
|
|
11
11
|
svg_ultralight/string_conversion.py,sha256=WEmpf75RJmJ2lfJluagAz2wPsz6wM8XvTEwkq4U0vEc,7353
|
|
12
|
+
svg_ultralight/transformations.py,sha256=CT43zaWSmEoWf-rQpFgP_1KX9wL6AzpR0cF8E1fIhAU,3625
|
|
12
13
|
svg_ultralight/unit_conversion.py,sha256=g07nhzXdjPvGcJmkhLdFbeDLrSmbI8uFoVgPo7G62Bg,9258
|
|
13
14
|
svg_ultralight/bounding_boxes/__init__.py,sha256=qUEn3r4s-1QNHaguhWhhaNfdP4tl_B6YEqxtiTFuzhQ,78
|
|
14
|
-
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=
|
|
15
|
-
svg_ultralight/bounding_boxes/supports_bounds.py,sha256=
|
|
16
|
-
svg_ultralight/bounding_boxes/
|
|
17
|
-
svg_ultralight/bounding_boxes/
|
|
18
|
-
svg_ultralight/bounding_boxes/
|
|
15
|
+
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=YMClhdekeYbzD_ijXDAer-H3moWKN3lUNnZs1UNFFKc,3458
|
|
16
|
+
svg_ultralight/bounding_boxes/supports_bounds.py,sha256=fbHV6mGdeIVV3lS15vBKSrHiHKR7DMg4K5X__4LLmCE,4569
|
|
17
|
+
svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=b89TM2UsdaeApyjTQeMbx_FG_WcCiLAImEiHiZ6EWPI,2600
|
|
18
|
+
svg_ultralight/bounding_boxes/type_bound_confederation.py,sha256=PAUa8LSfyw0v2eq8EU2B_V5dlsl7ddrrPIsD-5fWU8U,2602
|
|
19
|
+
svg_ultralight/bounding_boxes/type_bound_element.py,sha256=9RdxH8osOlAvPdWR0Ww9NsasHLPYFDs-MbydoV48x4E,2239
|
|
20
|
+
svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=cDrMp6uwaA--KJIQS2puG10qh8n3TBmiscg-cfk1f3w,16149
|
|
21
|
+
svg_ultralight/bounding_boxes/type_padded_text.py,sha256=QA6PfeO_sQYc5pEXuyfyQ3lRUcZAc4B2BthWXpdt3qQ,14848
|
|
19
22
|
svg_ultralight/constructors/__init__.py,sha256=YcnO0iBQc19aL8Iemw0Y452MBMBIT2AN5nZCnoGxpn0,327
|
|
20
23
|
svg_ultralight/constructors/new_element.py,sha256=VtMz9sPn9rMk6rui5Poysy3vezlOaS-tGIcGbu-SXmY,3406
|
|
21
24
|
svg_ultralight/strings/__init__.py,sha256=Zalrf-ThFz7b7xKELx5lb2gOlBgV-6jk_k_EeSdVCVk,295
|
|
22
25
|
svg_ultralight/strings/svg_strings.py,sha256=RYKMxOHq9abbZyGcFqsElBGLrBX-EjjNxln3s_ibi30,1296
|
|
23
|
-
svg_ultralight-0.
|
|
24
|
-
svg_ultralight-0.
|
|
25
|
-
svg_ultralight-0.
|
|
26
|
-
svg_ultralight-0.
|
|
26
|
+
svg_ultralight-0.30.0.dist-info/METADATA,sha256=vAGCbcN3nh37O6b5qU8tJbMfogcORk0u5-77R_2H8HM,8871
|
|
27
|
+
svg_ultralight-0.30.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
28
|
+
svg_ultralight-0.30.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
|
|
29
|
+
svg_ultralight-0.30.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|