svg-ultralight 0.47.0__py3-none-any.whl → 0.50.1__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.

Files changed (37) hide show
  1. svg_ultralight/__init__.py +108 -105
  2. svg_ultralight/animate.py +40 -40
  3. svg_ultralight/attrib_hints.py +13 -14
  4. svg_ultralight/bounding_boxes/__init__.py +5 -5
  5. svg_ultralight/bounding_boxes/bound_helpers.py +189 -201
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -206
  7. svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
  9. svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
  11. svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
  12. svg_ultralight/constructors/__init__.py +14 -14
  13. svg_ultralight/constructors/new_element.py +115 -115
  14. svg_ultralight/font_tools/__init__.py +5 -5
  15. svg_ultralight/font_tools/comp_results.py +295 -293
  16. svg_ultralight/font_tools/font_info.py +793 -784
  17. svg_ultralight/image_ops.py +156 -156
  18. svg_ultralight/inkscape.py +261 -261
  19. svg_ultralight/layout.py +290 -291
  20. svg_ultralight/main.py +183 -198
  21. svg_ultralight/metadata.py +122 -122
  22. svg_ultralight/nsmap.py +36 -36
  23. svg_ultralight/py.typed +5 -0
  24. svg_ultralight/query.py +254 -249
  25. svg_ultralight/read_svg.py +58 -0
  26. svg_ultralight/root_elements.py +87 -87
  27. svg_ultralight/string_conversion.py +244 -244
  28. svg_ultralight/strings/__init__.py +21 -13
  29. svg_ultralight/strings/svg_strings.py +106 -67
  30. svg_ultralight/transformations.py +140 -141
  31. svg_ultralight/unit_conversion.py +247 -248
  32. {svg_ultralight-0.47.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
  33. svg_ultralight-0.50.1.dist-info/RECORD +34 -0
  34. svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
  35. svg_ultralight-0.47.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.47.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.47.0.dist-info/top_level.txt +0 -1
svg_ultralight/main.py CHANGED
@@ -1,198 +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
- import sys
17
- from pathlib import Path
18
- from typing import IO, TYPE_CHECKING
19
-
20
- from lxml import etree
21
-
22
- from svg_ultralight.constructors import update_element
23
- from svg_ultralight.layout import pad_and_scale
24
- from svg_ultralight.nsmap import NSMAP
25
- from svg_ultralight.string_conversion import get_viewBox_str, svg_tostring
26
-
27
- if sys.version_info >= (3, 10):
28
- from typing import TypeGuard
29
- else:
30
- from typing_extensions import TypeGuard
31
-
32
- if TYPE_CHECKING:
33
- import os
34
-
35
- from lxml.etree import (
36
- _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
37
- )
38
-
39
- from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
40
- from svg_ultralight.layout import PadArg
41
-
42
-
43
- def _is_four_floats(objs: tuple[object, ...]) -> bool:
44
- """Is obj a 4-tuple of numbers?.
45
-
46
- :param objs: list of objects
47
- :return: True if all objects are numbers and there are 4 of them
48
- """
49
- return len(objs) == 4 and all(isinstance(x, (float, int)) for x in objs)
50
-
51
-
52
- def _is_io_bytes(obj: object) -> TypeGuard[IO[bytes]]:
53
- """Determine if an object is file-like.
54
-
55
- :param obj: object
56
- :return: True if object is file-like
57
- """
58
- return hasattr(obj, "read") and hasattr(obj, "write")
59
-
60
-
61
- def new_svg_root(
62
- x_: float | None = None,
63
- y_: float | None = None,
64
- width_: float | None = None,
65
- height_: float | None = None,
66
- *,
67
- pad_: PadArg = 0,
68
- print_width_: float | str | None = None,
69
- print_height_: float | str | None = None,
70
- dpu_: float = 1,
71
- nsmap: dict[str | None, str] | None = None,
72
- attrib: OptionalElemAttribMapping | None = None,
73
- **attributes: ElemAttrib,
74
- ) -> EtreeElement:
75
- """Create an svg root element from viewBox style parameters.
76
-
77
- :param x_: x value in upper-left corner
78
- :param y_: y value in upper-left corner
79
- :param width_: width of viewBox
80
- :param height_: height of viewBox
81
- :param pad_: optionally increase viewBox by pad in all directions. Acceps a
82
- single value or a tuple of values applied to (cycled over) top, right,
83
- bottom, left. pad can be floats or dimension strings*
84
- :param print_width_: optionally explicitly set unpadded width in units
85
- (float) or a dimension string*
86
- :param print_height_: optionally explicitly set unpadded height in units
87
- (float) or a dimension string*
88
- :param dpu_: dots per unit. Scale the output by this factor. This is
89
- different from print_width_ and print_height_ in that dpu_ scales the
90
- *padded* output.
91
- :param nsmap: optionally pass a namespace map of your choosing
92
- :param attrib: optionally pass additional attributes as a mapping instead of as
93
- anonymous kwargs. This is useful for pleasing the linter when unpacking a
94
- dictionary into a function call.
95
- :param attributes: element attribute names and values
96
- :return: root svg element
97
-
98
- * dimension strings are strings with a float value and a unit. Valid units
99
- are formatted as "1in", "2cm", or "3mm".
100
-
101
- All viewBox-style (trailing underscore) parameters are optional. Any kwargs
102
- will be passed to ``etree.Element`` as element parameters. These will
103
- supercede any parameters inferred from the trailing underscore parameters.
104
- """
105
- attributes.update(attrib or {})
106
- if nsmap is None:
107
- nsmap = NSMAP
108
-
109
- inferred_attribs: dict[str, ElemAttrib] = {}
110
- view_box_args = (x_, y_, width_, height_)
111
- if _is_four_floats(view_box_args):
112
- assert isinstance(x_, (float, int))
113
- assert isinstance(y_, (float, int))
114
- assert isinstance(width_, (float, int))
115
- assert isinstance(height_, (float, int))
116
- padded_viewbox, scale_attribs = pad_and_scale(
117
- (x_, y_, width_, height_), pad_, print_width_, print_height_, dpu_
118
- )
119
- inferred_attribs["viewBox"] = get_viewBox_str(*padded_viewbox)
120
- inferred_attribs.update(scale_attribs)
121
- inferred_attribs.update(attributes)
122
- # can only pass nsmap on instance creation
123
- svg_root = etree.Element(f"{{{nsmap[None]}}}svg", nsmap=nsmap)
124
- return update_element(svg_root, **inferred_attribs)
125
-
126
-
127
- def write_svg(
128
- svg: str | Path | IO[bytes],
129
- root: EtreeElement,
130
- stylesheet: str | os.PathLike[str] | None = None,
131
- *,
132
- do_link_css: bool = False,
133
- **tostring_kwargs: str | bool,
134
- ) -> str:
135
- r"""Write an xml element as an svg file.
136
-
137
- :param svg: open binary file object or path to output file (include extension .svg)
138
- :param root: root node of your svg geometry
139
- :param stylesheet: optional path to css stylesheet
140
- :param do_link_css: link to stylesheet, else (default) write contents of stylesheet
141
- into svg (ignored if stylesheet is None)
142
- :param tostring_kwargs: keyword arguments to etree.tostring. xml_header=True for
143
- sensible default values. See below.
144
- :return: svg filename
145
- :effects: creates svg file at ``svg``
146
- :raises TypeError: if ``svg`` is not a Path, str, or binary file object
147
-
148
- It's often useful to write a temporary svg file, so a tempfile.NamedTemporaryFile
149
- object (or any open binary file object can be passed instead of an svg filename).
150
-
151
- You may never need an xml_header. Inkscape doesn't need it, your browser doesn't
152
- need it, and it's forbidden if you'd like to "inline" your svg in an html file.
153
- The most pressing need might be to set an encoding. If you pass
154
- ``xml_declaration=True`` as a tostring_kwarg, this function will attempt to pass
155
- the following defaults to ``lxml.etree.tostring``:
156
-
157
- * doctype: str = (
158
- '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
159
- '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
160
- )
161
- * encoding = "UTF-8"
162
-
163
- Always, this function will default to ``pretty_print=True``
164
-
165
- These can be overridden by tostring_kwargs.
166
-
167
- e.g., ``write_svg(..., xml_declaration=True, doctype=None``)
168
- e.g., ``write_svg(..., xml_declaration=True, encoding='ascii')``
169
-
170
- ``lxml.etree.tostring`` is documented here: https://lxml.de/api/index.html,
171
- but I know that to be incomplete as of 2020 Feb 01, as it does not document the
172
- (perhaps important to you) 'encoding' kwarg.
173
- """
174
- if stylesheet is not None:
175
- if do_link_css is True:
176
- parent = Path(str(svg)).parent
177
- relative_css_path = Path(stylesheet).relative_to(parent)
178
- link = etree.PI(
179
- "xml-stylesheet", f'href="{relative_css_path}" type="text/css"'
180
- )
181
- root.addprevious(link)
182
- else:
183
- style = etree.Element("style", type="text/css")
184
- with Path(stylesheet).open(encoding="utf-8") as css_file:
185
- style.text = etree.CDATA("\n" + "".join(css_file.readlines()) + "\n")
186
- root.insert(0, style)
187
-
188
- svg_contents = svg_tostring(root, **tostring_kwargs)
189
-
190
- if _is_io_bytes(svg):
191
- _ = svg.write(svg_contents)
192
- return svg.name
193
- if isinstance(svg, (str, Path)):
194
- with Path(svg).open("wb") as svg_file:
195
- _ = svg_file.write(svg_contents)
196
- return str(svg)
197
- msg = f"svg must be a path-like object or a file-like object, not {type(svg)}"
198
- raise TypeError(msg)
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)
@@ -1,122 +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
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