svg-ultralight 0.33.0__tar.gz → 0.35.1__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.

Files changed (51) hide show
  1. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/.pre-commit-config.yaml +10 -22
  2. {svg_ultralight-0.33.0/src/svg_ultralight.egg-info → svg_ultralight-0.35.1}/PKG-INFO +1 -1
  3. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/pyproject.toml +6 -2
  4. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/__init__.py +12 -2
  5. svg_ultralight-0.35.1/src/svg_ultralight/bounding_boxes/bound_helpers.py +207 -0
  6. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/constructors/__init__.py +1 -1
  7. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/constructors/new_element.py +2 -1
  8. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/layout.py +4 -1
  9. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/query.py +0 -1
  10. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/strings/__init__.py +1 -1
  11. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1/src/svg_ultralight.egg-info}/PKG-INFO +1 -1
  12. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight.egg-info/SOURCES.txt +2 -1
  13. svg_ultralight-0.35.1/tests/conftest.py +16 -0
  14. svg_ultralight-0.35.1/tests/resources/arrow.svg +3 -0
  15. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_bounding.py +76 -1
  16. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_layout.py +21 -0
  17. svg_ultralight-0.33.0/src/svg_ultralight/bounding_boxes/bound_helpers.py +0 -97
  18. svg_ultralight-0.33.0/tests/conftest.py +0 -11
  19. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/.gitignore +0 -0
  20. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/README.md +0 -0
  21. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/setup.cfg +0 -0
  22. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/animate.py +0 -0
  23. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
  24. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
  25. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +0 -0
  26. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
  27. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +0 -0
  28. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
  29. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/inkscape.py +0 -0
  30. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/main.py +0 -0
  31. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/metadata.py +0 -0
  32. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/nsmap.py +0 -0
  33. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/py.typed +0 -0
  34. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/root_elements.py +0 -0
  35. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/string_conversion.py +0 -0
  36. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/strings/svg_strings.py +0 -0
  37. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/transformations.py +0 -0
  38. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight/unit_conversion.py +0 -0
  39. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
  40. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight.egg-info/requires.txt +0 -0
  41. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/src/svg_ultralight.egg-info/top_level.txt +0 -0
  42. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/__init__.py +0 -0
  43. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_inkscape.py +0 -0
  44. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_matrices.py +0 -0
  45. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_metadata.py +0 -0
  46. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_new_element.py +0 -0
  47. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_queries.py +0 -0
  48. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_root_elements.py +0 -0
  49. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_string_conversion.py +0 -0
  50. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tests/test_svg_ultralight.py +0 -0
  51. {svg_ultralight-0.33.0 → svg_ultralight-0.35.1}/tox.ini +0 -0
@@ -57,19 +57,6 @@ repos:
57
57
  # - --ignore-missing-imports
58
58
  # files: ^(src/|tests/)
59
59
 
60
- - repo: https://github.com/PyCQA/isort
61
- rev: 5.13.2
62
- hooks:
63
- - id: isort
64
- args: ["--profile", "black", "--filter-files", "--combine-as"]
65
-
66
- - repo: https://github.com/psf/black
67
- rev: 24.10.0
68
- hooks:
69
- - id: black
70
- language_version: python3.9
71
- args: ["--skip-magic-trailing-comma"]
72
-
73
60
  - repo: https://github.com/asottile/pyupgrade
74
61
  rev: v3.19.0
75
62
  hooks:
@@ -84,8 +71,6 @@ repos:
84
71
 
85
72
  - repo: https://github.com/charliermarsh/ruff-pre-commit
86
73
  # ignores
87
- # ANN101 Missing type annotation for self in method
88
- # ANN102 Missing type annotation for cls in classmethod
89
74
  # ANN201 Missing return type annotation for public function
90
75
  # ANN202 Missing return type annotation for private function (wants -> None everywhere)
91
76
  # B905 zip() without an explicit strict= parameter
@@ -103,25 +88,28 @@ repos:
103
88
  # S101 Use of `assert` detected
104
89
  # S603 `subprocess` call: check for execution of untrusted input
105
90
  # PLR2004 Magic value used in comparison
