svg-ultralight 0.26.0__tar.gz → 0.28.0__tar.gz
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-0.28.0/.gitignore +3 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/PKG-INFO +1 -1
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/pyproject.toml +6 -2
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/__init__.py +10 -0
- svg_ultralight-0.28.0/src/svg_ultralight/bounding_boxes/bound_helpers.py +95 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +73 -38
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/query.py +2 -1
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/root_elements.py +3 -11
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/PKG-INFO +1 -1
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/SOURCES.txt +2 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_inkscape.py +1 -4
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_queries.py +12 -19
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_root_elements.py +33 -5
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/.pre-commit-config.yaml +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/README.md +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/setup.cfg +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/animate.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/constructors/__init__.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/constructors/new_element.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/inkscape.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/layout.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/main.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/metadata.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/nsmap.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/py.typed +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/string_conversion.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/strings/__init__.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/unit_conversion.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/requires.txt +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/__init__.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/conftest.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_layout.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_metadata.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_new_element.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_string_conversion.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tests/test_svg_ultralight.py +0 -0
- {svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "svg-ultralight"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.0"
|
|
4
4
|
description = "a sensible way to create svg files with Python"
|
|
5
5
|
authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -20,6 +20,10 @@ build-backend = "setuptools.build_meta"
|
|
|
20
20
|
addopts = "--doctest-modules"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
[tool.isort]
|
|
24
|
+
profile = "black"
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
[tool.tox]
|
|
24
28
|
legacy_tox_ini = """
|
|
25
29
|
[tox]
|
|
@@ -33,7 +37,7 @@ legacy_tox_ini = """
|
|
|
33
37
|
|
|
34
38
|
[tool.commitizen]
|
|
35
39
|
name = "cz_conventional_commits"
|
|
36
|
-
version = "0.
|
|
40
|
+
version = "0.28.0"
|
|
37
41
|
tag_format = "$version"
|
|
38
42
|
version_files = ["pyproject.toml:^version"]
|
|
39
43
|
annotated_tag = true
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
the entire state of a BoundingBox instance.
|
|
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,
|
|
65
|
+
Under most circumstances, it will not be used.
|
|
28
66
|
|
|
29
|
-
:param
|
|
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
|
|
41
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
258
|
-
:param
|
|
259
|
-
|
|
260
|
-
Transformation attributes are _translation_x, _translation_y, and _scale
|
|
299
|
+
:param dx: x translation
|
|
300
|
+
:param dy: y translation
|
|
261
301
|
"""
|
|
262
|
-
|
|
263
|
-
self.
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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,3 +1,4 @@
|
|
|
1
|
+
.gitignore
|
|
1
2
|
.pre-commit-config.yaml
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
@@ -20,6 +21,7 @@ src/svg_ultralight.egg-info/dependency_links.txt
|
|
|
20
21
|
src/svg_ultralight.egg-info/requires.txt
|
|
21
22
|
src/svg_ultralight.egg-info/top_level.txt
|
|
22
23
|
src/svg_ultralight/bounding_boxes/__init__.py
|
|
24
|
+
src/svg_ultralight/bounding_boxes/bound_helpers.py
|
|
23
25
|
src/svg_ultralight/bounding_boxes/supports_bounds.py
|
|
24
26
|
src/svg_ultralight/bounding_boxes/type_bound_element.py
|
|
25
27
|
src/svg_ultralight/bounding_boxes/type_bounding_box.py
|
|
@@ -17,10 +17,7 @@ from svg_ultralight.inkscape import convert_text_to_path
|
|
|
17
17
|
from svg_ultralight.main import new_svg_root
|
|
18
18
|
from svg_ultralight.nsmap import NSMAP
|
|
19
19
|
|
|
20
|
-
INKSCAPE = Path(
|
|
21
|
-
r"C:\\Program Files\\WindowsApps\\25415Inkscape.Inkscape_1.3.2.0"
|
|
22
|
-
+ "_x64__9waqn51p1ttv2\\VFS\\ProgramFilesX64\\Inkscape\\bin\\inkscape.exe"
|
|
23
|
-
)
|
|
20
|
+
INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
|
|
24
21
|
|
|
25
22
|
if not INKSCAPE.with_suffix(".exe").exists():
|
|
26
23
|
msg = "Inkscape not found. Please install Inkscape or update the INKSCAPE path var."
|
|
@@ -7,20 +7,16 @@ A quick test. Won't be able to run it till you change INKSCAPE to the correct pa
|
|
|
7
7
|
your system.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import math
|
|
11
10
|
from dataclasses import dataclass
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
|
|
14
13
|
import pytest
|
|
15
14
|
|
|
16
|
-
from svg_ultralight import new_svg_root
|
|
15
|
+
from svg_ultralight import BoundingBox, new_svg_root
|
|
17
16
|
from svg_ultralight.constructors import new_sub_element
|
|
18
|
-
from svg_ultralight.query import
|
|
17
|
+
from svg_ultralight.query import map_ids_to_bounding_boxes
|
|
19
18
|
|
|
20
|
-
INKSCAPE = Path(
|
|
21
|
-
r"C:\\Program Files\\WindowsApps\\25415Inkscape.Inkscape_1.3.2.0"
|
|
22
|
-
+ "_x64__9waqn51p1ttv2\\VFS\\ProgramFilesX64\\Inkscape\\bin\\inkscape.exe"
|
|
23
|
-
)
|
|
19
|
+
INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
|
|
24
20
|
|
|
25
21
|
if not INKSCAPE.with_suffix(".exe").exists():
|
|
26
22
|
msg = "Inkscape not found. Please install Inkscape or update the INKSCAPE path var."
|
|
@@ -28,7 +24,6 @@ if not INKSCAPE.with_suffix(".exe").exists():
|
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
class TestMergeBoundingBoxes:
|
|
31
|
-
|
|
32
27
|
def test_new_merged_bbox(self):
|
|
33
28
|
bbox_a = BoundingBox(-2, -4, 10, 20)
|
|
34
29
|
bbox_b = BoundingBox(0, 0, 10, 10)
|
|
@@ -75,45 +70,45 @@ class TestBoundingBox:
|
|
|
75
70
|
assert bbox.width == 9000.0
|
|
76
71
|
assert bbox.height == 12000.0
|
|
77
72
|
|
|
78
|
-
def test_x(self, bounding_box):
|
|
73
|
+
def test_x(self, bounding_box: BoundingBox):
|
|
79
74
|
assert bounding_box.x == 0.0
|
|
80
75
|
bounding_box.x = 50.0
|
|
81
76
|
assert bounding_box.x == 50.0
|
|
82
77
|
assert bounding_box.cx == 100.0
|
|
83
78
|
|
|
84
|
-
def test_x2(self, bounding_box):
|
|
79
|
+
def test_x2(self, bounding_box: BoundingBox):
|
|
85
80
|
assert bounding_box.x2 == 100.0
|
|
86
81
|
bounding_box.x2 = 150.0
|
|
87
82
|
assert bounding_box.x2 == 150.0
|
|
88
83
|
assert bounding_box.cx == 100.0
|
|
89
84
|
|
|
90
|
-
def test_y(self, bounding_box):
|
|
85
|
+
def test_y(self, bounding_box: BoundingBox):
|
|
91
86
|
assert bounding_box.y == 0.0
|
|
92
87
|
bounding_box.y = 50.0
|
|
93
88
|
assert bounding_box.y == 50.0
|
|
94
89
|
assert bounding_box.cy == 150.0
|
|
95
90
|
|
|
96
|
-
def test_y2(self, bounding_box):
|
|
91
|
+
def test_y2(self, bounding_box: BoundingBox):
|
|
97
92
|
assert bounding_box.y2 == 200.0
|
|
98
93
|
bounding_box.y2 = 250.0
|
|
99
94
|
assert bounding_box.y2 == 250.0
|
|
100
95
|
assert bounding_box.cy == 150.0
|
|
101
96
|
|
|
102
|
-
def test_width(self, bounding_box):
|
|
97
|
+
def test_width(self, bounding_box: BoundingBox):
|
|
103
98
|
assert bounding_box.width == 100.0
|
|
104
99
|
bounding_box.width = 150.0
|
|
105
100
|
assert bounding_box.width == 150.0
|
|
106
101
|
assert bounding_box.x2 == 150.0
|
|
107
102
|
|
|
108
|
-
def test_height(self, bounding_box):
|
|
103
|
+
def test_height(self, bounding_box: BoundingBox):
|
|
109
104
|
assert bounding_box.height == 200.0
|
|
110
105
|
bounding_box.height = 250.0
|
|
111
106
|
assert bounding_box.height == 250.0
|
|
112
107
|
assert bounding_box.y2 == 250.0
|
|
113
108
|
|
|
114
|
-
def test_transform_string(self, bounding_box):
|
|
109
|
+
def test_transform_string(self, bounding_box: BoundingBox):
|
|
115
110
|
transform_string = bounding_box.transform_string
|
|
116
|
-
assert transform_string == "
|
|
111
|
+
assert transform_string == "matrix(1 0 0 1 0 0)"
|
|
117
112
|
|
|
118
113
|
def test_merge(self):
|
|
119
114
|
bbox1 = MockSupportsBounds(0, 0, 100, 200)
|
|
@@ -185,6 +180,4 @@ class TestAlterBoundingBox:
|
|
|
185
180
|
bbox.y = 200
|
|
186
181
|
bbox.height = 200
|
|
187
182
|
bbox.height = 40
|
|
188
|
-
assert
|
|
189
|
-
assert math.isclose(bbox._translation_x, 90) # type: ignore
|
|
190
|
-
assert math.isclose(bbox._translation_y, 180) # type: ignore
|
|
183
|
+
assert bbox.transform == (1, 0, 0, 1, 90, 180)
|
|
@@ -6,15 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from svg_ultralight.main import new_svg_root
|
|
10
|
-
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
11
9
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
12
10
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
13
11
|
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
14
12
|
from svg_ultralight.constructors import new_element
|
|
15
13
|
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
14
|
+
from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
|
|
16
15
|
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
17
|
-
from typing import Union
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
class TestNewSvgRootAroundBounds:
|
|
@@ -23,13 +21,13 @@ class TestNewSvgRootAroundBounds:
|
|
|
23
21
|
"""Raise ValueError if no bounding boxes found."""
|
|
24
22
|
with pytest.raises(ValueError) as excinfo:
|
|
25
23
|
_ = new_svg_root_around_bounds()
|
|
26
|
-
assert "
|
|
24
|
+
assert "At least one argument" in str(excinfo.value)
|
|
27
25
|
|
|
28
26
|
def test_no_bound_elements(self):
|
|
29
27
|
"""Raise ValueError if no BoundElements found."""
|
|
30
28
|
with pytest.raises(ValueError) as excinfo:
|
|
31
29
|
_ = new_svg_root_around_bounds(new_element("g"))
|
|
32
|
-
assert "
|
|
30
|
+
assert "At least one argument" in str(excinfo.value)
|
|
33
31
|
|
|
34
32
|
def test_bounding_boxes(self):
|
|
35
33
|
"""Create svg root element from bounding boxes."""
|
|
@@ -53,3 +51,33 @@ class TestNewSvgRootAroundBounds:
|
|
|
53
51
|
result = new_svg_root_around_bounds(*args)
|
|
54
52
|
assert isinstance(result, EtreeElement)
|
|
55
53
|
assert result.attrib["viewBox"] == "0 0 201 201"
|
|
54
|
+
|
|
55
|
+
class TestNewBoundUnion:
|
|
56
|
+
|
|
57
|
+
def test_bounding_boxes_only(self):
|
|
58
|
+
"""Raise an error if no elements found."""
|
|
59
|
+
bboxes = [BoundingBox(0, 0, 100, 100), BoundingBox(50, 50, 150, 150)]
|
|
60
|
+
with pytest.raises(ValueError) as excinfo:
|
|
61
|
+
_ = new_bound_union(*bboxes)
|
|
62
|
+
assert "must be a BoundElement, PaddedText, or Etree" in str(excinfo.value)
|
|
63
|
+
|
|
64
|
+
def test_elements_only(self):
|
|
65
|
+
"""Raise an error if no elements found."""
|
|
66
|
+
elems = [new_element("g"), new_element("g")]
|
|
67
|
+
with pytest.raises(ValueError) as excinfo:
|
|
68
|
+
_ = new_bound_union(*elems)
|
|
69
|
+
assert "must be a BoundElement, BoundingBox, or Padded" in str(excinfo.value)
|
|
70
|
+
|
|
71
|
+
def test_bound_elements(self):
|
|
72
|
+
"""Create svg root element from BoundElements."""
|
|
73
|
+
bboxes = [BoundingBox(0, 0, 100, 100), BoundingBox(50, 50, 150, 150)]
|
|
74
|
+
args = bboxes[0], BoundElement(new_element("g"), bboxes[1])
|
|
75
|
+
result = new_bound_union(*args)
|
|
76
|
+
assert isinstance(result, BoundElement)
|
|
77
|
+
|
|
78
|
+
def test_padded_text(self):
|
|
79
|
+
"""Create svg root element from BoundElements."""
|
|
80
|
+
bboxes = [BoundingBox(0, 0, 100, 100), BoundingBox(50, 50, 150, 150)]
|
|
81
|
+
args = bboxes[0], PaddedText(new_element("g"), bboxes[1], 1, 1, 1, 1)
|
|
82
|
+
result = new_bound_union(*args)
|
|
83
|
+
assert isinstance(result, BoundElement)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/__init__.py
RENAMED
|
File without changes
|
{svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight/constructors/new_element.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.26.0 → svg_ultralight-0.28.0}/src/svg_ultralight.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|