svg-ultralight 0.37.0__py3-none-any.whl → 0.39.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.

Potentially problematic release.


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

@@ -12,7 +12,7 @@ There is a getter and setter for each of the four padding values. These *do not*
12
12
  the text element. For instance, if you decrease the left padding, the left margin
13
13
  will move, *not* the text element.
14
14
 
15
- There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
15
+ _There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
16
16
  These *do* move the element, but do not scale it. For instance, if you move the
17
17
  leftmargin to the left, the right margin (and the text element with it) will move to
18
18
  the left.
@@ -66,16 +66,19 @@ from __future__ import annotations
66
66
 
67
67
  from typing import TYPE_CHECKING
68
68
 
69
- from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
69
+ from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
70
70
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
71
+ from svg_ultralight.transformations import new_transformation_matrix, transform_element
71
72
 
72
73
  if TYPE_CHECKING:
73
- from lxml.etree import _Element as EtreeElement # type: ignore
74
+ from lxml.etree import (
75
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
76
+ )
74
77
 
75
78
  _Matrix = tuple[float, float, float, float, float, float]
76
79
 
77
80
 
78
- class PaddedText(SupportsBounds):
81
+ class PaddedText(BoundElement):
79
82
  """A line of text with a bounding box and padding."""
80
83
 
81
84
  def __init__(
@@ -97,14 +100,14 @@ class PaddedText(SupportsBounds):
97
100
  :param lpad: Left padding.
98
101
  """
99
102
  self.elem = elem
100
- self.bbox = bbox
103
+ self.unpadded_bbox = bbox
101
104
  self.base_tpad = tpad
102
105
  self.rpad = rpad
103
106
  self.base_bpad = bpad
104
107
  self.lpad = lpad
105
108
 
106
109
  @property
107
- def padded_bbox(self) -> BoundingBox:
110
+ def bbox(self) -> BoundingBox:
108
111
  """Return a BoundingBox around the margins and cap/baseline.
109
112
 
110
113
  :return: A BoundingBox around the margins and cap/baseline.
@@ -115,22 +118,28 @@ class PaddedText(SupportsBounds):
115
118
  instance around multiple text elements (a <g> elem).
116
119
  """
117
120
  return BoundingBox(
118
- self.lmargin, self.capline, self.padded_width, self.padded_height
121
+ self.x,
122
+ self.y,
123
+ self.width,
124
+ self.height,
125
+ self.unpadded_bbox.transformation,
119
126
  )
120
127
 
121
- @property
122
- def transformation(self) -> _Matrix:
123
- """The transformation matrix of the bounding box."""
124
- return self.bbox.transformation
128
+ @bbox.setter
129
+ def bbox(self, value: BoundingBox) -> None:
130
+ """Set the bounding box of this PaddedText.
125
131
 
126
- def _update_elem(self):
127
- self.elem.attrib["transform"] = self.bbox.transform_string
132
+ :param value: The new bounding box.
133
+ :effects: The text element is transformed to fit the new bounding box.
134
+ """
135
+ msg = "Cannot set bbox of PaddedText, use transform() instead."
136
+ raise NotImplementedError(msg)
128
137
 
129
138
  def transform(
130
139
  self,
131
140
  transformation: _Matrix | None = None,
132
141
  *,
133
- scale: float | None = None,
142
+ scale: tuple[float, float] | None = None,
134
143
  dx: float | None = None,
135
144
  dy: float | None = None,
136
145
  ):
@@ -141,8 +150,9 @@ class PaddedText(SupportsBounds):
141
150
  :param dx: the x translation
142
151
  :param dy: the y translation
143
152
  """
144
- self.bbox.transform(transformation, scale=scale, dx=dx, dy=dy)
145
- self._update_elem()
153
+ tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
154
+ self.unpadded_bbox.transform(tmat)
155
+ _ = transform_element(self.elem, tmat)
146
156
 
147
157
  @property
148
158
  def tpad(self) -> float:
@@ -150,7 +160,7 @@ class PaddedText(SupportsBounds):
150
160
 
151
161
  :return: The scaled top padding of this line of text.
152
162
  """
153
- return self.base_tpad * self.bbox.scale
163
+ return self.base_tpad * self.unpadded_bbox.scale[1]
154
164
 
155
165
  @tpad.setter
156
166
  def tpad(self, value: float) -> None:
@@ -158,7 +168,7 @@ class PaddedText(SupportsBounds):
158
168
 
159
169
  :param value: The new top padding.
160
170
  """
161
- self.base_tpad = value / self.bbox.scale
171
+ self.base_tpad = value / self.unpadded_bbox.scale[1]
162
172
 
163
173
  @property
164
174
  def bpad(self) -> float:
@@ -166,7 +176,7 @@ class PaddedText(SupportsBounds):
166
176
 
167
177
  :return: The scaled bottom padding of this line of text.
168
178
  """
169
- return self.base_bpad * self.bbox.scale
179
+ return self.base_bpad * self.unpadded_bbox.scale[1]
170
180
 
171
181
  @bpad.setter
172
182
  def bpad(self, value: float) -> None:
@@ -174,82 +184,18 @@ class PaddedText(SupportsBounds):
174
184
 
175
185
  :param value: The new bottom padding.
176
186
  """
177
- self.base_bpad = value / self.bbox.scale
178
-
179
- @property
180
- def lmargin(self) -> float:
181
- """The left margin of this line of text.
182
-
183
- :return: The left margin of this line of text.
184
- """
185
- return self.bbox.x - self.lpad
186
-
187
- @lmargin.setter
188
- def lmargin(self, value: float) -> None:
189
- """Set the left margin of this line of text.
190
-
191
- :param value: The left margin of this line of text.
192
- """
193
- self.transform(dx=value + self.lpad - self.bbox.x)
194
-
195
- @property
196
- def rmargin(self) -> float:
197
- """The right margin of this line of text.
198
-
199
- :return: The right margin of this line of text.
200
- """
201
- return self.bbox.x2 + self.rpad
202
-
203
- @rmargin.setter
204
- def rmargin(self, value: float) -> None:
205
- """Set the right margin of this line of text.
206
-
207
- :param value: The right margin of this line of text.
208
- """
209
- self.transform(dx=value - self.rpad - self.bbox.x2)
210
-
211
- @property
212
- def capline(self) -> float:
213
- """The top of this line of text.
214
-
215
- :return: The top of this line of text.
216
- """
217
- return self.bbox.y - self.tpad
218
-
219
- @capline.setter
220
- def capline(self, value: float) -> None:
221
- """Set the top of this line of text.
222
-
223
- :param value: The top of this line of text.
224
- """
225
- self.transform(dy=value + self.tpad - self.bbox.y)
187
+ self.base_bpad = value / self.unpadded_bbox.scale[1]
226
188
 
227
189
  @property
228
- def baseline(self) -> float:
229
- """The bottom of this line of text.
230
-
231
- :return: The bottom of this line of text.
232
- """
233
- return self.bbox.y2 + self.bpad
234
-
235
- @baseline.setter
236
- def baseline(self, value: float) -> None:
237
- """Set the bottom of this line of text.
238
-
239
- :param value: The bottom of this line of text.
240
- """
241
- self.transform(dy=value - self.bpad - self.bbox.y2)
242
-
243
- @property
244
- def padded_width(self) -> float:
190
+ def width(self) -> float:
245
191
  """The width of this line of text with padding.
246
192
 
247
193
  :return: The scaled width of this line of text with padding.
248
194
  """
249
- return self.bbox.width + self.lpad + self.rpad
195
+ return self.unpadded_bbox.width + self.lpad + self.rpad
250
196
 
251
- @padded_width.setter
252
- def padded_width(self, width: float) -> None:
197
+ @width.setter
198
+ def width(self, value: float) -> None:
253
199
  """Scale to padded_width = width without scaling padding.
254
200
 
255
201
  :param width: The new width of this line of text.
@@ -261,27 +207,26 @@ class PaddedText(SupportsBounds):
261
207
  baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
262
208
  *and* y2) when scaling.
263
209
  """
264
- baseline = self.baseline
265
- self.bbox.width = width - self.lpad - self.rpad
266
- self.baseline = baseline
267
- self._update_elem()
210
+ y2 = self.y2
211
+ self.unpadded_bbox.width = value - self.lpad - self.rpad
212
+ self.y2 = y2
268
213
 
269
214
  @property
270
- def padded_height(self) -> float:
215
+ def height(self) -> float:
271
216
  """The height of this line of text with padding.
272
217
 
273
218
  :return: The scaled height of this line of text with padding.
274
219
  """
275
- return self.bbox.height + self.tpad + self.bpad
220
+ return self.unpadded_bbox.height + self.tpad + self.bpad
276
221
 
277
- @padded_height.setter
278
- def padded_height(self, height: float) -> None:
279
- """Scale to padded_height = height without scaling padding.
222
+ @height.setter
223
+ def height(self, value: float) -> None:
224
+ """Scale to height without scaling padding.
280
225
 
281
226
  :param height: The new height of this line of text.
282
227
  :effects: the text_element bounding box is scaled to height - tpad - bpad.
283
228
  """
284
- self.padded_width *= height / self.padded_height
229
+ self.width *= value / self.height
285
230
 
286
231
  @property
287
232
  def x(self) -> float:
@@ -289,15 +234,15 @@ class PaddedText(SupportsBounds):
289
234
 
290
235
  :return: The left margin of this line of text.
291
236
  """
292
- return self.lmargin
237
+ return self.unpadded_bbox.x - self.lpad
293
238
 
294
239
  @x.setter
295
240
  def x(self, value: float) -> None:
296
241
  """Set the left margin of this line of text.
297
242
 
298
- :param value: The new left margin of this line of text.
243
+ :param value: The left margin of this line of text.
299
244
  """
300
- self.lmargin = value
245
+ self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
301
246
 
302
247
  @property
303
248
  def x2(self) -> float:
@@ -305,134 +250,49 @@ class PaddedText(SupportsBounds):
305
250
 
306
251
  :return: The right margin of this line of text.
307
252
  """
308
- return self.rmargin
253
+ return self.unpadded_bbox.x2 + self.rpad
309
254
 
310
255
  @x2.setter
311
256
  def x2(self, value: float) -> None:
312
257
  """Set the right margin of this line of text.
313
258
 
314
- :param value: The new right margin of this line of this text.
259
+ :param value: The right margin of this line of text.
315
260
  """
316
- self.rmargin = value
261
+ self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
317
262
 
318
263
  @property
319
264
  def y(self) -> float:
320
- """The capline of this line of text.
265
+ """The top of this line of text.
321
266
 
322
- :return: The capline of this line of text.
267
+ :return: The top of this line of text.
323
268
  """
324
- return self.capline
269
+ return self.unpadded_bbox.y - self.tpad
325
270
 
326
271
  @y.setter
327
272
  def y(self, value: float) -> None:
328
- """Set the capline of this line of text.
273
+ """Set the top of this line of text.
329
274
 
330
- :param value: The new capline of this line of text.
275
+ :param value: The top of this line of text.
331
276
  """
332
- self.capline = value
277
+ self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
333
278
 
334
279
  @property
335
280
  def y2(self) -> float:
336
- """The baseline of this line of text.
281
+ """The bottom of this line of text.
337
282
 
338
- :return: The baseline of this line of text.
283
+ :return: The bottom of this line of text.
339
284
  """
340
- return self.baseline
285
+ return self.unpadded_bbox.y2 + self.bpad
341
286
 
342
287
  @y2.setter
343
288
  def y2(self, value: float) -> None:
344
- """Set the baseline of this line of text.
345
-
346
- :param value: The new baseline of this line of text.
347
- """
348
- self.baseline = value
349
-
350
- @property
351
- def width(self) -> float:
352
- """The width of this line of text with padding.
353
-
354
- :return: The scaled width of this line of text with padding.
355
- """
356
- return self.padded_width
357
-
358
- @width.setter
359
- def width(self, value: float) -> None:
360
- """Scale to width without scaling padding.
361
-
362
- :param value: The new width of this line of text.
363
- :effects: the text_element bounding box is scaled to width - lpad - rpad.
364
-
365
- Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
366
- consistent with how rectangles, viewboxes, and anything else defined by x, y,
367
- width, height behaves in SVG. This is unintuitive for text, because the
368
- baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
369
- *and* y2) when scaling.
370
- """
371
- baseline = self.baseline
372
- self.padded_width = value
373
- self.baseline = baseline
374
-
375
- @property
376
- def height(self) -> float:
377
- """The height of this line of text with padding.
378
-
379
- :return: The scaled height of this line of text with padding.
380
- """
381
- return self.padded_height
382
-
383
- @height.setter
384
- def height(self, value: float) -> None:
385
- """Scale to height without scaling padding.
386
-
387
- :param value: The new height of this line of text.
388
- :effects: the text_element bounding box is scaled to height - tpad - bpad.
389
- """
390
- self.padded_height = value
391
-
392
- @property
393
- def cx(self) -> float:
394
- """The x coordinate of the center between margins.
395
-
396
- :return: the x coordinate of the center between margins
397
- """
398
- return self.lmargin + self.padded_width / 2
399
-
400
- @cx.setter
401
- def cx(self, value: float):
402
- """Set the x coordinate of the center between margins.
403
-
404
- :param value: the new x coordinate of the center between margins
405
- """
406
- self.lmargin = value - self.padded_width / 2
407
-
408
- @property
409
- def cy(self) -> float:
410
- """The y coordinate of the center between baseline and capline.
411
-
412
- :return: the y coordinate of the center between baseline and capline
413
- """
414
- return self.capline + self.padded_height / 2
415
-
416
- @cy.setter
417
- def cy(self, value: float):
418
- """Set the y coordinate of the center between baseline and capline.
419
-
420
- :param value: the new y coordinate of the center between baseline and capline
421
- """
422
- self.capline = value - self.padded_height / 2
423
-
424
- @property
425
- def scale(self) -> float:
426
- """The scale of the text element.
289
+ """Set the bottom of this line of text.
427
290
 
428
- :return: the scale of the text element
291
+ :param value: The bottom of this line of text.
429
292
  """
430
- return self.bbox.scale
431
-
432
- @scale.setter
433
- def scale(self, value: float):
434
- """Set the scale of the text element.
293
+ self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
435
294
 
436
- :param value: the new scale of the text element
437
- """
438
- self.bbox.scale = value
295
+ lmargin = x
296
+ rmargin = x2
297
+ capline = y
298
+ baseline = y2
@@ -21,7 +21,9 @@ from svg_ultralight.string_conversion import set_attributes
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from lxml.etree import QName
24
- from lxml.etree import _Element as EtreeElement # type: ignore
24
+ from lxml.etree import (
25
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
26
+ )
25
27
 
26
28
 
27
29
  def new_element(tag: str | QName, **attributes: str | float) -> EtreeElement:
@@ -30,7 +30,9 @@ from lxml import etree
30
30
  from svg_ultralight import main
31
31
 
32
32
  if TYPE_CHECKING:
33
- from lxml.etree import _Element as EtreeElement # type: ignore
33
+ from lxml.etree import (
34
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
35
+ )
34
36
 
35
37
 
36
38
  def write_png_from_svg(
svg_ultralight/layout.py CHANGED
@@ -144,11 +144,11 @@ def pad_and_scale(
144
144
  """Expand and scale the pad argument. If necessary, scale image.
145
145
 
146
146
  :param viewbox: viewbox to pad (x, y, width height)
147
- :param pad: padding to add around image, in user units or inches. if a
147
+ :param pad: padding to add around image, in user units or inches. If a
148
148
  sequence, it should be (top, right, bottom, left). if a single float or
149
- string, it will be applied to all sides. if two floats, top and bottom
150
- then left and right. if three floats, top, left and right, then bottom.
151
- if four floats, top, right, bottom, left.
149
+ string, it will be applied to all sides. If two floats, top and bottom
150
+ then left and right. If three floats, top, left and right, then bottom.
151
+ If four floats, top, right, bottom, left.
152
152
  :param print_width: width of print area, in user units (float), a string
153
153
  with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
154
154
  "pt")