106
- # D413 [*] Missing blank line after last section ("Attributes")
107
- # D407 [*] Missing dashed underline after section ("Attributes")
108
- # D406 [*] Section name should end with a newline ("Attributes")
109
91
  # S320 Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks
110
92
  # S301 don't use pickle
111
93
  # B028 wants explicit stacklevel on warn
112
94
  # BLE001 Use of `except Exception:` detected
113
- rev: 'v0.7.4'
95
+ rev: 'v0.8.3'
114
96
  hooks:
115
97
  - id: ruff
98
+ name: "ruff-lint"
116
99
  exclude: "tests"
117
100
  args:
118
101
  - --target-version=py39
119
102
  - --select=ALL
120
- - --ignore=ANN101,ANN102,ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,D413,D407,D406,S320,S301,B028,BLE001
121
- # - --fix
103
+ - --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001
104
+ - --fix
105
+ - --fixable=RUF022
106
+ - id: ruff
107
+ args: ["check", "--select", "I", "--fix"]
108
+ - id: ruff-format
109
+ name: "ruff-format"
122
110
 
123
111
  # reads pyproject.toml for additional config
124
112
  - repo: https://github.com/RobertCraigie/pyright-python
125
- rev: v1.1.389
113
+ rev: v1.1.390
126
114
  hooks:
127
115
  - id: pyright
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: svg-ultralight
3
- Version: 0.33.0
3
+ Version: 0.35.1
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "svg-ultralight"
3
- version = "0.33.0"
3
+ version = "0.35.1"
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" }
@@ -18,6 +18,8 @@ build-backend = "setuptools.build_meta"
18
18
 
19
19
  [tool.pytest.ini_options]
20
20
  addopts = "--doctest-modules"
21
+ pythonpath = ["tests"]
22
+ log_cli = 1
21
23
 
22
24
 
23
25
  [tool.isort]
@@ -34,10 +36,12 @@ legacy_tox_ini = """
34
36
  commands = pytest tests
35
37
  """
36
38
 
39
+ [tool.ruff.lint.pydocstyle]
40
+ convention = "pep257"
37
41
 
38
42
  [tool.commitizen]
39
43
  name = "cz_conventional_commits"
40
- version = "0.33.0"
44
+ version = "0.35.1"
41
45
  tag_format = "$version"
42
46
  version_files = ["pyproject.toml:^version"]
43
47
  annotated_tag = true
