svg-ultralight 0.42.0__tar.gz → 0.43.0__tar.gz
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-0.42.0 → svg_ultralight-0.43.0}/.pre-commit-config.yaml +5 -3
- {svg_ultralight-0.42.0/src/svg_ultralight.egg-info → svg_ultralight-0.43.0}/PKG-INFO +2 -1
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/dev-requirements.txt +20 -12
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/pyproject.toml +3 -2
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/__init__.py +0 -2
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/padded_text_initializers.py +7 -2
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/font_tools/font_info.py +131 -1
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/string_conversion.py +14 -143
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0/src/svg_ultralight.egg-info}/PKG-INFO +2 -1
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight.egg-info/requires.txt +1 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_string_conversion.py +18 -86
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/.gitignore +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/README.md +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/experiments/encode_fonts3.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/experiments/font_css.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/setup.cfg +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/animate.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/constructors/__init__.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/constructors/new_element.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/font_tools/__init__.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/font_tools/comp_results.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/font_tools/globs.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/image_ops.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/inkscape.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/layout.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/main.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/metadata.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/nsmap.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/py.typed +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/query.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/root_elements.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/strings/__init__.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/transformations.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/unit_conversion.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight.egg-info/SOURCES.txt +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/__init__.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/conftest.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/resources/arrow.svg +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_bounding.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_inkscape.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_layout.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_matrices.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_metadata.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_new_element.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_padded_text_initializers.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_padding.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_queries.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_root_elements.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tests/test_svg_ultralight.py +0 -0
- {svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/tox.ini +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
ci:
|
|
2
2
|
skip: [pyright]
|
|
3
3
|
|
|
4
|
-
exclude: "
|
|
4
|
+
exclude: "experiments"
|
|
5
5
|
|
|
6
6
|
repos:
|
|
7
7
|
|
|
@@ -84,6 +84,7 @@ repos:
|
|
|
84
84
|
# PGH003 Use specific rule codes when ignoring type issues
|
|
85
85
|
# PLR0913 Too many arguments to function call
|
|
86
86
|
# PTH108 os.unlink should be replaced by .unlink()
|
|
87
|
+
# SIM108 insists on ternary operator
|
|
87
88
|
#
|
|
88
89
|
# S101 Use of `assert` detected
|
|
89
90
|
# S603 `subprocess` call: check for execution of untrusted input
|
|
@@ -94,15 +95,16 @@ repos:
|
|
|
94
95
|
# BLE001 Use of `except Exception:` detected
|
|
95
96
|
# ANN401 Any type disallowed
|
|
96
97
|
# FLY002 Consider f-string instead of string join
|
|
98
|
+
# S311 Standard pseudo-random generator used
|
|
97
99
|
rev: 'v0.11.11'
|
|
98
100
|
hooks:
|
|
99
101
|
- id: ruff
|
|
100
102
|
name: "ruff-lint"
|
|
101
|
-
exclude: "tests"
|
|
103
|
+
# exclude: "tests"
|
|
102
104
|
args:
|
|
103
105
|
- --target-version=py39
|
|
104
106
|
- --select=ALL
|
|
105
|
-
- --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001,ANN401,FLY002
|
|
107
|
+
- --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001,ANN401,FLY002,SIM108,S311
|
|
106
108
|
- --fix
|
|
107
109
|
- --fixable=RUF022
|
|
108
110
|
- id: ruff
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: svg-ultralight
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.43.0
|
|
4
4
|
Summary: a sensible way to create svg files with Python
|
|
5
5
|
Author-email: Shay Hill <shay_public@hotmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: fonttools
|
|
|
11
11
|
Requires-Dist: lxml
|
|
12
12
|
Requires-Dist: paragraphs
|
|
13
13
|
Requires-Dist: pillow
|
|
14
|
+
Requires-Dist: svg-path-data
|
|
14
15
|
Requires-Dist: types-lxml
|
|
15
16
|
Requires-Dist: typing-extensions
|
|
16
17
|
Provides-Extra: dev
|
|
@@ -8,7 +8,7 @@ argcomplete==3.6.2
|
|
|
8
8
|
# via commitizen
|
|
9
9
|
beautifulsoup4==4.13.4
|
|
10
10
|
# via types-lxml
|
|
11
|
-
cachetools==6.
|
|
11
|
+
cachetools==6.1.0
|
|
12
12
|
# via tox
|
|
13
13
|
cfgv==3.4.0
|
|
14
14
|
# via pre-commit
|
|
@@ -21,7 +21,7 @@ colorama==0.4.6
|
|
|
21
21
|
# commitizen
|
|
22
22
|
# pytest
|
|
23
23
|
# tox
|
|
24
|
-
commitizen==4.8.
|
|
24
|
+
commitizen==4.8.3
|
|
25
25
|
# via svg-ultralight (pyproject.toml)
|
|
26
26
|
cssselect==1.3.0
|
|
27
27
|
# via types-lxml
|
|
@@ -35,15 +35,17 @@ filelock==3.18.0
|
|
|
35
35
|
# via
|
|
36
36
|
# tox
|
|
37
37
|
# virtualenv
|
|
38
|
-
fonttools==4.58.
|
|
38
|
+
fonttools==4.58.5
|
|
39
39
|
# via svg-ultralight (pyproject.toml)
|
|
40
40
|
identify==2.6.12
|
|
41
41
|
# via pre-commit
|
|
42
|
+
importlib-metadata==8.7.0
|
|
43
|
+
# via commitizen
|
|
42
44
|
iniconfig==2.1.0
|
|
43
45
|
# via pytest
|
|
44
46
|
jinja2==3.1.6
|
|
45
47
|
# via commitizen
|
|
46
|
-
lxml==
|
|
48
|
+
lxml==6.0.0
|
|
47
49
|
# via svg-ultralight (pyproject.toml)
|
|
48
50
|
markupsafe==3.0.2
|
|
49
51
|
# via jinja2
|
|
@@ -58,8 +60,10 @@ packaging==25.0
|
|
|
58
60
|
# pytest
|
|
59
61
|
# tox
|
|
60
62
|
paragraphs==1.0.1
|
|
61
|
-
# via
|
|
62
|
-
|
|
63
|
+
# via
|
|
64
|
+
# svg-path-data
|
|
65
|
+
# svg-ultralight (pyproject.toml)
|
|
66
|
+
pillow==11.3.0
|
|
63
67
|
# via svg-ultralight (pyproject.toml)
|
|
64
68
|
platformdirs==4.3.8
|
|
65
69
|
# via
|
|
@@ -73,11 +77,11 @@ pre-commit==4.2.0
|
|
|
73
77
|
# via svg-ultralight (pyproject.toml)
|
|
74
78
|
prompt-toolkit==3.0.51
|
|
75
79
|
# via questionary
|
|
76
|
-
pygments==2.19.
|
|
80
|
+
pygments==2.19.2
|
|
77
81
|
# via pytest
|
|
78
82
|
pyproject-api==1.9.1
|
|
79
83
|
# via tox
|
|
80
|
-
pytest==8.4.
|
|
84
|
+
pytest==8.4.1
|
|
81
85
|
# via svg-ultralight (pyproject.toml)
|
|
82
86
|
pyyaml==6.0.2
|
|
83
87
|
# via
|
|
@@ -87,17 +91,19 @@ questionary==2.1.0
|
|
|
87
91
|
# via commitizen
|
|
88
92
|
soupsieve==2.7
|
|
89
93
|
# via beautifulsoup4
|
|
90
|
-
|
|
94
|
+
svg-path-data==0.3.0
|
|
95
|
+
# via svg-ultralight (pyproject.toml)
|
|
96
|
+
termcolor==3.1.0
|
|
91
97
|
# via commitizen
|
|
92
|
-
tomlkit==0.13.
|
|
98
|
+
tomlkit==0.13.3
|
|
93
99
|
# via commitizen
|
|
94
|
-
tox==4.
|
|
100
|
+
tox==4.27.0
|
|
95
101
|
# via svg-ultralight (pyproject.toml)
|
|
96
102
|
types-html5lib==1.1.11.20250516
|
|
97
103
|
# via types-lxml
|
|
98
104
|
types-lxml==2025.3.30
|
|
99
105
|
# via svg-ultralight (pyproject.toml)
|
|
100
|
-
typing-extensions==4.14.
|
|
106
|
+
typing-extensions==4.14.1
|
|
101
107
|
# via
|
|
102
108
|
# beautifulsoup4
|
|
103
109
|
# svg-ultralight (pyproject.toml)
|
|
@@ -108,3 +114,5 @@ virtualenv==20.31.2
|
|
|
108
114
|
# tox
|
|
109
115
|
wcwidth==0.2.13
|
|
110
116
|
# via prompt-toolkit
|
|
117
|
+
zipp==3.23.0
|
|
118
|
+
# via importlib-metadata
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "svg-ultralight"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.43.0"
|
|
4
4
|
description = "a sensible way to create svg files with Python"
|
|
5
5
|
authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
"lxml",
|
|
13
13
|
"paragraphs",
|
|
14
14
|
"pillow",
|
|
15
|
+
"svg-path-data",
|
|
15
16
|
"types-lxml",
|
|
16
17
|
"typing-extensions",
|
|
17
18
|
]
|
|
@@ -49,7 +50,7 @@ convention = "pep257"
|
|
|
49
50
|
|
|
50
51
|
[tool.commitizen]
|
|
51
52
|
name = "cz_conventional_commits"
|
|
52
|
-
version = "0.
|
|
53
|
+
version = "0.43.0"
|
|
53
54
|
tag_format = "$version"
|
|
54
55
|
version_files = ["pyproject.toml:^version"]
|
|
55
56
|
annotated_tag = true
|
|
@@ -51,7 +51,6 @@ from svg_ultralight.string_conversion import (
|
|
|
51
51
|
format_attr_dict,
|
|
52
52
|
format_number,
|
|
53
53
|
format_numbers,
|
|
54
|
-
format_numbers_in_string,
|
|
55
54
|
)
|
|
56
55
|
from svg_ultralight.transformations import (
|
|
57
56
|
mat_apply,
|
|
@@ -75,7 +74,6 @@ __all__ = [
|
|
|
75
74
|
"format_attr_dict",
|
|
76
75
|
"format_number",
|
|
77
76
|
"format_numbers",
|
|
78
|
-
"format_numbers_in_string",
|
|
79
77
|
"get_bounding_box",
|
|
80
78
|
"get_bounding_boxes",
|
|
81
79
|
"mat_apply",
|
|
@@ -122,12 +122,17 @@ def pad_text_ft(
|
|
|
122
122
|
"""
|
|
123
123
|
attributes_ = format_attr_dict(**attributes)
|
|
124
124
|
attributes_.update(get_svg_font_attributes(font))
|
|
125
|
-
attributes_["font-size"] = attributes_.get("font-size", format_number(font_size))
|
|
126
125
|
|
|
127
|
-
|
|
126
|
+
_ = attributes_.pop("font-size", None)
|
|
127
|
+
_ = attributes_.pop("font-family", None)
|
|
128
|
+
_ = attributes_.pop("font-style", None)
|
|
129
|
+
_ = attributes_.pop("font-weight", None)
|
|
130
|
+
_ = attributes_.pop("font-stretch", None)
|
|
131
|
+
|
|
128
132
|
info = get_padded_text_info(
|
|
129
133
|
font, text, font_size, ascent, descent, y_bounds_reference=y_bounds_reference
|
|
130
134
|
)
|
|
135
|
+
elem = info.new_element(**attributes_)
|
|
131
136
|
return PaddedText(elem, info.bbox, *info.padding, info.line_gap)
|
|
132
137
|
|
|
133
138
|
|
|
@@ -102,19 +102,101 @@ from contextlib import suppress
|
|
|
102
102
|
from pathlib import Path
|
|
103
103
|
from typing import TYPE_CHECKING, Any, cast
|
|
104
104
|
|
|
105
|
+
from fontTools.pens.basePen import BasePen
|
|
105
106
|
from fontTools.pens.boundsPen import BoundsPen
|
|
106
107
|
from fontTools.ttLib import TTFont
|
|
108
|
+
from svg_path_data import format_svgd_shortest, get_cpts_from_svgd, get_svgd_from_cpts
|
|
107
109
|
|
|
108
110
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
111
|
+
from svg_ultralight.constructors.new_element import new_element
|
|
109
112
|
from svg_ultralight.font_tools.globs import DEFAULT_FONT_SIZE
|
|
113
|
+
from svg_ultralight.string_conversion import format_numbers
|
|
110
114
|
|
|
111
115
|
if TYPE_CHECKING:
|
|
112
116
|
import os
|
|
117
|
+
from collections.abc import Iterator
|
|
113
118
|
|
|
119
|
+
from lxml.etree import _Element as EtreeElement
|
|
114
120
|
|
|
115
121
|
logging.getLogger("fontTools").setLevel(logging.ERROR)
|
|
116
122
|
|
|
117
123
|
|
|
124
|
+
def _split_into_quadratic(
|
|
125
|
+
*pts: tuple[float, float],
|
|
126
|
+
) -> Iterator[tuple[tuple[float, float], tuple[float, float]]]:
|
|
127
|
+
"""Connect a series of points with quadratic bezier segments.
|
|
128
|
+
|
|
129
|
+
:param points: a series of at least two (x, y) coordinates.
|
|
130
|
+
:return: an iterator of ((x, y), (x, y)) quadatic bezier control points (the
|
|
131
|
+
second and third points)
|
|
132
|
+
|
|
133
|
+
This is part of connecting a (not provided) current point to the last input
|
|
134
|
+
point. The other input points will be control points of a series of quadratic
|
|
135
|
+
Bezier curves. New Bezier curve endpoints will be created between these points.
|
|
136
|
+
|
|
137
|
+
given (B, C, D, E) (with A as the not-provided current point):
|
|
138
|
+
- [A, B, bc][1:]
|
|
139
|
+
- [bc, C, cd][1:]
|
|
140
|
+
- [cd, D, E][1:]
|
|
141
|
+
"""
|
|
142
|
+
if len(pts) < 2:
|
|
143
|
+
msg = "At least two points are required."
|
|
144
|
+
raise ValueError(msg)
|
|
145
|
+
for prev_cp, next_cp in it.pairwise(pts[:-1]):
|
|
146
|
+
xs, ys = zip(prev_cp, next_cp)
|
|
147
|
+
midpnt = sum(xs) / 2, sum(ys) / 2
|
|
148
|
+
yield prev_cp, midpnt
|
|
149
|
+
yield pts[-2], pts[-1]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PathPen(BasePen):
|
|
153
|
+
"""A pen to collect svg path data commands from a glyph."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, glyph_set: Any) -> None:
|
|
156
|
+
"""Initialize the PathPen with a glyph set.
|
|
157
|
+
|
|
158
|
+
:param glyph_set: TTFont(path).getGlyphSet()
|
|
159
|
+
"""
|
|
160
|
+
super().__init__(glyph_set)
|
|
161
|
+
self._cmds: list[str] = []
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def svgd(self) -> str:
|
|
165
|
+
"""Return an svg path data string for the glyph."""
|
|
166
|
+
if not self._cmds:
|
|
167
|
+
return ""
|
|
168
|
+
svgd = format_svgd_shortest(" ".join(self._cmds))
|
|
169
|
+
return "M" + svgd[1:]
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def cpts(self) -> list[list[tuple[float, float]]]:
|
|
173
|
+
"""Return as a list of lists of Bezier control points."""
|
|
174
|
+
return get_cpts_from_svgd(" ".join(self._cmds))
|
|
175
|
+
|
|
176
|
+
def moveTo(self, pt: tuple[float, float]) -> None:
|
|
177
|
+
"""Move the current point to a new location."""
|
|
178
|
+
self._cmds.extend(("M", *map(str, pt)))
|
|
179
|
+
|
|
180
|
+
def lineTo(self, pt: tuple[float, float]) -> None:
|
|
181
|
+
"""Add a line segment to the path."""
|
|
182
|
+
self._cmds.extend(("L", *map(str, pt)))
|
|
183
|
+
|
|
184
|
+
def curveTo(self, *pts: tuple[float, float]) -> None:
|
|
185
|
+
"""Add a series of cubic bezier segments to the path."""
|
|
186
|
+
msg = "Cubic Bezier curves not implemented for getting svg path data."
|
|
187
|
+
raise NotImplementedError(msg)
|
|
188
|
+
self._cmds.extend(("Q", *map(str, it.chain(*pts))))
|
|
189
|
+
|
|
190
|
+
def qCurveTo(self, *pts: tuple[float, float]) -> None:
|
|
191
|
+
"""Add a series of quadratic bezier segments to the path."""
|
|
192
|
+
for q_pts in _split_into_quadratic(*pts):
|
|
193
|
+
self._cmds.extend(("Q", *map(str, it.chain(*q_pts))))
|
|
194
|
+
|
|
195
|
+
def closePath(self):
|
|
196
|
+
"""Close the current path."""
|
|
197
|
+
self._cmds.append("Z")
|
|
198
|
+
|
|
199
|
+
|
|
118
200
|
class FTFontInfo:
|
|
119
201
|
"""Hide all the type kludging necessary to use fontTools."""
|
|
120
202
|
|
|
@@ -205,6 +287,26 @@ class FTFontInfo:
|
|
|
205
287
|
msg = f"Character '{char}' not found in font '{self.path}'."
|
|
206
288
|
raise ValueError(msg)
|
|
207
289
|
|
|
290
|
+
def get_char_svgd(self, char: str, dx: float = 0) -> str:
|
|
291
|
+
"""Return the svg path data for a glyph.
|
|
292
|
+
|
|
293
|
+
:param char: The character to get the svg path data for.
|
|
294
|
+
:param dx: An optional x translation to apply to the glyph.
|
|
295
|
+
:return: The svg path data for the character.
|
|
296
|
+
"""
|
|
297
|
+
glyph_set = self.font.getGlyphSet()
|
|
298
|
+
glyph_name = self.font.getBestCmap().get(ord(char))
|
|
299
|
+
path_pen = PathPen(glyph_set)
|
|
300
|
+
_ = glyph_set[glyph_name].draw(path_pen)
|
|
301
|
+
svgd = path_pen.svgd
|
|
302
|
+
if not dx or not svgd:
|
|
303
|
+
return svgd
|
|
304
|
+
cpts = path_pen.cpts
|
|
305
|
+
for i, curve in enumerate(cpts):
|
|
306
|
+
cpts[i][:] = [(x + dx, y) for x, y in curve]
|
|
307
|
+
svgd = format_svgd_shortest(get_svgd_from_cpts(cpts))
|
|
308
|
+
return "M" + svgd[1:]
|
|
309
|
+
|
|
208
310
|
def get_char_bounds(self, char: str) -> tuple[int, int, int, int]:
|
|
209
311
|
"""Return the min and max x and y coordinates of a glyph.
|
|
210
312
|
|
|
@@ -217,7 +319,6 @@ class FTFontInfo:
|
|
|
217
319
|
glyph_name = self.font.getBestCmap().get(ord(char))
|
|
218
320
|
bounds_pen = BoundsPen(glyph_set)
|
|
219
321
|
_ = glyph_set[glyph_name].draw(bounds_pen)
|
|
220
|
-
|
|
221
322
|
pen_bounds = cast("None | tuple[int, int, int, int]", bounds_pen.bounds)
|
|
222
323
|
if pen_bounds is None:
|
|
223
324
|
return 0, 0, 0, 0
|
|
@@ -264,6 +365,25 @@ class FTFontInfo:
|
|
|
264
365
|
max_y = max(max_ys)
|
|
265
366
|
return min_x, min_y, max_x, max_y
|
|
266
367
|
|
|
368
|
+
def get_text_svgd(self, text: str, dx: float = 0) -> str:
|
|
369
|
+
"""Return the svg path data for a string.
|
|
370
|
+
|
|
371
|
+
:param text: The text to get the svg path data for.
|
|
372
|
+
:param dx: An optional x translation to apply to the entire text.
|
|
373
|
+
:return: The svg path data for the text.
|
|
374
|
+
"""
|
|
375
|
+
hmtx = cast("dict[str, tuple[int, int]]", self.font["hmtx"])
|
|
376
|
+
svgd = ""
|
|
377
|
+
char_dx = dx
|
|
378
|
+
for c_this, c_next in it.pairwise(text):
|
|
379
|
+
this_name = self.get_glyph_name(c_this)
|
|
380
|
+
next_name = self.get_glyph_name(c_next)
|
|
381
|
+
svgd += self.get_char_svgd(c_this, char_dx)
|
|
382
|
+
char_dx += hmtx[this_name][0]
|
|
383
|
+
char_dx += self.kern_table.get((this_name, next_name), 0)
|
|
384
|
+
svgd += self.get_char_svgd(text[-1], char_dx)
|
|
385
|
+
return svgd
|
|
386
|
+
|
|
267
387
|
def get_text_bbox(self, text: str) -> BoundingBox:
|
|
268
388
|
"""Return the BoundingBox of a string svg coordinates.
|
|
269
389
|
|
|
@@ -333,6 +453,16 @@ class FTTextInfo:
|
|
|
333
453
|
"""
|
|
334
454
|
return self.font_size / self.font.units_per_em
|
|
335
455
|
|
|
456
|
+
def new_element(self, **attributes: str | float) -> EtreeElement:
|
|
457
|
+
"""Return an svg text element with the appropriate font attributes."""
|
|
458
|
+
matrix_vals = (self.scale, 0, 0, -self.scale, 0, 0)
|
|
459
|
+
matrix = f"matrix({' '.join(format_numbers(matrix_vals))})"
|
|
460
|
+
attributes["transform"] = matrix
|
|
461
|
+
stroke_width = attributes.get("stroke-width")
|
|
462
|
+
if stroke_width:
|
|
463
|
+
attributes["stroke-width"] = float(stroke_width) / self.scale
|
|
464
|
+
return new_element("path", d=self.font.get_text_svgd(self.text), **attributes)
|
|
465
|
+
|
|
336
466
|
@property
|
|
337
467
|
def bbox(self) -> BoundingBox:
|
|
338
468
|
"""Return the bounding box of the text.
|
|
@@ -11,10 +11,10 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import binascii
|
|
13
13
|
import re
|
|
14
|
-
from contextlib import suppress
|
|
15
14
|
from enum import Enum
|
|
16
15
|
from typing import TYPE_CHECKING, cast
|
|
17
16
|
|
|
17
|
+
import svg_path_data
|
|
18
18
|
from lxml import etree
|
|
19
19
|
|
|
20
20
|
from svg_ultralight.nsmap import NSMAP
|
|
@@ -27,109 +27,16 @@ if TYPE_CHECKING:
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
_MAYBE_FRACTION = r"(?:\.(?P<fraction>\d+))?"
|
|
33
|
-
_MAYBE_EXP = r"(?:[eE](?P<exponent>[+-]?\d+))?"
|
|
30
|
+
def format_number(num: float | str, resolution: int | None = 6) -> str:
|
|
31
|
+
"""Format a number into an svg-readable float string with resolution = 6.
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"""Split a float string into its sign, integer part, fractional part, and exponent.
|
|
41
|
-
|
|
42
|
-
:param num_str: A string representing the number (e.g., '1.23e+03').
|
|
43
|
-
:return: A tuple containing the integer part, fractional part, and exponent.
|
|
44
|
-
"""
|
|
45
|
-
if float(num) == 0:
|
|
46
|
-
return "", "", "", 0
|
|
47
|
-
num_str = str(num)
|
|
48
|
-
groups = FLOAT_PATTERN.fullmatch(num_str)
|
|
49
|
-
if not groups:
|
|
50
|
-
msg = "Invalid number string: {num_str}."
|
|
51
|
-
raise ValueError(msg.format(num_str=num_str))
|
|
52
|
-
|
|
53
|
-
sign = groups["negative"] or ""
|
|
54
|
-
integer = (groups["integer"] or "").lstrip("0")
|
|
55
|
-
fraction = (groups["fraction"] or "").rstrip("0")
|
|
56
|
-
exponent = int(groups["exponent"] or 0)
|
|
57
|
-
return sign, integer, fraction, exponent
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _format_as_fixed_point(num: str | float) -> str:
|
|
61
|
-
"""Format a number in fixed-point notation.
|
|
62
|
-
|
|
63
|
-
:param exp_str: A string representing the number in exponential notation
|
|
64
|
-
(e.g., '1.23e+03') or just a number.
|
|
65
|
-
:return: A string representing the number in fixed-point notation.
|
|
66
|
-
"""
|
|
67
|
-
sign, integer, fraction, exponent = _split_float_str(num)
|
|
68
|
-
if exponent > 0:
|
|
69
|
-
fraction = fraction.ljust(exponent, "0")
|
|
70
|
-
integer += fraction[:exponent]
|
|
71
|
-
fraction = fraction[exponent:]
|
|
72
|
-
elif exponent < 0:
|
|
73
|
-
integer = integer.rjust(-exponent, "0")
|
|
74
|
-
fraction = integer[exponent:] + fraction
|
|
75
|
-
integer = integer[:exponent]
|
|
76
|
-
|
|
77
|
-
fraction = "." + fraction if fraction else ""
|
|
78
|
-
return f"{sign}{integer}{fraction}" or "0"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _format_as_exponential(num: str | float) -> str:
|
|
82
|
-
"""Convert a number in fixed-point notation (as a string) to exponential notation.
|
|
83
|
-
|
|
84
|
-
:param num_str: A string representing the number in fixed-point notation
|
|
85
|
-
(e.g., '123000') or just a number.
|
|
86
|
-
:return: A string representing the number in exponential notation.
|
|
33
|
+
:param num: number to format (string or float)
|
|
34
|
+
:param resolution: number of digits after the decimal point, defaults to 6. None
|
|
35
|
+
to match behavior of `str(num)`.
|
|
36
|
+
:return: string representation of the number with six digits after the decimal
|
|
37
|
+
(if in fixed-point notation). Will return exponential notation when shorter.
|
|
87
38
|
"""
|
|
88
|
-
|
|
89
|
-
if len(integer) > 1:
|
|
90
|
-
exponent += len(integer) - 1
|
|
91
|
-
fraction = (integer[1:] + fraction).rstrip("0")
|
|
92
|
-
integer = integer[0]
|
|
93
|
-
elif not integer and fraction:
|
|
94
|
-
leading_zeroes = len(fraction) - len(fraction.lstrip("0"))
|
|
95
|
-
exponent -= leading_zeroes + 1
|
|
96
|
-
integer = fraction[leading_zeroes]
|
|
97
|
-
fraction = fraction[leading_zeroes + 1 :]
|
|
98
|
-
|
|
99
|
-
fraction = "." + fraction if fraction else ""
|
|
100
|
-
exp_str = f"e{exponent}" if exponent else ""
|
|
101
|
-
return f"{sign}{integer}{fraction}{exp_str}" or "0"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def format_number(num: float | str, precision: float | None = 6) -> str:
|
|
105
|
-
"""Format strings at limited precision.
|
|
106
|
-
|
|
107
|
-
:param num: anything that can print as a float.
|
|
108
|
-
:param precision: number of digits after the decimal point, default 6. You can
|
|
109
|
-
also pass None for no precision limit. This may produce some long strings,
|
|
110
|
-
but will retain as much information as possible when converting between
|
|
111
|
-
floats and strings.
|
|
112
|
-
:return: str
|
|
113
|
-
|
|
114
|
-
I've read articles that recommend no more than four digits before and two digits
|
|
115
|
-
after the decimal point to ensure good svg rendering. I'm being generous and
|
|
116
|
-
giving six. Mostly to eliminate exponential notation, but I'm "rstripping" the
|
|
117
|
-
strings to reduce filesize and increase readability
|
|
118
|
-
|
|
119
|
-
* reduce fp precision to (default) 6 digits
|
|
120
|
-
* remove trailing zeros
|
|
121
|
-
* remove trailing decimal point
|
|
122
|
-
* remove leading 0 in "0.123"
|
|
123
|
-
* convert "-0" to "0"
|
|
124
|
-
* use shorter of exponential or fixed-point notation
|
|
125
|
-
"""
|
|
126
|
-
if precision is not None:
|
|
127
|
-
num = f"{float(num):.{precision}f}"
|
|
128
|
-
exponential_str = _format_as_exponential(num)
|
|
129
|
-
fixed_point_str = _format_as_fixed_point(num)
|
|
130
|
-
if len(exponential_str) < len(fixed_point_str):
|
|
131
|
-
return exponential_str
|
|
132
|
-
return fixed_point_str
|
|
39
|
+
return svg_path_data.format_number(num, resolution=resolution)
|
|
133
40
|
|
|
134
41
|
|
|
135
42
|
def format_numbers(
|
|
@@ -143,44 +50,6 @@ def format_numbers(
|
|
|
143
50
|
return [format_number(num) for num in nums]
|
|
144
51
|
|
|
145
52
|
|
|
146
|
-
def _is_float_or_float_str(data: float | str) -> bool:
|
|
147
|
-
"""Check if a string is a float.
|
|
148
|
-
|
|
149
|
-
:param data: string to check
|
|
150
|
-
:return: bool
|
|
151
|
-
"""
|
|
152
|
-
try:
|
|
153
|
-
_ = float(data)
|
|
154
|
-
except ValueError:
|
|
155
|
-
return False
|
|
156
|
-
else:
|
|
157
|
-
return True
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def format_numbers_in_string(data: float | str) -> str:
|
|
161
|
-
"""Find and format floats in a string.
|
|
162
|
-
|
|
163
|
-
:param data: string with floats or a float value
|
|
164
|
-
:return: string with floats formatted to limited precision
|
|
165
|
-
|
|
166
|
-
Works as a more robust version of format_number. Will correctly handle input
|
|
167
|
-
floats in exponential notation. This should work for any parameter value in an
|
|
168
|
-
svg except 'text', 'id' and for any other value except hex color codes. The
|
|
169
|
-
function will fail with input strings like 'ice3.14bucket', because 'e3.14' will
|
|
170
|
-
be identified as a float. SVG param values will not have such strings, but the
|
|
171
|
-
'text' attribute could. This function will not handle that case. Do not attempt
|
|
172
|
-
to reformat 'text' attribute values.
|
|
173
|
-
"""
|
|
174
|
-
with suppress(ValueError):
|
|
175
|
-
# try as a regular number to strip spaces from simple float strings
|
|
176
|
-
return format_number(data)
|
|
177
|
-
if str(data).startswith("#"):
|
|
178
|
-
return str(data)
|
|
179
|
-
words = re.split(r"([^\d.eE-]+)", str(data))
|
|
180
|
-
words = [format_number(w) if _is_float_or_float_str(w) else w for w in words]
|
|
181
|
-
return "".join(words)
|
|
182
|
-
|
|
183
|
-
|
|
184
53
|
def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
|
|
185
54
|
"""Format one key, value pair for an svg element.
|
|
186
55
|
|
|
@@ -213,10 +82,12 @@ def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
|
|
|
213
82
|
else:
|
|
214
83
|
key_ = key.rstrip("_").replace("_", "-")
|
|
215
84
|
|
|
216
|
-
if
|
|
217
|
-
|
|
85
|
+
if isinstance(val, (int, float)):
|
|
86
|
+
val_ = format_number(val)
|
|
87
|
+
else:
|
|
88
|
+
val_ = val
|
|
218
89
|
|
|
219
|
-
return key_,
|
|
90
|
+
return key_, val_
|
|
220
91
|
|
|
221
92
|
|
|
222
93
|
def format_attr_dict(**attributes: str | float) -> dict[str, str]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: svg-ultralight
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.43.0
|
|
4
4
|
Summary: a sensible way to create svg files with Python
|
|
5
5
|
Author-email: Shay Hill <shay_public@hotmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -11,6 +11,7 @@ Requires-Dist: fonttools
|
|
|
11
11
|
Requires-Dist: lxml
|
|
12
12
|
Requires-Dist: paragraphs
|
|
13
13
|
Requires-Dist: pillow
|
|
14
|
+
Requires-Dist: svg-path-data
|
|
14
15
|
Requires-Dist: types-lxml
|
|
15
16
|
Requires-Dist: typing-extensions
|
|
16
17
|
Provides-Extra: dev
|
|
@@ -5,19 +5,18 @@
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
# pyright: reportPrivateUsage=false
|
|
8
|
-
import random
|
|
9
8
|
import itertools as it
|
|
9
|
+
import random
|
|
10
10
|
import string
|
|
11
11
|
from collections.abc import Iterator
|
|
12
|
-
from decimal import Decimal
|
|
13
12
|
|
|
14
13
|
import pytest
|
|
15
14
|
|
|
16
15
|
import svg_ultralight.string_conversion as mod
|
|
17
16
|
|
|
18
|
-
|
|
19
17
|
_FLOAT_ITERATIONS = 100
|
|
20
18
|
|
|
19
|
+
|
|
21
20
|
def random_floats() -> Iterator[float]:
|
|
22
21
|
"""Yield random float values within(-ish) precision limits.
|
|
23
22
|
|
|
@@ -26,6 +25,7 @@ def random_floats() -> Iterator[float]:
|
|
|
26
25
|
for _ in range(_FLOAT_ITERATIONS):
|
|
27
26
|
yield random.uniform(1e-20, 1e20)
|
|
28
27
|
|
|
28
|
+
|
|
29
29
|
def low_numbers() -> Iterator[float]:
|
|
30
30
|
"""Yield random float values below precision limits.
|
|
31
31
|
|
|
@@ -34,13 +34,14 @@ def low_numbers() -> Iterator[float]:
|
|
|
34
34
|
for _ in range(_FLOAT_ITERATIONS):
|
|
35
35
|
yield random.uniform(1e-25, 1e-24)
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
def high_numbers() -> Iterator[float]:
|
|
38
39
|
"""Yield random float values above precision limits.
|
|
39
40
|
|
|
40
41
|
Value may exceed the precision limits of the system.
|
|
41
42
|
"""
|
|
42
43
|
for _ in range(_FLOAT_ITERATIONS):
|
|
43
|
-
yield random.uniform(
|
|
44
|
+
yield random.uniform(1e24, 1e25)
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def random_ints() -> Iterator[int]:
|
|
@@ -49,12 +50,15 @@ def random_ints() -> Iterator[int]:
|
|
|
49
50
|
for _ in range(_FLOAT_ITERATIONS):
|
|
50
51
|
yield random.randint(-big_int, big_int)
|
|
51
52
|
|
|
53
|
+
|
|
52
54
|
def random_numbers() -> Iterator[float]:
|
|
53
55
|
"""Yield random numbers values."""
|
|
54
56
|
return it.chain(random_floats(), low_numbers(), high_numbers(), random_ints())
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
class TestFormatNuber:
|
|
60
|
+
"""Test format_number function."""
|
|
61
|
+
|
|
58
62
|
def test_negative_zero(self):
|
|
59
63
|
"""Remove "-" from "-0"."""
|
|
60
64
|
assert mod.format_number(-0.0000000001) == "0"
|
|
@@ -64,9 +68,9 @@ class TestFormatNuber:
|
|
|
64
68
|
assert mod.format_number(1.0000000001) == "1"
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
71
|
class TestFormatNumbers:
|
|
72
|
+
"""Test format_numbers function."""
|
|
73
|
+
|
|
70
74
|
def test_empty(self):
|
|
71
75
|
"""Return empty list."""
|
|
72
76
|
assert mod.format_numbers([]) == []
|
|
@@ -75,93 +79,18 @@ class TestFormatNumbers:
|
|
|
75
79
|
"""Return list of formatted strings."""
|
|
76
80
|
assert mod.format_numbers([1, 2, 3]) == ["1", "2", "3"]
|
|
77
81
|
|
|
78
|
-
@pytest.mark.parametrize("num", random_numbers())
|
|
79
|
-
def test_exp_vs_fp_notation(self, num: float):
|
|
80
|
-
"""Exponential and fp notation have the same value.
|
|
81
|
-
|
|
82
|
-
The first assertion is a sanity check.
|
|
83
|
-
"""
|
|
84
|
-
expect = float(str(num))
|
|
85
|
-
assert expect == float(Decimal(num))
|
|
86
|
-
assert expect == float(mod._format_as_fixed_point(str(num)))
|
|
87
|
-
assert expect == float(mod._format_as_exponential(str(num)))
|
|
88
|
-
|
|
89
|
-
@pytest.mark.parametrize("num", random_numbers())
|
|
90
|
-
def test_exponent_integer_part_is_len_1_or_stripped(self, num: float):
|
|
91
|
-
"""Integer part is one digit."""
|
|
92
|
-
exponential = mod._format_as_exponential(num)
|
|
93
|
-
# Result is exactly one digit
|
|
94
|
-
if "." not in exponential:
|
|
95
|
-
assert exponential.lstrip("-").isdigit()
|
|
96
|
-
# Result is nothing before decimal or one non-zero digit before decimal.
|
|
97
|
-
integer = exponential.split(".")[0].lstrip("-")
|
|
98
|
-
assert not integer or integer in "123456789"
|
|
99
|
-
|
|
100
|
-
class TestFormatNumbersInString:
|
|
101
|
-
def test_empty(self):
|
|
102
|
-
"""Return empty string.."""
|
|
103
|
-
assert mod.format_numbers_in_string("") == ""
|
|
104
|
-
|
|
105
|
-
def test_no_numbers(self):
|
|
106
|
-
"""Return string with no changes."""
|
|
107
|
-
assert mod.format_numbers_in_string("hello") == "hello"
|
|
108
|
-
|
|
109
|
-
def test_numbers(self):
|
|
110
|
-
"""Return string with numbers formatted."""
|
|
111
|
-
assert mod.format_numbers_in_string("1.0000000001") == "1"
|
|
112
|
-
|
|
113
|
-
def test_skip_text(self):
|
|
114
|
-
"""Skip text."""
|
|
115
|
-
key = "text"
|
|
116
|
-
val = "1.000000000000000000"
|
|
117
|
-
assert mod._fix_key_and_format_val(key, val) == (key, val)
|
|
118
|
-
|
|
119
|
-
def test_skip_id(self):
|
|
120
|
-
"""Skip text."""
|
|
121
|
-
key = "id"
|
|
122
|
-
val = "1.000000000000000000"
|
|
123
|
-
assert mod._fix_key_and_format_val(key, val) == (key, val)
|
|
124
|
-
|
|
125
|
-
def test_skip_hex_colors(self):
|
|
126
|
-
"""Skip hex colors."""
|
|
127
|
-
assert mod.format_numbers_in_string("#000000") == "#000000"
|
|
128
|
-
|
|
129
82
|
|
|
130
83
|
class TestFormatAttrDict:
|
|
84
|
+
"""Test format_attr_dict function."""
|
|
85
|
+
|
|
131
86
|
def test_float(self):
|
|
132
87
|
"""Return string of float."""
|
|
133
88
|
assert mod.format_attr_dict(x=1.0) == {"x": "1"}
|
|
134
89
|
|
|
135
|
-
def test_float_string(self):
|
|
136
|
-
"""Return string of float."""
|
|
137
|
-
assert mod.format_attr_dict(x="1.0") == {"x": "1"}
|
|
138
|
-
|
|
139
90
|
def test_exponential_float(self):
|
|
140
|
-
"""Return string of float."""
|
|
141
|
-
assert mod.format_attr_dict(x="1.0e-10") == {"x": "0"}
|
|
142
|
-
|
|
143
|
-
def test_exponential_float_string(self):
|
|
144
91
|
"""Return string of float."""
|
|
145
92
|
assert mod.format_attr_dict(x=1.0e-10) == {"x": "0"}
|
|
146
93
|
|
|
147
|
-
def test_datastring(self):
|
|
148
|
-
"""Find and format floats in a datastring."""
|
|
149
|
-
assert mod.format_attr_dict(d="M1.0,0 Q -0,.33333333333 1,2z") == {
|
|
150
|
-
"d": "M1,0 Q 0,.333333 1,2z"
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
def test_datastring_with_exponential_number(self):
|
|
154
|
-
"""Find and format floats in a datastring."""
|
|
155
|
-
assert mod.format_attr_dict(d="M1.0,1.0e-10 Q -0,.33333333 1,2z") == {
|
|
156
|
-
"d": "M1,0 Q 0,.333333 1,2z"
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
def test_format_string(self):
|
|
160
|
-
"""Format floats in a format SVG attribute string."""
|
|
161
|
-
assert mod.format_attr_dict(transform="translate(1.0, -0) scale(1.0, 1.0)") == {
|
|
162
|
-
"transform": "translate(1, 0) scale(1, 1)"
|
|
163
|
-
}
|
|
164
|
-
|
|
165
94
|
def test_trailing_underscore(self):
|
|
166
95
|
"""Remove trailing underscore from key."""
|
|
167
96
|
assert mod.format_attr_dict(x_=1) == {"x": "1"}
|
|
@@ -181,16 +110,19 @@ def _generate_random_utf8_string():
|
|
|
181
110
|
+ string.whitespace
|
|
182
111
|
+ additional_chars
|
|
183
112
|
)
|
|
184
|
-
|
|
185
|
-
|
|
113
|
+
return "".join(random.choice(characters) for _ in range(length))
|
|
114
|
+
|
|
186
115
|
|
|
187
116
|
@pytest.fixture(params=range(100))
|
|
188
117
|
def random_utf8_string(request: pytest.FixtureRequest) -> str:
|
|
118
|
+
"""Generate a random UTF-8 string for testing."""
|
|
189
119
|
_ = request.param
|
|
190
120
|
return _generate_random_utf8_string()
|
|
191
121
|
|
|
192
122
|
|
|
193
123
|
class TestEncodeCssClassName:
|
|
124
|
+
"""Test encode_to_css_class_name and decode_from_css_class_name functions."""
|
|
125
|
+
|
|
194
126
|
def test_encode_decode(self, random_utf8_string: str):
|
|
195
127
|
"""Encode - decode will return the original string."""
|
|
196
128
|
encoded = mod.encode_to_css_class_name(random_utf8_string)
|
|
@@ -202,4 +134,4 @@ class TestEncodeCssClassName:
|
|
|
202
134
|
def test_encode_valid(self, random_utf8_string: str):
|
|
203
135
|
"""All encoded strings will be ascii, _, and -."""
|
|
204
136
|
encoded = mod.encode_to_css_class_name(random_utf8_string)
|
|
205
|
-
assert all(c.isascii() or c in {
|
|
137
|
+
assert all(c.isascii() or c in {"_", "-"} for c in encoded)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/__init__.py
RENAMED
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py
RENAMED
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/constructors/new_element.py
RENAMED
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight/font_tools/comp_results.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{svg_ultralight-0.42.0 → svg_ultralight-0.43.0}/src/svg_ultralight.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|