@@ -169,7 +169,7 @@ def pad_and_scale(
169
169
  If the width and height *are* specified, the user units become whatever they
170
170
  need to be to fit that requirement. For instance, if the viewbox width is 96
171
171
  and the width argument is "1in", then the user units are *still* pixels,
172
- because there are 96 pixels in an inch. If the viewbox with is 2 and the
172
+ because there are 96 pixels in an inch. If the viewbox width is 2 and the
173
173
  width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
174
174
  pixels) each, because there are 2 user units in an inch. If the viewbox
175
175
  width is 3 and the width argument is "1yd", the each user unit is 1 foot.
@@ -195,17 +195,26 @@ def pad_and_scale(
195
195
  the unit designators without changing the scale.
196
196
 
197
197
  Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
198
- instance, If you take a 100x100 unit image then pass pad="0.25in" and
199
- print_width="12in", the output image will be 12.5 inches across. Whatever
200
- geometry was visible in the original viewbox will be much larger, but the
201
- padding will still be 0.25 inches. If you want to use padding and need a
202
- specific output image size, remember to subtract the padding width from your
203
- print_width or print_height.
198
+ instance, if you created two images
199
+
200
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
201
+
202
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
203
+
204
+ ... (note that the images only vary in print_width_), the first image would be
205
+ rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
206
+ content in the viewbox would be exactly twice as wide in the larger image, but
207
+ the padding would remain 0.25 in both images. Despite setting `print_width_` to
208
+ exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
209
+ Despite a viewbox aspect ratio of 1:2, you would not get an output image of
210
+ exactly 1:2. If you want to use padding and need a specific output image size or
211
+ aspect ratio, remember to subtract the padding width from your print_width or
212
+ print_height.
204
213
 
205
214
  Scaling attributes are returned as a dictonary that can be "exploded" into
206
215
  the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
207
216
 
208
- * If neighther a print_width nor print_height is specified, no scaling
217
+ * If neither a print_width nor print_height is specified, no scaling
209
218
  attributes will be returned.
210
219
 
211
220
  * If either is specified, both a width and height will be returned (even if
@@ -242,7 +251,7 @@ def pad_and_scale(
242
251
  a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
243
252
  ... then scale the printout with dpu_=2 to get a 32" x 18" image with the
244
253
  same viewbox. This means the padding will be 2" on all sides, but the image
245
- will be identical (just twice as large) as the 16" x 9" image.
254
+ will be identical (just twice as wide and twice as high) as the 16" x 9" image.
246
255
  """
247
256
  pads = expand_pad_arg(pad)
248
257
 
svg_ultralight/main.py CHANGED
@@ -13,6 +13,7 @@ should work with all Inkscape versions. Please report any issues.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import sys
16
17
  from pathlib import Path
17
18
  from typing import IO, TYPE_CHECKING
18
19
 
@@ -23,8 +24,15 @@ from svg_ultralight.layout import pad_and_scale
23
24
  from svg_ultralight.nsmap import NSMAP
24
25
  from svg_ultralight.string_conversion import get_viewBox_str, svg_tostring
25
26
 
27
+ if sys.version_info >= (3, 10):
28
+ from typing import TypeGuard
29
+ else:
30
+ from typing_extensions import TypeGuard
31
+
26
32
  if TYPE_CHECKING:
27
- from lxml.etree import _Element as EtreeElement # type: ignore
33
+ from lxml.etree import (
34
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
35
+ )
28
36
 
29
37
  from svg_ultralight.layout import PadArg
30
38
 
@@ -38,7 +46,7 @@ def _is_four_floats(objs: tuple[object, ...]) -> bool:
38
46
  return len(objs) == 4 and all(isinstance(x, (float, int)) for x in objs)
39
47
 
40
48
 
41
- def _is_io_bytes(obj: object) -> bool:
49
+ def _is_io_bytes(obj: object) -> TypeGuard[IO[bytes]]:
42
50
  """Determine if an object is file-like.
43
51
 
44
52
  :param obj: object
@@ -172,8 +180,8 @@ def write_svg(
172
180
  svg_contents = svg_tostring(root, **tostring_kwargs)
173
181
 
174
182
  if _is_io_bytes(svg):
175
- _ = svg.write(svg_contents) # type: ignore
176
- return svg.name # type: ignore
183
+ _ = svg.write(svg_contents)
184
+ return svg.name
177
185
  if isinstance(svg, (Path, str)):
178
186
  with Path(svg).open("wb") as svg_file:
179
187
  _ = svg_file.write(svg_contents)
@@ -9,7 +9,7 @@ fields.
9
9
 
10
10
  import warnings
11
11
 
12
- from lxml.etree import _Element as EtreeElement # type: ignore
12
+ from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
13
13
 
14
14
  from svg_ultralight.constructors.new_element import new_element, new_sub_element
15
15
  from svg_ultralight.nsmap import new_qname