@@ -5,9 +5,14 @@
5
5
  """
6
6
 
7
7
  from svg_ultralight.bounding_boxes.bound_helpers import (
8
+ bbox_dict,
9
+ cut_bbox,
10
+ new_bbox_rect,
8
11
  new_bbox_union,
9
12
  new_bound_union,
10
13
  new_element_union,
14
+ pad_bbox,
15
+ parse_bound_element,
11
16
  )
12
17
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
13
18
  from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
@@ -31,10 +36,10 @@ from svg_ultralight.main import new_svg_root, write_svg
31
36
  from svg_ultralight.metadata import new_metadata
32
37
  from svg_ultralight.nsmap import NSMAP, new_qname
33
38
  from svg_ultralight.query import (
39
+ clear_svg_ultralight_cache,
34
40
  get_bounding_box,
35
41
  get_bounding_boxes,
36
42
  pad_text,
37
- clear_svg_ultralight_cache,
38
43
  )
39
44
  from svg_ultralight.root_elements import new_svg_root_around_bounds
40
45
  from svg_ultralight.string_conversion import (
@@ -51,13 +56,15 @@ from svg_ultralight.transformations import (
51
56
  )
52
57
 
53
58
  __all__ = [
59
+ "NSMAP",
54
60
  "BoundCollection",
55
61
  "BoundElement",
56
62
  "BoundingBox",
57
- "NSMAP",
58
63
  "PaddedText",
59
64
  "SupportsBounds",
65
+ "bbox_dict",
60
66
  "clear_svg_ultralight_cache",
67
+ "cut_bbox",
61
68
  "deepcopy_element",
62
69
  "format_attr_dict",
63
70
  "format_number",
@@ -68,6 +75,7 @@ __all__ = [
68
75
  "mat_apply",
69
76
  "mat_dot",
70
77
  "mat_invert",
78
+ "new_bbox_rect",
71
79
  "new_bbox_union",
72
80
  "new_bound_union",
73
81
  "new_element",
@@ -77,7 +85,9 @@ __all__ = [
77
85
  "new_sub_element",
78
86
  "new_svg_root",
79
87
  "new_svg_root_around_bounds",
88
+ "pad_bbox",
80
89
  "pad_text",
90
+ "parse_bound_element",
81
91
  "transform_element",
82
92
  "update_element",
83
93
  "write_pdf",
@@ -0,0 +1,207 @@
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
+ import os
20
+
21
+ from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
22
+ from lxml import etree
23
+
24
+ _Matrix = tuple[float, float, float, float, float, float]
25
+
26
+
27
+ def new_element_union(
28
+ *elems: EtreeElement | SupportsBounds, **attributes: float | str
29
+ ) -> EtreeElement:
30
+ """Get the union of any elements found in the given arguments.
31
+
32
+ :param elems: BoundElements, PaddedTexts, or EtreeElements.
33
+ Other arguments will be ignored.
34
+ :return: a new group element containing all elements.
35
+
36
+ This does not support consolidating attributes. E.g., if all elements have the
37
+ same fill color, this will not be recognized and consilidated into a single
38
+ attribute for the group. Too many attributes change their behavior when applied
39
+ to a group.
40
+ """
41
+ elements_found: list[EtreeElement] = []
42
+ for elem in elems:
43
+ if isinstance(elem, (BoundElement, PaddedText)):
44
+ elements_found.append(elem.elem)
45
+ elif isinstance(elem, EtreeElement):
46
+ elements_found.append(elem)
47
+
48
+ if not elements_found:
49
+ msg = (
50
+ "Cannot find any elements to union. "
51
+ + "At least one argument must be a "
52
+ + "BoundElement, PaddedText, or EtreeElement."
53
+ )
54
+ raise ValueError(msg)
55
+ group = new_element("g", **attributes)
56
+ group.extend(elements_found)
57
+ return group
58
+
59
+
60
+ def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
61
+ """Get the union of the bounding boxes of the given elements.
62
+
63
+ :param blems: BoundElements, BoundingBoxes, or PaddedTexts.
64
+ Other arguments will be ignored.
65
+ :return: the union of all bounding boxes as a BoundingBox instance.
66
+
67
+ Will used the padded_box attribute of PaddedText instances.
68
+ """
69
+ bboxes: list[BoundingBox] = []
70
+ for blem in blems:
71
+ if isinstance(blem, BoundingBox):
72
+ bboxes.append(blem)
73
+ elif isinstance(blem, BoundElement):
74
+ bboxes.append(blem.bbox)
75
+ elif isinstance(blem, PaddedText):
76
+ bboxes.append(blem.padded_bbox)
77
+
78
+ if not bboxes:
79
+ msg = (
80
+ "Cannot find any bounding boxes to union. "
81
+ + "At least one argument must be a "
82
+ + "BoundElement, BoundingBox, or PaddedText."
83
+ )
84
+ raise ValueError(msg)
85
+
86
+ return BoundingBox.merged(*bboxes)
87
+
88
+
89
+ def new_bound_union(*blems: SupportsBounds | EtreeElement) -> BoundElement:
90
+ """Get the union of the bounding boxes of the given elements.
91
+
92
+ :param blems: BoundElements or EtreeElements.
93
+ At least one argument must be a BoundElement, BoundingBox, or PaddedText.
94
+ :return: the union of all arguments as a BoundElement instance.
95
+
96
+ Will used the padded_box attribute of PaddedText instances.
97
+ """
98
+ group = new_element_union(*blems)
99
+ bbox = new_bbox_union(*blems)
100
+ return BoundElement(group, bbox)
101
+
102
+
103
+ def _expand_pad(pad: float | tuple[float, ...]) -> tuple[float, float, float, float]:
104
+ """Expand a float pad argument into a 4-tuple."""
105
+ if isinstance(pad, (int, float)):
106
+ return pad, pad, pad, pad
107
+ if len(pad) == 1:
108
+ return pad[0], pad[0], pad[0], pad[0]
109
+ if len(pad) == 2:
110
+ return pad[0], pad[1], pad[0], pad[1]
111
+ if len(pad) == 3:
112
+ return pad[0], pad[1], pad[2], pad[1]
113
+ return pad[0], pad[1], pad[2], pad[3]
114
+
115
+
116
+ def cut_bbox(
117
+ bbox: SupportsBounds,
118
+ *,
119
+ x: float | None = None,
120
+ y: float | None = None,
121
+ x2: float | None = None,
122
+ y2: float | None = None,
123
+ ) -> BoundingBox:
124
+ """Return a new bounding box with updated limits.
125
+
126
+ :param bbox: the original bounding box or bounded element.
127
+ :param x: the new x-coordinate.
128
+ :param y: the new y-coordinate.
129
+ :param x2: the new x2-coordinate.
130
+ :param y2: the new y2-coordinate.
131
+ :return: a new bounding box with the updated limits.
132
+ """
133
+ x = bbox.x if x is None else x
134
+ y = bbox.y if y is None else y
135
+ x2 = bbox.x2 if x2 is None else x2
136
+ y2 = bbox.y2 if y2 is None else y2
137
+ width = x2 - x
138
+ height = y2 - y
139
+ return BoundingBox(x, y, width, height)
140
+
141
+
142
+ def pad_bbox(bbox: SupportsBounds, pad: float | tuple[float, ...]) -> BoundingBox:
143
+ """Return a new bounding box with padding.
144
+
145
+ :param bbox: the original bounding box or bounded element.
146
+ :param pad: the padding to apply.
147
+ If a single number, the same padding will be applied to all sides.
148
+ If a tuple, will be applied per css rules.
149
+ len = 1 : 0, 0, 0, 0
150
+ len = 2 : 0, 1, 0, 1
151
+ len = 3 : 0, 1, 2, 1
152
+ len = 4 : 0, 1, 2, 3
153
+ :return: a new bounding box with padding applied.
154
+ """
155
+ top, right, bottom, left = _expand_pad(pad)
156
+ return cut_bbox(
157
+ bbox, x=bbox.x - left, y=bbox.y - top, x2=bbox.x2 + right, y2=bbox.y2 + bottom
158
+ )
159
+
160
+
161
+ def bbox_dict(bbox: SupportsBounds) -> dict[str, float]:
162
+ """Return a dictionary representation of a bounding box.
163
+
164
+ :param bbox: the bounding box or bound element from which to extract dimensions.
165
+ :return: a dictionary with keys x, y, width, and height.
166
+ """
167
+ return {"x": bbox.x, "y": bbox.y, "width": bbox.width, "height": bbox.height}
168
+
169
+
170
+ def new_bbox_rect(bbox: BoundingBox, **kwargs: float | str) -> EtreeElement:
171
+ """Return a new rect element with the same dimensions as the bounding box.
172
+
173
+ :param bbox: the bounding box or bound element from which to extract dimensions.
174
+ :param kwargs: additional attributes for the rect element.
175
+ """
176
+ return new_element("rect", **bbox_dict(bbox), **kwargs)
177
+
178
+
179
+ def _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
180
+ """Return the view box of an element as a tuple of floats.
181
+
182
+ :param elem: the element from which to extract the view box.
183
+ :return: a tuple of floats representing the view box.
184
+
185
+ This will work on svg files created by this library and some others. Not all svg
186
+ files have a viewBox attribute.
187
+ """
188
+ view_box = elem.get("viewBox")
189
+ if view_box is None:
190
+ msg = "Element does not have a viewBox attribute."
191
+ raise ValueError(msg)
192
+ x, y, width, height = map(float, view_box.split())
193
+ return x, y, width, height
194
+
195
+
196
+ def parse_bound_element(svg_fil: str | os.PathLike[str]) -> BoundElement:
197
+ """Import an element as a BoundElement.
198
+
199
+ :param elem: the element to import.
200
+ :return: a BoundElement instance.
201
+ """
202
+ tree = etree.parse(svg_fil)
203
+ root = tree.getroot()
204
+ elem = new_element("g")
205
+ elem.extend(list(root))
206
+ bbox = BoundingBox(*_get_view_box(root))
207
+ return BoundElement(elem, bbox)
@@ -11,4 +11,4 @@ from svg_ultralight.constructors.new_element import (
11
11
  update_element,
12
12
  )
13
13
 
14
- __all__ = ["new_element", "new_sub_element", "update_element", "deepcopy_element"]
14
+ __all__ = ["deepcopy_element", "new_element", "new_sub_element", "update_element"]
@@ -20,7 +20,8 @@ from lxml import etree
20
20
  from svg_ultralight.string_conversion import set_attributes
21
21
 
22
22
  if TYPE_CHECKING:
23
- from lxml.etree import QName, _Element as EtreeElement # type: ignore
23
+ from lxml.etree import QName
24
+ from lxml.etree import _Element as EtreeElement # type: ignore
24
25
 
25
26
 
26
27
  def new_element(tag: str | QName, **attributes: str | float) -> EtreeElement:
@@ -43,7 +43,10 @@ def expand_pad_arg(pad: PadArg) -> tuple[float, float, float, float]:
43
43
  return expand_pad_arg([pad])
44
44
  as_ms = [m if isinstance(m, Measurement) else Measurement(m) for m in pad]
45
45
  as_units = [m.value for m in as_ms]
46
- as_units = [as_units[i % len(as_units)] for i in range(4)]
46
+ if len(as_units) == 3:
47
+ as_units = [*as_units, as_units[1]]
48
+ else:
49
+ as_units = [as_units[i % len(as_units)] for i in range(4)]
47
50
  return as_units[0], as_units[1], as_units[2], as_units[3]
48
51
 
49
52
 
@@ -31,7 +31,6 @@ from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
31
31
  from svg_ultralight.main import new_svg_root, write_svg
32
32
 
33
33
  if TYPE_CHECKING:
34
-
35
34
  from lxml.etree import _Element as EtreeElement # type: ignore
36
35
 
37
36
 
@@ -10,4 +10,4 @@ from svg_ultralight.strings.svg_strings import (
10
10
  svg_ints,
11
11
  )
12
12
 
13
- __all__ = ["svg_color_tuple", "svg_ints", "svg_float_tuples"]
13
+ __all__ = ["svg_color_tuple", "svg_float_tuples", "svg_ints"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: svg-ultralight
3
- Version: 0.33.0
3
+ Version: 0.35.1
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -43,4 +43,5 @@ tests/test_new_element.py
43
43
  tests/test_queries.py
44
44
  tests/test_root_elements.py
45
45
  tests/test_string_conversion.py
46
- tests/test_svg_ultralight.py
46
+ tests/test_svg_ultralight.py
47
+ tests/resources/arrow.svg
@@ -0,0 +1,16 @@
1
+ """Test configuration for pytest.
2
+
3
+ :author: Shay Hill
4
+ :created: 7/2/2019
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ def pytest_assertrepr_compare(config: Any, op: str, left: str, right: str):
11
+ """See full error diffs"""
12
+ if op in ("==", "!="):
13
+ return ["{0} {1} {2}".format(left, op, right)]
14
+
15
+ TEST_RESOURCES = Path(__file__).parent / "resources"
16
+
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="0 0 10 10">
2
+ <rect x="0" y="0" width="10" height="10"/>
3
+ </svg>
@@ -6,13 +6,23 @@
6
6
 
