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.
- svg_ultralight/__init__.py +108 -105
- svg_ultralight/animate.py +40 -40
- svg_ultralight/attrib_hints.py +13 -14
- svg_ultralight/bounding_boxes/__init__.py +5 -5
- svg_ultralight/bounding_boxes/bound_helpers.py +189 -201
- svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -206
- svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
- svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
- svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
- svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
- svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
- svg_ultralight/constructors/__init__.py +14 -14
- svg_ultralight/constructors/new_element.py +115 -115
- svg_ultralight/font_tools/__init__.py +5 -5
- svg_ultralight/font_tools/comp_results.py +295 -293
- svg_ultralight/font_tools/font_info.py +793 -784
- svg_ultralight/image_ops.py +156 -156
- svg_ultralight/inkscape.py +261 -261
- svg_ultralight/layout.py +290 -291
- svg_ultralight/main.py +183 -198
- svg_ultralight/metadata.py +122 -122
- svg_ultralight/nsmap.py +36 -36
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -249
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +87 -87
- svg_ultralight/string_conversion.py +244 -244
- svg_ultralight/strings/__init__.py +21 -13
- svg_ultralight/strings/svg_strings.py +106 -67
- svg_ultralight/transformations.py +140 -141
- svg_ultralight/unit_conversion.py +247 -248
- {svg_ultralight-0.47.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
- svg_ultralight-0.50.1.dist-info/RECORD +34 -0
- svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
- svg_ultralight-0.47.0.dist-info/RECORD +0 -34
- svg_ultralight-0.47.0.dist-info/WHEEL +0 -5
- svg_ultralight-0.47.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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
]
|