svg-ultralight 0.64.0__py3-none-any.whl → 0.73.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.
- svg_ultralight/__init__.py +8 -6
- svg_ultralight/bounding_boxes/bound_helpers.py +37 -50
- svg_ultralight/bounding_boxes/padded_text_initializers.py +148 -182
- svg_ultralight/bounding_boxes/type_bounding_box.py +31 -14
- svg_ultralight/bounding_boxes/type_padded_list.py +2 -7
- svg_ultralight/bounding_boxes/type_padded_text.py +240 -53
- svg_ultralight/constructors/new_element.py +37 -3
- svg_ultralight/font_tools/comp_results.py +18 -18
- svg_ultralight/font_tools/font_info.py +117 -36
- svg_ultralight/layout.py +37 -18
- svg_ultralight/main.py +26 -16
- svg_ultralight/root_elements.py +6 -4
- svg_ultralight/string_conversion.py +40 -8
- svg_ultralight/unit_conversion.py +104 -27
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/METADATA +1 -1
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/RECORD +17 -18
- svg_ultralight/read_svg.py +0 -58
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/WHEEL +0 -0
|
@@ -14,6 +14,8 @@ from __future__ import annotations
|
|
|
14
14
|
import dataclasses
|
|
15
15
|
import enum
|
|
16
16
|
import re
|
|
17
|
+
from contextlib import suppress
|
|
18
|
+
from typing import Any, Literal, TypeAlias, cast
|
|
17
19
|
|
|
18
20
|
from svg_ultralight.string_conversion import format_number
|
|
19
21
|
|
|
@@ -47,26 +49,33 @@ class Unit(enum.Enum):
|
|
|
47
49
|
USER = "", 1 # "user units" without a unit specifier
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| tuple[str, str]
|
|
55
|
-
| tuple[float, str]
|
|
56
|
-
| tuple[str, Unit]
|
|
57
|
-
| tuple[float, Unit]
|
|
58
|
-
| Unit
|
|
59
|
-
)
|
|
52
|
+
_UnitSpecifier: TypeAlias = Literal[
|
|
53
|
+
"in", "pt", "px", "mm", "cm", "m", "km", "Q", "pc", "yd", "ft", ""
|
|
54
|
+
]
|
|
55
|
+
|
|
60
56
|
|
|
61
57
|
_UNIT_SPECIFIER2UNIT = {x.value[0]: x for x in Unit}
|
|
62
58
|
|
|
63
59
|
_UNIT_SPECIFIERS = [x.value[0] for x in Unit]
|
|
64
60
|
_NUMBER = r"([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?"
|
|
65
|
-
_UNIT_RE = re.compile(rf"(?P<unit>{'|'.join(_UNIT_SPECIFIERS)})")
|
|
61
|
+
_UNIT_RE = re.compile(rf"(?P<unit>{'|'.join(_UNIT_SPECIFIERS)})$")
|
|
66
62
|
_NUMBER_RE = re.compile(rf"(?P<number>{_NUMBER})")
|
|
67
63
|
_NUMBER_AND_UNIT = re.compile(rf"^{_NUMBER_RE.pattern}{_UNIT_RE.pattern}$")
|
|
68
64
|
|
|
69
65
|
|
|
66
|
+
def is_measurement_arg(obj: object) -> bool:
|
|
67
|
+
"""Determine if an object is a valid measurement argument.
|
|
68
|
+
|
|
69
|
+
:param obj: object to check
|
|
70
|
+
:return: True if the object is a valid measurement argument
|
|
71
|
+
"""
|
|
72
|
+
maybe_measurement_arg = cast("Any", obj)
|
|
73
|
+
with suppress(ValueError):
|
|
74
|
+
_ = Measurement(maybe_measurement_arg)
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
70
79
|
def _parse_unit(measurement_arg: MeasurementArg) -> tuple[float, Unit]:
|
|
71
80
|
"""Split the value and unit from a string.
|
|
72
81
|
|
|
@@ -95,8 +104,10 @@ def _parse_unit(measurement_arg: MeasurementArg) -> tuple[float, Unit]:
|
|
|
95
104
|
| Measurement | Measurement("3in") | (3.0, Unit.IN) |
|
|
96
105
|
|
|
97
106
|
"""
|
|
107
|
+
if isinstance(measurement_arg, Measurement):
|
|
108
|
+
return measurement_arg.get_tuple()
|
|
98
109
|
failure_msg = f"Cannot parse value and unit from {measurement_arg}"
|
|
99
|
-
unit:
|
|
110
|
+
unit: _UnitSpecifier | Unit
|
|
100
111
|
try:
|
|
101
112
|
if isinstance(measurement_arg, tuple):
|
|
102
113
|
number, unit = float(measurement_arg[0]), measurement_arg[1]
|
|
@@ -110,13 +121,15 @@ def _parse_unit(measurement_arg: MeasurementArg) -> tuple[float, Unit]:
|
|
|
110
121
|
if isinstance(measurement_arg, Unit):
|
|
111
122
|
return _parse_unit((0, measurement_arg))
|
|
112
123
|
|
|
113
|
-
if number_unit := _NUMBER_AND_UNIT.match(
|
|
114
|
-
|
|
124
|
+
if number_unit := _NUMBER_AND_UNIT.match(measurement_arg):
|
|
125
|
+
unit = _UNIT_SPECIFIER2UNIT[number_unit["unit"]]
|
|
126
|
+
return _parse_unit((number_unit["number"], unit))
|
|
115
127
|
|
|
116
|
-
if unit_only := _UNIT_RE.match(
|
|
117
|
-
|
|
128
|
+
if unit_only := _UNIT_RE.match(measurement_arg):
|
|
129
|
+
unit = _UNIT_SPECIFIER2UNIT[unit_only["unit"]]
|
|
130
|
+
return _parse_unit((0, unit))
|
|
118
131
|
|
|
119
|
-
except (ValueError, KeyError) as e:
|
|
132
|
+
except (ValueError, KeyError, IndexError, TypeError) as e:
|
|
120
133
|
raise ValueError(failure_msg) from e
|
|
121
134
|
|
|
122
135
|
raise ValueError(failure_msg)
|
|
@@ -142,6 +155,16 @@ class Measurement:
|
|
|
142
155
|
value, self.native_unit = _parse_unit(measurement_arg)
|
|
143
156
|
self.value = value * self.native_unit.value[1]
|
|
144
157
|
|
|
158
|
+
def __float__(self) -> float:
|
|
159
|
+
"""Get the measurement in user units.
|
|
160
|
+
|
|
161
|
+
:return: value in user units
|
|
162
|
+
|
|
163
|
+
It's best to do all math with self.value, but this is here for conversion
|
|
164
|
+
with less precision loss.
|
|
165
|
+
"""
|
|
166
|
+
return self.value
|
|
167
|
+
|
|
145
168
|
def get_value(self, unit: Unit | None = None) -> float:
|
|
146
169
|
"""Get the measurement in the specified unit.
|
|
147
170
|
|
|
@@ -175,7 +198,7 @@ class Measurement:
|
|
|
175
198
|
single measurements. Single measurements can be defined by something like
|
|
176
199
|
`(1, "in")`, but groups can be passed as single or tuples, so there is no way
|
|
177
200
|
to differentiate between (1, "in") and "1in" or (1, "in") as ("1", "0in").
|
|
178
|
-
That is a limitation, but
|
|
201
|
+
That is a limitation, but doing it that way preserved the flexibility (and
|
|
179
202
|
backwards compatibility) of being able to define padding as "1in" everywhere
|
|
180
203
|
or (1, 2, 3, 4) for top, right, bottom, left.
|
|
181
204
|
|
|
@@ -190,35 +213,49 @@ class Measurement:
|
|
|
190
213
|
def get_svg(self, unit: Unit | None = None) -> str:
|
|
191
214
|
"""Get the measurement in the specified unit as it would be written in svg.
|
|
192
215
|
|
|
193
|
-
:param optional unit: the unit to convert to
|
|
216
|
+
:param optional unit: the unit to convert to (defaults to native unit)
|
|
194
217
|
:return: the measurement in the specified unit, always as a string
|
|
195
218
|
|
|
196
219
|
Rounds values to 6 decimal places as recommended by svg guidance online.
|
|
197
|
-
Higher
|
|
220
|
+
Higher resolution just changes file size without imroving quality.
|
|
198
221
|
"""
|
|
199
|
-
_, unit = self.get_tuple(unit)
|
|
222
|
+
_, unit = self.get_tuple(unit or self.native_unit)
|
|
200
223
|
value_as_str = format_number(self.get_value(unit))
|
|
201
224
|
return f"{value_as_str}{unit.value[0]}"
|
|
202
225
|
|
|
203
|
-
def __add__(self, other: Measurement) -> Measurement:
|
|
226
|
+
def __add__(self, other: Measurement | float) -> Measurement:
|
|
204
227
|
"""Add two measurements.
|
|
205
228
|
|
|
206
229
|
:param other: the other measurement
|
|
207
230
|
:return: the sum of the two measurements in self native unit
|
|
208
231
|
"""
|
|
209
232
|
result = Measurement(self.native_unit)
|
|
210
|
-
result.value = self.value + other
|
|
233
|
+
result.value = self.value + float(other)
|
|
211
234
|
return result
|
|
212
235
|
|
|
213
|
-
def
|
|
236
|
+
def __radd__(self, other: float) -> Measurement:
|
|
237
|
+
"""Add a measurement to a float.
|
|
238
|
+
|
|
239
|
+
:param other: the other measurement
|
|
240
|
+
:return: the sum of the two measurements in self native unit
|
|
241
|
+
"""
|
|
242
|
+
return self.__add__(other)
|
|
243
|
+
|
|
244
|
+
def __sub__(self, other: Measurement | float) -> Measurement:
|
|
214
245
|
"""Subtract two measurements.
|
|
215
246
|
|
|
216
247
|
:param other: the other measurement
|
|
217
248
|
:return: the difference of the two measurements in self native unit
|
|
218
249
|
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
return self.__add__(-other)
|
|
251
|
+
|
|
252
|
+
def __rsub__(self, other: float) -> Measurement:
|
|
253
|
+
"""Subtract a measurement from a float.
|
|
254
|
+
|
|
255
|
+
:param other: the other measurement
|
|
256
|
+
:return: the difference of the two measurements in self native unit
|
|
257
|
+
"""
|
|
258
|
+
return self.__mul__(-1).__add__(other)
|
|
222
259
|
|
|
223
260
|
def __mul__(self, scalar: float) -> Measurement:
|
|
224
261
|
"""Multiply a measurement by a scalar.
|
|
@@ -245,3 +282,43 @@ class Measurement:
|
|
|
245
282
|
:return: the measurement divided by the scalar in self native unit
|
|
246
283
|
"""
|
|
247
284
|
return self.__mul__(1.0 / scalar)
|
|
285
|
+
|
|
286
|
+
def __neg__(self) -> Measurement:
|
|
287
|
+
"""Negate a measurement.
|
|
288
|
+
|
|
289
|
+
:return: the negated measurement in self native unit
|
|
290
|
+
"""
|
|
291
|
+
return self.__mul__(-1)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def to_user_units(measurement_arg: MeasurementArg) -> float:
|
|
295
|
+
"""Convert a measurement argument to user units.
|
|
296
|
+
|
|
297
|
+
:param measurement_arg: The measurement argument to convert
|
|
298
|
+
:return: The measurement in user units
|
|
299
|
+
"""
|
|
300
|
+
if isinstance(measurement_arg, (int, float)):
|
|
301
|
+
return float(measurement_arg)
|
|
302
|
+
return Measurement(measurement_arg).value
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def to_svg_str(measurement_arg: MeasurementArg) -> str:
|
|
306
|
+
"""Convert a measurement argument to an svg string.
|
|
307
|
+
|
|
308
|
+
:param measurement_arg: The measurement argument to convert
|
|
309
|
+
:return: The measurement as an svg string
|
|
310
|
+
"""
|
|
311
|
+
return Measurement(measurement_arg).get_svg()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# the arguments this module will attempt to interpret as a string with a unit specifier
|
|
315
|
+
MeasurementArg: TypeAlias = (
|
|
316
|
+
float
|
|
317
|
+
| str
|
|
318
|
+
| tuple[str, _UnitSpecifier]
|
|
319
|
+
| tuple[float, _UnitSpecifier]
|
|
320
|
+
| tuple[str, Unit]
|
|
321
|
+
| tuple[float, Unit]
|
|
322
|
+
| Unit
|
|
323
|
+
| Measurement
|
|
324
|
+
)
|
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
svg_ultralight/__init__.py,sha256=
|
|
1
|
+
svg_ultralight/__init__.py,sha256=6rH78uWngTeTQkS636swQBM3B_7F57k3B2Mz3tkTU4o,2895
|
|
2
2
|
svg_ultralight/animate.py,sha256=pwboos8Z1GFNEGxHxIeeIyxyfRUew5pus2lG3P4MERU,1093
|
|
3
3
|
svg_ultralight/attrib_hints.py,sha256=_p85b77mcmj15IUnsDtgbYJ3FFd-Q1qM2OERv2UEZ20,419
|
|
4
4
|
svg_ultralight/bounding_boxes/__init__.py,sha256=YNCwgN-Ja2MFoCxIYxC3KZTCx_gFvPfRQ-8zBR5Q9mk,73
|
|
5
|
-
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=
|
|
6
|
-
svg_ultralight/bounding_boxes/padded_text_initializers.py,sha256=
|
|
5
|
+
svg_ultralight/bounding_boxes/bound_helpers.py,sha256=zTzsLagCESWsFZweS-ErFNSKi7z7OrCGcgjQgt8LCiY,6594
|
|
6
|
+
svg_ultralight/bounding_boxes/padded_text_initializers.py,sha256=EBI6uyO2PerVt4CSp-p9gycmB53SS6_iQF8gqnkW1b0,13494
|
|
7
7
|
svg_ultralight/bounding_boxes/supports_bounds.py,sha256=8rIklGICIx-DXELN7FjDwrzOi8iUrlstGrCIuN9ew3o,4458
|
|
8
8
|
svg_ultralight/bounding_boxes/type_bound_collection.py,sha256=ct8BLjqyHSCNhOGG0eYuubdOajI2KVo1nbTP3JxXZ00,2756
|
|
9
9
|
svg_ultralight/bounding_boxes/type_bound_element.py,sha256=gXsHCSJ6lxIGODm1oJ5yYAPzuIi7NkBIIzD_GX-cgo8,2322
|
|
10
|
-
svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=
|
|
11
|
-
svg_ultralight/bounding_boxes/type_padded_list.py,sha256=
|
|
12
|
-
svg_ultralight/bounding_boxes/type_padded_text.py,sha256=
|
|
10
|
+
svg_ultralight/bounding_boxes/type_bounding_box.py,sha256=V9i-EhJRIz6g1moPIuvFeh_zoASQVj0SdoD2FSknGKo,15314
|
|
11
|
+
svg_ultralight/bounding_boxes/type_padded_list.py,sha256=NAEIdSmm-D4Zjmtogj4K_n1ioqP3w1am97x3byz0N3Y,7918
|
|
12
|
+
svg_ultralight/bounding_boxes/type_padded_text.py,sha256=TPi2UO-sCsJOEau41uUc_QFsdbXMTZU10cbmkMpw65A,22208
|
|
13
13
|
svg_ultralight/constructors/__init__.py,sha256=fb-A50G3YTZNMQXpxcCl_QAcfssS0Us065_kdJRybDQ,313
|
|
14
|
-
svg_ultralight/constructors/new_element.py,sha256=
|
|
14
|
+
svg_ultralight/constructors/new_element.py,sha256=AppAt4t9VNDjnWChhGe-R-T696oNVne-rWOdOuHpGRY,4752
|
|
15
15
|
svg_ultralight/font_tools/__init__.py,sha256=b_VSvk5aODzS2wv48EMU2sey_mxq1o1SL8ecWTdy4kc,78
|
|
16
|
-
svg_ultralight/font_tools/comp_results.py,sha256
|
|
17
|
-
svg_ultralight/font_tools/font_info.py,sha256=
|
|
16
|
+
svg_ultralight/font_tools/comp_results.py,sha256=O0id9w8ORTxGWLVdhRxuh-MdzxixcrF-1pQCp2yQPRs,10311
|
|
17
|
+
svg_ultralight/font_tools/font_info.py,sha256=CweGwPo8C68FAOkiev_1AHO88jfs6Q59-BvyiR_5pCQ,34229
|
|
18
18
|
svg_ultralight/image_ops.py,sha256=cwR038ECUuiceIrk-9Peh5Ijp4tzjuaV5Y1cvYOt4TI,5454
|
|
19
19
|
svg_ultralight/inkscape.py,sha256=_aQ42ZQ1JP9TFdHmtxgCRsXvxfZPpdZWTGKEea9BgIU,9336
|
|
20
|
-
svg_ultralight/layout.py,sha256=
|
|
21
|
-
svg_ultralight/main.py,sha256=
|
|
20
|
+
svg_ultralight/layout.py,sha256=xj_QvRnzWpDHPvA1qWX9XQJmwJ0-ghWkd03-HcPLUKE,13036
|
|
21
|
+
svg_ultralight/main.py,sha256=zCBDB5QmkAFsnCCBkgfXzWVoFMNWR7Pp9MQ684x2YrM,7735
|
|
22
22
|
svg_ultralight/metadata.py,sha256=kBaJ2QPpCn1s2f-qBGJxivA3HXoGTVKp4sU3-U0SQMw,4112
|
|
23
23
|
svg_ultralight/nsmap.py,sha256=3N-x2I1iJ4EPWjYxIyHi4z8MVbJzxUyRvaGsiOijqmA,1208
|
|
24
24
|
svg_ultralight/py.typed,sha256=cnjZxaS4F-N8wLWhQK9YvZ-PaN5T1TK94E9VhPT_LGg,155
|
|
25
25
|
svg_ultralight/query.py,sha256=tSW6kvIm6Rq7IBterq6XHU90w8F2cWROSWri0JWl4nM,9680
|
|
26
|
-
svg_ultralight/
|
|
27
|
-
svg_ultralight/
|
|
28
|
-
svg_ultralight/string_conversion.py,sha256=pzqkbGb-wXdhGd8ojEx4Hy-HBCC8fOfcloaLrNMh8TM,8607
|
|
26
|
+
svg_ultralight/root_elements.py,sha256=NHUQEqb7YLoC1FCy5RMD5IkmIGlC3YzHoKnZM5W4MEY,3606
|
|
27
|
+
svg_ultralight/string_conversion.py,sha256=MQQyI7I1DOd43ZHQG-7NYL9PkMLRKbyFfqtm84T0-yQ,9796
|
|
29
28
|
svg_ultralight/strings/__init__.py,sha256=iaRwr9AF9bPDkG3XvgGRSf8JPAS2GQ8ds9yCNpPN4ng,365
|
|
30
29
|
svg_ultralight/strings/svg_strings.py,sha256=XlXQ5RqueGrROXBI4VzR2cK7e1NdNhYx5S84bgyqFUQ,3132
|
|
31
30
|
svg_ultralight/transformations.py,sha256=YyhehH0Hlui2U9t7hjwgHgRyHzUR7UCMSSo-G85J-bo,4784
|
|
32
|
-
svg_ultralight/unit_conversion.py,sha256=
|
|
33
|
-
svg_ultralight-0.
|
|
34
|
-
svg_ultralight-0.
|
|
35
|
-
svg_ultralight-0.
|
|
31
|
+
svg_ultralight/unit_conversion.py,sha256=72B1_1ubz3Rl498deSZkOih3lI2lpPxT2JlWuCLXZBo,11436
|
|
32
|
+
svg_ultralight-0.73.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
33
|
+
svg_ultralight-0.73.1.dist-info/METADATA,sha256=NeBy62XYkcTKV_fcENURDXYcPF1PN2RHIoVZ0s1hNIM,8691
|
|
34
|
+
svg_ultralight-0.73.1.dist-info/RECORD,,
|
svg_ultralight/read_svg.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Read SVG file and extract text content.
|
|
2
|
-
|
|
3
|
-
Note about svg resolution: Despite the fact that vector graphics have effectively
|
|
4
|
-
infinite resolution, ePub apparently uses the actual geometry size to determine the
|
|
5
|
-
resolution of the image. For images ike a business card drawn in real-world inches
|
|
6
|
-
(3.5" width), an ePub will assume a size of 3.5 pixels. There may be some unit for
|
|
7
|
-
the width and height variables (for InDesign, it's pnt) that addresses this, but I
|
|
8
|
-
don't trust it to be consistent across ePub readers. I adjust the units to something
|
|
9
|
-
large, then use CSS to scale it down to the correct size.
|
|
10
|
-
|
|
11
|
-
:author: Shay Hill
|
|
12
|
-
:created: 2025-07-28
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import os
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
from lxml import etree
|
|
19
|
-
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
20
|
-
|
|
21
|
-
import svg_ultralight as su
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_bounding_box_from_root(root: EtreeElement) -> su.BoundingBox:
|
|
25
|
-
"""Extract bounding box from SVG root element.
|
|
26
|
-
|
|
27
|
-
:param root: the root element of the SVG file
|
|
28
|
-
:raise ValueError: if the viewBox attribute is not present
|
|
29
|
-
|
|
30
|
-
"""
|
|
31
|
-
viewbox = root.get("viewBox", "")
|
|
32
|
-
try:
|
|
33
|
-
x, y, width, height = map(float, viewbox.split())
|
|
34
|
-
except ValueError as e:
|
|
35
|
-
msg = f"Invalid or missing viewBox attribute: '{viewbox}'"
|
|
36
|
-
raise ValueError(msg) from e
|
|
37
|
-
return su.BoundingBox(x, y, width, height)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def parse(svg_file: str | os.PathLike[str]) -> su.BoundElement:
|
|
41
|
-
"""Import an SVG file and return an SVG object.
|
|
42
|
-
|
|
43
|
-
:param svg_file: Path to the SVG file.
|
|
44
|
-
:return: A BoundElement containing the SVG content and the svg viewBox as a
|
|
45
|
-
BoundingBox.
|
|
46
|
-
|
|
47
|
-
Near equivalent to `etree.parse(file).getroot()`, but returns a BoundElement
|
|
48
|
-
instance. This will only work with SVG files that have a viewBox attribute.
|
|
49
|
-
"""
|
|
50
|
-
with Path(svg_file).open("r", encoding="utf-8") as f:
|
|
51
|
-
root = etree.parse(f).getroot()
|
|
52
|
-
if len(root) == 1:
|
|
53
|
-
elem = root[0]
|
|
54
|
-
else:
|
|
55
|
-
elem = su.new_element("g")
|
|
56
|
-
elem.extend(list(root))
|
|
57
|
-
bbox = get_bounding_box_from_root(root)
|
|
58
|
-
return su.BoundElement(elem, bbox)
|
|
File without changes
|