7
7
  import pytest
8
8
  import math
9
+ from conftest import TEST_RESOURCES
9
10
  from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
10
11
  from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
12
+ from lxml import etree
11
13
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
12
14
  from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
15
+ from svg_ultralight.bounding_boxes.bound_helpers import (
16
+ pad_bbox,
17
+ cut_bbox,
18
+ parse_bound_element,
19
+ bbox_dict,
20
+ new_bbox_rect,
21
+ )
13
22
  import copy
14
23
  from svg_ultralight.constructors import new_element
15
24
 
25
+
16
26
  class TestBoundElement:
17
27
  @pytest.fixture
18
28
  def bound_element(self) -> BoundElement:
@@ -84,7 +94,6 @@ class TestBoundElement:
84
94
  assert bound_element.y2 == 250.0
85
95
 
86
96
 
87
-
88
97
  class TestPaddedText:
89
98
  @pytest.fixture
90
99
  def bound_element(self) -> PaddedText:
@@ -155,6 +164,7 @@ class TestPaddedText:
155
164
  assert math.isclose(bound_element.height, 252.76)
156
165
  assert bound_element.y2 == 203.0
157
166
 
167
+
158
168
  class TestBoundCollection:
159
169
 
160
170
  @pytest.fixture
@@ -178,3 +188,68 @@ class TestBoundCollection:
178
188
  blem_trans = blem.elem.attrib["transform"]
