svg-ultralight 0.48.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 -189
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
  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 -792
  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.48.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.48.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.48.0.dist-info/top_level.txt +0 -1
@@ -1,244 +1,244 @@
1
- """Quasi-private functions for high-level string conversion.
2
-
3
- :author: Shay Hill
4
- :created: 7/26/2020
5
-
6
- Rounding some numbers to ensure quality svg rendering:
7
- * Rounding floats to six digits after the decimal
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import binascii
13
- import re
14
- from enum import Enum
15
- from typing import TYPE_CHECKING, cast
16
-
17
- import svg_path_data
18
- from lxml import etree
19
-
20
- from svg_ultralight.nsmap import NSMAP
21
-
22
- if TYPE_CHECKING:
23
- from collections.abc import Iterable
24
-
25
- from lxml.etree import (
26
- _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
27
- )
28
-
29
- from svg_ultralight.attrib_hints import ElemAttrib
30
-
31
-
32
- def format_number(num: float | str, resolution: int | None = 6) -> str:
33
- """Format a number into an svg-readable float string with resolution = 6.
34
-
35
- :param num: number to format (string or float)
36
- :param resolution: number of digits after the decimal point, defaults to 6. None
37
- to match behavior of `str(num)`.
38
- :return: string representation of the number with six digits after the decimal
39
- (if in fixed-point notation). Will return exponential notation when shorter.
40
- """
41
- return svg_path_data.format_number(num, resolution=resolution)
42
-
43
-
44
- def format_numbers(
45
- nums: Iterable[float] | Iterable[str] | Iterable[float | str],
46
- ) -> list[str]:
47
- """Format multiple strings to limited precision.
48
-
49
- :param nums: iterable of floats
50
- :return: list of formatted strings
51
- """
52
- return [format_number(num) for num in nums]
53
-
54
-
55
- def _fix_key_and_format_val(key: str, val: ElemAttrib) -> tuple[str, str]:
56
- """Format one key, value pair for an svg element.
57
-
58
- :param key: element attribute name
59
- :param val: element attribute value
60
- :return: tuple of key, value
61
- :raises ValueError: if key is 'd' and val is not a string
62
-
63
- etree.Elements will only accept string
64
- values. This saves having to convert input to strings.
65
-
66
- * convert float values to formatted strings
67
- * format datastring values when keyword is 'd'
68
- * replace '_' with '-' in keywords
69
- * remove trailing '_' from keywords
70
- * will convert `namespace:tag` to a qualified name
71
-
72
- SVG attribute names like `font-size` and `stroke-width` are not valid python
73
- keywords, but can be passed as `font-size` and `stroke-width`.
74
-
75
- Reserved Python keywords that are also valid and useful SVG attribute names (a
76
- popular one will be 'class') can be passed with a trailing underscore (e.g.,
77
- class_='body_text').
78
- """
79
- if "http:" in key or "https:" in key:
80
- key_ = key
81
- elif ":" in key:
82
- namespace, tag = key.split(":")
83
- key_ = str(etree.QName(NSMAP[namespace], tag))
84
- else:
85
- key_ = key.rstrip("_").replace("_", "-")
86
-
87
- if val is None:
88
- val_ = "none"
89
- elif isinstance(val, (int, float)):
90
- val_ = format_number(val)
91
- else:
92
- val_ = val
93
-
94
- return key_, val_
95
-
96
-
97
- def format_attr_dict(**attributes: ElemAttrib) -> dict[str, str]:
98
- """Use svg_ultralight key / value fixer to create a dict of attributes.
99
-
100
- :param attributes: element attribute names and values.
101
- :return: dict of attributes, each key a valid svg attribute name, each value a str
102
- """
103
- return dict(_fix_key_and_format_val(key, val) for key, val in attributes.items())
104
-
105
-
106
- def set_attributes(elem: EtreeElement, **attributes: ElemAttrib) -> None:
107
- """Set name: value items as element attributes. Make every value a string.
108
-
109
- :param elem: element to receive element.set(keyword, str(value)) calls
110
- :param attributes: element attribute names and values. Knows what to do with 'text'
111
- keyword.V :effects: updates ``elem``
112
- """
113
- attr_dict = format_attr_dict(**attributes)
114
-
115
- dots = {"text"}
116
- for dot in dots & set(attr_dict):
117
- setattr(elem, dot, attr_dict.pop(dot))
118
-
119
- for key, val in attr_dict.items():
120
- elem.set(key, val)
121
-
122
-
123
- class _TostringDefaults(Enum):
124
- """Default values for an svg xml_header."""
125
-
126
- DOCTYPE = (
127
- '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
128
- + '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
129
- )
130
- ENCODING = "UTF-8"
131
-
132
-
133
- def svg_tostring(xml: EtreeElement, **tostring_kwargs: str | bool | None) -> bytes:
134
- """Contents of svg file with optional xml declaration.
135
-
136
- :param xml: root node of your svg geometry
137
- :param tostring_kwargs: keyword arguments to etree.tostring.
138
- pass xml_header=True for sensible defaults, see further documentation on xml
139
- header in write_svg docstring.
140
- :return: bytestring of svg file contents
141
- """
142
- tostring_kwargs["pretty_print"] = tostring_kwargs.get("pretty_print", True)
143
- if tostring_kwargs.get("xml_declaration"):
144
- for default in _TostringDefaults:
145
- arg_name = default.name.lower()
146
- value = tostring_kwargs.get(arg_name, default.value)
147
- tostring_kwargs[arg_name] = value
148
- as_bytes = etree.tostring(etree.ElementTree(xml), **tostring_kwargs) # type: ignore
149
- return cast("bytes", as_bytes)
150
-
151
-
152
- def get_viewBox_str(
153
- x: float,
154
- y: float,
155
- width: float,
156
- height: float,
157
- pad: float | tuple[float, float, float, float] = 0,
158
- ) -> str:
159
- """Create a space-delimited string.
160
-
161
- :param x: x value in upper-left corner
162
- :param y: y value in upper-left corner
163
- :param width: width of viewBox
164
- :param height: height of viewBox
165
- :param pad: optionally increase viewBox by pad in all directions
166
- :return: space-delimited string "x y width height"
167
- """
168
- if not isinstance(pad, tuple):
169
- pad = (pad, pad, pad, pad)
170
- pad_t, pad_r, pad_b, pad_l = pad
171
- pad_h = pad_l + pad_r
172
- pad_v = pad_t + pad_b
173
- dims = [
174
- format_number(x) for x in (x - pad_l, y - pad_t, width + pad_h, height + pad_v)
175
- ]
176
- return " ".join(dims)
177
-
178
-
179
- # ===================================================================================
180
- # Encode and decode arbitrary strings to / from valid CSS class names.
181
- # ===================================================================================
182
-
183
-
184
- # insert his before any class name that would otherwise start with a digit
185
- _NAME_PREFIX = "__"
186
-
187
- _DELIMITED_HEX = re.compile(r"_[0-9a-fA-F]{2,8}_")
188
-
189
-
190
- def _encode_class_name_invalid_char_to_hex(char: str) -> str:
191
- """Encode any invalid single char to a hex representation prefixed with '_x'.
192
-
193
- :param char: The character to encode.
194
- :return: A string in the format '_x' followed by the hex value of the character.
195
-
196
- Return valid css-class-name characters unchanged. Encode others. Exception: This
197
- function encodes `_`, which *are* valid CSS class characters, in order to reserve
198
- underscores for `_` hex delimiters and `__` -name prefixes.
199
- """
200
- if re.match(r"[a-zA-Z0-9-]", char):
201
- return char
202
- hex_ = binascii.hexlify(char.encode("utf-8")).decode("ascii")
203
- return f"_{hex_}_"
204
-
205
-
206
- def encode_to_css_class_name(text: str) -> str:
207
- """Convert text to a valid CSS class name in a reversible way.
208
-
209
- :param text: The text to convert.
210
- :return: A valid CSS class name derived from the text. The intended use is to pass
211
- a font filename, so the filename can be decoded from the contents of an SVG
212
- file and each css class created from a font file will, if the style is not
213
- altered, have a unique class name.
214
-
215
- Non-ascii characters like `é` will be encoded as hex, even if they are, by
216
- documentation, valid CSS class characters. The class name will be ascii only.
217
- """
218
- css_class = "".join(_encode_class_name_invalid_char_to_hex(c) for c in text)
219
- # add a prefix if the name starts with a digit or is empty
220
- if not css_class or not re.match(r"^[a-zA-Z_]", css_class):
221
- css_class = _NAME_PREFIX + css_class
222
- return css_class
223
-
224
-
225
- def decode_from_css_class_name(css_class: str) -> str:
226
- """Reverse the conversion from `filename_to_css_class`.
227
-
228
- :param css_class: The CSS class name to convert back to text. This will not
229
- be meaningful if the class name was not created by encode_css_class_name. If
230
- you use another string, there is a potential for a hex decoding error.
231
- :return: The original filename passed to `filename_to_css_class`.
232
- """
233
- css_class = css_class.removeprefix(_NAME_PREFIX)
234
-
235
- result = ""
236
- while css_class:
237
- if match := _DELIMITED_HEX.match(css_class):
238
- hex_str = match.group(0)[1:-1]
239
- result += binascii.unhexlify(hex_str).decode("utf-8")
240
- css_class = css_class[len(match.group(0)) :]
241
- else:
242
- result += css_class[0]
243
- css_class = css_class[1:]
244
- return result
1
+ """Quasi-private functions for high-level string conversion.
2
+
3
+ :author: Shay Hill
4
+ :created: 7/26/2020
5
+
6
+ Rounding some numbers to ensure quality svg rendering:
7
+ * Rounding floats to six digits after the decimal
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import binascii
13
+ import re
14
+ from enum import Enum
15
+ from typing import TYPE_CHECKING, cast
16
+
17
+ import svg_path_data
18
+ from lxml import etree
19
+
20
+ from svg_ultralight.nsmap import NSMAP
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Iterable
24
+
25
+ from lxml.etree import (
26
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
27
+ )
28
+
29
+ from svg_ultralight.attrib_hints import ElemAttrib
30
+
31
+
32
+ def format_number(num: float | str, resolution: int | None = 6) -> str:
33
+ """Format a number into an svg-readable float string with resolution = 6.
34
+
35
+ :param num: number to format (string or float)
36
+ :param resolution: number of digits after the decimal point, defaults to 6. None
37
+ to match behavior of `str(num)`.
38
+ :return: string representation of the number with six digits after the decimal
39
+ (if in fixed-point notation). Will return exponential notation when shorter.
40
+ """
41
+ return svg_path_data.format_number(num, resolution=resolution)
42
+
43
+
44
+ def format_numbers(
45
+ nums: Iterable[float] | Iterable[str] | Iterable[float | str],
46
+ ) -> list[str]:
47
+ """Format multiple strings to limited precision.
48
+
49
+ :param nums: iterable of floats
50
+ :return: list of formatted strings
51
+ """
52
+ return [format_number(num) for num in nums]
53
+
54
+
55
+ def _fix_key_and_format_val(key: str, val: ElemAttrib) -> tuple[str, str]:
56
+ """Format one key, value pair for an svg element.
57
+
58
+ :param key: element attribute name
59
+ :param val: element attribute value
60
+ :return: tuple of key, value
61
+ :raises ValueError: if key is 'd' and val is not a string
62
+
63
+ etree.Elements will only accept string
64
+ values. This saves having to convert input to strings.
65
+
66
+ * convert float values to formatted strings
67
+ * format datastring values when keyword is 'd'
68
+ * replace '_' with '-' in keywords
69
+ * remove trailing '_' from keywords
70
+ * will convert `namespace:tag` to a qualified name
71
+
72
+ SVG attribute names like `font-size` and `stroke-width` are not valid python
73
+ keywords, but can be passed as `font-size` and `stroke-width`.
74
+
75
+ Reserved Python keywords that are also valid and useful SVG attribute names (a
76
+ popular one will be 'class') can be passed with a trailing underscore (e.g.,
77
+ class_='body_text').
78
+ """
79
+ if "http:" in key or "https:" in key:
80
+ key_ = key
81
+ elif ":" in key:
82
+ namespace, tag = key.split(":")
83
+ key_ = str(etree.QName(NSMAP[namespace], tag))
84
+ else:
85
+ key_ = key.rstrip("_").replace("_", "-")
86
+
87
+ if val is None:
88
+ val_ = "none"
89
+ elif isinstance(val, (int, float)):
90
+ val_ = format_number(val)
91
+ else:
92
+ val_ = val
93
+
94
+ return key_, val_
95
+
96
+
97
+ def format_attr_dict(**attributes: ElemAttrib) -> dict[str, str]:
98
+ """Use svg_ultralight key / value fixer to create a dict of attributes.
99
+
100
+ :param attributes: element attribute names and values.
101
+ :return: dict of attributes, each key a valid svg attribute name, each value a str
102
+ """
103
+ return dict(_fix_key_and_format_val(key, val) for key, val in attributes.items())
104
+
105
+
106
+ def set_attributes(elem: EtreeElement, **attributes: ElemAttrib) -> None:
107
+ """Set name: value items as element attributes. Make every value a string.
108
+
109
+ :param elem: element to receive element.set(keyword, str(value)) calls
110
+ :param attributes: element attribute names and values. Knows what to do with 'text'
111
+ keyword.V :effects: updates ``elem``
112
+ """
113
+ attr_dict = format_attr_dict(**attributes)
114
+
115
+ dots = {"text"}
116
+ for dot in dots & set(attr_dict):
117
+ setattr(elem, dot, attr_dict.pop(dot))
118
+
119
+ for key, val in attr_dict.items():
120
+ elem.set(key, val)
121
+
122
+
123
+ class _TostringDefaults(Enum):
124
+ """Default values for an svg xml_header."""
125
+
126
+ DOCTYPE = (
127
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
128
+ + '"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
129
+ )
130
+ ENCODING = "UTF-8"
131
+
132
+
133
+ def svg_tostring(xml: EtreeElement, **tostring_kwargs: str | bool | None) -> bytes:
134
+ """Contents of svg file with optional xml declaration.
135
+
136
+ :param xml: root node of your svg geometry
137
+ :param tostring_kwargs: keyword arguments to etree.tostring.
138
+ pass xml_header=True for sensible defaults, see further documentation on xml
139
+ header in write_svg docstring.
140
+ :return: bytestring of svg file contents
141
+ """
142
+ tostring_kwargs["pretty_print"] = tostring_kwargs.get("pretty_print", True)
143
+ if tostring_kwargs.get("xml_declaration"):
144
+ for default in _TostringDefaults:
145
+ arg_name = default.name.lower()
146
+ value = tostring_kwargs.get(arg_name, default.value)
147
+ tostring_kwargs[arg_name] = value
148
+ as_bytes = etree.tostring(etree.ElementTree(xml), **tostring_kwargs) # type: ignore
149
+ return cast("bytes", as_bytes)
150
+
151
+
152
+ def get_view_box_str(
153
+ x: float,
154
+ y: float,
155
+ width: float,
156
+ height: float,
157
+ pad: float | tuple[float, float, float, float] = 0,
158
+ ) -> str:
159
+ """Create a space-delimited string.
160
+
161
+ :param x: x value in upper-left corner
162
+ :param y: y value in upper-left corner
163
+ :param width: width of viewBox
164
+ :param height: height of viewBox
165
+ :param pad: optionally increase viewBox by pad in all directions
166
+ :return: space-delimited string "x y width height"
167
+ """
168
+ if not isinstance(pad, tuple):
169
+ pad = (pad, pad, pad, pad)
170
+ pad_t, pad_r, pad_b, pad_l = pad
171
+ pad_h = pad_l + pad_r
172
+ pad_v = pad_t + pad_b
173
+ dims = [
174
+ format_number(x) for x in (x - pad_l, y - pad_t, width + pad_h, height + pad_v)
175
+ ]
176
+ return " ".join(dims)
177
+
178
+
179
+ # ===================================================================================
180
+ # Encode and decode arbitrary strings to / from valid CSS class names.
181
+ # ===================================================================================
182
+
183
+
184
+ # insert his before any class name that would otherwise start with a digit
185
+ _NAME_PREFIX = "__"
186
+
187
+ _DELIMITED_HEX = re.compile(r"_[0-9a-fA-F]{2,8}_")
188
+
189
+
190
+ def _encode_class_name_invalid_char_to_hex(char: str) -> str:
191
+ """Encode any invalid single char to a hex representation prefixed with '_x'.
192
+
193
+ :param char: The character to encode.
194
+ :return: A string in the format '_x' followed by the hex value of the character.
195
+
196
+ Return valid css-class-name characters unchanged. Encode others. Exception: This
197
+ function encodes `_`, which *are* valid CSS class characters, in order to reserve
198
+ underscores for `_` hex delimiters and `__` -name prefixes.
199
+ """
200
+ if re.match(r"[a-zA-Z0-9-]", char):
201
+ return char
202
+ hex_ = binascii.hexlify(char.encode("utf-8")).decode("ascii")
203
+ return f"_{hex_}_"
204
+
205
+
206
+ def encode_to_css_class_name(text: str) -> str:
207
+ """Convert text to a valid CSS class name in a reversible way.
208
+
209
+ :param text: The text to convert.
210
+ :return: A valid CSS class name derived from the text. The intended use is to pass
211
+ a font filename, so the filename can be decoded from the contents of an SVG
212
+ file and each css class created from a font file will, if the style is not
213
+ altered, have a unique class name.
214
+
215
+ Non-ascii characters like `é` will be encoded as hex, even if they are, by
216
+ documentation, valid CSS class characters. The class name will be ascii only.
217
+ """
218
+ css_class = "".join(_encode_class_name_invalid_char_to_hex(c) for c in text)
219
+ # add a prefix if the name starts with a digit or is empty
220
+ if not css_class or not re.match(r"^[a-zA-Z_]", css_class):
221
+ css_class = _NAME_PREFIX + css_class
222
+ return css_class
223
+
224
+
225
+ def decode_from_css_class_name(css_class: str) -> str:
226
+ """Reverse the conversion from `filename_to_css_class`.
227
+
228
+ :param css_class: The CSS class name to convert back to text. This will not
229
+ be meaningful if the class name was not created by encode_css_class_name. If
230
+ you use another string, there is a potential for a hex decoding error.
231
+ :return: The original filename passed to `filename_to_css_class`.
232
+ """
233
+ css_class = css_class.removeprefix(_NAME_PREFIX)
234
+
235
+ result = ""
236
+ while css_class:
237
+ if match := _DELIMITED_HEX.match(css_class):
238
+ hex_str = match.group(0)[1:-1]
239
+ result += binascii.unhexlify(hex_str).decode("utf-8")
240
+ css_class = css_class[len(match.group(0)) :]
241
+ else:
242
+ result += css_class[0]
243
+ css_class = css_class[1:]
244
+ return result
@@ -1,13 +1,21 @@
1
- """Import svg_strings into the svg_ultralight.strings namespace.
2
-
3
- :author: Shay Hill
4
- :created: 2021-12-09
5
- """
6
-
7
- from svg_ultralight.strings.svg_strings import (
8
- svg_color_tuple,
9
- svg_float_tuples,
10
- svg_ints,
11
- )
12
-
13
- __all__ = ["svg_color_tuple", "svg_float_tuples", "svg_ints"]
1
+ """Import svg_strings into the svg_ultralight.strings namespace.
2
+
3
+ :author: Shay Hill
4
+ :created: 2021-12-09
5
+ """
6
+
7
+ from svg_ultralight.strings.svg_strings import (
8
+ svg_color_tuple,
9
+ svg_float_tuples,
10
+ svg_floats,
11
+ svg_ints,
12
+ svg_matrix,
13
+ )
14
+
15
+ __all__ = [
16
+ "svg_color_tuple",
17
+ "svg_float_tuples",
18
+ "svg_floats",
19
+ "svg_ints",
20
+ "svg_matrix",
21
+ ]