svg-ultralight 0.48.0__py3-none-any.whl → 0.50.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of svg-ultralight might be problematic. Click here for more details.

Files changed (37) hide show
  1. svg_ultralight/__init__.py +108 -105
  2. svg_ultralight/animate.py +40 -40
  3. svg_ultralight/attrib_hints.py +13 -14
  4. svg_ultralight/bounding_boxes/__init__.py +5 -5
  5. svg_ultralight/bounding_boxes/bound_helpers.py +189 -189
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
  7. svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
  9. svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
  11. svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
  12. svg_ultralight/constructors/__init__.py +14 -14
  13. svg_ultralight/constructors/new_element.py +115 -115
  14. svg_ultralight/font_tools/__init__.py +5 -5
  15. svg_ultralight/font_tools/comp_results.py +295 -293
  16. svg_ultralight/font_tools/font_info.py +793 -792
  17. svg_ultralight/image_ops.py +156 -156
  18. svg_ultralight/inkscape.py +261 -261
  19. svg_ultralight/layout.py +290 -291
  20. svg_ultralight/main.py +183 -198
  21. svg_ultralight/metadata.py +122 -122
  22. svg_ultralight/nsmap.py +36 -36
  23. svg_ultralight/py.typed +5 -0
  24. svg_ultralight/query.py +254 -249
  25. svg_ultralight/read_svg.py +58 -0
  26. svg_ultralight/root_elements.py +87 -87
  27. svg_ultralight/string_conversion.py +244 -244
  28. svg_ultralight/strings/__init__.py +21 -13
  29. svg_ultralight/strings/svg_strings.py +106 -67
  30. svg_ultralight/transformations.py +140 -141
  31. svg_ultralight/unit_conversion.py +247 -248
  32. {svg_ultralight-0.48.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
  33. svg_ultralight-0.50.1.dist-info/RECORD +34 -0
  34. svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
  35. svg_ultralight-0.48.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.48.0.dist-info/top_level.txt +0 -1
@@ -1,67 +1,106 @@
1
- """Explicit string formatting calls for arguments that aren't floats or strings.
2
-
3
- :author: Shay Hill
4
- :created: 10/30/2020
5
-
6
- The `string_conversion` module will format floats or strings. Some other formatters can
7
- make things easier.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import TYPE_CHECKING
13
-
14
- from svg_ultralight.string_conversion import format_number
15
-
16
- if TYPE_CHECKING:
17
- from collections.abc import Iterable
18
-
19
-
20
- _MAX_8BIT = 255
21
- _BIG_INT = 2**32 - 1
22
-
23
-
24
- def _float_to_8bit_int(clipped_float: float) -> int:
25
- """Convert a float between 0 and 255 to an int between 0 and 255.
26
-
27
- :param float_: a float in the closed interval [0 .. 255]. Values outside this
28
- range will be clipped.
29
- :return: an int in the closed interval [0 .. 255]
30
-
31
- Convert color floats [0 .. 255] to ints [0 .. 255] without rounding, which "short
32
- changes" 0 and 255.
33
- """
34
- clipped_float = min(_MAX_8BIT, max(0, clipped_float))
35
- if clipped_float % 1:
36
- high_int = int(clipped_float / _MAX_8BIT * _BIG_INT)
37
- return high_int >> 24
38
- return int(clipped_float)
39
-
40
-
41
- def svg_color_tuple(rgb_floats: tuple[float, float, float]) -> str:
42
- """Turn an rgb tuple (0-255, 0-255, 0-255) into an svg color definition.
43
-
44
- :param rgb_floats: (0-255, 0-255, 0-255)
45
- :return: "rgb(128,128,128)"
46
- """
47
- r, g, b = map(_float_to_8bit_int, rgb_floats)
48
- return f"rgb({r},{g},{b})"
49
-
50
-
51
- def svg_ints(floats: Iterable[float]) -> str:
52
- """Space-delimited ints.
53
-
54
- :param floats: and number of floats
55
- :return: each float rounded to an int, space delimited
56
- """
57
- return " ".join(str(round(x)) for x in floats)
58
-
59
-
60
- def svg_float_tuples(tuples: Iterable[tuple[float, float]]) -> str:
61
- """Space-delimited tuples.
62
-
63
- :param tuples: [(a, b), (c, d)]
64
- :return: "a,b c,d"
65
- """
66
- tuple_strings = [",".join(format_number(n) for n in t) for t in tuples]
67
- return " ".join(tuple_strings)
1
+ """Explicit string formatting calls for arguments that aren't floats or strings.
2
+
3
+ :author: Shay Hill
4
+ :created: 10/30/2020
5
+
6
+ The `string_conversion` module will format floats or strings. Some other formatters can
7
+ make things easier.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from svg_ultralight.string_conversion import format_number
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterable
18
+
19
+
20
+ _MAX_8BIT = 255
21
+ _BIG_INT = 2**32 - 1
22
+
23
+
24
+ def _float_to_8bit_int(clipped_float: float) -> int:
25
+ """Convert a float between 0 and 255 to an int between 0 and 255.
26
+
27
+ :param float_: a float in the closed interval [0 .. 255]. Values outside this
28
+ range will be clipped.
29
+ :return: an int in the closed interval [0 .. 255]
30
+
31
+ Convert color floats [0 .. 255] to ints [0 .. 255] without rounding, which "short
32
+ changes" 0 and 255.
33
+ """
34
+ clipped_float = min(_MAX_8BIT, max(0, clipped_float))
35
+ if clipped_float % 1:
36
+ high_int = int(clipped_float / _MAX_8BIT * _BIG_INT)
37
+ return high_int >> 24
38
+ return int(clipped_float)
39
+
40
+
41
+ def svg_ints(floats: Iterable[float]) -> str:
42
+ """Space-delimited ints.
43
+
44
+ :param floats: and number of floats
45
+ :return: each float rounded to an int, space delimited
46
+ """
47
+ return " ".join(str(round(x)) for x in floats)
48
+
49
+
50
+ def svg_floats(floats: Iterable[float]) -> str:
51
+ """Space-delimited floats.
52
+
53
+ :param floats: and number of floats
54
+ :return: each float formatted, space delimited
55
+
56
+ matrix strings, svg viewBox, and other attributes need space-delimited floats.
57
+ """
58
+ return " ".join(format_number(x) for x in floats)
59
+
60
+
61
+ def svg_float_tuples(tuples: Iterable[tuple[float, float]]) -> str:
62
+ """Space-delimited tuples.
63
+
64
+ :param tuples: [(a, b), (c, d)]
65
+ :return: "a,b c,d"
66
+ """
67
+ tuple_strings = [",".join(format_number(n) for n in t) for t in tuples]
68
+ return " ".join(tuple_strings)
69
+
70
+
71
+ # ===================================================================================
72
+ # Specific string formats
73
+ # ===================================================================================
74
+
75
+
76
+ def svg_color_tuple(rgb_floats: tuple[float, float, float]) -> str:
77
+ """Turn an rgb tuple (0-255, 0-255, 0-255) into an svg color definition.
78
+
79
+ :param rgb_floats: (0-255, 0-255, 0-255)
80
+ :return: "rgb(128,128,128)"
81
+ """
82
+ r, g, b = map(_float_to_8bit_int, rgb_floats)
83
+ return f"rgb({r},{g},{b})"
84
+
85
+
86
+ def svg_matrix(floats: Iterable[float]) -> str:
87
+ """Create a matrix string for the svg transform attribute.
88
+
89
+ a: scale x
90
+ b: skew y
91
+ c: skew x
92
+ d: scale y
93
+ e: translate x
94
+ f: translate y
95
+ :return: "matrix(a,b,c,d,e,f)"
96
+
97
+ The matrix() function defines a transformation in the 2D space. The six values
98
+ represent a 3x3 matrix that is used to perform linear transformations such as
99
+ translation, scaling, rotation, and skewing on SVG elements.
100
+ """
101
+ try:
102
+ a, b, c, d, e, f = floats
103
+ except ValueError as e:
104
+ msg = "svg_matrix() needs exactly 6 floats."
105
+ raise ValueError(msg) from e
106
+ return f"matrix({svg_floats((a, b, c, d, e, f))})"
@@ -1,141 +1,140 @@
1
- """Math and conversion for svg-style transformation matrices.
2
-
3
- :author: Shay Hill
4
- :created: 2024-05-05
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import numbers
10
- import re
11
- from contextlib import suppress
12
- from typing import TYPE_CHECKING, cast
13
-
14
- from svg_ultralight.string_conversion import format_number
15
-
16
- if TYPE_CHECKING:
17
- from lxml.etree import (
18
- _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
19
- )
20
-
21
-
22
- RE_MATRIX = re.compile(r"matrix\(([^)]+)\)")
23
-
24
- _Matrix = tuple[float, float, float, float, float, float]
25
-
26
-
27
- def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
28
- """Matrix multiplication for svg-style matrices.
29
-
30
- :param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
31
- :param mat2: transformation matrix (sx, 0, 0, sy, tx, ty)
32
-
33
- Svg uses an unusual matrix format. For 3x3 transformation matrix
34
-
35
- [[00, 01, 02],
36
- [10, 11, 12],
37
- [20, 21, 22]]
38
-
39
- The svg matrix is
40
- (00, 10, 01, 11, 02, 12)
41
-
42
- Values 10 and 01 are only used for skewing, which is not supported by a bounding
43
- box. Values 00 and 11 will always be identical for symmetric scaling, which is
44
- the only scaling implemented in my BoundingBox classes. However, all six values
45
- are implemented in case this function is used in other contexts.
46
- """
47
- aa = sum(mat1[x] * mat2[y] for x, y in ((0, 0), (2, 1)))
48
- bb = sum(mat1[x] * mat2[y] for x, y in ((1, 0), (3, 1)))
49
- cc = sum(mat1[x] * mat2[y] for x, y in ((0, 2), (2, 3)))
50
- dd = sum(mat1[x] * mat2[y] for x, y in ((1, 2), (3, 3)))
51
- ee = sum(mat1[x] * mat2[y] for x, y in ((0, 4), (2, 5))) + mat1[4]
52
- ff = sum(mat1[x] * mat2[y] for x, y in ((1, 4), (3, 5))) + mat1[5]
53
- return (aa, bb, cc, dd, ee, ff)
54
-
55
-
56
- def mat_apply(matrix: _Matrix, point: tuple[float, float]) -> tuple[float, float]:
57
- """Apply an svg-style transformation matrix to a point.
58
-
59
- :param mat1: transformation matrix (a, b, c, d, e, f) describing a 3x3 matrix
60
- with an implied third row of (0, 0, 1)
61
- [[a, c, e], [b, d, f], [0, 0, 1]]
62
- :param mat2: point (x, y)
63
- """
64
- a, b, c, d, e, f = matrix
65
- x, y = point
66
- result_x = a * x + c * y + e
67
- result_y = b * x + d * y + f
68
- return result_x, result_y
69
-
70
-
71
- def mat_invert(tmat: _Matrix) -> _Matrix:
72
- """Invert a 2D transformation matrix in svg format."""
73
- a, b, c, d, e, f = tmat
74
- det = a * d - b * c
75
- if det == 0:
76
- msg = "Matrix is not invertible"
77
- raise ValueError(msg)
78
- return (
79
- d / det,
80
- -b / det,
81
- -c / det,
82
- a / det,
83
- (c * f - d * e) / det,
84
- (b * e - a * f) / det,
85
- )
86
-
87
-
88
- def get_transform_matrix(elem: EtreeElement) -> _Matrix:
89
- """Get the transformation matrix from an svg element.
90
-
91
- :param element: svg element
92
- """
93
- transform = elem.attrib.get("transform")
94
- if not transform:
95
- return (1, 0, 0, 1, 0, 0)
96
- values_str = ""
97
- with suppress(AttributeError):
98
- values_str = cast("re.Match[str]", RE_MATRIX.match(transform)).group(1)
99
- with suppress(ValueError):
100
- aa, bb, cc, dd, ee, ff = (float(val) for val in values_str.split())
101
- return (aa, bb, cc, dd, ee, ff)
102
- msg = f"Could not parse transformation matrix from {transform}"
103
- raise ValueError(msg)
104
-
105
-
106
- def new_transformation_matrix(
107
- transformation: _Matrix | None = None,
108
- *,
109
- scale: tuple[float, float] | float | None = None,
110
- dx: float | None = None,
111
- dy: float | None = None,
112
- ) -> _Matrix:
113
- """Create a new transformation matrix.
114
-
115
- This takes the standard arguments in the BoundingBox classes and returns an
116
- svg-style transformation matrix.
117
- """
118
- transformation = transformation or (1, 0, 0, 1, 0, 0)
119
-
120
- if isinstance(scale, (float, int, numbers.Real)):
121
- scale_x, scale_y = (scale, scale)
122
- elif scale is None:
123
- scale_x, scale_y = (1, 1)
124
- else:
125
- scale_x, scale_y = scale
126
-
127
- dx = dx or 0
128
- dy = dy or 0
129
- return mat_dot((float(scale_x), 0, 0, float(scale_y), dx, dy), transformation)
130
-
131
-
132
- def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
133
- """Apply a transformation matrix to an svg element.
134
-
135
- :param elem: svg element
136
- :param matrix: transformation matrix
137
- """
138
- current = get_transform_matrix(elem)
139
- updated = map(format_number, mat_dot(matrix, current))
140
- elem.attrib["transform"] = f"matrix({' '.join(updated)})"
141
- return elem
1
+ """Math and conversion for svg-style transformation matrices.
2
+
3
+ :author: Shay Hill
4
+ :created: 2024-05-05
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numbers
10
+ import re
11
+ from contextlib import suppress
12
+ from typing import TYPE_CHECKING, cast
13
+
14
+ from svg_ultralight.strings import svg_matrix
15
+
16
+ if TYPE_CHECKING:
17
+ from lxml.etree import (
18
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
19
+ )
20
+
21
+
22
+ RE_MATRIX = re.compile(r"matrix\(([^)]+)\)")
23
+
24
+ _Matrix = tuple[float, float, float, float, float, float]
25
+
26
+
27
+ def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
28
+ """Matrix multiplication for svg-style matrices.
29
+
30
+ :param mat1: transformation matrix (sx, 0, 0, sy, tx, ty)
31
+ :param mat2: transformation matrix (sx, 0, 0, sy, tx, ty)
32
+
33
+ Svg uses an unusual matrix format. For 3x3 transformation matrix
34
+
35
+ [[00, 01, 02],
36
+ [10, 11, 12],
37
+ [20, 21, 22]]
38
+
39
+ The svg matrix is
40
+ (00, 10, 01, 11, 02, 12)
41
+
42
+ Values 10 and 01 are only used for skewing, which is not supported by a bounding
43
+ box. Values 00 and 11 will always be identical for symmetric scaling, which is
44
+ the only scaling implemented in my BoundingBox classes. However, all six values
45
+ are implemented in case this function is used in other contexts.
46
+ """
47
+ aa = sum(mat1[x] * mat2[y] for x, y in ((0, 0), (2, 1)))
48
+ bb = sum(mat1[x] * mat2[y] for x, y in ((1, 0), (3, 1)))
49
+ cc = sum(mat1[x] * mat2[y] for x, y in ((0, 2), (2, 3)))
50
+ dd = sum(mat1[x] * mat2[y] for x, y in ((1, 2), (3, 3)))
51
+ ee = sum(mat1[x] * mat2[y] for x, y in ((0, 4), (2, 5))) + mat1[4]
52
+ ff = sum(mat1[x] * mat2[y] for x, y in ((1, 4), (3, 5))) + mat1[5]
53
+ return (aa, bb, cc, dd, ee, ff)
54
+
55
+
56
+ def mat_apply(matrix: _Matrix, point: tuple[float, float]) -> tuple[float, float]:
57
+ """Apply an svg-style transformation matrix to a point.
58
+
59
+ :param mat1: transformation matrix (a, b, c, d, e, f) describing a 3x3 matrix
60
+ with an implied third row of (0, 0, 1)
61
+ [[a, c, e], [b, d, f], [0, 0, 1]]
62
+ :param mat2: point (x, y)
63
+ """
64
+ a, b, c, d, e, f = matrix
65
+ x, y = point
66
+ result_x = a * x + c * y + e
67
+ result_y = b * x + d * y + f
68
+ return result_x, result_y
69
+
70
+
71
+ def mat_invert(tmat: _Matrix) -> _Matrix:
72
+ """Invert a 2D transformation matrix in svg format."""
73
+ a, b, c, d, e, f = tmat
74
+ det = a * d - b * c
75
+ if det == 0:
76
+ msg = "Matrix is not invertible"
77
+ raise ValueError(msg)
78
+ return (
79
+ d / det,
80
+ -b / det,
81
+ -c / det,
82
+ a / det,
83
+ (c * f - d * e) / det,
84
+ (b * e - a * f) / det,
85
+ )
86
+
87
+
88
+ def get_transform_matrix(elem: EtreeElement) -> _Matrix:
89
+ """Get the transformation matrix from an svg element.
90
+
91
+ :param element: svg element
92
+ """
93
+ transform = elem.attrib.get("transform")
94
+ if not transform:
95
+ return (1, 0, 0, 1, 0, 0)
96
+ values_str = ""
97
+ with suppress(AttributeError):
98
+ values_str = cast("re.Match[str]", RE_MATRIX.match(transform)).group(1)
99
+ with suppress(ValueError):
100
+ aa, bb, cc, dd, ee, ff = (float(val) for val in values_str.split())
101
+ return (aa, bb, cc, dd, ee, ff)
102
+ msg = f"Could not parse transformation matrix from {transform}"
103
+ raise ValueError(msg)
104
+
105
+
106
+ def new_transformation_matrix(
107
+ transformation: _Matrix | None = None,
108
+ *,
109
+ scale: tuple[float, float] | float | None = None,
110
+ dx: float | None = None,
111
+ dy: float | None = None,
112
+ ) -> _Matrix:
113
+ """Create a new transformation matrix.
114
+
115
+ This takes the standard arguments in the BoundingBox classes and returns an
116
+ svg-style transformation matrix.
117
+ """
118
+ transformation = transformation or (1, 0, 0, 1, 0, 0)
119
+
120
+ if isinstance(scale, (float, int, numbers.Real)):
121
+ scale_x, scale_y = (scale, scale)
122
+ elif scale is None:
123
+ scale_x, scale_y = (1, 1)
124
+ else:
125
+ scale_x, scale_y = scale
126
+
127
+ dx = dx or 0
128
+ dy = dy or 0
129
+ return mat_dot((float(scale_x), 0, 0, float(scale_y), dx, dy), transformation)
130
+
131
+
132
+ def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
133
+ """Apply a transformation matrix to an svg element.
134
+
135
+ :param elem: svg element
136
+ :param matrix: transformation matrix
137
+ """
138
+ current = get_transform_matrix(elem)
139
+ elem.attrib["transform"] = svg_matrix(mat_dot(matrix, current))
140
+ return elem