svg-ultralight 0.64.0__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.
- svg_ultralight/__init__.py +112 -0
- svg_ultralight/animate.py +40 -0
- svg_ultralight/attrib_hints.py +14 -0
- svg_ultralight/bounding_boxes/__init__.py +5 -0
- svg_ultralight/bounding_boxes/bound_helpers.py +200 -0
- svg_ultralight/bounding_boxes/padded_text_initializers.py +442 -0
- svg_ultralight/bounding_boxes/supports_bounds.py +167 -0
- svg_ultralight/bounding_boxes/type_bound_collection.py +74 -0
- svg_ultralight/bounding_boxes/type_bound_element.py +68 -0
- svg_ultralight/bounding_boxes/type_bounding_box.py +432 -0
- svg_ultralight/bounding_boxes/type_padded_list.py +208 -0
- svg_ultralight/bounding_boxes/type_padded_text.py +502 -0
- svg_ultralight/constructors/__init__.py +14 -0
- svg_ultralight/constructors/new_element.py +117 -0
- svg_ultralight/font_tools/__init__.py +5 -0
- svg_ultralight/font_tools/comp_results.py +291 -0
- svg_ultralight/font_tools/font_info.py +849 -0
- svg_ultralight/image_ops.py +156 -0
- svg_ultralight/inkscape.py +261 -0
- svg_ultralight/layout.py +291 -0
- svg_ultralight/main.py +183 -0
- svg_ultralight/metadata.py +122 -0
- svg_ultralight/nsmap.py +36 -0
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -0
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +96 -0
- svg_ultralight/string_conversion.py +244 -0
- svg_ultralight/strings/__init__.py +21 -0
- svg_ultralight/strings/svg_strings.py +106 -0
- svg_ultralight/transformations.py +152 -0
- svg_ultralight/unit_conversion.py +247 -0
- svg_ultralight-0.64.0.dist-info/METADATA +208 -0
- svg_ultralight-0.64.0.dist-info/RECORD +35 -0
- svg_ultralight-0.64.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +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_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))})"
|
|
@@ -0,0 +1,152 @@
|
|
|
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, TypeAlias, 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: TypeAlias = 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(
|
|
133
|
+
elem: EtreeElement, matrix: _Matrix, *, reverse: bool = False
|
|
134
|
+
) -> EtreeElement:
|
|
135
|
+
"""Apply a transformation matrix to an svg element.
|
|
136
|
+
|
|
137
|
+
:param elem: svg element
|
|
138
|
+
:par m matrix: transformation matrix
|
|
139
|
+
|
|
140
|
+
:param reverse: If you have a transformation matrix, A, and wish to apply an
|
|
141
|
+
additional transform, B, the result is B @ A. This is how an element can be
|
|
142
|
+
cumulatively transformed in svg.
|
|
143
|
+
|
|
144
|
+
If the element is transformed by A and is a part of a GROUP transformed by B,
|
|
145
|
+
then the result is the reverse: A @ B.
|
|
146
|
+
"""
|
|
147
|
+
current = get_transform_matrix(elem)
|
|
148
|
+
if reverse:
|
|
149
|
+
elem.attrib["transform"] = svg_matrix(mat_dot(current, matrix))
|
|
150
|
+
else:
|
|
151
|
+
elem.attrib["transform"] = svg_matrix(mat_dot(matrix, current))
|
|
152
|
+
return elem
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Convert between absolute units.
|
|
2
|
+
|
|
3
|
+
Model everything in user units, then use "width" and "height" in the svg root element
|
|
4
|
+
to scale these units to the desired size.
|
|
5
|
+
|
|
6
|
+
I borrowed test values and and conventions from Inkscape's `inkek.units.py`.
|
|
7
|
+
|
|
8
|
+
:author: Shay Hill
|
|
9
|
+
:created: 2023-02-12
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import enum
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
from svg_ultralight.string_conversion import format_number
|
|
19
|
+
|
|
20
|
+
# units per inch
|
|
21
|
+
_UPI = 96
|
|
22
|
+
|
|
23
|
+
# units per centimeter
|
|
24
|
+
_UPC = 96 / 2.54
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Unit(enum.Enum):
|
|
28
|
+
"""SVG Units of measurement.
|
|
29
|
+
|
|
30
|
+
Value is (unit conversion, unit specifier)
|
|
31
|
+
|
|
32
|
+
The unit specifier string are how various units are identified in SVG.
|
|
33
|
+
e.g., "44in"
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
IN = "in", _UPI # inches
|
|
37
|
+
PT = "pt", 4 / 3 # points
|
|
38
|
+
PX = "px", 1 # pixels
|
|
39
|
+
MM = "mm", _UPC / 10 # millimeters
|
|
40
|
+
CM = "cm", _UPC # centimeters
|
|
41
|
+
M = "m", _UPC * 100 # meters
|
|
42
|
+
KM = "km", _UPC * 100000 # kilometers
|
|
43
|
+
Q = "Q", _UPC / 40 # quarter-millimeters
|
|
44
|
+
PC = "pc", _UPI / 6 # picas
|
|
45
|
+
YD = "yd", _UPI * 36 # yards
|
|
46
|
+
FT = "ft", _UPI * 12 # feet
|
|
47
|
+
USER = "", 1 # "user units" without a unit specifier
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# the arguments this module will attempt to interpret as a string with a unit specifier
|
|
51
|
+
MeasurementArg = (
|
|
52
|
+
float
|
|
53
|
+
| str
|
|
54
|
+
| tuple[str, str]
|
|
55
|
+
| tuple[float, str]
|
|
56
|
+
| tuple[str, Unit]
|
|
57
|
+
| tuple[float, Unit]
|
|
58
|
+
| Unit
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_UNIT_SPECIFIER2UNIT = {x.value[0]: x for x in Unit}
|
|
62
|
+
|
|
63
|
+
_UNIT_SPECIFIERS = [x.value[0] for x in Unit]
|
|
64
|
+
_NUMBER = r"([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?"
|
|
65
|
+
_UNIT_RE = re.compile(rf"(?P<unit>{'|'.join(_UNIT_SPECIFIERS)})")
|
|
66
|
+
_NUMBER_RE = re.compile(rf"(?P<number>{_NUMBER})")
|
|
67
|
+
_NUMBER_AND_UNIT = re.compile(rf"^{_NUMBER_RE.pattern}{_UNIT_RE.pattern}$")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_unit(measurement_arg: MeasurementArg) -> tuple[float, Unit]:
|
|
71
|
+
"""Split the value and unit from a string.
|
|
72
|
+
|
|
73
|
+
:param measurement_arg: The value to parse (e.g. "55.32px")
|
|
74
|
+
:return: A tuple of the value and Unit
|
|
75
|
+
:raise ValueError: If the value cannot be parsed
|
|
76
|
+
|
|
77
|
+
Take a value such as "55.32px" and return (55.32, Unit.PX). Preserves non-units,
|
|
78
|
+
so "55.32" returns (55.32, Unit.USER) These are actually pixels, but you don't
|
|
79
|
+
want "px" in your viewbox calls. It is best to work in non-specified "user units"
|
|
80
|
+
and then set the svg width and height to an specified unit.
|
|
81
|
+
|
|
82
|
+
Can handle a lot of args:
|
|
83
|
+
|
|
84
|
+
| arg type | example | result |
|
|
85
|
+
| ------------- | ------------------ | ------------------ |
|
|
86
|
+
| float | 55.32 | (55.32, Unit.USER) |
|
|
87
|
+
| str | "55.32px" | (55.32, Unit.PX) |
|
|
88
|
+
| str | "55.32" | (55.32, Unit.USER) |
|
|
89
|
+
| str | "px" | (0.0, Unit.PX) |
|
|
90
|
+
| (str, str) | ("55.32", "px") | (55.32, Unit.PX) |
|
|
91
|
+
| (float, str) | (55.32, "px") | (55.32, Unit.PX) |
|
|
92
|
+
| (str, Unit) | ("55.32", Unit.PX) | (55.32, Unit.PX) |
|
|
93
|
+
| (float, Unit) | (55.32, Unit.PX) | (55.32, Unit.PX) |
|
|
94
|
+
| Unit | Unit.PX | (0.0, Unit.PX) |
|
|
95
|
+
| Measurement | Measurement("3in") | (3.0, Unit.IN) |
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
failure_msg = f"Cannot parse value and unit from {measurement_arg}"
|
|
99
|
+
unit: str | Unit
|
|
100
|
+
try:
|
|
101
|
+
if isinstance(measurement_arg, tuple):
|
|
102
|
+
number, unit = float(measurement_arg[0]), measurement_arg[1]
|
|
103
|
+
if isinstance(unit, Unit):
|
|
104
|
+
return number, unit
|
|
105
|
+
return number, _UNIT_SPECIFIER2UNIT[unit]
|
|
106
|
+
|
|
107
|
+
if isinstance(measurement_arg, (int, float)):
|
|
108
|
+
return _parse_unit((measurement_arg, ""))
|
|
109
|
+
|
|
110
|
+
if isinstance(measurement_arg, Unit):
|
|
111
|
+
return _parse_unit((0, measurement_arg))
|
|
112
|
+
|
|
113
|
+
if number_unit := _NUMBER_AND_UNIT.match(str(measurement_arg)):
|
|
114
|
+
return _parse_unit((number_unit["number"], number_unit["unit"]))
|
|
115
|
+
|
|
116
|
+
if unit_only := _UNIT_RE.match(str(measurement_arg)):
|
|
117
|
+
return _parse_unit((0, unit_only["unit"]))
|
|
118
|
+
|
|
119
|
+
except (ValueError, KeyError) as e:
|
|
120
|
+
raise ValueError(failure_msg) from e
|
|
121
|
+
|
|
122
|
+
raise ValueError(failure_msg)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclasses.dataclass
|
|
126
|
+
class Measurement:
|
|
127
|
+
"""Measurement with unit of measurement.
|
|
128
|
+
|
|
129
|
+
Converts to and stores the value in user units. Also retains the input units so
|
|
130
|
+
you can update the value then convert back.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
value: float
|
|
134
|
+
native_unit: Unit
|
|
135
|
+
|
|
136
|
+
def __init__(self, measurement_arg: MeasurementArg) -> None:
|
|
137
|
+
"""Create a measurement from a string or float.
|
|
138
|
+
|
|
139
|
+
:param measurement_arg: a float (user units) or string with unit specifier.
|
|
140
|
+
:raises ValueError: if the input units cannot be identified
|
|
141
|
+
"""
|
|
142
|
+
value, self.native_unit = _parse_unit(measurement_arg)
|
|
143
|
+
self.value = value * self.native_unit.value[1]
|
|
144
|
+
|
|
145
|
+
def get_value(self, unit: Unit | None = None) -> float:
|
|
146
|
+
"""Get the measurement in the specified unit.
|
|
147
|
+
|
|
148
|
+
:param unit: optional unit to convert to
|
|
149
|
+
:return: value in specified units
|
|
150
|
+
|
|
151
|
+
It's best to do all math with self.value, but this is here for conversion
|
|
152
|
+
with less precision loss.
|
|
153
|
+
"""
|
|
154
|
+
if unit is None:
|
|
155
|
+
return self.value
|
|
156
|
+
if isinstance(unit, str):
|
|
157
|
+
unit = _UNIT_SPECIFIER2UNIT[unit]
|
|
158
|
+
return self.value / unit.value[1]
|
|
159
|
+
|
|
160
|
+
def get_tuple(self, unit: Unit | None = None) -> tuple[float, Unit]:
|
|
161
|
+
"""Get the measurement as a tuple of value and unit.
|
|
162
|
+
|
|
163
|
+
:param unit: optional unit to convert to
|
|
164
|
+
:return: value in specified as a tuple
|
|
165
|
+
"""
|
|
166
|
+
return self.get_value(unit), unit or Unit.USER
|
|
167
|
+
|
|
168
|
+
def get_str(self, unit: Unit | None = None) -> str:
|
|
169
|
+
"""Get the measurement in the specified unit as a string.
|
|
170
|
+
|
|
171
|
+
:param optional unit: the unit to convert to
|
|
172
|
+
:return: the measurement in the specified unit as a string
|
|
173
|
+
|
|
174
|
+
The input arguments for groups of measurements are less flexible than for
|
|
175
|
+
single measurements. Single measurements can be defined by something like
|
|
176
|
+
`(1, "in")`, but groups can be passed as single or tuples, so there is no way
|
|
177
|
+
to differentiate between (1, "in") and "1in" or (1, "in") as ("1", "0in").
|
|
178
|
+
That is a limitation, but doint it that way preserved the flexibility (and
|
|
179
|
+
backwards compatibility) of being able to define padding as "1in" everywhere
|
|
180
|
+
or (1, 2, 3, 4) for top, right, bottom, left.
|
|
181
|
+
|
|
182
|
+
The string from this method is different from the string in the `get_svg`
|
|
183
|
+
method, because this string will print a full printable precision, while the
|
|
184
|
+
svg string will print a reduced precision. So this string can be used to as
|
|
185
|
+
an argument to pad or print_width without losing precision.
|
|
186
|
+
"""
|
|
187
|
+
value, unit = self.get_tuple(unit)
|
|
188
|
+
return f"{value}{unit.value[0]}"
|
|
189
|
+
|
|
190
|
+
def get_svg(self, unit: Unit | None = None) -> str:
|
|
191
|
+
"""Get the measurement in the specified unit as it would be written in svg.
|
|
192
|
+
|
|
193
|
+
:param optional unit: the unit to convert to
|
|
194
|
+
:return: the measurement in the specified unit, always as a string
|
|
195
|
+
|
|
196
|
+
Rounds values to 6 decimal places as recommended by svg guidance online.
|
|
197
|
+
Higher precision just changes file size without imroving quality.
|
|
198
|
+
"""
|
|
199
|
+
_, unit = self.get_tuple(unit)
|
|
200
|
+
value_as_str = format_number(self.get_value(unit))
|
|
201
|
+
return f"{value_as_str}{unit.value[0]}"
|
|
202
|
+
|
|
203
|
+
def __add__(self, other: Measurement) -> Measurement:
|
|
204
|
+
"""Add two measurements.
|
|
205
|
+
|
|
206
|
+
:param other: the other measurement
|
|
207
|
+
:return: the sum of the two measurements in self native unit
|
|
208
|
+
"""
|
|
209
|
+
result = Measurement(self.native_unit)
|
|
210
|
+
result.value = self.value + other.value
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
def __sub__(self, other: Measurement) -> Measurement:
|
|
214
|
+
"""Subtract two measurements.
|
|
215
|
+
|
|
216
|
+
:param other: the other measurement
|
|
217
|
+
:return: the difference of the two measurements in self native unit
|
|
218
|
+
"""
|
|
219
|
+
result = Measurement(self.native_unit)
|
|
220
|
+
result.value = self.value - other.value
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
def __mul__(self, scalar: float) -> Measurement:
|
|
224
|
+
"""Multiply a measurement by a scalar.
|
|
225
|
+
|
|
226
|
+
:param scalar: the scalar to multiply by
|
|
227
|
+
:return: the measurement multiplied by the scalar in self native unit
|
|
228
|
+
"""
|
|
229
|
+
result = Measurement(self.native_unit)
|
|
230
|
+
result.value = self.value * scalar
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
def __rmul__(self, scalar: float) -> Measurement:
|
|
234
|
+
"""Multiply a measurement by a scalar.
|
|
235
|
+
|
|
236
|
+
:param scalar: the scalar to multiply by
|
|
237
|
+
:return: the measurement multiplied by the scalar in self native unit
|
|
238
|
+
"""
|
|
239
|
+
return self.__mul__(scalar)
|
|
240
|
+
|
|
241
|
+
def __truediv__(self, scalar: float) -> Measurement:
|
|
242
|
+
"""Divide a measurement by a scalar.
|
|
243
|
+
|
|
244
|
+
:param scalar: the scalar to divide by
|
|
245
|
+
:return: the measurement divided by the scalar in self native unit
|
|
246
|
+
"""
|
|
247
|
+
return self.__mul__(1.0 / scalar)
|