svg-ultralight 0.32.2__tar.gz → 0.33.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.32.2 → svg_ultralight-0.33.0}/.pre-commit-config.yaml +10 -34
- {svg_ultralight-0.32.2/src/svg_ultralight.egg-info → svg_ultralight-0.33.0}/PKG-INFO +1 -1
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/pyproject.toml +2 -2
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/__init__.py +9 -3
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/animate.py +2 -2
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/layout.py +1 -1
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/metadata.py +2 -2
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/query.py +80 -26
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/transformations.py +3 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0/src/svg_ultralight.egg-info}/PKG-INFO +1 -1
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight.egg-info/SOURCES.txt +1 -4
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_matrices.py +5 -3
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_queries.py +49 -14
- svg_ultralight-0.32.2/src/svg_ultralight/import_svg.py +0 -50
- svg_ultralight-0.32.2/tests/resources/arrow.svg +0 -21
- svg_ultralight-0.32.2/tests/test_import_svg.py +0 -32
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/.gitignore +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/README.md +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/setup.cfg +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/constructors/__init__.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/constructors/new_element.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/inkscape.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/main.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/nsmap.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/py.typed +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/root_elements.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/string_conversion.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/strings/__init__.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/unit_conversion.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight.egg-info/requires.txt +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/__init__.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/conftest.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_bounding.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_inkscape.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_layout.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_metadata.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_new_element.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_root_elements.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_string_conversion.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tests/test_svg_ultralight.py +0 -0
- {svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/tox.ini +0 -0
|
@@ -6,7 +6,7 @@ exclude: "tests"
|
|
|
6
6
|
repos:
|
|
7
7
|
|
|
8
8
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
9
|
-
rev:
|
|
9
|
+
rev: v5.0.0
|
|
10
10
|
hooks:
|
|
11
11
|
- id: check-added-large-files
|
|
12
12
|
- id: check-ast
|
|
@@ -42,7 +42,7 @@ repos:
|
|
|
42
42
|
# files: .pre-commit-config.yaml
|
|
43
43
|
|
|
44
44
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
45
|
-
rev: v1.
|
|
45
|
+
rev: v1.13.0
|
|
46
46
|
hooks:
|
|
47
47
|
- id: mypy
|
|
48
48
|
name: mypy
|
|
@@ -64,14 +64,14 @@ repos:
|
|
|
64
64
|
args: ["--profile", "black", "--filter-files", "--combine-as"]
|
|
65
65
|
|
|
66
66
|
- repo: https://github.com/psf/black
|
|
67
|
-
rev: 24.
|
|
67
|
+
rev: 24.10.0
|
|
68
68
|
hooks:
|
|
69
69
|
- id: black
|
|
70
70
|
language_version: python3.9
|
|
71
71
|
args: ["--skip-magic-trailing-comma"]
|
|
72
72
|
|
|
73
73
|
- repo: https://github.com/asottile/pyupgrade
|
|
74
|
-
rev: v3.
|
|
74
|
+
rev: v3.19.0
|
|
75
75
|
hooks:
|
|
76
76
|
- args:
|
|
77
77
|
- --py39-plus
|
|
@@ -82,33 +82,6 @@ repos:
|
|
|
82
82
|
hooks:
|
|
83
83
|
- id: remove-tabs
|
|
84
84
|
|
|
85
|
-
# - repo: https://github.com/commitizen-tools/commitizen
|
|
86
|
-
# rev: v2.40.0
|
|
87
|
-
# hooks:
|
|
88
|
-
# - id: commitizen
|
|
89
|
-
|
|
90
|
-
# Pylint is still broken in Python 3.12
|
|
91
|
-
# - repo: https://github.com/pre-commit/mirrors-pylint
|
|
92
|
-
# rev: v3.0.0a5
|
|
93
|
-
# hooks:
|
|
94
|
-
# - id: pylint
|
|
95
|
-
# exclude: "tests"
|
|
96
|
-
# name: pylint
|
|
97
|
-
# args:
|
|
98
|
-
# - --good-names=i,j,_,f,x,y,x2,y2,cx,cy,x_,y_,r,g,b,e,get_viewBox_str
|
|
99
|
-
# - --disable=protected-access
|
|
100
|
-
# - --disable=no-member
|
|
101
|
-
# - --disable=import-error
|
|
102
|
-
# - --disable=no-name-in-module
|
|
103
|
-
# - --disable=too-many-arguments
|
|
104
|
-
# - --disable=too-many-instance-attributes
|
|
105
|
-
# - --disable=unnecessary-ellipsis
|
|
106
|
-
# - --load-plugins=pylint.extensions.docparams
|
|
107
|
-
# - --accept-no-param-doc=n
|
|
108
|
-
# - --accept-no-raise-doc=n
|
|
109
|
-
# - --accept-no-return-doc=n
|
|
110
|
-
# - --accept-no-yields-doc=n
|
|
111
|
-
|
|
112
85
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
|
113
86
|
# ignores
|
|
114
87
|
# ANN101 Missing type annotation for self in method
|
|
@@ -134,18 +107,21 @@ repos:
|
|
|
134
107
|
# D407 [*] Missing dashed underline after section ("Attributes")
|
|
135
108
|
# D406 [*] Section name should end with a newline ("Attributes")
|
|
136
109
|
# S320 Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks
|
|
137
|
-
|
|
110
|
+
# S301 don't use pickle
|
|
111
|
+
# B028 wants explicit stacklevel on warn
|
|
112
|
+
# BLE001 Use of `except Exception:` detected
|
|
113
|
+
rev: 'v0.7.4'
|
|
138
114
|
hooks:
|
|
139
115
|
- id: ruff
|
|
140
116
|
exclude: "tests"
|
|
141
117
|
args:
|
|
142
118
|
- --target-version=py39
|
|
143
119
|
- --select=ALL
|
|
144
|
-
- --ignore=ANN101,ANN102,ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,D413,D407,D406,S320
|
|
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
|
|
145
121
|
# - --fix
|
|
146
122
|
|
|
147
123
|
# reads pyproject.toml for additional config
|
|
148
124
|
- repo: https://github.com/RobertCraigie/pyright-python
|
|
149
|
-
rev: v1.1.
|
|
125
|
+
rev: v1.1.389
|
|
150
126
|
hooks:
|
|
151
127
|
- id: pyright
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "svg-ultralight"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.33.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" }
|
|
@@ -37,7 +37,7 @@ legacy_tox_ini = """
|
|
|
37
37
|
|
|
38
38
|
[tool.commitizen]
|
|
39
39
|
name = "cz_conventional_commits"
|
|
40
|
-
version = "0.
|
|
40
|
+
version = "0.33.0"
|
|
41
41
|
tag_format = "$version"
|
|
42
42
|
version_files = ["pyproject.toml:^version"]
|
|
43
43
|
annotated_tag = true
|
|
@@ -20,7 +20,6 @@ from svg_ultralight.constructors.new_element import (
|
|
|
20
20
|
new_sub_element,
|
|
21
21
|
update_element,
|
|
22
22
|
)
|
|
23
|
-
from svg_ultralight.import_svg import import_svg
|
|
24
23
|
from svg_ultralight.inkscape import (
|
|
25
24
|
write_pdf,
|
|
26
25
|
write_pdf_from_svg,
|
|
@@ -31,7 +30,12 @@ from svg_ultralight.inkscape import (
|
|
|
31
30
|
from svg_ultralight.main import new_svg_root, write_svg
|
|
32
31
|
from svg_ultralight.metadata import new_metadata
|
|
33
32
|
from svg_ultralight.nsmap import NSMAP, new_qname
|
|
34
|
-
from svg_ultralight.query import
|
|
33
|
+
from svg_ultralight.query import (
|
|
34
|
+
get_bounding_box,
|
|
35
|
+
get_bounding_boxes,
|
|
36
|
+
pad_text,
|
|
37
|
+
clear_svg_ultralight_cache,
|
|
38
|
+
)
|
|
35
39
|
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
36
40
|
from svg_ultralight.string_conversion import (
|
|
37
41
|
format_attr_dict,
|
|
@@ -53,12 +57,14 @@ __all__ = [
|
|
|
53
57
|
"NSMAP",
|
|
54
58
|
"PaddedText",
|
|
55
59
|
"SupportsBounds",
|
|
60
|
+
"clear_svg_ultralight_cache",
|
|
56
61
|
"deepcopy_element",
|
|
57
62
|
"format_attr_dict",
|
|
58
63
|
"format_number",
|
|
59
64
|
"format_numbers",
|
|
60
65
|
"format_numbers_in_string",
|
|
61
|
-
"
|
|
66
|
+
"get_bounding_box",
|
|
67
|
+
"get_bounding_boxes",
|
|
62
68
|
"mat_apply",
|
|
63
69
|
"mat_dot",
|
|
64
70
|
"mat_invert",
|
|
@@ -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]
|
|
38
|
+
images[0].save(
|
|
39
39
|
gif, save_all=True, append_images=images[1:], duration=duration, loop=loop
|
|
40
40
|
)
|
|
@@ -99,7 +99,7 @@ def _infer_scale(
|
|
|
99
99
|
* print_h == 0 / viewbox_h > 0
|
|
100
100
|
|
|
101
101
|
The print area is invalid, but there is special handling for this. Interpret
|
|
102
|
-
viewbox units as print_w.native_unit and
|
|
102
|
+
viewbox units as print_w.native_unit and determe print area from viewbox area 1
|
|
103
103
|
to 1.
|
|
104
104
|
|
|
105
105
|
>>> _infer_scale(Measurement("in"), Measurement("in"), 1, 2)
|
|
@@ -65,8 +65,8 @@ def _wrap_bag(title: str) -> EtreeElement:
|
|
|
65
65
|
"""
|
|
66
66
|
items = title.split(",")
|
|
67
67
|
agent = new_element(new_qname("rdf", "Bag"))
|
|
68
|
-
for
|
|
69
|
-
_ = new_sub_element(agent, new_qname("rdf", "li"), text=
|
|
68
|
+
for title_item in items:
|
|
69
|
+
_ = new_sub_element(agent, new_qname("rdf", "li"), text=title_item)
|
|
70
70
|
return agent
|
|
71
71
|
|
|
72
72
|
|
|
@@ -11,24 +11,36 @@ business card). Getting bounding boxes from Inkscape is not exceptionally fast.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import hashlib
|
|
14
15
|
import os
|
|
16
|
+
import pickle
|
|
15
17
|
import re
|
|
16
18
|
import uuid
|
|
19
|
+
from contextlib import suppress
|
|
17
20
|
from copy import deepcopy
|
|
21
|
+
from pathlib import Path
|
|
18
22
|
from subprocess import PIPE, Popen
|
|
19
|
-
from tempfile import NamedTemporaryFile
|
|
23
|
+
from tempfile import NamedTemporaryFile, TemporaryFile
|
|
20
24
|
from typing import TYPE_CHECKING
|
|
25
|
+
from warnings import warn
|
|
26
|
+
|
|
27
|
+
from lxml import etree
|
|
21
28
|
|
|
22
29
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
23
30
|
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
24
31
|
from svg_ultralight.main import new_svg_root, write_svg
|
|
25
32
|
|
|
26
33
|
if TYPE_CHECKING:
|
|
27
|
-
from pathlib import Path
|
|
28
34
|
|
|
29
35
|
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
with TemporaryFile() as f:
|
|
39
|
+
_CACHE_DIR = Path(f.name).parent / "svg_ultralight_cache"
|
|
40
|
+
|
|
41
|
+
_CACHE_DIR.mkdir(exist_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
def _fill_ids(*elem_args: EtreeElement) -> None:
|
|
33
45
|
"""Set the id attribute of an element and all its children. Keep existing ids.
|
|
34
46
|
|
|
@@ -66,8 +78,8 @@ def _envelop_copies(*elem_args: EtreeElement) -> EtreeElement:
|
|
|
66
78
|
:param elem_args: one or more etree elements
|
|
67
79
|
:return: an etree element enveloping copies of elem_args with all views normalized
|
|
68
80
|
"""
|
|
69
|
-
envelope = new_svg_root(0, 0, 1, 1
|
|
70
|
-
envelope.extend(deepcopy(e) for e in elem_args)
|
|
81
|
+
envelope = new_svg_root(0, 0, 1, 1)
|
|
82
|
+
envelope.extend([deepcopy(e) for e in elem_args])
|
|
71
83
|
_normalize_views(envelope)
|
|
72
84
|
return envelope
|
|
73
85
|
|
|
@@ -106,6 +118,8 @@ def map_ids_to_bounding_boxes(
|
|
|
106
118
|
a (0, 0, 1, 1) root. This will put the boxes where you'd expect them to be, no
|
|
107
119
|
matter what root you use.
|
|
108
120
|
"""
|
|
121
|
+
if not elem_args:
|
|
122
|
+
return {}
|
|
109
123
|
_fill_ids(*elem_args)
|
|
110
124
|
envelope = _envelop_copies(*elem_args)
|
|
111
125
|
|
|
@@ -123,40 +137,82 @@ def map_ids_to_bounding_boxes(
|
|
|
123
137
|
return id2bbox
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def
|
|
140
|
+
def _hash_elem(elem: EtreeElement) -> str:
|
|
141
|
+
"""Hash an EtreeElement.
|
|
142
|
+
|
|
143
|
+
Will match identical (excepting id) elements.
|
|
144
|
+
"""
|
|
145
|
+
elem_copy = deepcopy(elem)
|
|
146
|
+
with suppress(KeyError):
|
|
147
|
+
_ = elem_copy.attrib.pop("id")
|
|
148
|
+
hash_object = hashlib.sha256(etree.tostring(elem_copy))
|
|
149
|
+
return hash_object.hexdigest()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _try_bbox_cache(elem_hash: str) -> BoundingBox | None:
|
|
153
|
+
"""Try to load a cached bounding box."""
|
|
154
|
+
cache_path = _CACHE_DIR / elem_hash
|
|
155
|
+
if not cache_path.exists():
|
|
156
|
+
return None
|
|
157
|
+
try:
|
|
158
|
+
with cache_path.open("rb") as f:
|
|
159
|
+
return pickle.load(f)
|
|
160
|
+
except (EOFError, pickle.UnpicklingError) as e:
|
|
161
|
+
msg = f"Error loading cache file {cache_path}: {e}"
|
|
162
|
+
warn(msg)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
msg = f"Unexpected error loading cache file {cache_path}: {e}"
|
|
165
|
+
warn(msg)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_bounding_boxes(
|
|
127
170
|
inkscape: str | Path, *elem_args: EtreeElement
|
|
128
|
-
) ->
|
|
171
|
+
) -> tuple[BoundingBox, ...]:
|
|
129
172
|
r"""Get bounding box around a single element (or multiple elements).
|
|
130
173
|
|
|
131
174
|
:param inkscape: path to an inkscape executable on your local file system
|
|
132
175
|
IMPORTANT: path cannot end with ``.exe``.
|
|
133
176
|
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
134
177
|
:param elem_args: xml elements
|
|
135
|
-
:return: a BoundingBox instance around a
|
|
136
|
-
instances if multiple elem_args are passed.
|
|
178
|
+
:return: a BoundingBox instance around a each elem_arg
|
|
137
179
|
|
|
138
180
|
This will work most of the time, but if you're missing an nsmap, you'll need to
|
|
139
181
|
create an entire xml file with a custom nsmap (using
|
|
140
182
|
`svg_ultralight.new_svg_root`) then call `map_ids_to_bounding_boxes` directly.
|
|
141
183
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if
|
|
145
|
-
return
|
|
146
|
-
return tuple(bboxes)
|
|
184
|
+
elem2hash = {elem: _hash_elem(elem) for elem in elem_args}
|
|
185
|
+
cached = [_try_bbox_cache(h) for h in elem2hash.values()]
|
|
186
|
+
if None not in cached:
|
|
187
|
+
return tuple(filter(None, cached))
|
|
147
188
|
|
|
189
|
+
hash2bbox = {h: c for h, c in zip(elem2hash.values(), cached) if c is not None}
|
|
190
|
+
remainder = [e for e, c in zip(elem_args, cached) if c is None]
|
|
191
|
+
id2bbox = map_ids_to_bounding_boxes(inkscape, *remainder)
|
|
192
|
+
for elem in remainder:
|
|
193
|
+
hash_ = elem2hash[elem]
|
|
194
|
+
hash2bbox[hash_] = id2bbox[elem.attrib["id"]]
|
|
195
|
+
with (_CACHE_DIR / hash_).open("wb") as f:
|
|
196
|
+
pickle.dump(hash2bbox[hash_], f)
|
|
197
|
+
return tuple(hash2bbox[h] for h in elem2hash.values())
|
|
148
198
|
|
|
149
|
-
def _replace_text(text_elem: EtreeElement, new_text: str) -> None:
|
|
150
|
-
"""Replace the text in a text element.
|
|
151
199
|
|
|
152
|
-
|
|
153
|
-
|
|
200
|
+
def get_bounding_box(inkscape: str | Path, elem: EtreeElement) -> BoundingBox:
|
|
201
|
+
r"""Get bounding box around a single element.
|
|
154
202
|
|
|
155
|
-
|
|
203
|
+
:param inkscape: path to an inkscape executable on your local file system
|
|
204
|
+
IMPORTANT: path cannot end with ``.exe``.
|
|
205
|
+
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
206
|
+
:param elem: xml element
|
|
207
|
+
:return: a BoundingBox instance around a single elem
|
|
156
208
|
"""
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
209
|
+
return get_bounding_boxes(inkscape, elem)[0]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def clear_svg_ultralight_cache() -> None:
|
|
213
|
+
"""Clear all cached bounding boxes."""
|
|
214
|
+
for cache_file in _CACHE_DIR.glob("*"):
|
|
215
|
+
cache_file.unlink()
|
|
160
216
|
|
|
161
217
|
|
|
162
218
|
def pad_text(
|
|
@@ -179,12 +235,10 @@ def pad_text(
|
|
|
179
235
|
_ = rmargin_ref.attrib.pop("id", None)
|
|
180
236
|
_ = capline_ref.attrib.pop("id", None)
|
|
181
237
|
rmargin_ref.attrib["text-anchor"] = "end"
|
|
182
|
-
|
|
183
|
-
id2bbox = map_ids_to_bounding_boxes(inkscape, text_elem, rmargin_ref, capline_ref)
|
|
238
|
+
capline_ref.text = capline_reference_char
|
|
184
239
|
|
|
185
|
-
|
|
186
|
-
rmargin_bbox =
|
|
187
|
-
capline_bbox = id2bbox[capline_ref.attrib["id"]]
|
|
240
|
+
bboxes = get_bounding_boxes(inkscape, text_elem, rmargin_ref, capline_ref)
|
|
241
|
+
bbox, rmargin_bbox, capline_bbox = bboxes
|
|
188
242
|
|
|
189
243
|
tpad = bbox.y - capline_bbox.y
|
|
190
244
|
rpad = -rmargin_bbox.x2
|
|
@@ -63,6 +63,9 @@ def mat_invert(tmat: _Matrix) -> _Matrix:
|
|
|
63
63
|
"""Invert a 2D transformation matrix in svg format."""
|
|
64
64
|
a, b, c, d, e, f = tmat
|
|
65
65
|
det = a * d - b * c
|
|
66
|
+
if det == 0:
|
|
67
|
+
msg = "Matrix is not invertible"
|
|
68
|
+
raise ValueError(msg)
|
|
66
69
|
return (
|
|
67
70
|
d / det,
|
|
68
71
|
-b / det,
|
|
@@ -5,7 +5,6 @@ pyproject.toml
|
|
|
5
5
|
tox.ini
|
|
6
6
|
src/svg_ultralight/__init__.py
|
|
7
7
|
src/svg_ultralight/animate.py
|
|
8
|
-
src/svg_ultralight/import_svg.py
|
|
9
8
|
src/svg_ultralight/inkscape.py
|
|
10
9
|
src/svg_ultralight/layout.py
|
|
11
10
|
src/svg_ultralight/main.py
|
|
@@ -36,7 +35,6 @@ src/svg_ultralight/strings/svg_strings.py
|
|
|
36
35
|
tests/__init__.py
|
|
37
36
|
tests/conftest.py
|
|
38
37
|
tests/test_bounding.py
|
|
39
|
-
tests/test_import_svg.py
|
|
40
38
|
tests/test_inkscape.py
|
|
41
39
|
tests/test_layout.py
|
|
42
40
|
tests/test_matrices.py
|
|
@@ -45,5 +43,4 @@ tests/test_new_element.py
|
|
|
45
43
|
tests/test_queries.py
|
|
46
44
|
tests/test_root_elements.py
|
|
47
45
|
tests/test_string_conversion.py
|
|
48
|
-
tests/test_svg_ultralight.py
|
|
49
|
-
tests/resources/arrow.svg
|
|
46
|
+
tests/test_svg_ultralight.py
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from svg_ultralight.transformations import mat_dot, mat_apply, mat_invert
|
|
8
8
|
import random
|
|
9
9
|
import math
|
|
10
|
+
from contextlib import suppress
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TestMat:
|
|
@@ -29,6 +30,7 @@ class TestMat:
|
|
|
29
30
|
random.randint(-10, 10),
|
|
30
31
|
random.randint(-10, 10),
|
|
31
32
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
with suppress(ValueError):
|
|
34
|
+
result = mat_dot(tmat, mat_invert(tmat))
|
|
35
|
+
for x, y in zip(result, identity):
|
|
36
|
+
assert math.isclose(x, y, abs_tol=0.0001)
|
|
@@ -13,8 +13,8 @@ from pathlib import Path
|
|
|
13
13
|
import pytest
|
|
14
14
|
|
|
15
15
|
from svg_ultralight import BoundingBox, new_svg_root
|
|
16
|
-
from svg_ultralight.constructors import
|
|
17
|
-
from svg_ultralight.query import map_ids_to_bounding_boxes,
|
|
16
|
+
from svg_ultralight.constructors import new_sub_element
|
|
17
|
+
from svg_ultralight.query import map_ids_to_bounding_boxes, get_bounding_boxes, get_bounding_box
|
|
18
18
|
|
|
19
19
|
INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
|
|
20
20
|
|
|
@@ -171,6 +171,53 @@ class TestMapIdsToBoundingBoxes:
|
|
|
171
171
|
assert rect2.get("id") in result
|
|
172
172
|
assert rect3.get("id") in result
|
|
173
173
|
|
|
174
|
+
def test_get_bboxes_explicit(self) -> None:
|
|
175
|
+
"""Returns a dict with an entry for each element plus an envelope entry."""
|
|
176
|
+
xml = new_svg_root(10, 20, 160, 19, id="svg1")
|
|
177
|
+
rect1 = new_sub_element(xml, "rect", x=0, y=0, width=16, height=9)
|
|
178
|
+
rect2 = new_sub_element(xml, "rect", x=0, y=0, width=8, height=32)
|
|
179
|
+
rect3 = new_sub_element(xml, "rect", x=0, y=0, width=12, height=18)
|
|
180
|
+
rect4 = new_sub_element(xml, "rect", x=0, y=0, width=12, height=18)
|
|
181
|
+
|
|
182
|
+
result = get_bounding_boxes(INKSCAPE, xml, rect1, rect2, rect3, rect4)
|
|
183
|
+
assert result[0] == BoundingBox(
|
|
184
|
+
_x=0.0,
|
|
185
|
+
_y=0.0,
|
|
186
|
+
_width=16.0,
|
|
187
|
+
_height=32.0,
|
|
188
|
+
_transformation=(1, 0, 0, 1, 0, 0),
|
|
189
|
+
)
|
|
190
|
+
assert result[1] == BoundingBox(
|
|
191
|
+
_x=0.0, _y=0.0, _width=16.0, _height=9.0, _transformation=(1, 0, 0, 1, 0, 0)
|
|
192
|
+
)
|
|
193
|
+
assert result[2] == BoundingBox(
|
|
194
|
+
_x=0.0, _y=0.0, _width=8.0, _height=32.0, _transformation=(1, 0, 0, 1, 0, 0)
|
|
195
|
+
)
|
|
196
|
+
assert result[3] == BoundingBox(
|
|
197
|
+
_x=0.0,
|
|
198
|
+
_y=0.0,
|
|
199
|
+
_width=12.0,
|
|
200
|
+
_height=18.0,
|
|
201
|
+
_transformation=(1, 0, 0, 1, 0, 0),
|
|
202
|
+
)
|
|
203
|
+
assert result[4] == BoundingBox(
|
|
204
|
+
_x=0.0,
|
|
205
|
+
_y=0.0,
|
|
206
|
+
_width=12.0,
|
|
207
|
+
_height=18.0,
|
|
208
|
+
_transformation=(1, 0, 0, 1, 0, 0),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def test_get_bbox_vs_boxes(self) -> None:
|
|
212
|
+
"""Multiple calls to get_bounding_box are equivalent to a single call."""
|
|
213
|
+
xml = new_svg_root(10, 20, 160, 19, id="svg1")
|
|
214
|
+
rect1 = new_sub_element(xml, "rect", x=0, y=0, width=16, height=9)
|
|
215
|
+
rect2 = new_sub_element(xml, "rect", x=0, y=0, width=8, height=32)
|
|
216
|
+
rect3 = new_sub_element(xml, "rect", x=0, y=0, width=12, height=18)
|
|
217
|
+
rect4 = new_sub_element(xml, "rect", x=0, y=0, width=12, height=18)
|
|
218
|
+
elems = (xml, rect1, rect2, rect3, rect4)
|
|
219
|
+
result = get_bounding_boxes(INKSCAPE, *elems)
|
|
220
|
+
assert result == tuple(get_bounding_box(INKSCAPE, e) for e in elems)
|
|
174
221
|
|
|
175
222
|
class TestAlterBoundingBox:
|
|
176
223
|
def test_reverse_width(self) -> None:
|
|
@@ -181,15 +228,3 @@ class TestAlterBoundingBox:
|
|
|
181
228
|
bbox.height = 200
|
|
182
229
|
bbox.height = 40
|
|
183
230
|
assert bbox.transformation == (1, 0, 0, 1, 90, 180)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class TestPadText:
|
|
187
|
-
def test_tspans(self) -> None:
|
|
188
|
-
"""Text in tspan are padded the same as text in text."""
|
|
189
|
-
no_tspan = new_element("text", font_size=100, text="typography")
|
|
190
|
-
in_tspan = new_element("text")
|
|
191
|
-
_ = new_sub_element(in_tspan, "tspan", font_size=100, text="typography")
|
|
192
|
-
padded_with = pad_text(INKSCAPE, no_tspan)
|
|
193
|
-
padded_without = pad_text(INKSCAPE, in_tspan)
|
|
194
|
-
assert padded_with.bbox == padded_without.bbox
|
|
195
|
-
assert padded_with.padded_bbox == padded_without.padded_bbox
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Import an svg file as a BoundElement.
|
|
2
|
-
|
|
3
|
-
:author: Shay Hill
|
|
4
|
-
:created: 2024-05-28
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
10
|
-
|
|
11
|
-
from lxml import etree
|
|
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.constructors import new_element
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
import os
|
|
19
|
-
|
|
20
|
-
from lxml.etree import _Element as EtreeElement # type: ignore
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _get_bounds_from_viewbox(root: EtreeElement) -> BoundingBox:
|
|
24
|
-
"""Get the BoundingBox from the viewbox attribute of the root element.
|
|
25
|
-
|
|
26
|
-
:param root: The root element of the svg.
|
|
27
|
-
:return: The BoundingBox of the svg.
|
|
28
|
-
"""
|
|
29
|
-
viewbox = root.attrib.get("viewBox")
|
|
30
|
-
if viewbox is None:
|
|
31
|
-
msg = "SVG file has no viewBox attribute. Failed to create BoundingBox."
|
|
32
|
-
raise ValueError(msg)
|
|
33
|
-
x, y, width, height = map(float, viewbox.split())
|
|
34
|
-
return BoundingBox(x, y, width, height)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def import_svg(svg: str | os.PathLike[str]) -> BoundElement:
|
|
38
|
-
"""Import an svg file as a BoundElement.
|
|
39
|
-
|
|
40
|
-
:param svg: The path to the svg file.
|
|
41
|
-
:return: The BoundElement representation of the svg.
|
|
42
|
-
|
|
43
|
-
The viewbox of the svg is used to create the BoundingBox of the BoundElement.
|
|
44
|
-
"""
|
|
45
|
-
tree = etree.parse(svg)
|
|
46
|
-
root = tree.getroot()
|
|
47
|
-
bbox = _get_bounds_from_viewbox(root)
|
|
48
|
-
root_as_elem = new_element("g")
|
|
49
|
-
root_as_elem.extend(root)
|
|
50
|
-
return BoundElement(root_as_elem, bbox)
|
|
@@ -1,21 +0,0 @@
|
|
|
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="-3 -3 128 128">
|
|
2
|
-
<defs>
|
|
3
|
-
<clipPath id="clip">
|
|
4
|
-
<path d="M0,122L0,103.7L40.666667,73.2L81.333333,79.3L99.556877,49.231152L122,12.2L122,122Z"/>
|
|
5
|
-
</clipPath>
|
|
6
|
-
<mask id="mask">
|
|
7
|
-
<g>
|
|
8
|
-
<rect x="0" y="0" width="122" height="122" fill="#FFFFFF"/>
|
|
9
|
-
<path d="M0,91.45Q0,91.2 0.2,91.05L37.845867,62.8156Q38.013194,62.690105 38.220037,62.721131L75.903051,68.373583Q76.232274,68.422967 76.40482,68.138266L90.745748,44.475734Q91.004899,44.048135 90.5773,43.788984L78.917555,36.722472Q78.176932,36.273609 78.935969,35.856642L121.240962,12.616967Q122,12.2 121.981585,13.06583L120.955238,61.322866Q120.936823,62.188695 120.1962,61.739833L108.536455,54.67332Q108.108856,54.41417 107.849705,54.841768L86.606939,89.892332Q86.434393,90.177033 86.10517,90.12765L43.526984,83.740922Q43.32014,83.709895 43.152813,83.83539L0.8,115.6Q0,116.2 0,115.2Z" fill="#000000" stroke="#000000" stroke-width="3.1"/>
|
|
10
|
-
</g>
|
|
11
|
-
</mask>
|
|
12
|
-
</defs>
|
|
13
|
-
<g clip-path="url(#clip)" mask="url(#mask)">
|
|
14
|
-
<rect x="0" y="0" width="23.6" height="122" fill="orange"/>
|
|
15
|
-
<rect x="24.6" y="0" width="23.6" height="122" fill="orange"/>
|
|
16
|
-
<rect x="49.2" y="0" width="23.6" height="122" fill="orange"/>
|
|
17
|
-
<rect x="73.8" y="0" width="23.6" height="122" fill="orange"/>
|
|
18
|
-
<rect x="98.4" y="0" width="23.6" height="122" fill="orange"/>
|
|
19
|
-
</g>
|
|
20
|
-
<path d="M0,91.45Q0,91.2 0.2,91.05L37.845867,62.8156Q38.013194,62.690105 38.220037,62.721131L75.903051,68.373583Q76.232274,68.422967 76.40482,68.138266L90.745748,44.475734Q91.004899,44.048135 90.5773,43.788984L78.917555,36.722472Q78.176932,36.273609 78.935969,35.856642L121.240962,12.616967Q122,12.2 121.981585,13.06583L120.955238,61.322866Q120.936823,62.188695 120.1962,61.739833L108.536455,54.67332Q108.108856,54.41417 107.849705,54.841768L86.606939,89.892332Q86.434393,90.177033 86.10517,90.12765L43.526984,83.740922Q43.32014,83.709895 43.152813,83.83539L0.8,115.6Q0,116.2 0,115.2Z" fill="#00bfff" stroke="none"/>
|
|
21
|
-
</svg>
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Test importing an SVG file.
|
|
2
|
-
|
|
3
|
-
:author: Shay Hill
|
|
4
|
-
:created: 2024-05-28
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from svg_ultralight.import_svg import import_svg
|
|
9
|
-
from lxml import etree
|
|
10
|
-
|
|
11
|
-
_TEST_RESOURCES = Path(__file__).parent / "resources"
|
|
12
|
-
_TEST_FILE = _TEST_RESOURCES / "arrow.svg"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TestImportSvg:
|
|
17
|
-
def test_get_bbox(self):
|
|
18
|
-
"""Import an svg file as a BoundElement instance."""
|
|
19
|
-
blem = import_svg(_TEST_FILE)
|
|
20
|
-
assert blem.bbox.x == -3
|
|
21
|
-
assert blem.bbox.y == -3
|
|
22
|
-
assert blem.bbox.width == 128
|
|
23
|
-
assert blem.bbox.height == 128
|
|
24
|
-
|
|
25
|
-
def test_get_geometry(self):
|
|
26
|
-
tree = etree.parse(_TEST_FILE)
|
|
27
|
-
root = tree.getroot()
|
|
28
|
-
blem = import_svg(_TEST_FILE)
|
|
29
|
-
assert [x.tag for x in blem.elem] == [x.tag for x in root]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/__init__.py
RENAMED
|
File without changes
|
{svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py
RENAMED
|
File without changes
|
{svg_ultralight-0.32.2 → svg_ultralight-0.33.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.32.2 → svg_ultralight-0.33.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
|
{svg_ultralight-0.32.2 → svg_ultralight-0.33.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|