179
189
  elem_trans = elem.attrib["transform"]
180
190
  assert blem_trans == elem_trans
191
+
192
+
193
+ class TestBoundHelpers:
194
+ def test_pad_bbox(self):
195
+ bbox = BoundingBox(0, 0, 4, 4)
196
+ padded = pad_bbox(bbox, 1)
197
+ assert padded.x == -1
198
+ assert padded.y == -1
199
+ assert padded.width == 6
200
+ assert padded.height == 6
201
+
202
+ def test_pad_bbox_t1(self):
203
+ bbox = BoundingBox(0, 0, 4, 4)
204
+ padded = pad_bbox(bbox, (1,))
205
+ assert padded.x == -1
206
+ assert padded.y == -1
207
+ assert padded.width == 6
208
+ assert padded.height == 6
209
+
210
+ def test_pad_bbox_t2(self):
211
+ bbox = BoundingBox(0, 0, 4, 4)
212
+ padded = pad_bbox(bbox, (1, 2))
213
+ assert padded.x == -2
214
+ assert padded.y == -1
215
+ assert padded.width == 8
216
+ assert padded.height == 6
217
+
218
+ def test_pad_bbox_t3(self):
219
+ bbox = BoundingBox(0, 0, 4, 4)
220
+ padded = pad_bbox(bbox, (1, 2, 3))
221
+ assert padded.x == -2
222
+ assert padded.y == -1
223
+ assert padded.width == 8
224
+ assert padded.height == 8
225
+
226
+ def test_pad_bbox_t4(self):
227
+ bbox = BoundingBox(0, 0, 4, 4)
228
+ padded = pad_bbox(bbox, (1, 2, 3, 4))
229
+ assert padded.x == -4
230
+ assert padded.y == -1
231
+ assert padded.width == 10
232
+ assert padded.height == 8
233
+
234
+ def test_cut_bbox(self):
235
+ bbox = BoundingBox(0, 0, 4, 4)
236
+ cut = cut_bbox(bbox, x=1)
237
+ assert cut.x == 1
238
+ assert cut.y == 0
239
+ assert cut.width == 3
240
+ assert cut.height == 4
241
+
242
+ def test_bbox_dict(self):
243
+ bbox = BoundingBox(0, 1, 2, 3)
244
+ assert bbox_dict(bbox) == {"x": 0, "y": 1, "width": 2, "height": 3}
245
+
246
+ def test_new_bbox_rect(self):
247
+ bbox = BoundingBox(0, 1, 2, 3)
248
+ elem = new_bbox_rect(bbox)
249
+ assert elem.attrib == {"x": "0", "y": "1", "width": "2", "height": "3"}
250
+
251
+ def test_import_bound_element():
252
+ blem = parse_bound_element(TEST_RESOURCES / "arrow.svg")
253
+ assert blem.bbox == BoundingBox(_x=0, _y=0, _width=10, _height=10, _transformation=(1, 0, 0, 1, 0, 0))
254
+ assert etree.tostring(blem.elem) == b'<g><ns0:rect xmlns:ns0="http://www.w3.org/2000/svg"' + b' x="0" y="0" width="10" height="10"/>\n</g>'
255
+
@@ -123,6 +123,27 @@ class TestMeasurement:
123
123
  assert (Measurement((1, unit)) / 4).value == Measurement((1 / 4, unit)).value
