svg-ultralight 0.43.1__tar.gz → 0.45.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.43.1 → svg_ultralight-0.45.0}/.pre-commit-config.yaml +5 -6
- {svg_ultralight-0.43.1/src/svg_ultralight.egg-info → svg_ultralight-0.45.0}/PKG-INFO +1 -1
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/pyproject.toml +5 -2
- svg_ultralight-0.45.0/src/svg_ultralight/attrib_hints.py +14 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py +5 -4
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/padded_text_initializers.py +16 -7
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/constructors/new_element.py +6 -4
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/comp_results.py +17 -7
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/font_info.py +79 -10
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/main.py +8 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/root_elements.py +8 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/string_conversion.py +8 -4
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0/src/svg_ultralight.egg-info}/PKG-INFO +1 -1
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/SOURCES.txt +2 -1
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/conftest.py +7 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_bounding.py +14 -13
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_layout.py +16 -19
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_matrices.py +3 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_metadata.py +4 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_padded_text_initializers.py +3 -2
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_queries.py +2 -7
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_root_elements.py +8 -13
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_svg_ultralight.py +39 -26
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/.gitignore +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/README.md +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/dev-requirements.txt +0 -0
- /svg_ultralight-0.43.1/experiments/encode_fonts3.py → /svg_ultralight-0.45.0/experiments/encode_fonts3.txt +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/experiments/font_css.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/setup.cfg +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/animate.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/constructors/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/globs.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/image_ops.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/inkscape.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/layout.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/metadata.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/nsmap.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/py.typed +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/query.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/strings/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/transformations.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/unit_conversion.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/requires.txt +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/__init__.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/resources/arrow.svg +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_inkscape.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_new_element.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_padding.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tests/test_string_conversion.py +0 -0
- {svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/tox.ini +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
ci:
|
|
2
2
|
skip: [pyright]
|
|
3
3
|
|
|
4
|
-
exclude: "experiments"
|
|
4
|
+
exclude: "tests|experiments"
|
|
5
5
|
|
|
6
6
|
repos:
|
|
7
7
|
|
|
@@ -42,7 +42,7 @@ repos:
|
|
|
42
42
|
# files: .pre-commit-config.yaml
|
|
43
43
|
|
|
44
44
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
45
|
-
rev: v1.
|
|
45
|
+
rev: v1.16.1
|
|
46
46
|
hooks:
|
|
47
47
|
- id: mypy
|
|
48
48
|
name: mypy
|
|
@@ -89,14 +89,13 @@ repos:
|
|
|
89
89
|
# S101 Use of `assert` detected
|
|
90
90
|
# S603 `subprocess` call: check for execution of untrusted input
|
|
91
91
|
# PLR2004 Magic value used in comparison
|
|
92
|
-
# S320 Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks
|
|
93
92
|
# S301 don't use pickle
|
|
94
93
|
# B028 wants explicit stacklevel on warn
|
|
95
94
|
# BLE001 Use of `except Exception:` detected
|
|
96
95
|
# ANN401 Any type disallowed
|
|
97
96
|
# FLY002 Consider f-string instead of string join
|
|
98
97
|
# S311 Standard pseudo-random generator used
|
|
99
|
-
rev: 'v0.
|
|
98
|
+
rev: 'v0.12.2'
|
|
100
99
|
hooks:
|
|
101
100
|
- id: ruff
|
|
102
101
|
name: "ruff-lint"
|
|
@@ -104,7 +103,7 @@ repos:
|
|
|
104
103
|
args:
|
|
105
104
|
- --target-version=py39
|
|
106
105
|
- --select=ALL
|
|
107
|
-
- --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,
|
|
106
|
+
- --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S301,B028,BLE001,ANN401,FLY002,SIM108,S311
|
|
108
107
|
- --fix
|
|
109
108
|
- --fixable=RUF022
|
|
110
109
|
- id: ruff
|
|
@@ -114,6 +113,6 @@ repos:
|
|
|
114
113
|
|
|
115
114
|
# reads pyproject.toml for additional config
|
|
116
115
|
- repo: https://github.com/RobertCraigie/pyright-python
|
|
117
|
-
rev: v1.1.
|
|
116
|
+
rev: v1.1.403
|
|
118
117
|
hooks:
|
|
119
118
|
- id: pyright
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "svg-ultralight"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.45.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" }
|
|
@@ -48,9 +48,12 @@ legacy_tox_ini = """
|
|
|
48
48
|
[tool.ruff.lint.pydocstyle]
|
|
49
49
|
convention = "pep257"
|
|
50
50
|
|
|
51
|
+
[tool.ruff.lint.per-file-ignores]
|
|
52
|
+
"tests/*.py" = ["S101", "D", "F401"] # Ignore assertions, docstrings, unused imports in test files
|
|
53
|
+
|
|
51
54
|
[tool.commitizen]
|
|
52
55
|
name = "cz_conventional_commits"
|
|
53
|
-
version = "0.
|
|
56
|
+
version = "0.45.0"
|
|
54
57
|
tag_format = "$version"
|
|
55
58
|
version_files = ["pyproject.toml:^version"]
|
|
56
59
|
annotated_tag = true
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Type hints for pass-through arguments to lxml constructors.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2025-07-09
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
# Types svg_ultralight can format to pass through to lxml constructors.
|
|
11
|
+
ElemAttrib = Union[str, float, None]
|
|
12
|
+
|
|
13
|
+
# Type for an optional dictionary of element attributes.
|
|
14
|
+
OptionalElemAttribMapping = Union[Mapping[str, ElemAttrib], None]
|
{svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py
RENAMED
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
|
+
from lxml import etree
|
|
11
12
|
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
12
13
|
|
|
13
14
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
@@ -19,14 +20,14 @@ from svg_ultralight.constructors import new_element
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
import os
|
|
21
22
|
|
|
23
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
22
24
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
23
|
-
from lxml import etree
|
|
24
25
|
|
|
25
26
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def new_element_union(
|
|
29
|
-
*elems: EtreeElement | SupportsBounds, **attributes:
|
|
30
|
+
*elems: EtreeElement | SupportsBounds, **attributes: ElemAttrib
|
|
30
31
|
) -> EtreeElement:
|
|
31
32
|
"""Get the union of any elements found in the given arguments.
|
|
32
33
|
|
|
@@ -186,13 +187,13 @@ def _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
|
|
|
186
187
|
return x, y, width, height
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
def parse_bound_element(
|
|
190
|
+
def parse_bound_element(svg_file: str | os.PathLike[str]) -> BoundElement:
|
|
190
191
|
"""Import an element as a BoundElement.
|
|
191
192
|
|
|
192
193
|
:param elem: the element to import.
|
|
193
194
|
:return: a BoundElement instance.
|
|
194
195
|
"""
|
|
195
|
-
tree = etree.parse(
|
|
196
|
+
tree = etree.parse(svg_file)
|
|
196
197
|
root = tree.getroot()
|
|
197
198
|
elem = new_element("g")
|
|
198
199
|
elem.extend(list(root))
|
|
@@ -27,10 +27,7 @@ from svg_ultralight.font_tools.font_info import (
|
|
|
27
27
|
)
|
|
28
28
|
from svg_ultralight.font_tools.globs import DEFAULT_FONT_SIZE
|
|
29
29
|
from svg_ultralight.query import get_bounding_boxes
|
|
30
|
-
from svg_ultralight.string_conversion import
|
|
31
|
-
format_attr_dict,
|
|
32
|
-
format_number,
|
|
33
|
-
)
|
|
30
|
+
from svg_ultralight.string_conversion import format_attr_dict, format_number
|
|
34
31
|
|
|
35
32
|
if TYPE_CHECKING:
|
|
36
33
|
import os
|
|
@@ -39,6 +36,8 @@ if TYPE_CHECKING:
|
|
|
39
36
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
40
37
|
)
|
|
41
38
|
|
|
39
|
+
from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
|
|
40
|
+
|
|
42
41
|
DEFAULT_Y_BOUNDS_REFERENCE = "{[|gjpqyf"
|
|
43
42
|
|
|
44
43
|
|
|
@@ -99,7 +98,8 @@ def pad_text_ft(
|
|
|
99
98
|
descent: float | None = None,
|
|
100
99
|
*,
|
|
101
100
|
y_bounds_reference: str | None = None,
|
|
102
|
-
|
|
101
|
+
attrib: OptionalElemAttribMapping = None,
|
|
102
|
+
**attributes: ElemAttrib,
|
|
103
103
|
) -> PaddedText:
|
|
104
104
|
"""Create a new PaddedText instance using fontTools.
|
|
105
105
|
|
|
@@ -115,11 +115,15 @@ def pad_text_ft(
|
|
|
115
115
|
extents of the capline reference. This argument is provided to mimic the
|
|
116
116
|
behavior of the query module's `pad_text` function. `pad_text` does no
|
|
117
117
|
inspect font files and relies on Inkscape to measure reference characters.
|
|
118
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
119
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
120
|
+
dictionary into a function call.
|
|
118
121
|
:param attributes: additional attributes to set on the text element. There is a
|
|
119
122
|
chance these will cause the font element to exceed the BoundingBox of the
|
|
120
123
|
PaddedText instance.
|
|
121
124
|
:return: a PaddedText instance with a line_gap defined.
|
|
122
125
|
"""
|
|
126
|
+
attributes.update(attrib or {})
|
|
123
127
|
attributes_ = format_attr_dict(**attributes)
|
|
124
128
|
attributes_.update(get_svg_font_attributes(font))
|
|
125
129
|
|
|
@@ -145,7 +149,8 @@ def pad_text_mix(
|
|
|
145
149
|
descent: float | None = None,
|
|
146
150
|
*,
|
|
147
151
|
y_bounds_reference: str | None = None,
|
|
148
|
-
|
|
152
|
+
attrib: OptionalElemAttribMapping = None,
|
|
153
|
+
**attributes: ElemAttrib,
|
|
149
154
|
) -> PaddedText:
|
|
150
155
|
"""Use Inkscape text bounds and fill missing with fontTools.
|
|
151
156
|
|
|
@@ -161,11 +166,15 @@ def pad_text_mix(
|
|
|
161
166
|
extents of the capline reference. This argument is provided to mimic the
|
|
162
167
|
behavior of the query module's `pad_text` function. `pad_text` does no
|
|
163
168
|
inspect font files and relies on Inkscape to measure reference characters.
|
|
169
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
170
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
171
|
+
dictionary into a function call.
|
|
164
172
|
:param attributes: additional attributes to set on the text element. There is a
|
|
165
173
|
chance these will cause the font element to exceed the BoundingBox of the
|
|
166
174
|
PaddedText instance.
|
|
167
175
|
:return: a PaddedText instance with a line_gap defined.
|
|
168
176
|
"""
|
|
177
|
+
attributes.update(attrib or {})
|
|
169
178
|
elem = new_element("text", text=text, **attributes)
|
|
170
179
|
padded_inkscape = pad_text(inkscape, elem, y_bounds_reference, font=font)
|
|
171
180
|
padded_fonttools = pad_text_ft(
|
|
@@ -175,7 +184,7 @@ def pad_text_mix(
|
|
|
175
184
|
ascent,
|
|
176
185
|
descent,
|
|
177
186
|
y_bounds_reference=y_bounds_reference,
|
|
178
|
-
|
|
187
|
+
attrib=attributes,
|
|
179
188
|
)
|
|
180
189
|
bbox = padded_inkscape.unpadded_bbox
|
|
181
190
|
rpad = padded_inkscape.rpad
|
{svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/constructors/new_element.py
RENAMED
|
@@ -25,8 +25,10 @@ if TYPE_CHECKING:
|
|
|
25
25
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
def new_element(tag: str | QName, **attributes: ElemAttrib) -> EtreeElement:
|
|
30
32
|
"""Create an etree.Element, make every kwarg value a string.
|
|
31
33
|
|
|
32
34
|
:param tag: element tag
|
|
@@ -62,7 +64,7 @@ def new_element(tag: str | QName, **attributes: str | float) -> EtreeElement:
|
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def new_sub_element(
|
|
65
|
-
parent: EtreeElement, tag: str | QName, **attributes:
|
|
67
|
+
parent: EtreeElement, tag: str | QName, **attributes: ElemAttrib
|
|
66
68
|
) -> EtreeElement:
|
|
67
69
|
"""Create an etree.SubElement, make every kwarg value a string.
|
|
68
70
|
|
|
@@ -81,7 +83,7 @@ def new_sub_element(
|
|
|
81
83
|
return elem
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def update_element(elem: EtreeElement, **attributes:
|
|
86
|
+
def update_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
|
|
85
87
|
"""Update an existing etree.Element with additional params.
|
|
86
88
|
|
|
87
89
|
:param elem: at etree element
|
|
@@ -94,7 +96,7 @@ def update_element(elem: EtreeElement, **attributes: str | float) -> EtreeElemen
|
|
|
94
96
|
return elem
|
|
95
97
|
|
|
96
98
|
|
|
97
|
-
def deepcopy_element(elem: EtreeElement, **attributes:
|
|
99
|
+
def deepcopy_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
|
|
98
100
|
"""Create a deepcopy of an element. Optionally pass additional params.
|
|
99
101
|
|
|
100
102
|
:param elem: at etree element or list of elements
|
{svg_ultralight-0.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/comp_results.py
RENAMED
|
@@ -31,8 +31,8 @@ from svg_ultralight.bounding_boxes.padded_text_initializers import (
|
|
|
31
31
|
pad_text_ft,
|
|
32
32
|
)
|
|
33
33
|
from svg_ultralight.constructors import new_element
|
|
34
|
-
from svg_ultralight.font_tools.font_info import get_svg_font_attributes
|
|
35
|
-
from svg_ultralight.
|
|
34
|
+
from svg_ultralight.font_tools.font_info import FTFontInfo, get_svg_font_attributes
|
|
35
|
+
from svg_ultralight.inkscape import write_root
|
|
36
36
|
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
37
37
|
|
|
38
38
|
if TYPE_CHECKING:
|
|
@@ -201,7 +201,15 @@ def draw_comparison(
|
|
|
201
201
|
text = Path(font).stem
|
|
202
202
|
font_size = 12
|
|
203
203
|
font_attributes = get_svg_font_attributes(font)
|
|
204
|
-
text_elem = new_element(
|
|
204
|
+
text_elem = new_element(
|
|
205
|
+
"text",
|
|
206
|
+
text=text,
|
|
207
|
+
**font_attributes,
|
|
208
|
+
font_size=font_size,
|
|
209
|
+
fill="none",
|
|
210
|
+
stroke="green",
|
|
211
|
+
stroke_width=0.1,
|
|
212
|
+
)
|
|
205
213
|
padded_pt = pad_text(inkscape, text_elem)
|
|
206
214
|
padded_ft = pad_text_ft(
|
|
207
215
|
font,
|
|
@@ -210,10 +218,10 @@ def draw_comparison(
|
|
|
210
218
|
y_bounds_reference=DEFAULT_Y_BOUNDS_REFERENCE,
|
|
211
219
|
fill="none",
|
|
212
220
|
stroke="orange",
|
|
213
|
-
stroke_width=0.
|
|
221
|
+
stroke_width=0.1,
|
|
214
222
|
)
|
|
215
223
|
|
|
216
|
-
root = new_svg_root_around_bounds(pad_bbox(padded_pt.bbox,
|
|
224
|
+
root = new_svg_root_around_bounds(pad_bbox(padded_pt.bbox, 1))
|
|
217
225
|
root.append(
|
|
218
226
|
new_bbox_rect(
|
|
219
227
|
padded_pt.unpadded_bbox, fill="none", stroke_width=0.07, stroke="red"
|
|
@@ -227,7 +235,7 @@ def draw_comparison(
|
|
|
227
235
|
root.append(padded_pt.elem)
|
|
228
236
|
root.append(padded_ft.elem)
|
|
229
237
|
_ = sys.stdout.write(f"{Path(font).stem} comparison drawn at {output}.\n")
|
|
230
|
-
_ =
|
|
238
|
+
_ = write_root(inkscape, Path(output), root)
|
|
231
239
|
|
|
232
240
|
|
|
233
241
|
def _iter_fonts(*fonts_dirs: Path) -> Iterator[Path]:
|
|
@@ -280,4 +288,6 @@ if __name__ == "__main__":
|
|
|
280
288
|
_test_every_font_on_my_system(_INKSCAPE, _FONT_DIRS)
|
|
281
289
|
|
|
282
290
|
font = Path(r"C:\Windows\Fonts\arial.ttf")
|
|
283
|
-
|
|
291
|
+
font = Path("C:/Windows/Fonts/Aptos-Display-Bold.ttf")
|
|
292
|
+
info = FTFontInfo(font)
|
|
293
|
+
draw_comparison(_INKSCAPE, "temp.svg", font, "AApple")
|
|
@@ -105,6 +105,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
105
105
|
from fontTools.pens.basePen import BasePen
|
|
106
106
|
from fontTools.pens.boundsPen import BoundsPen
|
|
107
107
|
from fontTools.ttLib import TTFont
|
|
108
|
+
from paragraphs import par
|
|
108
109
|
from svg_path_data import format_svgd_shortest, get_cpts_from_svgd, get_svgd_from_cpts
|
|
109
110
|
|
|
110
111
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
@@ -118,12 +119,68 @@ if TYPE_CHECKING:
|
|
|
118
119
|
|
|
119
120
|
from lxml.etree import _Element as EtreeElement
|
|
120
121
|
|
|
122
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
123
|
+
|
|
121
124
|
logging.getLogger("fontTools").setLevel(logging.ERROR)
|
|
122
125
|
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
) ->
|
|
127
|
+
# extract_gpos_kerning is an unfinished attempt to extract kerning from the GPOS
|
|
128
|
+
# table.
|
|
129
|
+
def get_gpos_kerning(font: TTFont) -> dict[tuple[str, str], int]:
|
|
130
|
+
"""Extract kerning pairs from the GPOS table of a font.
|
|
131
|
+
|
|
132
|
+
:param font: A fontTools TTFont object.
|
|
133
|
+
:return: A dictionary mapping glyph pairs to their kerning values.
|
|
134
|
+
:raises ValueError: If the font does not have a GPOS table.
|
|
135
|
+
|
|
136
|
+
This is the more elaborate kerning that is used in OTF fonts and some TTF fonts.
|
|
137
|
+
It has several flavors, I'm only implementing glyph-pair kerning (Format 1),
|
|
138
|
+
because I don't have fonts to test anything else.
|
|
139
|
+
"""
|
|
140
|
+
if "GPOS" not in font:
|
|
141
|
+
msg = "Font does not have a GPOS table."
|
|
142
|
+
raise ValueError(msg)
|
|
143
|
+
|
|
144
|
+
gpos = font["GPOS"].table
|
|
145
|
+
kern_table: dict[tuple[str, str], int] = {}
|
|
146
|
+
|
|
147
|
+
type2_lookups = (x for x in gpos.LookupList.Lookup if x.LookupType == 2)
|
|
148
|
+
subtables = list(it.chain(*(x.SubTable for x in type2_lookups)))
|
|
149
|
+
for subtable in (x for x in subtables if x.Format == 1): # glyph-pair kerning
|
|
150
|
+
for pair_set, glyph1 in zip(subtable.PairSet, subtable.Coverage.glyphs):
|
|
151
|
+
for pair_value in pair_set.PairValueRecord:
|
|
152
|
+
glyph2 = pair_value.SecondGlyph
|
|
153
|
+
value1 = pair_value.Value1
|
|
154
|
+
xadv = getattr(value1, "XAdvance", None)
|
|
155
|
+
xpla = getattr(value1, "XPlacement", None)
|
|
156
|
+
value = xadv or xpla or 0
|
|
157
|
+
if value != 0: # only record non-zero kerning values
|
|
158
|
+
kern_table[(glyph1, glyph2)] = value
|
|
159
|
+
|
|
160
|
+
for subtable in (x for x in subtables if x.Format == 2): # class-based kerning
|
|
161
|
+
defs1 = subtable.ClassDef1.classDefs
|
|
162
|
+
defs2 = subtable.ClassDef2.classDefs
|
|
163
|
+
record1 = subtable.Class1Record
|
|
164
|
+
defs1 = {k: v for k, v in defs1.items() if v < len(record1)}
|
|
165
|
+
for (glyph1, class1), (glyph2, class2) in it.product(
|
|
166
|
+
defs1.items(), defs2.items()
|
|
167
|
+
):
|
|
168
|
+
class1_record = record1[class1]
|
|
169
|
+
if class2 < len(class1_record.Class2Record):
|
|
170
|
+
value1 = class1_record.Class2Record[class2].Value1
|
|
171
|
+
xadv = getattr(value1, "XAdvance", None)
|
|
172
|
+
xpla = getattr(value1, "XPlacement", None)
|
|
173
|
+
value = xadv or xpla or 0
|
|
174
|
+
if value != 0:
|
|
175
|
+
kern_table[(glyph1, glyph2)] = value
|
|
176
|
+
|
|
177
|
+
return kern_table
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
_XYTuple = tuple[float, float]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _split_into_quadratic(*pts: _XYTuple) -> Iterator[tuple[_XYTuple, _XYTuple]]:
|
|
127
184
|
"""Connect a series of points with quadratic bezier segments.
|
|
128
185
|
|
|
129
186
|
:param points: a series of at least two (x, y) coordinates.
|
|
@@ -183,9 +240,16 @@ class PathPen(BasePen):
|
|
|
183
240
|
|
|
184
241
|
def curveTo(self, *pts: tuple[float, float]) -> None:
|
|
185
242
|
"""Add a series of cubic bezier segments to the path."""
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
243
|
+
if len(pts) > 3:
|
|
244
|
+
msg = par(
|
|
245
|
+
"""I'm uncertain how to decompose these points into cubics (if the
|
|
246
|
+
goal is to match font rendering in Inkscape and elsewhere. There is
|
|
247
|
+
function, decomposeSuperBezierSegment, in fontTools, but I cannot
|
|
248
|
+
find a reference for the algorithm. I'm hoping to run into one in a
|
|
249
|
+
font file so I have a test case."""
|
|
250
|
+
)
|
|
251
|
+
raise NotImplementedError(msg)
|
|
252
|
+
self._cmds.extend(("C", *map(str, it.chain(*pts))))
|
|
189
253
|
|
|
190
254
|
def qCurveTo(self, *pts: tuple[float, float]) -> None:
|
|
191
255
|
"""Add a series of quadratic bezier segments to the path."""
|
|
@@ -252,13 +316,18 @@ class FTFontInfo:
|
|
|
252
316
|
method would give precedence to the first occurrence. That behavior is copied
|
|
253
317
|
from examples found online.
|
|
254
318
|
"""
|
|
255
|
-
|
|
319
|
+
try:
|
|
256
320
|
kern_tables = cast(
|
|
257
321
|
"list[dict[tuple[str, str], int]]",
|
|
258
322
|
[x.kernTable for x in self.font["kern"].kernTables],
|
|
259
323
|
)
|
|
260
|
-
|
|
261
|
-
|
|
324
|
+
kern = dict(x for d in reversed(kern_tables) for x in d.items())
|
|
325
|
+
except (KeyError, AttributeError):
|
|
326
|
+
kern = {}
|
|
327
|
+
with suppress(Exception):
|
|
328
|
+
kern.update(get_gpos_kerning(self.font))
|
|
329
|
+
|
|
330
|
+
return kern
|
|
262
331
|
|
|
263
332
|
@ft.cached_property
|
|
264
333
|
def hhea(self) -> Any:
|
|
@@ -453,7 +522,7 @@ class FTTextInfo:
|
|
|
453
522
|
"""
|
|
454
523
|
return self.font_size / self.font.units_per_em
|
|
455
524
|
|
|
456
|
-
def new_element(self, **attributes:
|
|
525
|
+
def new_element(self, **attributes: ElemAttrib) -> EtreeElement:
|
|
457
526
|
"""Return an svg text element with the appropriate font attributes."""
|
|
458
527
|
matrix_vals = (self.scale, 0, 0, -self.scale, 0, 0)
|
|
459
528
|
matrix = f"matrix({' '.join(format_numbers(matrix_vals))})"
|
|
@@ -36,6 +36,7 @@ if TYPE_CHECKING:
|
|
|
36
36
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
+
from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
|
|
39
40
|
from svg_ultralight.layout import PadArg
|
|
40
41
|
|
|
41
42
|
|
|
@@ -68,7 +69,8 @@ def new_svg_root(
|
|
|
68
69
|
print_height_: float | str | None = None,
|
|
69
70
|
dpu_: float = 1,
|
|
70
71
|
nsmap: dict[str | None, str] | None = None,
|
|
71
|
-
|
|
72
|
+
attrib: OptionalElemAttribMapping | None = None,
|
|
73
|
+
**attributes: ElemAttrib,
|
|
72
74
|
) -> EtreeElement:
|
|
73
75
|
"""Create an svg root element from viewBox style parameters.
|
|
74
76
|
|
|
@@ -87,6 +89,9 @@ def new_svg_root(
|
|
|
87
89
|
different from print_width_ and print_height_ in that dpu_ scales the
|
|
88
90
|
*padded* output.
|
|
89
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.
|
|
90
95
|
:param attributes: element attribute names and values
|
|
91
96
|
:return: root svg element
|
|
92
97
|
|
|
@@ -97,10 +102,11 @@ def new_svg_root(
|
|
|
97
102
|
will be passed to ``etree.Element`` as element parameters. These will
|
|
98
103
|
supercede any parameters inferred from the trailing underscore parameters.
|
|
99
104
|
"""
|
|
105
|
+
attributes.update(attrib or {})
|
|
100
106
|
if nsmap is None:
|
|
101
107
|
nsmap = NSMAP
|
|
102
108
|
|
|
103
|
-
inferred_attribs: dict[str,
|
|
109
|
+
inferred_attribs: dict[str, ElemAttrib] = {}
|
|
104
110
|
view_box_args = (x_, y_, width_, height_)
|
|
105
111
|
if _is_four_floats(view_box_args):
|
|
106
112
|
assert isinstance(x_, (float, int))
|
|
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
17
17
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
|
|
20
21
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
21
22
|
from svg_ultralight.layout import PadArg
|
|
22
23
|
|
|
@@ -43,7 +44,8 @@ def new_svg_root_around_bounds(
|
|
|
43
44
|
print_height_: float | str | None = None,
|
|
44
45
|
dpu_: float = 1,
|
|
45
46
|
nsmap: dict[str | None, str] | None = None,
|
|
46
|
-
|
|
47
|
+
attrib: OptionalElemAttribMapping = None,
|
|
48
|
+
**attributes: ElemAttrib,
|
|
47
49
|
) -> EtreeElement:
|
|
48
50
|
"""Create svg root around BoundElements.
|
|
49
51
|
|
|
@@ -61,10 +63,14 @@ def new_svg_root_around_bounds(
|
|
|
61
63
|
different from print_width_ and print_height_ in that dpu_ scales the
|
|
62
64
|
*padded* output.
|
|
63
65
|
:param nsmap: optionally pass a namespace map of your choosing
|
|
66
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
67
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
68
|
+
dictionary into a function call.
|
|
64
69
|
:param attributes: element attribute names and values
|
|
65
70
|
:return: root svg element
|
|
66
71
|
:raise ValueError: if no bounding boxes are found in bounded
|
|
67
72
|
"""
|
|
73
|
+
attributes.update(attrib or {})
|
|
68
74
|
bbox = bound.new_bbox_union(*bounded)
|
|
69
75
|
viewbox = _viewbox_args_from_bboxes(bbox)
|
|
70
76
|
return new_svg_root(
|
|
@@ -77,5 +83,5 @@ def new_svg_root_around_bounds(
|
|
|
77
83
|
print_height_=print_height_,
|
|
78
84
|
dpu_=dpu_,
|
|
79
85
|
nsmap=nsmap,
|
|
80
|
-
|
|
86
|
+
attrib=attributes,
|
|
81
87
|
)
|
|
@@ -26,6 +26,8 @@ if TYPE_CHECKING:
|
|
|
26
26
|
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
30
|
+
|
|
29
31
|
|
|
30
32
|
def format_number(num: float | str, resolution: int | None = 6) -> str:
|
|
31
33
|
"""Format a number into an svg-readable float string with resolution = 6.
|
|
@@ -50,7 +52,7 @@ def format_numbers(
|
|
|
50
52
|
return [format_number(num) for num in nums]
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
def _fix_key_and_format_val(key: str, val:
|
|
55
|
+
def _fix_key_and_format_val(key: str, val: ElemAttrib) -> tuple[str, str]:
|
|
54
56
|
"""Format one key, value pair for an svg element.
|
|
55
57
|
|
|
56
58
|
:param key: element attribute name
|
|
@@ -82,7 +84,9 @@ def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
|
|
|
82
84
|
else:
|
|
83
85
|
key_ = key.rstrip("_").replace("_", "-")
|
|
84
86
|
|
|
85
|
-
if
|
|
87
|
+
if val is None:
|
|
88
|
+
val_ = "none"
|
|
89
|
+
elif isinstance(val, (int, float)):
|
|
86
90
|
val_ = format_number(val)
|
|
87
91
|
else:
|
|
88
92
|
val_ = val
|
|
@@ -90,7 +94,7 @@ def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
|
|
|
90
94
|
return key_, val_
|
|
91
95
|
|
|
92
96
|
|
|
93
|
-
def format_attr_dict(**attributes:
|
|
97
|
+
def format_attr_dict(**attributes: ElemAttrib) -> dict[str, str]:
|
|
94
98
|
"""Use svg_ultralight key / value fixer to create a dict of attributes.
|
|
95
99
|
|
|
96
100
|
:param attributes: element attribute names and values.
|
|
@@ -99,7 +103,7 @@ def format_attr_dict(**attributes: str | float) -> dict[str, str]:
|
|
|
99
103
|
return dict(_fix_key_and_format_val(key, val) for key, val in attributes.items())
|
|
100
104
|
|
|
101
105
|
|
|
102
|
-
def set_attributes(elem: EtreeElement, **attributes:
|
|
106
|
+
def set_attributes(elem: EtreeElement, **attributes: ElemAttrib) -> None:
|
|
103
107
|
"""Set name: value items as element attributes. Make every value a string.
|
|
104
108
|
|
|
105
109
|
:param elem: element to receive element.set(keyword, str(value)) calls
|
|
@@ -4,10 +4,11 @@ README.md
|
|
|
4
4
|
dev-requirements.txt
|
|
5
5
|
pyproject.toml
|
|
6
6
|
tox.ini
|
|
7
|
-
experiments/encode_fonts3.
|
|
7
|
+
experiments/encode_fonts3.txt
|
|
8
8
|
experiments/font_css.py
|
|
9
9
|
src/svg_ultralight/__init__.py
|
|
10
10
|
src/svg_ultralight/animate.py
|
|
11
|
+
src/svg_ultralight/attrib_hints.py
|
|
11
12
|
src/svg_ultralight/image_ops.py
|
|
12
13
|
src/svg_ultralight/inkscape.py
|
|
13
14
|
src/svg_ultralight/layout.py
|
|
@@ -13,16 +13,21 @@ if TYPE_CHECKING:
|
|
|
13
13
|
import os
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def pytest_assertrepr_compare(
|
|
16
|
+
def pytest_assertrepr_compare(
|
|
17
|
+
config: Any, op: str, left: str, right: str
|
|
18
|
+
) -> list[str] | None:
|
|
17
19
|
"""See full error diffs"""
|
|
20
|
+
del config
|
|
18
21
|
if op in ("==", "!="):
|
|
19
|
-
return ["{
|
|
22
|
+
return [f"{left} {op} {right}"]
|
|
23
|
+
return None
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
TEST_RESOURCES = Path(__file__).parent / "resources"
|
|
23
27
|
|
|
24
28
|
INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
|
|
25
29
|
|
|
30
|
+
|
|
26
31
|
def has_inkscape(inkscape: str | os.PathLike[str]) -> bool:
|
|
27
32
|
"""Check if Inkscape is available at a path.
|
|
28
33
|
|
|
@@ -4,22 +4,24 @@
|
|
|
4
4
|
:created: 2024-05-05
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import copy
|
|
8
8
|
import math
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
9
11
|
from conftest import TEST_RESOURCES
|
|
10
|
-
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
11
|
-
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
12
12
|
from lxml import etree
|
|
13
|
-
|
|
14
|
-
from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
|
|
13
|
+
|
|
15
14
|
from svg_ultralight.bounding_boxes.bound_helpers import (
|
|
16
|
-
pad_bbox,
|
|
17
|
-
cut_bbox,
|
|
18
|
-
parse_bound_element,
|
|
19
15
|
bbox_dict,
|
|
16
|
+
cut_bbox,
|
|
20
17
|
new_bbox_rect,
|
|
18
|
+
pad_bbox,
|
|
19
|
+
parse_bound_element,
|
|
21
20
|
)
|
|
22
|
-
import
|
|
21
|
+
from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
|
|
22
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
23
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
24
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
23
25
|
from svg_ultralight.constructors import new_element
|
|
24
26
|
|
|
25
27
|
|
|
@@ -126,7 +128,7 @@ class TestPaddedText:
|
|
|
126
128
|
assert bound_element.cy == 148.0
|
|
127
129
|
|
|
128
130
|
def test_cx(self, bound_element: PaddedText):
|
|
129
|
-
bbox_x = bound_element.x
|
|
131
|
+
bbox_x = bound_element.x
|
|
130
132
|
bbox_x2 = bound_element.x2
|
|
131
133
|
bbox_cx = bound_element.cx
|
|
132
134
|
bound_element.cx += 100.0
|
|
@@ -135,13 +137,13 @@ class TestPaddedText:
|
|
|
135
137
|
assert bound_element.cx == bbox_cx + 100.0
|
|
136
138
|
|
|
137
139
|
def test_cy(self, bound_element: PaddedText):
|
|
138
|
-
bbox_y = bound_element.y
|
|
140
|
+
bbox_y = bound_element.y
|
|
139
141
|
bbox_y2 = bound_element.y2
|
|
140
142
|
bbox_cy = bound_element.cy
|
|
141
143
|
bound_element.cy += 100.0
|
|
142
144
|
assert bound_element.y == bbox_y + 100.0
|
|
143
145
|
assert bound_element.y2 == bbox_y2 + 100.0
|
|
144
|
-
assert bound_element.cy == bbox_cy + 100.0
|
|
146
|
+
assert bound_element.cy == bbox_cy + 100.0
|
|
145
147
|
|
|
146
148
|
def test_width(self, bound_element: PaddedText):
|
|
147
149
|
assert bound_element.width == 106.0
|
|
@@ -188,7 +190,6 @@ class TestPaddedText:
|
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
class TestBoundCollection:
|
|
191
|
-
|
|
192
193
|
@pytest.fixture
|
|
193
194
|
def bound_collection(self) -> BoundCollection:
|
|
194
195
|
elem = new_element("rect", x=0, y=0, width=100, height=200)
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
# pyright: reportMissingParameterType = false
|
|
12
12
|
# pyright: reportPrivateUsage = false
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
import itertools as it
|
|
16
15
|
import math
|
|
17
16
|
from collections.abc import Iterator
|
|
@@ -39,13 +38,13 @@ INKSCAPE_SCALARS = {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
@pytest.fixture(
|
|
43
|
-
def unit(request) -> Unit:
|
|
41
|
+
@pytest.fixture(params=Unit)
|
|
42
|
+
def unit(request: pytest.FixtureRequest) -> Unit:
|
|
44
43
|
return request.param
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
@pytest.fixture(
|
|
48
|
-
def unit_pair(request) -> Iterator[Unit]:
|
|
46
|
+
@pytest.fixture(params=it.product(Unit, Unit))
|
|
47
|
+
def unit_pair(request: pytest.FixtureRequest) -> Iterator[Unit]:
|
|
49
48
|
return request.param
|
|
50
49
|
|
|
51
50
|
|
|
@@ -80,17 +79,17 @@ class TestParseUnit:
|
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
class TestMeasurement:
|
|
83
|
-
def test_unit_identified(self, unit):
|
|
82
|
+
def test_unit_identified(self, unit: Unit):
|
|
84
83
|
"""Test that unit is identified correctly."""
|
|
85
84
|
assert Measurement(f"1{unit.value[0]}").native_unit == unit
|
|
86
85
|
|
|
87
|
-
def test_value_scaled(self, unit):
|
|
86
|
+
def test_value_scaled(self, unit: Unit):
|
|
88
87
|
"""Value is scaled per Inkscape conversion values."""
|
|
89
88
|
assert math.isclose(
|
|
90
89
|
Measurement(f"1{unit.value[0]}").value, INKSCAPE_SCALARS[unit.value[0]]
|
|
91
90
|
)
|
|
92
91
|
|
|
93
|
-
def test_conversion(self, unit_pair):
|
|
92
|
+
def test_conversion(self, unit_pair: tuple[Unit, Unit]):
|
|
94
93
|
"""Test that value is converted to other units."""
|
|
95
94
|
unit_a, unit_b = unit_pair
|
|
96
95
|
a_unit = Measurement(f"1{unit_a.value[0]}")
|
|
@@ -98,27 +97,27 @@ class TestMeasurement:
|
|
|
98
97
|
b_unit = Measurement(f"{a_as_b}{unit_b.value[0]}")
|
|
99
98
|
assert math.isclose(b_unit.value, a_unit.value)
|
|
100
99
|
|
|
101
|
-
def test_add(self, unit):
|
|
100
|
+
def test_add(self, unit: Unit):
|
|
102
101
|
"""Test that values are added."""
|
|
103
102
|
a_unit = Measurement(f"1{unit.value[0]}")
|
|
104
103
|
b_unit = Measurement(f"2{unit.value[0]}")
|
|
105
104
|
assert (a_unit + b_unit).value == Measurement(f"3{unit.value[0]}").value
|
|
106
105
|
|
|
107
|
-
def test_subtract(self, unit):
|
|
106
|
+
def test_subtract(self, unit: Unit):
|
|
108
107
|
"""Test that values are subtracted."""
|
|
109
108
|
a_unit = Measurement(f"1{unit.value[0]}")
|
|
110
109
|
b_unit = Measurement(f"2{unit.value[0]}")
|
|
111
110
|
assert (a_unit - b_unit).value == Measurement(f"-1{unit.value[0]}").value
|
|
112
111
|
|
|
113
|
-
def test_multiply(self, unit):
|
|
112
|
+
def test_multiply(self, unit: Unit):
|
|
114
113
|
"""Test that values are multiplied."""
|
|
115
114
|
assert (Measurement((1, unit)) * 4).value == Measurement((4, unit)).value
|
|
116
115
|
|
|
117
|
-
def test_rmultiply(self, unit):
|
|
116
|
+
def test_rmultiply(self, unit: Unit):
|
|
118
117
|
"""Test that values are multiplied."""
|
|
119
118
|
assert (4 * Measurement((1, unit))).value == Measurement((4, unit)).value
|
|
120
119
|
|
|
121
|
-
def test_divide(self, unit):
|
|
120
|
+
def test_divide(self, unit: Unit):
|
|
122
121
|
"""Test that values are multiplied."""
|
|
123
122
|
assert (Measurement((1, unit)) / 4).value == Measurement((1 / 4, unit)).value
|
|
124
123
|
|
|
@@ -144,6 +143,7 @@ class TestExpandPadArg:
|
|
|
144
143
|
"""Test that a single value is expanded to a 4-tuple per css rules."""
|
|
145
144
|
assert layout.expand_pad_arg((1, 2, 3, 4)) == (1, 2, 3, 4)
|
|
146
145
|
|
|
146
|
+
|
|
147
147
|
class TestLayout:
|
|
148
148
|
def test_standard(self):
|
|
149
149
|
"""No print dimensions give expanded pad argument
|
|
@@ -194,9 +194,8 @@ class TestLayout:
|
|
|
194
194
|
def test_infinite_width(self):
|
|
195
195
|
"""Raise ValueError if no non-infinite scale can be inferred."""
|
|
196
196
|
viewbox = (0, 0, 0, 0)
|
|
197
|
-
with pytest.raises(ValueError)
|
|
197
|
+
with pytest.raises(ValueError, match="infinite"):
|
|
198
198
|
_ = layout.pad_and_scale(viewbox, "0.25in", "2in")
|
|
199
|
-
assert "infinite" in str(excinfo.value)
|
|
200
199
|
|
|
201
200
|
def test_pad_print_at_input_scale(self):
|
|
202
201
|
"""Test that padding is applied at the input scale.
|
|
@@ -229,7 +228,7 @@ class TestLayout:
|
|
|
229
228
|
assert width_attribs == {"width": "12in", "height": "22in"}
|
|
230
229
|
|
|
231
230
|
padded, width_attribs = layout.pad_and_scale(viewbox, "1in", "100in")
|
|
232
|
-
assert [format_number(x) for x in padded] == [
|
|
231
|
+
assert [format_number(x) for x in padded] == ["-.1", "-.1", "10.2", "20.2"]
|
|
233
232
|
assert width_attribs == {"width": "102in", "height": "202in"}
|
|
234
233
|
|
|
235
234
|
def test_dpu_(self):
|
|
@@ -248,6 +247,4 @@ class TestLayout:
|
|
|
248
247
|
viewbox = (0, 0, 1, 1)
|
|
249
248
|
padded, width_attribs = layout.pad_and_scale(viewbox, 1, dpu=2)
|
|
250
249
|
assert padded == (-1, -1, 3, 3)
|
|
251
|
-
assert width_attribs == {
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
assert width_attribs == {"width": "6", "height": "6"}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
:created: 2024-05-05
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from svg_ultralight.transformations import mat_dot, mat_apply, mat_invert
|
|
8
|
-
import random
|
|
9
7
|
import math
|
|
8
|
+
import random
|
|
10
9
|
from contextlib import suppress
|
|
11
10
|
|
|
11
|
+
from svg_ultralight.transformations import mat_apply, mat_dot, mat_invert
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class TestMat:
|
|
14
15
|
def test_explicit(self):
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
:created: 2024-01-30
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from svg_ultralight.metadata import new_metadata
|
|
8
|
-
from svg_ultralight.main import new_svg_root
|
|
9
7
|
from lxml import etree
|
|
10
8
|
|
|
9
|
+
from svg_ultralight.main import new_svg_root
|
|
10
|
+
from svg_ultralight.metadata import new_metadata
|
|
11
|
+
|
|
11
12
|
_inkscape_output = """
|
|
12
13
|
<metadata id="metadata1">
|
|
13
14
|
<rdf:RDF>
|
|
@@ -53,6 +54,7 @@ _inkscape_output = """
|
|
|
53
54
|
|
|
54
55
|
_inkscape_output = "".join([x.strip() for x in _inkscape_output.splitlines()])
|
|
55
56
|
|
|
57
|
+
|
|
56
58
|
class TestMetedata:
|
|
57
59
|
def test_inkscape_explicit(self):
|
|
58
60
|
"""Output matches Inkscape output.
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
:author: Shay Hill
|
|
4
4
|
:created: 2025-06-09
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
10
|
+
from conftest import INKSCAPE, has_inkscape
|
|
9
11
|
|
|
10
12
|
from svg_ultralight.bounding_boxes.padded_text_initializers import pad_text, pad_text_ft
|
|
11
13
|
from svg_ultralight.constructors import new_element
|
|
12
|
-
from conftest import has_inkscape, INKSCAPE
|
|
13
|
-
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestPadText:
|
|
@@ -57,6 +57,7 @@ class TestPadText:
|
|
|
57
57
|
padded.line_gap = 5
|
|
58
58
|
assert padded.leading == padded.height + 5
|
|
59
59
|
|
|
60
|
+
|
|
60
61
|
class TestPadTextFt:
|
|
61
62
|
def test_has_line_gap(self) -> None:
|
|
62
63
|
"""Test pad_text_ft with a font file."""
|
|
@@ -8,11 +8,10 @@ your system.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from pathlib import Path
|
|
12
11
|
|
|
13
12
|
import pytest
|
|
14
|
-
|
|
15
13
|
from conftest import INKSCAPE, has_inkscape
|
|
14
|
+
|
|
16
15
|
from svg_ultralight import BoundingBox, new_svg_root
|
|
17
16
|
from svg_ultralight.constructors import new_sub_element
|
|
18
17
|
from svg_ultralight.query import (
|
|
@@ -22,7 +21,6 @@ from svg_ultralight.query import (
|
|
|
22
21
|
)
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
|
|
26
24
|
class TestMergeBoundingBoxes:
|
|
27
25
|
def test_new_merged_bbox(self):
|
|
28
26
|
bbox_a = BoundingBox(-2, -4, 10, 20)
|
|
@@ -223,6 +221,7 @@ class TestMapElemsToBoundingBoxes:
|
|
|
223
221
|
result = get_bounding_boxes(INKSCAPE, *elems)
|
|
224
222
|
assert result == tuple(get_bounding_box(INKSCAPE, e) for e in elems)
|
|
225
223
|
|
|
224
|
+
|
|
226
225
|
class TestAlterBoundingBox:
|
|
227
226
|
def test_reverse_width(self) -> None:
|
|
228
227
|
"""adjust width one way then the other returns to original box."""
|
|
@@ -232,7 +231,3 @@ class TestAlterBoundingBox:
|
|
|
232
231
|
bbox.height = 200
|
|
233
232
|
bbox.height = 40
|
|
234
233
|
assert bbox.transformation == (1, 0, 0, 1, 90, 180)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
@@ -5,29 +5,26 @@
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
+
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
8
9
|
|
|
9
|
-
from svg_ultralight.bounding_boxes.
|
|
10
|
+
from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
|
|
10
11
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
12
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
11
13
|
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
12
14
|
from svg_ultralight.constructors import new_element
|
|
13
15
|
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
14
|
-
from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
|
|
15
|
-
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class TestNewSvgRootAroundBounds:
|
|
19
|
-
|
|
20
19
|
def test_empty(self):
|
|
21
20
|
"""Raise ValueError if no bounding boxes found."""
|
|
22
|
-
with pytest.raises(ValueError
|
|
21
|
+
with pytest.raises(ValueError, match="At least one argument"):
|
|
23
22
|
_ = new_svg_root_around_bounds()
|
|
24
|
-
assert "At least one argument" in str(excinfo.value)
|
|
25
23
|
|
|
26
24
|
def test_no_bound_elements(self):
|
|
27
25
|
"""Raise ValueError if no BoundElements found."""
|
|
28
|
-
with pytest.raises(ValueError
|
|
26
|
+
with pytest.raises(ValueError, match="At least one argument"):
|
|
29
27
|
_ = new_svg_root_around_bounds(new_element("g"))
|
|
30
|
-
assert "At least one argument" in str(excinfo.value)
|
|
31
28
|
|
|
32
29
|
def test_bounding_boxes(self):
|
|
33
30
|
"""Create svg root element from bounding boxes."""
|
|
@@ -52,21 +49,19 @@ class TestNewSvgRootAroundBounds:
|
|
|
52
49
|
assert isinstance(result, EtreeElement)
|
|
53
50
|
assert result.attrib["viewBox"] == "0 0 201 201"
|
|
54
51
|
|
|
55
|
-
class TestNewBoundUnion:
|
|
56
52
|
|
|
53
|
+
class TestNewBoundUnion:
|
|
57
54
|
def test_bounding_boxes_only(self):
|
|
58
55
|
"""Raise an error if no elements found."""
|
|
59
56
|
bboxes = [BoundingBox(0, 0, 100, 100), BoundingBox(50, 50, 150, 150)]
|
|
60
|
-
with pytest.raises(ValueError
|
|
57
|
+
with pytest.raises(ValueError, match="must be a BoundElement, PaddedText"):
|
|
61
58
|
_ = new_bound_union(*bboxes)
|
|
62
|
-
assert "must be a BoundElement, PaddedText, or Etree" in str(excinfo.value)
|
|
63
59
|
|
|
64
60
|
def test_elements_only(self):
|
|
65
61
|
"""Raise an error if no elements found."""
|
|
66
62
|
elems = [new_element("g"), new_element("g")]
|
|
67
|
-
with pytest.raises(ValueError
|
|
63
|
+
with pytest.raises(ValueError, match="must be a BoundElement, BoundingBox"):
|
|
68
64
|
_ = new_bound_union(*elems)
|
|
69
|
-
assert "must be a BoundElement, BoundingBox, or Padded" in str(excinfo.value)
|
|
70
65
|
|
|
71
66
|
def test_bound_elements(self):
|
|
72
67
|
"""Create svg root element from BoundElements."""
|
|
@@ -8,13 +8,14 @@ No tests for png writing.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
|
+
|
|
11
12
|
import os
|
|
12
13
|
import tempfile
|
|
13
14
|
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
14
16
|
|
|
15
17
|
import pytest
|
|
16
18
|
from lxml import etree
|
|
17
|
-
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
18
19
|
|
|
19
20
|
from svg_ultralight import NSMAP
|
|
20
21
|
|
|
@@ -22,8 +23,13 @@ from svg_ultralight import NSMAP
|
|
|
22
23
|
from svg_ultralight.main import new_svg_root, write_svg
|
|
23
24
|
from svg_ultralight.string_conversion import svg_tostring
|
|
24
25
|
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from lxml.etree import (
|
|
28
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
29
|
+
)
|
|
30
|
+
|
|
25
31
|
|
|
26
|
-
@pytest.fixture
|
|
32
|
+
@pytest.fixture
|
|
27
33
|
def css_source():
|
|
28
34
|
"""Temporary css file object with meaningless contents."""
|
|
29
35
|
with tempfile.NamedTemporaryFile(mode="w", delete=False) as css_source:
|
|
@@ -32,24 +38,29 @@ def css_source():
|
|
|
32
38
|
os.unlink(css_source.name)
|
|
33
39
|
|
|
34
40
|
|
|
35
|
-
@pytest.fixture
|
|
41
|
+
@pytest.fixture
|
|
36
42
|
def temp_filename(mode: str = "w"):
|
|
37
43
|
"""Temporary file object to capture test output."""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
yield svg_output.name
|
|
44
|
+
with tempfile.NamedTemporaryFile(mode=mode, delete=False) as svg_output:
|
|
45
|
+
yield svg_output.name
|
|
41
46
|
os.unlink(svg_output.name)
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
class TestWriteSvg:
|
|
45
|
-
def test_linked(
|
|
50
|
+
def test_linked(
|
|
51
|
+
self, css_source: str | os.PathLike[str], temp_filename: str | os.PathLike[str]
|
|
52
|
+
) -> None:
|
|
46
53
|
"""Insert stylesheet reference."""
|
|
47
54
|
blank = etree.Element("blank")
|
|
48
|
-
write_svg(
|
|
49
|
-
temp_filename,
|
|
55
|
+
_ = write_svg(
|
|
56
|
+
Path(temp_filename),
|
|
57
|
+
blank,
|
|
58
|
+
css_source,
|
|
59
|
+
do_link_css=True,
|
|
60
|
+
xml_declaration=True,
|
|
50
61
|
)
|
|
51
|
-
with open(
|
|
52
|
-
svg_lines = [x.decode() for x in svg_binary
|
|
62
|
+
with Path(temp_filename).open("rb") as svg_binary:
|
|
63
|
+
svg_lines = [x.decode() for x in svg_binary]
|
|
53
64
|
|
|
54
65
|
relative_css_path = Path(css_source).relative_to(Path(temp_filename).parent)
|
|
55
66
|
assert svg_lines == [
|
|
@@ -60,12 +71,14 @@ class TestWriteSvg:
|
|
|
60
71
|
"<blank/>\n",
|
|
61
72
|
]
|
|
62
73
|
|
|
63
|
-
def test_not_linked(
|
|
74
|
+
def test_not_linked(
|
|
75
|
+
self, css_source: str | os.PathLike[str], temp_filename: str | os.PathLike[str]
|
|
76
|
+
) -> None:
|
|
64
77
|
"""Copy css_source contents into svg file."""
|
|
65
78
|
blank = etree.Element("blank")
|
|
66
|
-
write_svg(temp_filename, blank, css_source)
|
|
67
|
-
with open(
|
|
68
|
-
svg_lines = [x.decode() for x in svg_binary
|
|
79
|
+
_ = write_svg(Path(temp_filename), blank, css_source)
|
|
80
|
+
with Path(temp_filename).open("rb") as svg_binary:
|
|
81
|
+
svg_lines = [x.decode() for x in svg_binary]
|
|
69
82
|
|
|
70
83
|
assert svg_lines == [
|
|
71
84
|
"<blank>\n",
|
|
@@ -75,19 +88,19 @@ class TestWriteSvg:
|
|
|
75
88
|
"</blank>\n",
|
|
76
89
|
]
|
|
77
90
|
|
|
78
|
-
def test_css_none(self, temp_filename) -> None:
|
|
91
|
+
def test_css_none(self, temp_filename: str | os.PathLike[str]) -> None:
|
|
79
92
|
"""Do not link or copy in css if no css_source is passed."""
|
|
80
93
|
blank = etree.Element("blank")
|
|
81
|
-
write_svg(temp_filename, blank, do_link_css=True)
|
|
82
|
-
with open(
|
|
83
|
-
svg_lines = [x.decode() for x in svg_binary
|
|
94
|
+
_ = write_svg(Path(temp_filename), blank, do_link_css=True)
|
|
95
|
+
with Path(temp_filename).open("rb") as svg_binary:
|
|
96
|
+
svg_lines = [x.decode() for x in svg_binary]
|
|
84
97
|
|
|
85
98
|
assert svg_lines == ["<blank/>\n"]
|
|
86
99
|
|
|
87
100
|
# test with do_link_css = False
|
|
88
|
-
write_svg(temp_filename, blank)
|
|
89
|
-
with open(
|
|
90
|
-
svg_lines_false = [x.decode() for x in svg_binary
|
|
101
|
+
_ = write_svg(Path(temp_filename), blank)
|
|
102
|
+
with Path(temp_filename).open("rb") as svg_binary:
|
|
103
|
+
svg_lines_false = [x.decode() for x in svg_binary]
|
|
91
104
|
assert svg_lines_false == svg_lines
|
|
92
105
|
|
|
93
106
|
|
|
@@ -106,7 +119,7 @@ def svg_root(**kwargs: str | float) -> EtreeElement:
|
|
|
106
119
|
xmlns = [f'xmlns="{namespace[0][1]}"']
|
|
107
120
|
xmlns += [f'xmlns:{k}="{v}"' for k, v in namespace[1:]]
|
|
108
121
|
attributes = " ".join([f'{k}="{v}"' for k, v in kwargs.items()])
|
|
109
|
-
return etree.fromstring(f
|
|
122
|
+
return etree.fromstring(f"<svg {' '.join(xmlns)} {attributes}/>".encode())
|
|
110
123
|
|
|
111
124
|
|
|
112
125
|
class TestNewSvgRoot:
|
|
@@ -120,19 +133,19 @@ class TestNewSvgRoot:
|
|
|
120
133
|
|
|
121
134
|
Build the svg element namespace from NSMAP and compare to output
|
|
122
135
|
"""
|
|
123
|
-
expect = svg_root(
|
|
136
|
+
expect = svg_root(viewBox="0 1 2 3")
|
|
124
137
|
result = new_svg_root(0, 1, 2, 3)
|
|
125
138
|
assert result.attrib == expect.attrib
|
|
126
139
|
|
|
127
140
|
def test_additional_params(self) -> None:
|
|
128
141
|
"""Pass additional params."""
|
|
129
|
-
expect = svg_root(
|
|
142
|
+
expect = svg_root(attr="value", viewBox="0 1 2 3")
|
|
130
143
|
result = new_svg_root(0, 1, 2, 3, attr="value")
|
|
131
144
|
assert result.attrib == expect.attrib
|
|
132
145
|
|
|
133
146
|
def test_conflicting_params(self) -> None:
|
|
134
147
|
"""Explicit params overwrite trailing-underscore-inferred params."""
|
|
135
|
-
expect = svg_root(
|
|
148
|
+
expect = svg_root(viewBox="0 1 2 3", height="30")
|
|
136
149
|
result = new_svg_root(0, 1, 2, 3, height=30)
|
|
137
150
|
assert result.attrib == expect.attrib
|
|
138
151
|
|
|
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.43.1 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/__init__.py
RENAMED
|
File without changes
|
{svg_ultralight-0.43.1 → svg_ultralight-0.45.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
|
|
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.43.1 → svg_ultralight-0.45.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
|