svg-ultralight 0.47.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 -201
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -206
  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 -784
  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.47.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.47.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.47.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.47.0.dist-info/top_level.txt +0 -1
@@ -1,411 +1,411 @@
1
- """A padded bounding box around a line of text.
2
-
3
- A text element (presumably), an svg_ultralight BoundingBox around that element, and
4
- padding on each side of that box. This is to simplify treating scaling and moving a
5
- text element as if it were written on a ruled sheet of paper.
6
-
7
- Padding represents the left margin, right margin, baseline, and capline of the text.
8
- Baseling and capline padding will often be less than zero, as descenders and
9
- ascenders will extend below the baseline and above the capline.
10
-
11
- There is a getter and setter for each of the four padding values. These *do not* move
12
- the text element. For instance, if you decrease the left padding, the left margin
13
- will move, *not* the text element.
14
-
15
- _There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
16
- These *do* move the element, but do not scale it. For instance, if you move the
17
- leftmargin to the left, the right margin (and the text element with it) will move to
18
- the left.
19
-
20
- There is a getter and setter for padded_width and padded_height. These scale the
21
- element and the top and bottom padding, but *not* the left and right padding. This is
22
- one of two quirks which make this PaddedText class different from a generalized
23
- padded bounding box.
24
-
25
- 1. As above, the left and right padding are not scaled with the text element, the top
26
- and bottom padding are. This preserves but does not exaggerate the natural
27
- sidebearings of the text element. This lack of scaling will be pronounced if
28
- adjacent padded lines are scaled to dramatically different sizes. The idea is to
29
- scale each PaddedText as little as possible to match widths (or any other
30
- relationship) then scale the resulting transformed text elements another way. For
31
- instance, create multiple PaddedText instances, scale their padded_width atributes to
32
- match, then put the resulting elements in a <g> element and scale the <g> element to
33
- the ultimate desired size.
34
-
35
- 2. The left margin and baseline (*bottom* and left) do not move when the height or
36
- width is changed. This is in contrast to an InkScape rect element, which, when the
37
- width or height is changed, preserve the *top* and left boundaries.
38
-
39
- Building an honest instance of this class is fairly involved:
40
-
41
- 1. Create a left-aligned text element.
42
-
43
- 2. Create a BoundingBox around the left-aligned text element. The difference between
44
- 0 and that BoundingBox's left edge is the left padding.
45
-
46
- 3. Create a right-aligned copy of the text element.
47
-
48
- 4. Create a BoundingBox around the right-aligned text element. The difference between
49
- the BoundingBox's right edge 0 is the right padding.
50
-
51
- 5. Use a BoundingBox around a "normal" capital (e.g. "M") to infer the baseline and
52
- capline and then calculate the top and bottom margins.
53
-
54
- There is a function to do this is `svg_ultralight.query.py` with sensible defaults.
55
-
56
- A lot can be done with a dishonest instance of this class. For instance, you could
57
- align and scale text while preserving left margin. The capline would scale with the
58
- height or width, so a left margin and capline (assume baseline is zero) would be
59
- enough to lay out text on a business card.
60
-
61
- :author: Shay Hill
62
- :created: 2021-11-28
63
- """
64
-
65
- from __future__ import annotations
66
-
67
- import math
68
- from typing import TYPE_CHECKING
69
-
70
- from paragraphs import par
71
-
72
- from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
73
- from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
74
- from svg_ultralight.transformations import new_transformation_matrix, transform_element
75
-
76
- if TYPE_CHECKING:
77
- from lxml.etree import (
78
- _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
79
- )
80
-
81
- _Matrix = tuple[float, float, float, float, float, float]
82
-
83
- _no_line_gap_msg = par(
84
- """No line_gap defined. Line gap is an inherent font attribute defined within a
85
- font file. If this PaddedText instance was created with `pad_text` from reference
86
- elements, a line_gap was not defined. Reading line_gap from the font file
87
- requires creating a PaddedText instance with `pad_text_ft` or `pad_text_mixed`.
88
- You can set an arbitrary line_gap after init with `instance.line_gap = value`."""
89
- )
90
-
91
-
92
- class PaddedText(BoundElement):
93
- """A line of text with a bounding box and padding."""
94
-
95
- def __init__(
96
- self,
97
- elem: EtreeElement,
98
- bbox: BoundingBox,
99
- tpad: float,
100
- rpad: float,
101
- bpad: float,
102
- lpad: float,
103
- line_gap: float | None = None,
104
- ) -> None:
105
- """Initialize a PaddedText instance.
106
-
107
- :param elem: The text element.
108
- :param bbox: The bounding box around text element.
109
- :param tpad: Top padding.
110
- :param rpad: Right padding.
111
- :param bpad: Bottom padding.
112
- :param lpad: Left padding.
113
- """
114
- self.elem = elem
115
- self.unpadded_bbox = bbox
116
- self.base_tpad = tpad
117
- self.rpad = rpad
118
- self.base_bpad = bpad
119
- self.lpad = lpad
120
- self._line_gap = line_gap
121
-
122
- @property
123
- def bbox(self) -> BoundingBox:
124
- """Return a BoundingBox around the margins and cap/baseline.
125
-
126
- :return: A BoundingBox around the margins and cap/baseline.
127
-
128
- This is useful for creating a merged bounding box with
129
- `svg_ultralight.BoundingBox.merged`. The merged bbox and merged_bbox
130
- attributes of multiple bounding boxes can be used to create a PaddedText
131
- instance around multiple text elements (a <g> elem).
132
- """
133
- return BoundingBox(
134
- self.x,
135
- self.y,
136
- self.width,
137
- self.height,
138
- )
139
-
140
- @bbox.setter
141
- def bbox(self, value: BoundingBox) -> None:
142
- """Set the bounding box of this PaddedText.
143
-
144
- :param value: The new bounding box.
145
- :effects: The text element is transformed to fit the new bounding box.
146
- """
147
- msg = "Cannot set bbox of PaddedText, use transform() instead."
148
- raise NotImplementedError(msg)
149
-
150
- def transform(
151
- self,
152
- transformation: _Matrix | None = None,
153
- *,
154
- scale: tuple[float, float] | float | None = None,
155
- dx: float | None = None,
156
- dy: float | None = None,
157
- ):
158
- """Transform the element and bounding box.
159
-
160
- :param transformation: a 6-tuple transformation matrix
161
- :param scale: a scaling factor
162
- :param dx: the x translation
163
- :param dy: the y translation
164
- """
165
- tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
166
- self.unpadded_bbox.transform(tmat)
167
- _ = transform_element(self.elem, tmat)
168
-
169
- @property
170
- def line_gap(self) -> float:
171
- """The line gap between this line of text and the next.
172
-
173
- :return: The line gap between this line of text and the next.
174
- """
175
- if self._line_gap is None:
176
- raise AttributeError(_no_line_gap_msg)
177
- return self._line_gap
178
-
179
- @line_gap.setter
180
- def line_gap(self, value: float) -> None:
181
- """Set the line gap between this line of text and the next.
182
-
183
- :param value: The new line gap.
184
- """
185
- self._line_gap = value
186
-
187
- @property
188
- def leading(self) -> float:
189
- """The leading of this line of text.
190
-
191
- :return: The line gap plus the height of this line of text.
192
- """
193
- return self.height + self.line_gap
194
-
195
- @property
196
- def tpad(self) -> float:
197
- """The top padding of this line of text.
198
-
199
- :return: The scaled top padding of this line of text.
200
- """
201
- return self.base_tpad * self.unpadded_bbox.scale[1]
202
-
203
- @tpad.setter
204
- def tpad(self, value: float) -> None:
205
- """Set the top padding of this line of text.
206
-
207
- :param value: The new top padding.
208
- """
209
- self.base_tpad = value / self.unpadded_bbox.scale[1]
210
-
211
- @property
212
- def bpad(self) -> float:
213
- """The bottom padding of this line of text.
214
-
215
- :return: The scaled bottom padding of this line of text.
216
- """
217
- return self.base_bpad * self.unpadded_bbox.scale[1]
218
-
219
- @bpad.setter
220
- def bpad(self, value: float) -> None:
221
- """Set the bottom padding of this line of text.
222
-
223
- :param value: The new bottom padding.
224
- """
225
- self.base_bpad = value / self.unpadded_bbox.scale[1]
226
-
227
- @property
228
- def scale(self) -> tuple[float, float]:
229
- """Get scale of the bounding box.
230
-
231
- :return: uniform scale of the bounding box
232
-
233
- Use caution, the scale attribute can cause errors in intuition. Changing
234
- width or height will change the scale attribute, but not the x or y values.
235
- The scale setter, on the other hand, will work in the tradational manner.
236
- I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
237
- width*scale, height => height*scale, scale => scale*scale. This matches how
238
- scale works in almost every other context.
239
- """
240
- xx, xy, yx, yy, *_ = self.unpadded_bbox.transformation
241
- return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
242
-
243
- @scale.setter
244
- def scale(self, value: tuple[float, float]) -> None:
245
- """Scale the bounding box by a uniform factor.
246
-
247
- :param value: new scale value
248
-
249
- Don't miss this! You are setting the scale, not scaling the scale! If you
250
- have a previously defined scale other than 1, this is probably not what you
251
- want. Most of the time, you will want to use the *= operator.
252
-
253
- `scale = 2` -> ignore whatever scale was previously defined and set scale to 2
254
- `scale *= 2` -> make it twice as big as it was.
255
- """
256
- new_scale = (
257
- value[0] / self.unpadded_bbox.scale[0],
258
- value[1] / self.unpadded_bbox.scale[1],
259
- )
260
- self.transform(scale=new_scale)
261
-
262
- @property
263
- def width(self) -> float:
264
- """The width of this line of text with padding.
265
-
266
- :return: The scaled width of this line of text with padding.
267
- """
268
- return self.unpadded_bbox.width + self.lpad + self.rpad
269
-
270
- @width.setter
271
- def width(self, value: float) -> None:
272
- """Scale to padded_width = width without scaling padding.
273
-
274
- :param width: The new width of this line of text.
275
- :effects: the text_element bounding box is scaled to width - lpad - rpad.
276
-
277
- Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
278
- consistent with how rectangles, viewboxes, and anything else defined by x, y,
279
- width, height behaves in SVG. This is unintuitive for text, because the
280
- baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
281
- *and* y2) when scaling.
282
- """
283
- y2 = self.y2
284
-
285
- no_margins_old = self.unpadded_bbox.width
286
- no_margins_new = value - self.lpad - self.rpad
287
- scale = no_margins_new / no_margins_old
288
- self.transform(scale=(scale, scale))
289
-
290
- self.y2 = y2
291
-
292
- @property
293
- def height(self) -> float:
294
- """The height of this line of text with padding.
295
-
296
- :return: The scaled height of this line of text with padding.
297
- """
298
- return self.unpadded_bbox.height + self.tpad + self.bpad
299
-
300
- @height.setter
301
- def height(self, value: float) -> None:
302
- """Scale to height without scaling padding.
303
-
304
- :param height: The new height of this line of text.
305
- :effects: the text_element bounding box is scaled to height - tpad - bpad.
306
- """
307
- y2 = self.y2
308
- scale = value / self.height
309
- self.transform(scale=(scale, scale))
310
- self.y2 = y2
311
-
312
- @property
313
- def x(self) -> float:
314
- """The left margin of this line of text.
315
-
316
- :return: The left margin of this line of text.
317
- """
318
- return self.unpadded_bbox.x - self.lpad
319
-
320
- @x.setter
321
- def x(self, value: float) -> None:
322
- """Set the left margin of this line of text.
323
-
324
- :param value: The left margin of this line of text.
325
- """
326
- self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
327
-
328
- @property
329
- def cx(self) -> float:
330
- """The horizontal center of this line of text.
331
-
332
- :return: The horizontal center of this line of text.
333
- """
334
- return self.x + self.width / 2
335
-
336
- @cx.setter
337
- def cx(self, value: float) -> None:
338
- """Set the horizontal center of this line of text.
339
-
340
- :param value: The horizontal center of this line of text.
341
- """
342
- self.x += value - self.cx
343
-
344
- @property
345
- def x2(self) -> float:
346
- """The right margin of this line of text.
347
-
348
- :return: The right margin of this line of text.
349
- """
350
- return self.unpadded_bbox.x2 + self.rpad
351
-
352
- @x2.setter
353
- def x2(self, value: float) -> None:
354
- """Set the right margin of this line of text.
355
-
356
- :param value: The right margin of this line of text.
357
- """
358
- self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
359
-
360
- @property
361
- def y(self) -> float:
362
- """The top of this line of text.
363
-
364
- :return: The top of this line of text.
365
- """
366
- return self.unpadded_bbox.y - self.tpad
367
-
368
- @y.setter
369
- def y(self, value: float) -> None:
370
- """Set the top of this line of text.
371
-
372
- :param value: The top of this line of text.
373
- """
374
- self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
375
-
376
- @property
377
- def cy(self) -> float:
378
- """The horizontal center of this line of text.
379
-
380
- :return: The horizontal center of this line of text.
381
- """
382
- return self.y + self.height / 2
383
-
384
- @cy.setter
385
- def cy(self, value: float) -> None:
386
- """Set the horizontal center of this line of text.
387
-
388
- :param value: The horizontal center of this line of text.
389
- """
390
- self.y += value - self.cy
391
-
392
- @property
393
- def y2(self) -> float:
394
- """The bottom of this line of text.
395
-
396
- :return: The bottom of this line of text.
397
- """
398
- return self.unpadded_bbox.y2 + self.bpad
399
-
400
- @y2.setter
401
- def y2(self, value: float) -> None:
402
- """Set the bottom of this line of text.
403
-
404
- :param value: The bottom of this line of text.
405
- """
406
- self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
407
-
408
- lmargin = x
409
- rmargin = x2
410
- capline = y
411
- baseline = y2
1
+ """A padded bounding box around a line of text.
2
+
3
+ A text element (presumably), an svg_ultralight BoundingBox around that element, and
4
+ padding on each side of that box. This is to simplify treating scaling and moving a
5
+ text element as if it were written on a ruled sheet of paper.
6
+
7
+ Padding represents the left margin, right margin, baseline, and capline of the text.
8
+ Baseling and capline padding will often be less than zero, as descenders and
9
+ ascenders will extend below the baseline and above the capline.
10
+
11
+ There is a getter and setter for each of the four padding values. These *do not* move
12
+ the text element. For instance, if you decrease the left padding, the left margin
13
+ will move, *not* the text element.
14
+
15
+ _There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
16
+ These *do* move the element, but do not scale it. For instance, if you move the
17
+ leftmargin to the left, the right margin (and the text element with it) will move to
18
+ the left.
19
+
20
+ There is a getter and setter for padded_width and padded_height. These scale the
21
+ element and the top and bottom padding, but *not* the left and right padding. This is
22
+ one of two quirks which make this PaddedText class different from a generalized
23
+ padded bounding box.
24
+
25
+ 1. As above, the left and right padding are not scaled with the text element, the top
26
+ and bottom padding are. This preserves but does not exaggerate the natural
27
+ sidebearings of the text element. This lack of scaling will be pronounced if
28
+ adjacent padded lines are scaled to dramatically different sizes. The idea is to
29
+ scale each PaddedText as little as possible to match widths (or any other
30
+ relationship) then scale the resulting transformed text elements another way. For
31
+ instance, create multiple PaddedText instances, scale their padded_width atributes to
32
+ match, then put the resulting elements in a <g> element and scale the <g> element to
33
+ the ultimate desired size.
34
+
35
+ 2. The left margin and baseline (*bottom* and left) do not move when the height or
36
+ width is changed. This is in contrast to an InkScape rect element, which, when the
37
+ width or height is changed, preserve the *top* and left boundaries.
38
+
39
+ Building an honest instance of this class is fairly involved:
40
+
41
+ 1. Create a left-aligned text element.
42
+
43
+ 2. Create a BoundingBox around the left-aligned text element. The difference between
44
+ 0 and that BoundingBox's left edge is the left padding.
45
+
46
+ 3. Create a right-aligned copy of the text element.
47
+
48
+ 4. Create a BoundingBox around the right-aligned text element. The difference between
49
+ the BoundingBox's right edge 0 is the right padding.
50
+
51
+ 5. Use a BoundingBox around a "normal" capital (e.g. "M") to infer the baseline and
52
+ capline and then calculate the top and bottom margins.
53
+
54
+ There is a function to do this is `svg_ultralight.query.py` with sensible defaults.
55
+
56
+ A lot can be done with a dishonest instance of this class. For instance, you could
57
+ align and scale text while preserving left margin. The capline would scale with the
58
+ height or width, so a left margin and capline (assume baseline is zero) would be
59
+ enough to lay out text on a business card.
60
+
61
+ :author: Shay Hill
62
+ :created: 2021-11-28
63
+ """
64
+
65
+ from __future__ import annotations
66
+
67
+ import math
68
+ from typing import TYPE_CHECKING
69
+
70
+ from paragraphs import par
71
+
72
+ from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
73
+ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
74
+ from svg_ultralight.transformations import new_transformation_matrix, transform_element
75
+
76
+ if TYPE_CHECKING:
77
+ from lxml.etree import (
78
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
79
+ )
80
+
81
+ _Matrix = tuple[float, float, float, float, float, float]
82
+
83
+ _no_line_gap_msg = par(
84
+ """No line_gap defined. Line gap is an inherent font attribute defined within a
85
+ font file. If this PaddedText instance was created with `pad_text` from reference
86
+ elements, a line_gap was not defined. Reading line_gap from the font file
87
+ requires creating a PaddedText instance with `pad_text_ft` or `pad_text_mixed`.
88
+ You can set an arbitrary line_gap after init with `instance.line_gap = value`."""
89
+ )
90
+
91
+
92
+ class PaddedText(BoundElement):
93
+ """A line of text with a bounding box and padding."""
94
+
95
+ def __init__(
96
+ self,
97
+ elem: EtreeElement,
98
+ bbox: BoundingBox,
99
+ tpad: float,
100
+ rpad: float,
101
+ bpad: float,
102
+ lpad: float,
103
+ line_gap: float | None = None,
104
+ ) -> None:
105
+ """Initialize a PaddedText instance.
106
+
107
+ :param elem: The text element.
108
+ :param bbox: The bounding box around text element.
109
+ :param tpad: Top padding.
110
+ :param rpad: Right padding.
111
+ :param bpad: Bottom padding.
112
+ :param lpad: Left padding.
113
+ """
114
+ self.elem = elem
115
+ self.unpadded_bbox = bbox
116
+ self.base_tpad = tpad
117
+ self.rpad = rpad
118
+ self.base_bpad = bpad
119
+ self.lpad = lpad
120
+ self._line_gap = line_gap
121
+
122
+ @property
123
+ def bbox(self) -> BoundingBox:
124
+ """Return a BoundingBox around the margins and cap/baseline.
125
+
126
+ :return: A BoundingBox around the margins and cap/baseline.
127
+
128
+ This is useful for creating a merged bounding box with
129
+ `svg_ultralight.BoundingBox.merged`. The merged bbox and merged_bbox
130
+ attributes of multiple bounding boxes can be used to create a PaddedText
131
+ instance around multiple text elements (a <g> elem).
132
+ """
133
+ return BoundingBox(
134
+ self.x,
135
+ self.y,
136
+ self.width,
137
+ self.height,
138
+ )
139
+
140
+ @bbox.setter
141
+ def bbox(self, value: BoundingBox) -> None:
142
+ """Set the bounding box of this PaddedText.
143
+
144
+ :param value: The new bounding box.
145
+ :effects: The text element is transformed to fit the new bounding box.
146
+ """
147
+ msg = "Cannot set bbox of PaddedText, use transform() instead."
148
+ raise NotImplementedError(msg)
149
+
150
+ def transform(
151
+ self,
152
+ transformation: _Matrix | None = None,
153
+ *,
154
+ scale: tuple[float, float] | float | None = None,
155
+ dx: float | None = None,
156
+ dy: float | None = None,
157
+ ) -> None:
158
+ """Transform the element and bounding box.
159
+
160
+ :param transformation: a 6-tuple transformation matrix
161
+ :param scale: a scaling factor
162
+ :param dx: the x translation
163
+ :param dy: the y translation
164
+ """
165
+ tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
166
+ self.unpadded_bbox.transform(tmat)
167
+ _ = transform_element(self.elem, tmat)
168
+
169
+ @property
170
+ def line_gap(self) -> float:
171
+ """The line gap between this line of text and the next.
172
+
173
+ :return: The line gap between this line of text and the next.
174
+ """
175
+ if self._line_gap is None:
176
+ raise AttributeError(_no_line_gap_msg)
177
+ return self._line_gap
178
+
179
+ @line_gap.setter
180
+ def line_gap(self, value: float) -> None:
181
+ """Set the line gap between this line of text and the next.
182
+
183
+ :param value: The new line gap.
184
+ """
185
+ self._line_gap = value
186
+
187
+ @property
188
+ def leading(self) -> float:
189
+ """The leading of this line of text.
190
+
191
+ :return: The line gap plus the height of this line of text.
192
+ """
193
+ return self.height + self.line_gap
194
+
195
+ @property
196
+ def tpad(self) -> float:
197
+ """The top padding of this line of text.
198
+
199
+ :return: The scaled top padding of this line of text.
200
+ """
201
+ return self.base_tpad * self.unpadded_bbox.scale[1]
202
+
203
+ @tpad.setter
204
+ def tpad(self, value: float) -> None:
205
+ """Set the top padding of this line of text.
206
+
207
+ :param value: The new top padding.
208
+ """
209
+ self.base_tpad = value / self.unpadded_bbox.scale[1]
210
+
211
+ @property
212
+ def bpad(self) -> float:
213
+ """The bottom padding of this line of text.
214
+
215
+ :return: The scaled bottom padding of this line of text.
216
+ """
217
+ return self.base_bpad * self.unpadded_bbox.scale[1]
218
+
219
+ @bpad.setter
220
+ def bpad(self, value: float) -> None:
221
+ """Set the bottom padding of this line of text.
222
+
223
+ :param value: The new bottom padding.
224
+ """
225
+ self.base_bpad = value / self.unpadded_bbox.scale[1]
226
+
227
+ @property
228
+ def scale(self) -> tuple[float, float]:
229
+ """Get scale of the bounding box.
230
+
231
+ :return: uniform scale of the bounding box
232
+
233
+ Use caution, the scale attribute can cause errors in intuition. Changing
234
+ width or height will change the scale attribute, but not the x or y values.
235
+ The scale setter, on the other hand, will work in the tradational manner.
236
+ I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
237
+ width*scale, height => height*scale, scale => scale*scale. This matches how
238
+ scale works in almost every other context.
239
+ """
240
+ xx, xy, yx, yy, *_ = self.unpadded_bbox.transformation
241
+ return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
242
+
243
+ @scale.setter
244
+ def scale(self, value: tuple[float, float]) -> None:
245
+ """Scale the bounding box by a uniform factor.
246
+
247
+ :param value: new scale value
248
+
249
+ Don't miss this! You are setting the scale, not scaling the scale! If you
250
+ have a previously defined scale other than 1, this is probably not what you
251
+ want. Most of the time, you will want to use the *= operator.
252
+
253
+ `scale = 2` -> ignore whatever scale was previously defined and set scale to 2
254
+ `scale *= 2` -> make it twice as big as it was.
255
+ """
256
+ new_scale = (
257
+ value[0] / self.unpadded_bbox.scale[0],
258
+ value[1] / self.unpadded_bbox.scale[1],
259
+ )
260
+ self.transform(scale=new_scale)
261
+
262
+ @property
263
+ def width(self) -> float:
264
+ """The width of this line of text with padding.
265
+
266
+ :return: The scaled width of this line of text with padding.
267
+ """
268
+ return self.unpadded_bbox.width + self.lpad + self.rpad
269
+
270
+ @width.setter
271
+ def width(self, value: float) -> None:
272
+ """Scale to padded_width = width without scaling padding.
273
+
274
+ :param width: The new width of this line of text.
275
+ :effects: the text_element bounding box is scaled to width - lpad - rpad.
276
+
277
+ Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
278
+ consistent with how rectangles, viewboxes, and anything else defined by x, y,
279
+ width, height behaves in SVG. This is unintuitive for text, because the
280
+ baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
281
+ *and* y2) when scaling.
282
+ """
283
+ y2 = self.y2
284
+
285
+ no_margins_old = self.unpadded_bbox.width
286
+ no_margins_new = value - self.lpad - self.rpad
287
+ scale = no_margins_new / no_margins_old
288
+ self.transform(scale=(scale, scale))
289
+
290
+ self.y2 = y2
291
+
292
+ @property
293
+ def height(self) -> float:
294
+ """The height of this line of text with padding.
295
+
296
+ :return: The scaled height of this line of text with padding.
297
+ """
298
+ return self.unpadded_bbox.height + self.tpad + self.bpad
299
+
300
+ @height.setter
301
+ def height(self, value: float) -> None:
302
+ """Scale to height without scaling padding.
303
+
304
+ :param height: The new height of this line of text.
305
+ :effects: the text_element bounding box is scaled to height - tpad - bpad.
306
+ """
307
+ y2 = self.y2
308
+ scale = value / self.height
309
+ self.transform(scale=(scale, scale))
310
+ self.y2 = y2
311
+
312
+ @property
313
+ def x(self) -> float:
314
+ """The left margin of this line of text.
315
+
316
+ :return: The left margin of this line of text.
317
+ """
318
+ return self.unpadded_bbox.x - self.lpad
319
+
320
+ @x.setter
321
+ def x(self, value: float) -> None:
322
+ """Set the left margin of this line of text.
323
+
324
+ :param value: The left margin of this line of text.
325
+ """
326
+ self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
327
+
328
+ @property
329
+ def cx(self) -> float:
330
+ """The horizontal center of this line of text.
331
+
332
+ :return: The horizontal center of this line of text.
333
+ """
334
+ return self.x + self.width / 2
335
+
336
+ @cx.setter
337
+ def cx(self, value: float) -> None:
338
+ """Set the horizontal center of this line of text.
339
+
340
+ :param value: The horizontal center of this line of text.
341
+ """
342
+ self.x += value - self.cx
343
+
344
+ @property
345
+ def x2(self) -> float:
346
+ """The right margin of this line of text.
347
+
348
+ :return: The right margin of this line of text.
349
+ """
350
+ return self.unpadded_bbox.x2 + self.rpad
351
+
352
+ @x2.setter
353
+ def x2(self, value: float) -> None:
354
+ """Set the right margin of this line of text.
355
+
356
+ :param value: The right margin of this line of text.
357
+ """
358
+ self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
359
+
360
+ @property
361
+ def y(self) -> float:
362
+ """The top of this line of text.
363
+
364
+ :return: The top of this line of text.
365
+ """
366
+ return self.unpadded_bbox.y - self.tpad
367
+
368
+ @y.setter
369
+ def y(self, value: float) -> None:
370
+ """Set the top of this line of text.
371
+
372
+ :param value: The top of this line of text.
373
+ """
374
+ self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
375
+
376
+ @property
377
+ def cy(self) -> float:
378
+ """The horizontal center of this line of text.
379
+
380
+ :return: The horizontal center of this line of text.
381
+ """
382
+ return self.y + self.height / 2
383
+
384
+ @cy.setter
385
+ def cy(self, value: float) -> None:
386
+ """Set the horizontal center of this line of text.
387
+
388
+ :param value: The horizontal center of this line of text.
389
+ """
390
+ self.y += value - self.cy
391
+
392
+ @property
393
+ def y2(self) -> float:
394
+ """The bottom of this line of text.
395
+
396
+ :return: The bottom of this line of text.
397
+ """
398
+ return self.unpadded_bbox.y2 + self.bpad
399
+
400
+ @y2.setter
401
+ def y2(self, value: float) -> None:
402
+ """Set the bottom of this line of text.
403
+
404
+ :param value: The bottom of this line of text.
405
+ """
406
+ self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
407
+
408
+ lmargin = x
409
+ rmargin = x2
410
+ capline = y
411
+ baseline = y2