124
124
 
125
125
 
126
+ class TestExpandPadArg:
127
+ def test_expand_val(self):
128
+ """Test that a single value is expanded to a 4-tuple."""
129
+ assert layout.expand_pad_arg(1) == (1, 1, 1, 1)
130
+
131
+ def test_expand_1tuple(self):
132
+ """Test that a single value is expanded to a 4-tuple."""
133
+ assert layout.expand_pad_arg(1) == (1, 1, 1, 1)
134
+
135
+ def test_expand_2tuple(self):
136
+ """Test that a single value is expanded to a 4-tuple."""
137
+ assert layout.expand_pad_arg((1, 2)) == (1, 2, 1, 2)
138
+
139
+ def test_expand_3tuple(self):
140
+ """Test that a single value is expanded to a 4-tuple per css rules."""
141
+ assert layout.expand_pad_arg((1, 2, 3)) == (1, 2, 3, 2)
142
+
143
+ def test_expand_4tuple(self):
144
+ """Test that a single value is expanded to a 4-tuple per css rules."""
145
+ assert layout.expand_pad_arg((1, 2, 3, 4)) == (1, 2, 3, 4)
146
+
126
147
  class TestLayout:
127
148
  def test_standard(self):
128
149
  """No print dimensions give expanded pad argument
@@ -1,97 +0,0 @@
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
- _Matrix = tuple[float, float, float, float, float, float]
22
-
23
-
24
- def new_element_union(
25
- *elems: EtreeElement | SupportsBounds, **attributes: float | str
26
- ) -> EtreeElement:
27
- """Get the union of any elements found in the given arguments.
28
-
29
- :param elems: BoundElements, PaddedTexts, or EtreeElements.
30
- Other arguments will be ignored.
31
- :return: a new group element containing all elements.
32
-
33
- This does not support consolidating attributes. E.g., if all elements have the
34
- same fill color, this will not be recognized and consilidated into a single
35
- attribute for the group. Too many attributes change their behavior when applied
36
- to a group.
37
- """
38
- elements_found: list[EtreeElement] = []
39
- for elem in elems:
40
- if isinstance(elem, (BoundElement, PaddedText)):
41
- elements_found.append(elem.elem)
42
- elif isinstance(elem, EtreeElement):
43
- elements_found.append(elem)
44
-
45
- if not elements_found:
46
- msg = (
47
- "Cannot find any elements to union. "
48
- + "At least one argument must be a "
49
- + "BoundElement, PaddedText, or EtreeElement."
50
- )
51
- raise ValueError(msg)
52
- group = new_element("g", **attributes)
53
- group.extend(elements_found)
54
- return group
55
-
56
-
57
- def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
58
- """Get the union of the bounding boxes of the given elements.
59
-
60
- :param blems: BoundElements, BoundingBoxes, or PaddedTexts.
61
- Other arguments will be ignored.
62
- :return: the union of all bounding boxes as a BoundingBox instance.
63
-
64
- Will used the padded_box attribute of PaddedText instances.
65
- """
66
- bboxes: list[BoundingBox] = []
67
- for blem in blems:
68
- if isinstance(blem, BoundingBox):
69
- bboxes.append(blem)
70
- elif isinstance(blem, BoundElement):
71
- bboxes.append(blem.bbox)
72
- elif isinstance(blem, PaddedText):
73
- bboxes.append(blem.padded_bbox)
74
-
75
- if not bboxes:
76
- msg = (
77
- "Cannot find any bounding boxes to union. "
78
- + "At least one argument must be a "
79
- + "BoundElement, BoundingBox, or PaddedText."
80
- )
81
- raise ValueError(msg)
82
-
83
- return BoundingBox.merged(*bboxes)
84
-
85
-
86
- def new_bound_union(*blems: SupportsBounds | EtreeElement) -> BoundElement:
87
- """Get the union of the bounding boxes of the given elements.
88
-
89
- :param blems: BoundElements or EtreeElements.
90
- At least one argument must be a BoundElement, BoundingBox, or PaddedText.
91
- :return: the union of all arguments as a BoundElement instance.
92
-
93
- Will used the padded_box attribute of PaddedText instances.
94
- """
95
- group = new_element_union(*blems)
96
- bbox = new_bbox_union(*blems)
97
- return BoundElement(group, bbox)
@@ -1,11 +0,0 @@
1
- """Test configuration for pytest.
2
-
3
- :author: Shay Hill
4
- :created: 7/2/2019
5
- """
6
-
7
-
8
- def pytest_assertrepr_compare(config, op, left, right):
9
- """See full error diffs."""
10
- if op in ("==", "!="):
11
- return [f"{left} {op} {right}"]
File without changes