svg-ultralight 0.64.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.
- svg_ultralight/__init__.py +112 -0
- svg_ultralight/animate.py +40 -0
- svg_ultralight/attrib_hints.py +14 -0
- svg_ultralight/bounding_boxes/__init__.py +5 -0
- svg_ultralight/bounding_boxes/bound_helpers.py +200 -0
- svg_ultralight/bounding_boxes/padded_text_initializers.py +442 -0
- svg_ultralight/bounding_boxes/supports_bounds.py +167 -0
- svg_ultralight/bounding_boxes/type_bound_collection.py +74 -0
- svg_ultralight/bounding_boxes/type_bound_element.py +68 -0
- svg_ultralight/bounding_boxes/type_bounding_box.py +432 -0
- svg_ultralight/bounding_boxes/type_padded_list.py +208 -0
- svg_ultralight/bounding_boxes/type_padded_text.py +502 -0
- svg_ultralight/constructors/__init__.py +14 -0
- svg_ultralight/constructors/new_element.py +117 -0
- svg_ultralight/font_tools/__init__.py +5 -0
- svg_ultralight/font_tools/comp_results.py +291 -0
- svg_ultralight/font_tools/font_info.py +849 -0
- svg_ultralight/image_ops.py +156 -0
- svg_ultralight/inkscape.py +261 -0
- svg_ultralight/layout.py +291 -0
- svg_ultralight/main.py +183 -0
- svg_ultralight/metadata.py +122 -0
- svg_ultralight/nsmap.py +36 -0
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -0
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +96 -0
- svg_ultralight/string_conversion.py +244 -0
- svg_ultralight/strings/__init__.py +21 -0
- svg_ultralight/strings/svg_strings.py +106 -0
- svg_ultralight/transformations.py +152 -0
- svg_ultralight/unit_conversion.py +247 -0
- svg_ultralight-0.64.0.dist-info/METADATA +208 -0
- svg_ultralight-0.64.0.dist-info/RECORD +35 -0
- svg_ultralight-0.64.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Import functions into the package namespace.
|
|
2
|
+
|
|
3
|
+
:author: ShayHill
|
|
4
|
+
:created: 2019-12-22
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from svg_ultralight.bounding_boxes.bound_helpers import (
|
|
8
|
+
bbox_dict,
|
|
9
|
+
cut_bbox,
|
|
10
|
+
new_bbox_rect,
|
|
11
|
+
new_bbox_union,
|
|
12
|
+
new_bound_union,
|
|
13
|
+
new_element_union,
|
|
14
|
+
pad_bbox,
|
|
15
|
+
parse_bound_element,
|
|
16
|
+
)
|
|
17
|
+
from svg_ultralight.bounding_boxes.padded_text_initializers import (
|
|
18
|
+
pad_text,
|
|
19
|
+
pad_text_ft,
|
|
20
|
+
pad_text_mix,
|
|
21
|
+
wrap_text_ft,
|
|
22
|
+
)
|
|
23
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
24
|
+
from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
|
|
25
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
26
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
27
|
+
from svg_ultralight.bounding_boxes.type_padded_list import PaddedList
|
|
28
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
29
|
+
from svg_ultralight.constructors.new_element import (
|
|
30
|
+
deepcopy_element,
|
|
31
|
+
new_element,
|
|
32
|
+
new_sub_element,
|
|
33
|
+
update_element,
|
|
34
|
+
)
|
|
35
|
+
from svg_ultralight.font_tools.comp_results import check_font_tools_alignment
|
|
36
|
+
from svg_ultralight.inkscape import (
|
|
37
|
+
write_pdf,
|
|
38
|
+
write_pdf_from_svg,
|
|
39
|
+
write_png,
|
|
40
|
+
write_png_from_svg,
|
|
41
|
+
write_root,
|
|
42
|
+
)
|
|
43
|
+
from svg_ultralight.main import new_svg_root, write_svg
|
|
44
|
+
from svg_ultralight.metadata import new_metadata
|
|
45
|
+
from svg_ultralight.nsmap import NSMAP, new_qname
|
|
46
|
+
from svg_ultralight.query import (
|
|
47
|
+
clear_svg_ultralight_cache,
|
|
48
|
+
get_bounding_box,
|
|
49
|
+
get_bounding_boxes,
|
|
50
|
+
)
|
|
51
|
+
from svg_ultralight.read_svg import get_bounding_box_from_root, parse
|
|
52
|
+
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
53
|
+
from svg_ultralight.string_conversion import (
|
|
54
|
+
format_attr_dict,
|
|
55
|
+
format_number,
|
|
56
|
+
format_numbers,
|
|
57
|
+
)
|
|
58
|
+
from svg_ultralight.transformations import (
|
|
59
|
+
mat_apply,
|
|
60
|
+
mat_dot,
|
|
61
|
+
mat_invert,
|
|
62
|
+
transform_element,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"NSMAP",
|
|
67
|
+
"BoundCollection",
|
|
68
|
+
"BoundElement",
|
|
69
|
+
"BoundingBox",
|
|
70
|
+
"PaddedList",
|
|
71
|
+
"PaddedText",
|
|
72
|
+
"SupportsBounds",
|
|
73
|
+
"bbox_dict",
|
|
74
|
+
"check_font_tools_alignment",
|
|
75
|
+
"clear_svg_ultralight_cache",
|
|
76
|
+
"cut_bbox",
|
|
77
|
+
"deepcopy_element",
|
|
78
|
+
"format_attr_dict",
|
|
79
|
+
"format_number",
|
|
80
|
+
"format_numbers",
|
|
81
|
+
"get_bounding_box",
|
|
82
|
+
"get_bounding_box_from_root",
|
|
83
|
+
"get_bounding_boxes",
|
|
84
|
+
"mat_apply",
|
|
85
|
+
"mat_dot",
|
|
86
|
+
"mat_invert",
|
|
87
|
+
"new_bbox_rect",
|
|
88
|
+
"new_bbox_union",
|
|
89
|
+
"new_bound_union",
|
|
90
|
+
"new_element",
|
|
91
|
+
"new_element_union",
|
|
92
|
+
"new_metadata",
|
|
93
|
+
"new_qname",
|
|
94
|
+
"new_sub_element",
|
|
95
|
+
"new_svg_root",
|
|
96
|
+
"new_svg_root_around_bounds",
|
|
97
|
+
"pad_bbox",
|
|
98
|
+
"pad_text",
|
|
99
|
+
"pad_text_ft",
|
|
100
|
+
"pad_text_mix",
|
|
101
|
+
"parse",
|
|
102
|
+
"parse_bound_element",
|
|
103
|
+
"transform_element",
|
|
104
|
+
"update_element",
|
|
105
|
+
"wrap_text_ft",
|
|
106
|
+
"write_pdf",
|
|
107
|
+
"write_pdf_from_svg",
|
|
108
|
+
"write_png",
|
|
109
|
+
"write_png_from_svg",
|
|
110
|
+
"write_root",
|
|
111
|
+
"write_svg",
|
|
112
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""One script to animate a list of pngs.
|
|
2
|
+
|
|
3
|
+
Requires: pillow, which is an optional project dependency.
|
|
4
|
+
|
|
5
|
+
:author: Shay Hill
|
|
6
|
+
:created: 7/26/2020
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from PIL import Image
|
|
13
|
+
except ModuleNotFoundError as exc:
|
|
14
|
+
MSG = "`pip install pillow` to use svg_ultralight.animate module"
|
|
15
|
+
raise ModuleNotFoundError(MSG) from exc
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import os
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def write_gif(
|
|
24
|
+
gif: str | os.PathLike[str],
|
|
25
|
+
pngs: Iterable[str] | Iterable[os.PathLike[str]] | Iterable[str | os.PathLike[str]],
|
|
26
|
+
duration: float = 100,
|
|
27
|
+
loop: int = 0,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Create a gif from a sequence of pngs.
|
|
30
|
+
|
|
31
|
+
:param gif: output filename (include .gif extension)
|
|
32
|
+
:param pngs: png filenames
|
|
33
|
+
:param duration: milliseconds per frame
|
|
34
|
+
:param loop: how many times to loop gif. 0 -> forever
|
|
35
|
+
:effects: write file to gif
|
|
36
|
+
"""
|
|
37
|
+
images = [Image.open(x) for x in pngs]
|
|
38
|
+
images[0].save(
|
|
39
|
+
gif, save_all=True, append_images=images[1:], duration=duration, loop=loop
|
|
40
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Type hints for pass-through arguments to lxml constructors.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2025-07-09
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import TypeAlias
|
|
9
|
+
|
|
10
|
+
# Types svg_ultralight can format to pass through to lxml constructors.
|
|
11
|
+
ElemAttrib: TypeAlias = str | float | None
|
|
12
|
+
|
|
13
|
+
# Type for an optional dictionary of element attributes.
|
|
14
|
+
OptionalElemAttribMapping: TypeAlias = Mapping[str, ElemAttrib] | None
|
|
@@ -0,0 +1,200 @@
|
|
|
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 import etree
|
|
12
|
+
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
13
|
+
|
|
14
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
15
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
16
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox, HasBoundingBox
|
|
17
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
18
|
+
from svg_ultralight.constructors import new_element
|
|
19
|
+
from svg_ultralight.layout import PadArg, expand_pad_arg
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
25
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
26
|
+
|
|
27
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def new_element_union(
|
|
31
|
+
*elems: EtreeElement | SupportsBounds, **attributes: ElemAttrib
|
|
32
|
+
) -> EtreeElement:
|
|
33
|
+
"""Get the union of any elements found in the given arguments.
|
|
34
|
+
|
|
35
|
+
:param elems: BoundElements, PaddedTexts, or EtreeElements.
|
|
36
|
+
Other arguments will be ignored.
|
|
37
|
+
:return: a new group element containing all elements.
|
|
38
|
+
|
|
39
|
+
This does not support consolidating attributes. E.g., if all elements have the
|
|
40
|
+
same fill color, this will not be recognized and consilidated into a single
|
|
41
|
+
attribute for the group. Too many attributes change their behavior when applied
|
|
42
|
+
to a group.
|
|
43
|
+
"""
|
|
44
|
+
elements_found: list[EtreeElement] = []
|
|
45
|
+
for elem in elems:
|
|
46
|
+
if isinstance(elem, (BoundElement, PaddedText)):
|
|
47
|
+
elements_found.append(elem.elem)
|
|
48
|
+
elif isinstance(elem, EtreeElement):
|
|
49
|
+
elements_found.append(elem)
|
|
50
|
+
|
|
51
|
+
if not elements_found:
|
|
52
|
+
msg = (
|
|
53
|
+
"Cannot find any elements to union. "
|
|
54
|
+
+ "At least one argument must be a "
|
|
55
|
+
+ "BoundElement, PaddedText, or EtreeElement."
|
|
56
|
+
)
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
group = new_element("g", **attributes)
|
|
59
|
+
group.extend(elements_found)
|
|
60
|
+
return group
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
|
|
64
|
+
"""Get the union of the bounding boxes of the given elements.
|
|
65
|
+
|
|
66
|
+
:param blems: BoundElements, BoundingBoxes, or PaddedTexts.
|
|
67
|
+
Other arguments will be ignored.
|
|
68
|
+
:return: the union of all bounding boxes as a BoundingBox instance.
|
|
69
|
+
|
|
70
|
+
Will used the padded_box attribute of PaddedText instances.
|
|
71
|
+
"""
|
|
72
|
+
bboxes = [x.bbox for x in blems if isinstance(x, HasBoundingBox)]
|
|
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.union(*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)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cut_bbox(
|
|
99
|
+
bbox: SupportsBounds,
|
|
100
|
+
*,
|
|
101
|
+
x: float | None = None,
|
|
102
|
+
y: float | None = None,
|
|
103
|
+
x2: float | None = None,
|
|
104
|
+
y2: float | None = None,
|
|
105
|
+
) -> BoundingBox:
|
|
106
|
+
"""Return a new bounding box with updated limits.
|
|
107
|
+
|
|
108
|
+
:param bbox: the original bounding box or bounded element.
|
|
109
|
+
:param x: the new x-coordinate.
|
|
110
|
+
:param y: the new y-coordinate.
|
|
111
|
+
:param x2: the new x2-coordinate.
|
|
112
|
+
:param y2: the new y2-coordinate.
|
|
113
|
+
:return: a new bounding box with the updated limits.
|
|
114
|
+
"""
|
|
115
|
+
x = bbox.x if x is None else x
|
|
116
|
+
y = bbox.y if y is None else y
|
|
117
|
+
x2 = bbox.x2 if x2 is None else x2
|
|
118
|
+
y2 = bbox.y2 if y2 is None else y2
|
|
119
|
+
x, x2 = sorted((x, x2))
|
|
120
|
+
y, y2 = sorted((y, y2))
|
|
121
|
+
width = x2 - x
|
|
122
|
+
height = y2 - y
|
|
123
|
+
return BoundingBox(x, y, width, height)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def pad_bbox(bbox: SupportsBounds, pad: PadArg) -> BoundingBox:
|
|
127
|
+
"""Return a new bounding box with padding.
|
|
128
|
+
|
|
129
|
+
:param bbox: the original bounding box or bounded element.
|
|
130
|
+
:param pad: the padding to apply.
|
|
131
|
+
If a single number, the same padding will be applied to all sides.
|
|
132
|
+
If a tuple, will be applied per css rules.
|
|
133
|
+
len = 1 : 0, 0, 0, 0
|
|
134
|
+
len = 2 : 0, 1, 0, 1
|
|
135
|
+
len = 3 : 0, 1, 2, 1
|
|
136
|
+
len = 4 : 0, 1, 2, 3
|
|
137
|
+
:return: a new bounding box with padding applied.
|
|
138
|
+
"""
|
|
139
|
+
top, right, bottom, left = expand_pad_arg(pad)
|
|
140
|
+
return cut_bbox(
|
|
141
|
+
bbox, x=bbox.x - left, y=bbox.y - top, x2=bbox.x2 + right, y2=bbox.y2 + bottom
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def bbox_dict(bbox: SupportsBounds) -> dict[str, float]:
|
|
146
|
+
"""Return a dictionary representation of a bounding box.
|
|
147
|
+
|
|
148
|
+
:param bbox: the bounding box or bound element from which to extract dimensions.
|
|
149
|
+
:return: a dictionary with keys x, y, width, and height.
|
|
150
|
+
"""
|
|
151
|
+
return {"x": bbox.x, "y": bbox.y, "width": bbox.width, "height": bbox.height}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def new_bbox_rect(bbox: BoundingBox, **kwargs: float | str) -> EtreeElement:
|
|
155
|
+
"""Return a new rect element with the same dimensions as the bounding box.
|
|
156
|
+
|
|
157
|
+
:param bbox: the bounding box or bound element from which to extract dimensions.
|
|
158
|
+
:param kwargs: additional attributes for the rect element.
|
|
159
|
+
"""
|
|
160
|
+
return new_element("rect", **bbox_dict(bbox), **kwargs)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
|
|
164
|
+
"""Return the view box of an element as a tuple of floats.
|
|
165
|
+
|
|
166
|
+
:param elem: the element from which to extract the view box.
|
|
167
|
+
:return: a tuple of floats representing the view box.
|
|
168
|
+
|
|
169
|
+
This will work on svg files created by this library and some others. Not all svg
|
|
170
|
+
files have a viewBox attribute.
|
|
171
|
+
"""
|
|
172
|
+
view_box = elem.get("viewBox")
|
|
173
|
+
if view_box is None:
|
|
174
|
+
msg = "Element does not have a viewBox attribute."
|
|
175
|
+
raise ValueError(msg)
|
|
176
|
+
x, y, width, height = map(float, view_box.split())
|
|
177
|
+
return x, y, width, height
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_bound_element(svg_file: str | os.PathLike[str]) -> BoundElement:
|
|
181
|
+
"""Import an element as a BoundElement.
|
|
182
|
+
|
|
183
|
+
:param elem: the element to import.
|
|
184
|
+
:return: a BoundElement instance.
|
|
185
|
+
:raises ValueError: if the SVG file does not contain any elements.
|
|
186
|
+
|
|
187
|
+
This will work on any svg file that has a root element with a viewBox attribute.
|
|
188
|
+
That's any svg created by this library and most others.
|
|
189
|
+
"""
|
|
190
|
+
tree = etree.parse(svg_file)
|
|
191
|
+
root = tree.getroot()
|
|
192
|
+
if len(root) == 0:
|
|
193
|
+
msg = "SVG file does not contain any elements."
|
|
194
|
+
raise ValueError(msg)
|
|
195
|
+
elem = new_element("g")
|
|
196
|
+
elem.extend(list(root))
|
|
197
|
+
if len(elem) == 1:
|
|
198
|
+
elem = elem[0]
|
|
199
|
+
bbox = BoundingBox(*_get_view_box(root))
|
|
200
|
+
return BoundElement(elem, bbox)
|