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
svg_ultralight/layout.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Manage inferences for pad_ and dpu_ arguments.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2023-02-12
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from typing import TypeAlias
|
|
11
|
+
|
|
12
|
+
from svg_ultralight.string_conversion import format_number
|
|
13
|
+
from svg_ultralight.unit_conversion import Measurement, MeasurementArg
|
|
14
|
+
|
|
15
|
+
PadArg: TypeAlias = float | str | Measurement | Sequence[float | str | Measurement]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def expand_pad_arg(pad: PadArg) -> tuple[float, float, float, float]:
|
|
19
|
+
"""Transform a single value or tuple of values to a 4-tuple of user units.
|
|
20
|
+
|
|
21
|
+
:param pad: padding value(s)
|
|
22
|
+
:return: 4-tuple of padding values in (scaled) user units
|
|
23
|
+
|
|
24
|
+
>>> expand_pad_arg(1)
|
|
25
|
+
(1.0, 1.0, 1.0, 1.0)
|
|
26
|
+
|
|
27
|
+
>>> expand_pad_arg((1, 2))
|
|
28
|
+
(1.0, 2.0, 1.0, 2.0)
|
|
29
|
+
|
|
30
|
+
>>> expand_pad_arg("1in")
|
|
31
|
+
(96.0, 96.0, 96.0, 96.0)
|
|
32
|
+
|
|
33
|
+
>>> expand_pad_arg(("1in", "2in"))
|
|
34
|
+
(96.0, 192.0, 96.0, 192.0)
|
|
35
|
+
|
|
36
|
+
>>> expand_pad_arg(Measurement("1in"))
|
|
37
|
+
(96.0, 96.0, 96.0, 96.0)
|
|
38
|
+
|
|
39
|
+
>>> expand_pad_arg((Measurement("1in"), Measurement("2in")))
|
|
40
|
+
(96.0, 192.0, 96.0, 192.0)
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(pad, str) or not isinstance(pad, Sequence):
|
|
43
|
+
return expand_pad_arg([pad])
|
|
44
|
+
as_ms = [m if isinstance(m, Measurement) else Measurement(m) for m in pad]
|
|
45
|
+
as_units = [m.value for m in as_ms]
|
|
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)]
|
|
50
|
+
return as_units[0], as_units[1], as_units[2], as_units[3]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pad_viewbox(
|
|
54
|
+
viewbox: tuple[float, float, float, float], pads: tuple[float, float, float, float]
|
|
55
|
+
) -> tuple[float, float, float, float]:
|
|
56
|
+
"""Expand viewbox by padding.
|
|
57
|
+
|
|
58
|
+
:param viewbox: viewbox to pad (x, y, width height)
|
|
59
|
+
:param pads: padding (top, right, bottom, left)
|
|
60
|
+
:return: padded viewbox
|
|
61
|
+
"""
|
|
62
|
+
x, y, width, height = viewbox
|
|
63
|
+
top, right, bottom, left = pads
|
|
64
|
+
return x - left, y - top, width + left + right, height + top + bottom
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _scale_pads(
|
|
68
|
+
pads: tuple[float, float, float, float], scale: float
|
|
69
|
+
) -> tuple[float, float, float, float]:
|
|
70
|
+
"""Scale padding by a factor.
|
|
71
|
+
|
|
72
|
+
:param pads: padding to scale (top, right, bottom, left)
|
|
73
|
+
:param scale: factor to scale by
|
|
74
|
+
:return: scaled padding
|
|
75
|
+
"""
|
|
76
|
+
top, right, bottom, left = pads
|
|
77
|
+
return top * scale, right * scale, bottom * scale, left * scale
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _infer_scale(
|
|
81
|
+
print_h: Measurement, print_w: Measurement, viewbox_h: float, viewbox_w: float
|
|
82
|
+
) -> float:
|
|
83
|
+
"""Determine size of viewbox units.
|
|
84
|
+
|
|
85
|
+
:param print_h: height of print area
|
|
86
|
+
:param print_w: width of print area
|
|
87
|
+
:param viewbox_h: height of viewbox
|
|
88
|
+
:param viewbox_w: width of viewbox
|
|
89
|
+
:return: scale factor to apply to viewbox to match print area
|
|
90
|
+
:raises ValueError: if no valid scale can be determined
|
|
91
|
+
|
|
92
|
+
If one of width or height cannot be used, will defer to the other.
|
|
93
|
+
|
|
94
|
+
Will ignore ONE, but not both of these conditions:
|
|
95
|
+
* print_w > 0 / viewbox_w == 0
|
|
96
|
+
* print_h > 0 / viewbox_h == 0
|
|
97
|
+
|
|
98
|
+
Any potential scale would be infinite, so this raises a ValueError
|
|
99
|
+
|
|
100
|
+
Will ignore ONE, but not both of these conditions:
|
|
101
|
+
* print_w == 0 / viewbox_w > 0
|
|
102
|
+
* print_h == 0 / viewbox_h > 0
|
|
103
|
+
|
|
104
|
+
The print area is invalid, but there is special handling for this. Interpret
|
|
105
|
+
viewbox units as print_w.native_unit and determe print area from viewbox area 1
|
|
106
|
+
to 1.
|
|
107
|
+
|
|
108
|
+
>>> _infer_scale(Measurement("in"), Measurement("in"), 1, 2)
|
|
109
|
+
96
|
|
110
|
+
|
|
111
|
+
Will additionally raise a ValueError for any negative measurement.
|
|
112
|
+
|
|
113
|
+
Scaling is safe for zero values. If both are zero, the scaling will be 1.
|
|
114
|
+
Padding might add a non-zero value to width or height later, producing a valid
|
|
115
|
+
viewbox, but that isn't guaranteed here.
|
|
116
|
+
"""
|
|
117
|
+
if any(x < 0 for x in (print_h.value, print_w.value, viewbox_h, viewbox_w)):
|
|
118
|
+
msg = "Negative values are not allowed"
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
|
|
121
|
+
candidate_scales: set[float] = set()
|
|
122
|
+
if print_w.value and viewbox_w:
|
|
123
|
+
candidate_scales.add(print_w.value / viewbox_w)
|
|
124
|
+
if print_h.value and viewbox_h:
|
|
125
|
+
candidate_scales.add(print_h.value / viewbox_h)
|
|
126
|
+
if candidate_scales:
|
|
127
|
+
# size of picture is determined by print area
|
|
128
|
+
return min(candidate_scales)
|
|
129
|
+
if any([print_w.value, print_h.value]):
|
|
130
|
+
msg = "All potential scales would be infinite."
|
|
131
|
+
raise ValueError(msg)
|
|
132
|
+
# a print unit was given, but not a print size. Size of picture is determined
|
|
133
|
+
# by interpreting viewbox dimensions as print_width or print_height units
|
|
134
|
+
return print_w.native_unit.value[1]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def pad_and_scale(
|
|
138
|
+
viewbox: tuple[float, float, float, float],
|
|
139
|
+
pad: PadArg,
|
|
140
|
+
print_width: MeasurementArg | None = None,
|
|
141
|
+
print_height: MeasurementArg | None = None,
|
|
142
|
+
dpu: float = 1,
|
|
143
|
+
) -> tuple[tuple[float, float, float, float], dict[str, float | str]]:
|
|
144
|
+
"""Expand and scale the pad argument. If necessary, scale image.
|
|
145
|
+
|
|
146
|
+
:param viewbox: viewbox to pad (x, y, width height)
|
|
147
|
+
:param pad: padding to add around image, in user units or inches. If a
|
|
148
|
+
sequence, it should be (top, right, bottom, left). if a single float or
|
|
149
|
+
string, it will be applied to all sides. If two floats, top and bottom
|
|
150
|
+
then left and right. If three floats, top, left and right, then bottom.
|
|
151
|
+
If four floats, top, right, bottom, left.
|
|
152
|
+
:param print_width: width of print area, in user units (float), a string
|
|
153
|
+
with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
|
|
154
|
+
"pt")
|
|
155
|
+
:param print_height: height of print area, in user units (float), a string
|
|
156
|
+
with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
|
|
157
|
+
"pt")
|
|
158
|
+
:param dpu: scale the print units. This is useful when you want to print the
|
|
159
|
+
same image at different sizes.
|
|
160
|
+
:return: padded viewbox 4-tuple and scaling attributes
|
|
161
|
+
|
|
162
|
+
SVGs are built in "user units". An optional width and height (not the
|
|
163
|
+
viewbox with and height, these are separate arguments) define the size of
|
|
164
|
+
those user units.
|
|
165
|
+
|
|
166
|
+
* If the width and height are not specified, the user units are 1 pixel
|
|
167
|
+
(1/96th of an inch).
|
|
168
|
+
|
|
169
|
+
If the width and height *are* specified, the user units become whatever they
|
|
170
|
+
need to be to fit that requirement. For instance, if the viewbox width is 96
|
|
171
|
+
and the width argument is "1in", then the user units are *still* pixels,
|
|
172
|
+
because there are 96 pixels in an inch. If the viewbox width is 2 and the
|
|
173
|
+
width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
|
|
174
|
+
pixels) each, because there are 2 user units in an inch. If the viewbox
|
|
175
|
+
width is 3 and the width argument is "1yd", the each user unit is 1 foot.
|
|
176
|
+
|
|
177
|
+
To pad around the viewbox, we need to first figure out what the user units
|
|
178
|
+
are then scale the padding so it will print (or display) correctly. For
|
|
179
|
+
instance, if
|
|
180
|
+
|
|
181
|
+
* the viewbox width is 3;
|
|
182
|
+
* the width argument is "1yd"; and
|
|
183
|
+
* the pad argument is "1in"
|
|
184
|
+
|
|
185
|
+
the printed result will be 38" wide. That's 1yd for the width plus 1 inch of
|
|
186
|
+
padding on each side. The viewbox will have 1/12 of a unit (3 user units
|
|
187
|
+
over 1 yard = 1 foot per user unit) added on each side.
|
|
188
|
+
|
|
189
|
+
Ideally, we know the size of the print or display area from the beginning
|
|
190
|
+
and build the geometry out at whatever size we want, so no scaling is
|
|
191
|
+
necessarily required. Even that won't always work, because some software
|
|
192
|
+
doesn't like "user units" and insists on 'pt' or 'in'. If everything is
|
|
193
|
+
already in 'pt' or 'in' and you want to keep it that way, just call the
|
|
194
|
+
function with print_width="pt" or print_height="in". The function will add
|
|
195
|
+
the unit designators without changing the scale.
|
|
196
|
+
|
|
197
|
+
Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
|
|
198
|
+
instance, if you created two images
|
|
199
|
+
|
|
200
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
|
|
201
|
+
|
|
202
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
|
|
203
|
+
|
|
204
|
+
... (note that the images only vary in print_width_), the first image would be
|
|
205
|
+
rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
|
|
206
|
+
content in the viewbox would be exactly twice as wide in the larger image, but
|
|
207
|
+
the padding would remain 0.25 in both images. Despite setting `print_width_` to
|
|
208
|
+
exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
|
|
209
|
+
Despite a viewbox aspect ratio of 1:2, you would not get an output image of
|
|
210
|
+
exactly 1:2. If you want to use padding and need a specific output image size or
|
|
211
|
+
aspect ratio, remember to subtract the padding width from your print_width or
|
|
212
|
+
print_height.
|
|
213
|
+
|
|
214
|
+
Scaling attributes are returned as a dictonary that can be "exploded" into
|
|
215
|
+
the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
|
|
216
|
+
|
|
217
|
+
* If neither a print_width nor print_height is specified, no scaling
|
|
218
|
+
attributes will be returned.
|
|
219
|
+
|
|
220
|
+
* If either is specified, both a width and height will be returned (even if
|
|
221
|
+
one argument is None). These will always match the viewbox aspect ratio,
|
|
222
|
+
so there is no additional information supplied by giving both, but I've
|
|
223
|
+
had unexpected behavior from pandoc when one was missing.
|
|
224
|
+
|
|
225
|
+
* If only a unit is given, (e.g., "pt"), the user units (viewbox width and
|
|
226
|
+
height) will be interpreted as that unit. This is important for InDesign,
|
|
227
|
+
which may not display in image at all if the width and height are not
|
|
228
|
+
explicitly "pt".
|
|
229
|
+
|
|
230
|
+
* Print ratios are discarded. The viwebox ratio is preserved. For instance,
|
|
231
|
+
if the viewbox is (0, 0, 16, 9), giving a 16:9 aspect ratio and the
|
|
232
|
+
print_width and print_height are both 100, giving a 1:1 aspect ratio, the
|
|
233
|
+
output scaling attributes will be {"width": "100", "height", "56.25"},
|
|
234
|
+
preserving viewbox aspect ratio with a "best fit" scaling (i.e, the image
|
|
235
|
+
is as large as it can be without exceeding the specified print area).
|
|
236
|
+
|
|
237
|
+
You can pass something impossible like a viewbox width of 1 and a print box
|
|
238
|
+
of 0. The function will give up, set scaling to 1, and pad the viewbox. This
|
|
239
|
+
does not try to guard against bad values sent to lxml.
|
|
240
|
+
|
|
241
|
+
All of the above is important when you want your padding in real-world units
|
|
242
|
+
(e.g., when you need to guarantee a certain amount of padding above and
|
|
243
|
+
below an image in a book layout). However, it does add some complexity,
|
|
244
|
+
because aspect ratio is not maintained when print_width increases. Worse, if
|
|
245
|
+
there is some geomtry like a background pattern in your padding, then more
|
|
246
|
+
or less of that pattern will be visible depending on the print_width.
|
|
247
|
+
|
|
248
|
+
That's not hard to work around, just change the padding every time you
|
|
249
|
+
change the width. Or, to make it even simpler, use the dpu argument. The dpu
|
|
250
|
+
argument will scale the width and the padding together. So, you can produce
|
|
251
|
+
a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
|
|
252
|
+
... then scale the printout with dpu_=2 to get a 32" x 18" image with the
|
|
253
|
+
same viewbox. This means the padding will be 2" on all sides, but the image
|
|
254
|
+
will be identical (just twice as wide and twice as high) as the 16" x 9" image.
|
|
255
|
+
"""
|
|
256
|
+
pads = expand_pad_arg(pad)
|
|
257
|
+
|
|
258
|
+
# no print information given, pad and return viewbox
|
|
259
|
+
if print_width is None and print_height is None:
|
|
260
|
+
padded = pad_viewbox(viewbox, pads)
|
|
261
|
+
dims: dict[str, float | str] = {}
|
|
262
|
+
if dpu != 1:
|
|
263
|
+
dims["width"] = format_number(padded[2] * dpu)
|
|
264
|
+
dims["height"] = format_number(padded[3] * dpu)
|
|
265
|
+
return padded, dims
|
|
266
|
+
|
|
267
|
+
_, _, viewbox_w, viewbox_h = viewbox
|
|
268
|
+
print_w = Measurement(print_width or 0)
|
|
269
|
+
print_h = Measurement(print_height or 0)
|
|
270
|
+
|
|
271
|
+
# match unspecified (None) width or height units.
|
|
272
|
+
if print_width is None:
|
|
273
|
+
print_w.native_unit = print_h.native_unit
|
|
274
|
+
elif print_height is None:
|
|
275
|
+
print_h.native_unit = print_w.native_unit
|
|
276
|
+
|
|
277
|
+
scale = _infer_scale(print_h, print_w, viewbox_h, viewbox_w)
|
|
278
|
+
|
|
279
|
+
print_w.value = viewbox_w * scale
|
|
280
|
+
print_h.value = viewbox_h * scale
|
|
281
|
+
|
|
282
|
+
# add padding and increase print area
|
|
283
|
+
print_w.value += pads[1] + pads[3]
|
|
284
|
+
print_h.value += pads[0] + pads[2]
|
|
285
|
+
|
|
286
|
+
# scale pads to viewbox to match input size when later scaled to print area
|
|
287
|
+
padded_viewbox = pad_viewbox(viewbox, _scale_pads(pads, 1 / scale))
|
|
288
|
+
return padded_viewbox, {
|
|
289
|
+
"width": (print_w * dpu).get_svg(print_w.native_unit),
|
|
290
|
+
"height": (print_h * dpu).get_svg(print_h.native_unit),
|
|
291
|
+
}
|
svg_ultralight/main.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
r"""Simple functions to LIGHTLY assist in creating Scalable Vector Graphics.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
created: 10/7/2019
|
|
5
|
+
|
|
6
|
+
Some functions here require a path to an Inkscape executable on your filesystem.
|
|
7
|
+
IMPORTANT: path cannot end with ``.exe``.
|
|
8
|
+
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
9
|
+
|
|
10
|
+
Inkscape changed their command-line interface with version 1.0. These functions
|
|
11
|
+
should work with all Inkscape versions. Please report any issues.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import IO, TYPE_CHECKING, TypeGuard
|
|
18
|
+
|
|
19
|
+
from lxml import etree
|
|
20
|
+
|
|
21
|
+
from svg_ultralight.constructors import update_element
|
|
22
|
+
from svg_ultralight.layout import pad_and_scale
|
|
23
|
+
from svg_ultralight.nsmap import NSMAP
|
|
24
|
+
from svg_ultralight.string_conversion import get_view_box_str, svg_tostring
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
from lxml.etree import (
|
|
30
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
|
|
34
|
+
from svg_ultralight.layout import PadArg
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_io_bytes(obj: object) -> TypeGuard[IO[bytes]]:
|
|
38
|
+
"""Determine if an object is file-like.
|
|
39
|
+
|
|
40
|
+
:param obj: object
|
|
41
|
+
:return: True if object is file-like
|
|
42
|
+
"""
|
|
43
|
+
return hasattr(obj, "read") and hasattr(obj, "write")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def new_svg_root(
|
|
47
|
+
x_: float | None = None,
|
|
48
|
+
y_: float | None = None,
|
|
49
|
+
width_: float | None = None,
|
|
50
|
+
height_: float | None = None,
|
|
51
|
+
*,
|
|
52
|
+
pad_: PadArg = 0,
|
|
53
|
+
print_width_: float | str | None = None,
|
|
54
|
+
print_height_: float | str | None = None,
|
|
55
|
+
dpu_: float = 1,
|
|
56
|
+
nsmap: dict[str | None, str] | None = None,
|
|
57
|
+
attrib: OptionalElemAttribMapping | None = None,
|
|
58
|
+
**attributes: ElemAttrib,
|
|
59
|
+
) -> EtreeElement:
|
|
60
|
+
"""Create an svg root element from viewBox style parameters.
|
|
61
|
+
|
|
62
|
+
:param x_: x value in upper-left corner
|
|
63
|
+
:param y_: y value in upper-left corner
|
|
64
|
+
:param width_: width of viewBox
|
|
65
|
+
:param height_: height of viewBox
|
|
66
|
+
:param pad_: optionally increase viewBox by pad in all directions. Acceps a
|
|
67
|
+
single value or a tuple of values applied to (cycled over) top, right,
|
|
68
|
+
bottom, left. pad can be floats or dimension strings*
|
|
69
|
+
:param print_width_: optionally explicitly set unpadded width in units
|
|
70
|
+
(float) or a dimension string*
|
|
71
|
+
:param print_height_: optionally explicitly set unpadded height in units
|
|
72
|
+
(float) or a dimension string*
|
|
73
|
+
:param dpu_: dots per unit. Scale the output by this factor. This is
|
|
74
|
+
different from print_width_ and print_height_ in that dpu_ scales the
|
|
75
|
+
*padded* output.
|
|
76
|
+
:param nsmap: optionally pass a namespace map of your choosing
|
|
77
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
78
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
79
|
+
dictionary into a function call.
|
|
80
|
+
:param attributes: element attribute names and values
|
|
81
|
+
:return: root svg element
|
|
82
|
+
|
|
83
|
+
* dimension strings are strings with a float value and a unit. Valid units
|
|
84
|
+
are formatted as "1in", "2cm", or "3mm".
|
|
85
|
+
|
|
86
|
+
All viewBox-style (trailing underscore) parameters are optional. Any kwargs
|
|
87
|
+
will be passed to ``etree.Element`` as element parameters. These will
|
|
88
|
+
supercede any parameters inferred from the trailing underscore parameters.
|
|
89
|
+
"""
|
|
90
|
+
attributes.update(attrib or {})
|
|
91
|
+
if nsmap is None:
|
|
92
|
+
nsmap = NSMAP
|
|
93
|
+
|
|
94
|
+
inferred_attribs: dict[str, ElemAttrib] = {}
|
|
95
|
+
if (
|
|
96
|
+
isinstance(x_, (float, int))
|
|
97
|
+
and isinstance(y_, (float, int))
|
|
98
|
+
and isinstance(width_, (float, int))
|
|
99
|
+
and isinstance(height_, (float, int))
|
|
100
|
+
):
|
|
101
|
+
padded_viewbox, scale_attribs = pad_and_scale(
|
|
102
|
+
(x_, y_, width_, height_), pad_, print_width_, print_height_, dpu_
|
|
103
|
+
)
|
|
104
|
+
inferred_attribs["viewBox"] = get_view_box_str(*padded_viewbox)
|
|
105
|
+
inferred_attribs.update(scale_attribs)
|
|
106
|
+
inferred_attribs.update(attributes)
|
|
107
|
+
# can only pass nsmap on instance creation
|
|
108
|
+
svg_root = etree.Element(f"{{{nsmap[None]}}}svg", nsmap=nsmap)
|
|
109
|
+
return update_element(svg_root, **inferred_attribs)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def write_svg(
|
|
113
|
+
svg: str | Path | IO[bytes],
|
|
114
|
+
root: EtreeElement,
|
|
115
|
+
stylesheet: str | os.PathLike[str] | None = None,
|
|
116
|
+
*,
|
|
117
|
+
do_link_css: bool = False,
|
|
118
|
+
**tostring_kwargs: str | bool,
|
|
119
|
+
) -> str:
|
|
120
|
+
r"""Write an xml element as an svg file.
|
|
121
|
+
|
|
122
|
+
:param svg: open binary file object or path to output file (include extension .svg)
|
|
123
|
+
:param root: root node of your svg geometry
|
|
124
|
+
:param stylesheet: optional path to css stylesheet
|
|
125
|
+
:param do_link_css: link to stylesheet, else (default) write contents of stylesheet
|
|
126
|
+
into svg (ignored if stylesheet is None)
|
|
127
|
+
:param tostring_kwargs: keyword arguments to etree.tostring. xml_header=True for
|
|
128
|
+
sensible default values. See below.
|
|
129
|
+
:return: svg filename
|
|
130
|
+
:effects: creates svg file at ``svg``
|
|
131
|
+
:raises TypeError: if ``svg`` is not a Path, str, or binary file object
|
|
132
|
+
|
|
133
|
+
It's often useful to write a temporary svg file, so a tempfile.NamedTemporaryFile
|
|
134
|
+
object (or any open binary file object can be passed instead of an svg filename).
|
|
135
|
+
|
|
136
|
+
You may never need an xml_header. Inkscape doesn't need it, your browser doesn't
|
|
137
|
+
need it, and it's forbidden if you'd like to "inline" your svg in an html file.
|
|
138
|
+
The most pressing need might be to set an encoding. If you pass
|
|
139
|
+
``xml_declaration=True`` as a tostring_kwarg, this function will attempt to pass
|
|
140
|
+
the following defaults to ``lxml.etree.tostring``:
|
|
141
|
+
|
|
142
|
+
* doctype: str = (
|
|
143
|
+
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
|
|
144
|
+
'"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
|
|
145
|
+
)
|
|
146
|
+
* encoding = "UTF-8"
|
|
147
|
+
|
|
148
|
+
Always, this function will default to ``pretty_print=True``
|
|
149
|
+
|
|
150
|
+
These can be overridden by tostring_kwargs.
|
|
151
|
+
|
|
152
|
+
e.g., ``write_svg(..., xml_declaration=True, doctype=None``)
|
|
153
|
+
e.g., ``write_svg(..., xml_declaration=True, encoding='ascii')``
|
|
154
|
+
|
|
155
|
+
``lxml.etree.tostring`` is documented here: https://lxml.de/api/index.html,
|
|
156
|
+
but I know that to be incomplete as of 2020 Feb 01, as it does not document the
|
|
157
|
+
(perhaps important to you) 'encoding' kwarg.
|
|
158
|
+
"""
|
|
159
|
+
if stylesheet is not None:
|
|
160
|
+
if do_link_css is True:
|
|
161
|
+
parent = Path(str(svg)).parent
|
|
162
|
+
relative_css_path = Path(stylesheet).relative_to(parent)
|
|
163
|
+
link = etree.PI(
|
|
164
|
+
"xml-stylesheet", f'href="{relative_css_path}" type="text/css"'
|
|
165
|
+
)
|
|
166
|
+
root.addprevious(link)
|
|
167
|
+
else:
|
|
168
|
+
style = etree.Element("style", type="text/css")
|
|
169
|
+
with Path(stylesheet).open(encoding="utf-8") as css_file:
|
|
170
|
+
style.text = etree.CDATA("\n" + "".join(css_file.readlines()) + "\n")
|
|
171
|
+
root.insert(0, style)
|
|
172
|
+
|
|
173
|
+
svg_contents = svg_tostring(root, **tostring_kwargs)
|
|
174
|
+
|
|
175
|
+
if _is_io_bytes(svg):
|
|
176
|
+
_ = svg.write(svg_contents)
|
|
177
|
+
return svg.name
|
|
178
|
+
if isinstance(svg, (str, Path)):
|
|
179
|
+
with Path(svg).open("wb") as svg_file:
|
|
180
|
+
_ = svg_file.write(svg_contents)
|
|
181
|
+
return str(svg)
|
|
182
|
+
msg = f"svg must be a path-like object or a file-like object, not {type(svg)}"
|
|
183
|
+
raise TypeError(msg)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Add metadata exactly as Inkscape formats it.
|
|
2
|
+
|
|
3
|
+
See https://paladini.github.io/dublin-core-basics/ for a description of the metadata
|
|
4
|
+
fields.
|
|
5
|
+
|
|
6
|
+
:author: Shay Hill
|
|
7
|
+
:created: 2024-01-29
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
13
|
+
|
|
14
|
+
from svg_ultralight.constructors.new_element import new_element, new_sub_element
|
|
15
|
+
from svg_ultralight.nsmap import new_qname
|
|
16
|
+
|
|
17
|
+
_KNOWN_METADATA_FIELDS = {
|
|
18
|
+
# tags in both Dublin Core and the Inkscape interface
|
|
19
|
+
"title",
|
|
20
|
+
"date",
|
|
21
|
+
"creator",
|
|
22
|
+
"rights",
|
|
23
|
+
"publisher",
|
|
24
|
+
"identifier",
|
|
25
|
+
"source",
|
|
26
|
+
"relation",
|
|
27
|
+
"language",
|
|
28
|
+
"coverage",
|
|
29
|
+
"description",
|
|
30
|
+
"contributor",
|
|
31
|
+
# *almost* in both. This is "contributors" in the Inkscape interface. Output as
|
|
32
|
+
# "contributor".
|
|
33
|
+
"contributors",
|
|
34
|
+
# tags in Dublin Core but not the Inkscape interface.
|
|
35
|
+
"subject",
|
|
36
|
+
"type",
|
|
37
|
+
"format",
|
|
38
|
+
# tags in the Inkscape interface but not Dublin Core
|
|
39
|
+
"keywords", # Inkscape alias for "subject". Will be subject in the output.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _wrap_agent(title: str) -> EtreeElement:
|
|
44
|
+
"""Create nested elements for creator, rights, publisher, and contributors.
|
|
45
|
+
|
|
46
|
+
:param title: The text to put in the nested element.
|
|
47
|
+
:return: an agent element to be places in a dc:creator, dc:rights, dc:publisher,
|
|
48
|
+
or dc:contributors element.
|
|
49
|
+
|
|
50
|
+
This is the way Inkscape formats these fields.
|
|
51
|
+
"""
|
|
52
|
+
agent = new_element(new_qname("cc", "Agent"))
|
|
53
|
+
_ = new_sub_element(agent, new_qname("dc", "title"), text=title)
|
|
54
|
+
return agent
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _wrap_bag(title: str) -> EtreeElement:
|
|
58
|
+
"""Create nested elements for keywords.
|
|
59
|
+
|
|
60
|
+
:param title: The text to put in the nested element.
|
|
61
|
+
:return: an agent element to be places in a dc:subject element.
|
|
62
|
+
|
|
63
|
+
This is the way Inkscape formats these fields. Keywords are put in a subject
|
|
64
|
+
element.
|
|
65
|
+
"""
|
|
66
|
+
items = title.split(",")
|
|
67
|
+
agent = new_element(new_qname("rdf", "Bag"))
|
|
68
|
+
for title_item in items:
|
|
69
|
+
_ = new_sub_element(agent, new_qname("rdf", "li"), text=title_item)
|
|
70
|
+
return agent
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def new_metadata(**kwargs: str) -> EtreeElement:
|
|
74
|
+
"""Return a new metadata string.
|
|
75
|
+
|
|
76
|
+
:param kwargs: The metadata fields to include.
|
|
77
|
+
|
|
78
|
+
This is the way Inkscape formats metadata. If you create a metadata element,
|
|
79
|
+
svg_ultralight will create an empty `dc:title` element even if no title keyword
|
|
80
|
+
is passed.
|
|
81
|
+
|
|
82
|
+
The following keywords can be used without a warning:
|
|
83
|
+
title, date, creator, rights, publisher, identifier, source, relation, language,
|
|
84
|
+
coverage, description, contributor, contributors, subject, type, format, keywords.
|
|
85
|
+
|
|
86
|
+
Only the keywords `keywords` and `subject` accept treat comma-delimited strings
|
|
87
|
+
as multiple values. Every other value will be treated as a single string. You can
|
|
88
|
+
pass other fields. They will be included as
|
|
89
|
+
`<dc:other_field>value</dc:other_field>`.
|
|
90
|
+
|
|
91
|
+
Will hardcode the id to "metadata1" because that's what Inkscape does. If that
|
|
92
|
+
doesn't satisfy, replace id after description.
|
|
93
|
+
"""
|
|
94
|
+
for tag in kwargs:
|
|
95
|
+
if tag not in _KNOWN_METADATA_FIELDS:
|
|
96
|
+
msg = f"Unknown metadata field: {tag}"
|
|
97
|
+
warnings.warn(msg, stacklevel=2)
|
|
98
|
+
|
|
99
|
+
metadata = new_element("metadata", id_="metadata1")
|
|
100
|
+
rdf = new_sub_element(metadata, new_qname("rdf", "RDF"))
|
|
101
|
+
work = new_sub_element(rdf, new_qname("cc", "Work"), **{"rdf:about": ""})
|
|
102
|
+
|
|
103
|
+
_ = new_sub_element(work, new_qname("dc", "title"), text=kwargs.pop("title", ""))
|
|
104
|
+
for k, v in kwargs.items():
|
|
105
|
+
tag = k
|
|
106
|
+
# aliases
|
|
107
|
+
if tag == "contributors":
|
|
108
|
+
tag = "contributor"
|
|
109
|
+
elif tag == "subject":
|
|
110
|
+
tag = "keywords"
|
|
111
|
+
|
|
112
|
+
if tag in {"creator", "rights", "publisher", "contributor"}:
|
|
113
|
+
elem = new_sub_element(work, new_qname("dc", tag))
|
|
114
|
+
elem.append(_wrap_agent(v))
|
|
115
|
+
continue
|
|
116
|
+
if tag == "keywords":
|
|
117
|
+
elem = new_sub_element(work, new_qname("dc", "subject"))
|
|
118
|
+
elem.append(_wrap_bag(v))
|
|
119
|
+
continue
|
|
120
|
+
_ = new_sub_element(work, new_qname("dc", tag), text=v)
|
|
121
|
+
|
|
122
|
+
return metadata
|
svg_ultralight/nsmap.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""xml namespace entries for svg files.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 1/14/2021
|
|
5
|
+
|
|
6
|
+
I started by copying out entries from Inkscape output. Added more as I found them
|
|
7
|
+
necessary. This is a pretty robust list. Can be pared down as documented at
|
|
8
|
+
https://shayallenhill.com/svg-with-css-in-python/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from lxml.etree import QName
|
|
14
|
+
|
|
15
|
+
_SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
16
|
+
NSMAP = {
|
|
17
|
+
None: _SVG_NAMESPACE,
|
|
18
|
+
"dc": "http://purl.org/dc/elements/1.1/",
|
|
19
|
+
"cc": "http://creativecommons.org/ns#",
|
|
20
|
+
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
21
|
+
"svg": _SVG_NAMESPACE,
|
|
22
|
+
"xlink": "http://www.w3.org/1999/xlink",
|
|
23
|
+
"sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
|
|
24
|
+
"inkscape": "http://www.inkscape.org/namespaces/inkscape",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def new_qname(namespace_abbreviation: str | None, tag: str) -> QName:
|
|
29
|
+
"""Create a qualified name for an svg element.
|
|
30
|
+
|
|
31
|
+
:param namespace_abbreviation: The namespace abbreviation. This
|
|
32
|
+
will have to be a key in NSMAP (e.g., "dc", "cc", "rdf").
|
|
33
|
+
:param tag: The tag name of the element.
|
|
34
|
+
:return: A qualified name for the element.
|
|
35
|
+
"""
|
|
36
|
+
return QName(NSMAP[namespace_abbreviation], tag)
|