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.
@@ -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
- # 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
- )
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: str | 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(str(measurement_arg)):
114
- return _parse_unit((number_unit["number"], number_unit["unit"]))
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(str(measurement_arg)):
117
- return _parse_unit((0, unit_only["unit"]))
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 doint it that way preserved the flexibility (and
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 precision just changes file size without imroving quality.
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.value
233
+ result.value = self.value + float(other)
211
234
  return result
212
235
 
213
- def __sub__(self, other: Measurement) -> Measurement:
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
- result = Measurement(self.native_unit)
220
- result.value = self.value - other.value
221
- return result
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svg-ultralight
3
- Version: 0.64.0
3
+ Version: 0.73.1
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author: Shay Hill
6
6
  Author-email: Shay Hill <shay_public@hotmail.com>
@@ -1,35 +1,34 @@
1
- svg_ultralight/__init__.py,sha256=DhQPgcNsTz8ZIeKPZGMXlOpZZS28BsMPVi0ELg00E_M,2858
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=660DLfjnOOdLE-GmsDIZhF4JckdiK9O7DcFGWQ-g4Ww,7010
6
- svg_ultralight/bounding_boxes/padded_text_initializers.py,sha256=xv34iEmy8gvCgBZmYnLf26Y5MCjnCt1VF3jyLZvkfws,15085
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=gnbZjj32eaaRA0cSJwUsnfbhFqOlH35GTfYcVhRjAog,14644
11
- svg_ultralight/bounding_boxes/type_padded_list.py,sha256=8Osu-sfyzuvBrNlrkAtJnMLgS7HP1DT5ojjecI_KNPo,8125
12
- svg_ultralight/bounding_boxes/type_padded_text.py,sha256=OvMMJ2NWAXavjvs2l8MPwCVdRyy6PBI3wmmkcSugejY,17272
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=7n4LQROxi-Lzi1t-85704H5heADeyoctfJEvEBLlT50,3431
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=-GZ3wvnWER7IWurL2IzZym-_0wcLT3y3jGukV4V4pTI,10341
17
- svg_ultralight/font_tools/font_info.py,sha256=mLI4dRS_zb6FeTSfpP0Zw_GxsXIDsu3vkQ0X-gOdZbU,31968
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=yiTAYtSsYywdCObIdjkK3A4fiEYLNPqD3BOQKxfj6gQ,12514
21
- svg_ultralight/main.py,sha256=T5S2vtLU99JTVH1RucsWmQPlyltruWmQBMXAZEKbuok,7178
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/read_svg.py,sha256=07azKpJciJEiw_VJt_YHd-PnUWxjnw5nE1ruSP0qTLo,2090
27
- svg_ultralight/root_elements.py,sha256=V9XsIHQBnik6D6inlV0TiO_2RkFPLbn6qFkLR2l_saM,3464
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=Y32GZ5TLkvjDHM2kBw52M8ZxDNlpVSlDkSdRa1S_X-A,8985
33
- svg_ultralight-0.64.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
34
- svg_ultralight-0.64.0.dist-info/METADATA,sha256=M31DX00UVc1Qolt45PUk7WlcOGXVH52VfU1ehsKIHMI,8691
35
- svg_ultralight-0.64.0.dist-info/RECORD,,
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,,
@@ -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)