svg-ultralight 0.32.2__py3-none-any.whl → 0.33.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of svg-ultralight might be problematic. Click here for more details.
- svg_ultralight/__init__.py +9 -3
- svg_ultralight/animate.py +2 -2
- svg_ultralight/layout.py +1 -1
- svg_ultralight/metadata.py +2 -2
- svg_ultralight/query.py +80 -26
- svg_ultralight/transformations.py +3 -0
- {svg_ultralight-0.32.2.dist-info → svg_ultralight-0.33.0.dist-info}/METADATA +5 -5
- {svg_ultralight-0.32.2.dist-info → svg_ultralight-0.33.0.dist-info}/RECORD +10 -11
- {svg_ultralight-0.32.2.dist-info → svg_ultralight-0.33.0.dist-info}/WHEEL +1 -1
- svg_ultralight/import_svg.py +0 -50
- {svg_ultralight-0.32.2.dist-info → svg_ultralight-0.33.0.dist-info}/top_level.txt +0 -0
svg_ultralight/__init__.py
CHANGED
|
@@ -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",
|
svg_ultralight/animate.py
CHANGED
|
@@ -34,7 +34,7 @@ def write_gif(
|
|
|
34
34
|
:param loop: how many times to loop gif. 0 -> forever
|
|
35
35
|
:effects: write file to gif
|
|
36
36
|
"""
|
|
37
|
-
images = [Image.open(x) for x in pngs]
|
|
38
|
-
images[0].save(
|
|
37
|
+
images = [Image.open(x) for x in pngs]
|
|
38
|
+
images[0].save(
|
|
39
39
|
gif, save_all=True, append_images=images[1:], duration=duration, loop=loop
|
|
40
40
|
)
|
svg_ultralight/layout.py
CHANGED
|
@@ -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)
|
svg_ultralight/metadata.py
CHANGED
|
@@ -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
|
|
svg_ultralight/query.py
CHANGED
|
@@ -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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: svg-ultralight
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.0
|
|
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
|
|
@@ -11,10 +11,10 @@ Requires-Dist: pillow
|
|
|
11
11
|
Requires-Dist: paragraphs
|
|
12
12
|
Requires-Dist: types-lxml
|
|
13
13
|
Provides-Extra: dev
|
|
14
|
-
Requires-Dist: pytest
|
|
15
|
-
Requires-Dist: commitizen
|
|
16
|
-
Requires-Dist: pre-commit
|
|
17
|
-
Requires-Dist: tox
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: commitizen; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
17
|
+
Requires-Dist: tox; extra == "dev"
|
|
18
18
|
|
|
19
19
|
# svg_ultralight
|
|
20
20
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
svg_ultralight/__init__.py,sha256=
|
|
2
|
-
svg_ultralight/animate.py,sha256=
|
|
3
|
-
svg_ultralight/import_svg.py,sha256=TVLbinch2g0RN-OlWyGXoQEO6vc2I_LsMXpOchwTJzE,1548
|
|
1
|
+
svg_ultralight/__init__.py,sha256=RNm7gHwJoffbMEbT4zx3Z5xmhvfvRnkvswA5IVlzoxw,2360
|
|
2
|
+
svg_ultralight/animate.py,sha256=JSrBm-59BcNXDF0cGgl4-C89eBunjevZnwZxIWt48TU,1112
|
|
4
3
|
svg_ultralight/inkscape.py,sha256=M8yTxXOu4NlXnhsMycvEJiIDpnDeiZ_bZakJBM38ZoU,9152
|
|
5
|
-
svg_ultralight/layout.py,sha256=
|
|
4
|
+
svg_ultralight/layout.py,sha256=TTETT_8WLBXnQxDGXdAeczCFN5pFo5kKY3Q6zv4FPX4,12238
|
|
6
5
|
svg_ultralight/main.py,sha256=6oNkZfD27UMdP-oYqp5agS_IGcYb8NkUZwM9Zdyb3SA,7287
|
|
7
|
-
svg_ultralight/metadata.py,sha256=
|
|
6
|
+
svg_ultralight/metadata.py,sha256=xR3ObM0QV7OQ90IKvfigR5B6e0JW6GGVGvTlL5NswWI,4211
|
|
8
7
|
svg_ultralight/nsmap.py,sha256=y63upO78Rr-JJT56RWWZuyrsILh6HPoY4GhbYnK1A0g,1244
|
|
9
8
|
svg_ultralight/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
svg_ultralight/query.py,sha256=
|
|
9
|
+
svg_ultralight/query.py,sha256=F_yl-zbNQSOFxiuHT4qZ0LqLoYdD36BGTREJady5sBc,9430
|
|
11
10
|
svg_ultralight/root_elements.py,sha256=pt9J6mPrnoTAZVF6vKTZoM_o947I8UCj6MbGcD2JUCk,2869
|
|
12
11
|
svg_ultralight/string_conversion.py,sha256=WEmpf75RJmJ2lfJluagAz2wPsz6wM8XvTEwkq4U0vEc,7353
|
|
13
|
-
svg_ultralight/transformations.py,sha256=
|
|
12
|
+
svg_ultralight/transformations.py,sha256=7-6NNh6xZ45mM_933fMuQfRGpI7q9Qrt5qse9e6FUek,4036
|
|
14
13
|
svg_ultralight/unit_conversion.py,sha256=g07nhzXdjPvGcJmkhLdFbeDLrSmbI8uFoVgPo7G62Bg,9258
|
|
15
14
|
svg_ultralight/bounding_boxes/__init__.py,sha256=qUEn3r4s-1QNHaguhWhhaNfdP4tl_B6YEqxtiTFuzhQ,78
|
|
16
15
|
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=YMClhdekeYbzD_ijXDAer-H3moWKN3lUNnZs1UNFFKc,3458
|
|
@@ -23,7 +22,7 @@ svg_ultralight/constructors/__init__.py,sha256=YcnO0iBQc19aL8Iemw0Y452MBMBIT2AN5
|
|
|
23
22
|
svg_ultralight/constructors/new_element.py,sha256=VtMz9sPn9rMk6rui5Poysy3vezlOaS-tGIcGbu-SXmY,3406
|
|
24
23
|
svg_ultralight/strings/__init__.py,sha256=Zalrf-ThFz7b7xKELx5lb2gOlBgV-6jk_k_EeSdVCVk,295
|
|
25
24
|
svg_ultralight/strings/svg_strings.py,sha256=RYKMxOHq9abbZyGcFqsElBGLrBX-EjjNxln3s_ibi30,1296
|
|
26
|
-
svg_ultralight-0.
|
|
27
|
-
svg_ultralight-0.
|
|
28
|
-
svg_ultralight-0.
|
|
29
|
-
svg_ultralight-0.
|
|
25
|
+
svg_ultralight-0.33.0.dist-info/METADATA,sha256=dQ7MJsfoX8zthow6dSWRi4onL4izCdqCnX2IhSVn1aE,8867
|
|
26
|
+
svg_ultralight-0.33.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
27
|
+
svg_ultralight-0.33.0.dist-info/top_level.txt,sha256=se-6yqM_0Yg5orJKvKWdjQZ4iR4G_EjhL7oRgju-fdY,15
|
|
28
|
+
svg_ultralight-0.33.0.dist-info/RECORD,,
|
svg_ultralight/import_svg.py
DELETED
|
@@ -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)
|
|
File without changes
|