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,502 @@
|
|
|
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 space between the direction-most point of the text and the
|
|
8
|
+
left margin, right margin, descent, and ascent of the text. Top and bottom padding
|
|
9
|
+
may be less than zero if the constructor used a `y_bounds_reference` argument, as
|
|
10
|
+
descenders and ascenders may extend below and above the bounds of that reference
|
|
11
|
+
character.
|
|
12
|
+
|
|
13
|
+
There is a getter and setter for each of the four padding values. These *do not* move
|
|
14
|
+
the text element. For instance, if you decrease the left padding, the left margin
|
|
15
|
+
will move, *not* the text element.
|
|
16
|
+
|
|
17
|
+
There is a getter and setter for each of x, cx, x2, y, cy, and y2. These *do* move
|
|
18
|
+
the element, but do not scale it. For instance, if you move the left margin (x value)
|
|
19
|
+
to the left, the right margin (and the text element with it) will move to the left.
|
|
20
|
+
|
|
21
|
+
There are getters and setters for width, height, and scale. These scale the text and
|
|
22
|
+
the padding values.
|
|
23
|
+
|
|
24
|
+
`set_width_preserve_sidebearings()`, `set_height_preserve_sidebearings(), and
|
|
25
|
+
`transform_preserve_sidebearings()` methods scale the text and the top and bottom
|
|
26
|
+
padding, but not the left or right padding. These also keep the text element anchored
|
|
27
|
+
on `x` and `y2`. These methods are useful for aligning text of different sizes on,
|
|
28
|
+
for instance, a business card so that Ls or Hs of different sizes line up vertically.
|
|
29
|
+
|
|
30
|
+
Building an honest instance of this class is fairly involved:
|
|
31
|
+
|
|
32
|
+
1. Create a left-aligned text element.
|
|
33
|
+
|
|
34
|
+
2. Create a BoundingBox around the left-aligned text element. The difference between
|
|
35
|
+
0 and that BoundingBox's left edge is the left padding.
|
|
36
|
+
|
|
37
|
+
3. Create a right-aligned copy of the text element.
|
|
38
|
+
|
|
39
|
+
4. Create a BoundingBox around the right-aligned text element. The difference between
|
|
40
|
+
the BoundingBox's right edge 0 is the right padding.
|
|
41
|
+
|
|
42
|
+
5. Use a BoundingBox around a "normal" capital (e.g. "M") to infer the baseline and
|
|
43
|
+
capline and then calculate the top and bottom margins.
|
|
44
|
+
|
|
45
|
+
The padded text initializers in bounding_boxes.padded_text_initializers create
|
|
46
|
+
PaddedText instances with sensible defaults.
|
|
47
|
+
|
|
48
|
+
:author: Shay Hill
|
|
49
|
+
:created: 2021-11-28
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import math
|
|
55
|
+
from typing import TYPE_CHECKING
|
|
56
|
+
|
|
57
|
+
from paragraphs import par
|
|
58
|
+
|
|
59
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
60
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
61
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from lxml.etree import (
|
|
65
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
69
|
+
|
|
70
|
+
_no_line_gap_msg = par(
|
|
71
|
+
"""No line_gap defined. Line gap is an inherent font attribute defined within a
|
|
72
|
+
font file. If this PaddedText instance was created with `pad_text` from reference
|
|
73
|
+
elements, a line_gap was not defined. Reading line_gap from the font file
|
|
74
|
+
requires creating a PaddedText instance with `pad_text_ft` or `pad_text_mixed`.
|
|
75
|
+
You can set an arbitrary line_gap after init with `instance.line_gap = value`."""
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
_no_font_size_msg = par(
|
|
79
|
+
"""No font_size defined. Font size is an inherent font attribute defined within a
|
|
80
|
+
font file or an argument passed to `pad_text`. Any instance created with a padded
|
|
81
|
+
text initializer should have this property."""
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PaddedText(BoundElement):
|
|
86
|
+
"""A line of text with a bounding box and padding."""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
elem: EtreeElement,
|
|
91
|
+
bbox: BoundingBox,
|
|
92
|
+
tpad: float,
|
|
93
|
+
rpad: float,
|
|
94
|
+
bpad: float,
|
|
95
|
+
lpad: float,
|
|
96
|
+
line_gap: float | None = None,
|
|
97
|
+
font_size: float | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Initialize a PaddedText instance.
|
|
100
|
+
|
|
101
|
+
:param elem: The text element.
|
|
102
|
+
:param bbox: The bounding box around text element.
|
|
103
|
+
:param tpad: Top padding.
|
|
104
|
+
:param rpad: Right padding.
|
|
105
|
+
:param bpad: Bottom padding.
|
|
106
|
+
:param lpad: Left padding.
|
|
107
|
+
:param line_gap: The line gap between this line of text and the next. This is
|
|
108
|
+
an inherent font attribute sometimes defined within a font file.
|
|
109
|
+
"""
|
|
110
|
+
self.elem = elem
|
|
111
|
+
self.unpadded_bbox = bbox
|
|
112
|
+
self.base_tpad = tpad
|
|
113
|
+
self.rpad = rpad
|
|
114
|
+
self.base_bpad = bpad
|
|
115
|
+
self.lpad = lpad
|
|
116
|
+
self._line_gap = line_gap
|
|
117
|
+
self._font_size = font_size
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def tbox(self) -> BoundingBox:
|
|
121
|
+
"""Return the unpadded BoundingBox around the text element.
|
|
122
|
+
|
|
123
|
+
Tight bbox or True bbox. An alias for unpadded_bbox.
|
|
124
|
+
|
|
125
|
+
:return: The unpadded BoundingBox around the text element.
|
|
126
|
+
"""
|
|
127
|
+
return self.unpadded_bbox
|
|
128
|
+
|
|
129
|
+
@tbox.setter
|
|
130
|
+
def tbox(self, value: BoundingBox) -> None:
|
|
131
|
+
"""Set the unpadded BoundingBox around the text element.
|
|
132
|
+
|
|
133
|
+
:param value: The new unpadded BoundingBox.
|
|
134
|
+
"""
|
|
135
|
+
self.unpadded_bbox = value
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def bbox(self) -> BoundingBox:
|
|
139
|
+
"""Return a BoundingBox around the margins and cap/baseline.
|
|
140
|
+
|
|
141
|
+
:return: A BoundingBox around the margins and cap/baseline.
|
|
142
|
+
|
|
143
|
+
This is useful for creating a merged bounding box with
|
|
144
|
+
`svg_ultralight.BoundingBox.merged`. The merged bbox and merged_bbox
|
|
145
|
+
attributes of multiple bounding boxes can be used to create a PaddedText
|
|
146
|
+
instance around multiple text elements (a <g> elem).
|
|
147
|
+
"""
|
|
148
|
+
return BoundingBox(
|
|
149
|
+
self.x,
|
|
150
|
+
self.y,
|
|
151
|
+
self.width,
|
|
152
|
+
self.height,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@bbox.setter
|
|
156
|
+
def bbox(self, value: BoundingBox) -> None:
|
|
157
|
+
"""Set the bounding box of this PaddedText.
|
|
158
|
+
|
|
159
|
+
:param value: The new bounding box.
|
|
160
|
+
:effects: The text element is transformed to fit the new bounding box.
|
|
161
|
+
"""
|
|
162
|
+
msg = "Cannot set bbox of PaddedText, use transform() instead."
|
|
163
|
+
raise NotImplementedError(msg)
|
|
164
|
+
|
|
165
|
+
def transform(
|
|
166
|
+
self,
|
|
167
|
+
transformation: _Matrix | None = None,
|
|
168
|
+
*,
|
|
169
|
+
scale: tuple[float, float] | float | None = None,
|
|
170
|
+
dx: float | None = None,
|
|
171
|
+
dy: float | None = None,
|
|
172
|
+
reverse: bool = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Transform the element and bounding box.
|
|
175
|
+
|
|
176
|
+
:param transformation: a 6-tuple transformation matrix
|
|
177
|
+
:param scale: a scaling factor
|
|
178
|
+
:param dx: the x translation
|
|
179
|
+
:param dy: the y translation
|
|
180
|
+
:param reverse: Transform the element as if it were in a <g> element
|
|
181
|
+
transformed by tmat.
|
|
182
|
+
"""
|
|
183
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
184
|
+
self.unpadded_bbox.transform(tmat, reverse=reverse)
|
|
185
|
+
_ = transform_element(self.elem, tmat, reverse=reverse)
|
|
186
|
+
x_norm = pow(tmat[0] ** 2 + tmat[1] ** 2, 1 / 2)
|
|
187
|
+
self.lpad *= x_norm
|
|
188
|
+
self.rpad *= x_norm
|
|
189
|
+
if self._line_gap or self._font_size:
|
|
190
|
+
y_norm = pow(tmat[2] ** 2 + tmat[3] ** 2, 1 / 2)
|
|
191
|
+
if self._line_gap:
|
|
192
|
+
self._line_gap *= y_norm
|
|
193
|
+
if self._font_size:
|
|
194
|
+
self._font_size *= y_norm
|
|
195
|
+
|
|
196
|
+
def transform_preserve_sidebearings(
|
|
197
|
+
self,
|
|
198
|
+
transformation: _Matrix | None = None,
|
|
199
|
+
*,
|
|
200
|
+
scale: tuple[float, float] | float | None = None,
|
|
201
|
+
dx: float | None = None,
|
|
202
|
+
dy: float | None = None,
|
|
203
|
+
reverse: bool = False,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Transform the element and bounding box preserving sidebearings.
|
|
206
|
+
|
|
207
|
+
:param transformation: a 6-tuple transformation matrix
|
|
208
|
+
:param scale: a scaling factor
|
|
209
|
+
:param dx: the x translation
|
|
210
|
+
:param dy: the y translation
|
|
211
|
+
:param reverse: Transform the element as if it were in a <g> element
|
|
212
|
+
transformed by tmat.
|
|
213
|
+
"""
|
|
214
|
+
lpad = self.lpad
|
|
215
|
+
rpad = self.rpad
|
|
216
|
+
x = self.x
|
|
217
|
+
y2 = self.y2
|
|
218
|
+
self.transform(transformation, scale=scale, dx=dx, dy=dy, reverse=reverse)
|
|
219
|
+
self.lpad = lpad
|
|
220
|
+
self.rpad = rpad
|
|
221
|
+
self.x = x
|
|
222
|
+
self.y2 = y2
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def line_gap(self) -> float:
|
|
226
|
+
"""The line gap between this line of text and the next.
|
|
227
|
+
|
|
228
|
+
:return: The line gap between this line of text and the next.
|
|
229
|
+
"""
|
|
230
|
+
if self._line_gap is None:
|
|
231
|
+
raise AttributeError(_no_line_gap_msg)
|
|
232
|
+
return self._line_gap
|
|
233
|
+
|
|
234
|
+
@line_gap.setter
|
|
235
|
+
def line_gap(self, value: float) -> None:
|
|
236
|
+
"""Set the line gap between this line of text and the next.
|
|
237
|
+
|
|
238
|
+
:param value: The new line gap.
|
|
239
|
+
"""
|
|
240
|
+
self._line_gap = value
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def font_size(self) -> float:
|
|
244
|
+
"""The font size of this line of text.
|
|
245
|
+
|
|
246
|
+
:return: The font size of this line of text.
|
|
247
|
+
"""
|
|
248
|
+
if self._font_size is None:
|
|
249
|
+
raise AttributeError(_no_font_size_msg)
|
|
250
|
+
return self._font_size
|
|
251
|
+
|
|
252
|
+
@font_size.setter
|
|
253
|
+
def font_size(self, value: float) -> None:
|
|
254
|
+
"""Set the font size of this line of text.
|
|
255
|
+
|
|
256
|
+
:param value: The new font size.
|
|
257
|
+
"""
|
|
258
|
+
self.transform(scale=value / self.font_size)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def leading(self) -> float:
|
|
262
|
+
"""The leading of this line of text.
|
|
263
|
+
|
|
264
|
+
:return: The line gap plus the height of this line of text.
|
|
265
|
+
"""
|
|
266
|
+
return self.height + self.line_gap
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def tpad(self) -> float:
|
|
270
|
+
"""The top padding of this line of text.
|
|
271
|
+
|
|
272
|
+
:return: The scaled top padding of this line of text.
|
|
273
|
+
"""
|
|
274
|
+
return self.base_tpad * self.tbox.scale[1]
|
|
275
|
+
|
|
276
|
+
@tpad.setter
|
|
277
|
+
def tpad(self, value: float) -> None:
|
|
278
|
+
"""Set the top padding of this line of text.
|
|
279
|
+
|
|
280
|
+
:param value: The new top padding.
|
|
281
|
+
"""
|
|
282
|
+
self.base_tpad = value / self.tbox.scale[1]
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def bpad(self) -> float:
|
|
286
|
+
"""The bottom padding of this line of text.
|
|
287
|
+
|
|
288
|
+
:return: The scaled bottom padding of this line of text.
|
|
289
|
+
"""
|
|
290
|
+
return self.base_bpad * self.tbox.scale[1]
|
|
291
|
+
|
|
292
|
+
@bpad.setter
|
|
293
|
+
def bpad(self, value: float) -> None:
|
|
294
|
+
"""Set the bottom padding of this line of text.
|
|
295
|
+
|
|
296
|
+
:param value: The new bottom padding.
|
|
297
|
+
"""
|
|
298
|
+
self.base_bpad = value / self.tbox.scale[1]
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def scale(self) -> tuple[float, float]:
|
|
302
|
+
"""Get scale of the bounding box.
|
|
303
|
+
|
|
304
|
+
:return: uniform scale of the bounding box
|
|
305
|
+
|
|
306
|
+
Use caution, the scale attribute can cause errors in intuition. Changing
|
|
307
|
+
width or height will change the scale attribute, but not the x or y values.
|
|
308
|
+
The scale setter, on the other hand, will work in the tradational manner.
|
|
309
|
+
I.e., x => x*scale, y => y*scale, x2 => x*scale, y2 => y*scale, width =>
|
|
310
|
+
width*scale, height => height*scale, scale => scale*scale. This matches how
|
|
311
|
+
scale works in almost every other context.
|
|
312
|
+
"""
|
|
313
|
+
xx, xy, yx, yy, *_ = self.tbox.transformation
|
|
314
|
+
return math.sqrt(xx * xx + xy * xy), math.sqrt(yx * yx + yy * yy)
|
|
315
|
+
|
|
316
|
+
@scale.setter
|
|
317
|
+
def scale(self, value: tuple[float, float]) -> None:
|
|
318
|
+
"""Scale the bounding box by a uniform factor.
|
|
319
|
+
|
|
320
|
+
:param value: new scale value
|
|
321
|
+
|
|
322
|
+
Don't miss this! You are setting the scale, not scaling the scale! If you
|
|
323
|
+
have a previously defined scale other than 1, this is probably not what you
|
|
324
|
+
want. Most of the time, you will want to use the *= operator.
|
|
325
|
+
|
|
326
|
+
`scale = 2` -> ignore whatever scale was previously defined and set scale to 2
|
|
327
|
+
`scale *= 2` -> make it twice as big as it was.
|
|
328
|
+
"""
|
|
329
|
+
new_scale = (
|
|
330
|
+
value[0] / self.tbox.scale[0],
|
|
331
|
+
value[1] / self.tbox.scale[1],
|
|
332
|
+
)
|
|
333
|
+
self.transform(scale=new_scale)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def uniform_scale(self) -> float:
|
|
337
|
+
"""Get uniform scale of the bounding box.
|
|
338
|
+
|
|
339
|
+
:return: uniform scale of the bounding box
|
|
340
|
+
:raises ValueError: if the scale is non-uniform.
|
|
341
|
+
"""
|
|
342
|
+
scale = self.scale
|
|
343
|
+
if math.isclose(scale[0], scale[1]):
|
|
344
|
+
return scale[0]
|
|
345
|
+
msg = f"Non-uniform scale detected: sx={scale[0]}, sy={scale[1]}"
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def width(self) -> float:
|
|
350
|
+
"""The width of this line of text with padding.
|
|
351
|
+
|
|
352
|
+
:return: The scaled width of this line of text with padding.
|
|
353
|
+
"""
|
|
354
|
+
return self.tbox.width + self.lpad + self.rpad
|
|
355
|
+
|
|
356
|
+
@width.setter
|
|
357
|
+
def width(self, value: float) -> None:
|
|
358
|
+
"""Scale to padded_width = width without scaling padding.
|
|
359
|
+
|
|
360
|
+
:param width: The new width of this line of text.
|
|
361
|
+
:effects: the text_element bounding box is scaled to width - lpad - rpad.
|
|
362
|
+
|
|
363
|
+
Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
|
|
364
|
+
consistent with how rectangles, viewboxes, and anything else defined by x, y,
|
|
365
|
+
width, height behaves in SVG. This is unintuitive for text, because the
|
|
366
|
+
baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
|
|
367
|
+
*and* y2) when scaling.
|
|
368
|
+
"""
|
|
369
|
+
self.transform(scale=value / self.width)
|
|
370
|
+
|
|
371
|
+
def set_width_preserve_sidebearings(self, value: float) -> None:
|
|
372
|
+
"""Set the width of this line of text without scaling sidebearings.
|
|
373
|
+
|
|
374
|
+
:param value: The new width of this line of text.
|
|
375
|
+
:effects: the text_element bounding box is scaled to width - lpad - rpad.
|
|
376
|
+
"""
|
|
377
|
+
no_margins_old = self.tbox.width
|
|
378
|
+
no_margins_new = value - self.lpad - self.rpad
|
|
379
|
+
scale = no_margins_new / no_margins_old
|
|
380
|
+
self.transform_preserve_sidebearings(scale=scale)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def height(self) -> float:
|
|
384
|
+
"""The height of this line of text with padding.
|
|
385
|
+
|
|
386
|
+
:return: The scaled height of this line of text with padding.
|
|
387
|
+
"""
|
|
388
|
+
return self.tbox.height + self.tpad + self.bpad
|
|
389
|
+
|
|
390
|
+
@height.setter
|
|
391
|
+
def height(self, value: float) -> None:
|
|
392
|
+
"""Scale to height without scaling padding.
|
|
393
|
+
|
|
394
|
+
:param height: The new height of this line of text.
|
|
395
|
+
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
396
|
+
"""
|
|
397
|
+
scale = value / self.height
|
|
398
|
+
self.transform(scale=scale)
|
|
399
|
+
|
|
400
|
+
def set_height_preserve_sidebearings(self, value: float) -> None:
|
|
401
|
+
"""Set the height of this line of text without scaling sidebearings.
|
|
402
|
+
|
|
403
|
+
:param value: The new height of this line of text.
|
|
404
|
+
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
405
|
+
"""
|
|
406
|
+
self.transform_preserve_sidebearings(scale=value / self.height)
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def x(self) -> float:
|
|
410
|
+
"""The left margin of this line of text.
|
|
411
|
+
|
|
412
|
+
:return: The left margin of this line of text.
|
|
413
|
+
"""
|
|
414
|
+
return self.tbox.x - self.lpad
|
|
415
|
+
|
|
416
|
+
@x.setter
|
|
417
|
+
def x(self, value: float) -> None:
|
|
418
|
+
"""Set the left margin of this line of text.
|
|
419
|
+
|
|
420
|
+
:param value: The left margin of this line of text.
|
|
421
|
+
"""
|
|
422
|
+
self.transform(dx=value + self.lpad - self.tbox.x)
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def cx(self) -> float:
|
|
426
|
+
"""The horizontal center of this line of text.
|
|
427
|
+
|
|
428
|
+
:return: The horizontal center of this line of text.
|
|
429
|
+
"""
|
|
430
|
+
return self.x + self.width / 2
|
|
431
|
+
|
|
432
|
+
@cx.setter
|
|
433
|
+
def cx(self, value: float) -> None:
|
|
434
|
+
"""Set the horizontal center of this line of text.
|
|
435
|
+
|
|
436
|
+
:param value: The horizontal center of this line of text.
|
|
437
|
+
"""
|
|
438
|
+
self.x += value - self.cx
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def x2(self) -> float:
|
|
442
|
+
"""The right margin of this line of text.
|
|
443
|
+
|
|
444
|
+
:return: The right margin of this line of text.
|
|
445
|
+
"""
|
|
446
|
+
return self.tbox.x2 + self.rpad
|
|
447
|
+
|
|
448
|
+
@x2.setter
|
|
449
|
+
def x2(self, value: float) -> None:
|
|
450
|
+
"""Set the right margin of this line of text.
|
|
451
|
+
|
|
452
|
+
:param value: The right margin of this line of text.
|
|
453
|
+
"""
|
|
454
|
+
self.transform(dx=value - self.rpad - self.tbox.x2)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def y(self) -> float:
|
|
458
|
+
"""The top of this line of text.
|
|
459
|
+
|
|
460
|
+
:return: The top of this line of text.
|
|
461
|
+
"""
|
|
462
|
+
return self.tbox.y - self.tpad
|
|
463
|
+
|
|
464
|
+
@y.setter
|
|
465
|
+
def y(self, value: float) -> None:
|
|
466
|
+
"""Set the top of this line of text.
|
|
467
|
+
|
|
468
|
+
:param value: The top of this line of text.
|
|
469
|
+
"""
|
|
470
|
+
self.transform(dy=value + self.tpad - self.tbox.y)
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def cy(self) -> float:
|
|
474
|
+
"""The horizontal center of this line of text.
|
|
475
|
+
|
|
476
|
+
:return: The horizontal center of this line of text.
|
|
477
|
+
"""
|
|
478
|
+
return self.y + self.height / 2
|
|
479
|
+
|
|
480
|
+
@cy.setter
|
|
481
|
+
def cy(self, value: float) -> None:
|
|
482
|
+
"""Set the horizontal center of this line of text.
|
|
483
|
+
|
|
484
|
+
:param value: The horizontal center of this line of text.
|
|
485
|
+
"""
|
|
486
|
+
self.y += value - self.cy
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def y2(self) -> float:
|
|
490
|
+
"""The bottom of this line of text.
|
|
491
|
+
|
|
492
|
+
:return: The bottom of this line of text.
|
|
493
|
+
"""
|
|
494
|
+
return self.tbox.y2 + self.bpad
|
|
495
|
+
|
|
496
|
+
@y2.setter
|
|
497
|
+
def y2(self, value: float) -> None:
|
|
498
|
+
"""Set the bottom of this line of text.
|
|
499
|
+
|
|
500
|
+
:param value: The bottom of this line of text.
|
|
501
|
+
"""
|
|
502
|
+
self.transform(dy=value - self.bpad - self.tbox.y2)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Raise the level of the constructors module.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
created: 12/22/2019.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from svg_ultralight.constructors.new_element import (
|
|
8
|
+
deepcopy_element,
|
|
9
|
+
new_element,
|
|
10
|
+
new_sub_element,
|
|
11
|
+
update_element,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["deepcopy_element", "new_element", "new_sub_element", "update_element"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""SVG Element constructors. Create an svg element from a dictionary.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 1/31/2020
|
|
5
|
+
|
|
6
|
+
This is principally to allow passing values, rather than strings, as svg element
|
|
7
|
+
parameters.
|
|
8
|
+
|
|
9
|
+
Will translate ``stroke_width=10`` to ``stroke-width="10"``
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import warnings
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
from svg_ultralight.string_conversion import set_attributes
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from lxml.etree import (
|
|
24
|
+
QName,
|
|
25
|
+
)
|
|
26
|
+
from lxml.etree import (
|
|
27
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def new_element(tag: str | QName, **attributes: ElemAttrib) -> EtreeElement:
|
|
34
|
+
"""Create an etree.Element, make every kwarg value a string.
|
|
35
|
+
|
|
36
|
+
:param tag: element tag
|
|
37
|
+
:param attributes: element attribute names and values
|
|
38
|
+
:returns: new ``tag`` element
|
|
39
|
+
|
|
40
|
+
>>> elem = new_element('line', x1=0, y1=0, x2=5, y2=5)
|
|
41
|
+
>>> etree.tostring(elem)
|
|
42
|
+
b'<line x1="0" y1="0" x2="5" y2="5"/>'
|
|
43
|
+
|
|
44
|
+
Strips trailing underscores
|
|
45
|
+
|
|
46
|
+
>>> elem = new_element('line', in_="SourceAlpha")
|
|
47
|
+
>>> etree.tostring(elem)
|
|
48
|
+
b'<line in="SourceAlpha"/>'
|
|
49
|
+
|
|
50
|
+
Translates other underscores to hyphens
|
|
51
|
+
|
|
52
|
+
>>> elem = new_element('line', stroke_width=1)
|
|
53
|
+
>>> etree.tostring(elem)
|
|
54
|
+
b'<line stroke-width="1"/>'
|
|
55
|
+
|
|
56
|
+
Special handling for a 'text' argument. Places value between element tags.
|
|
57
|
+
|
|
58
|
+
>>> elem = new_element('text', text='please star my project')
|
|
59
|
+
>>> etree.tostring(elem)
|
|
60
|
+
b'<text>please star my project</text>'
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
elem = etree.Element(tag)
|
|
64
|
+
set_attributes(elem, **attributes)
|
|
65
|
+
return elem
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def new_sub_element(
|
|
69
|
+
parent: EtreeElement, tag: str | QName, **attributes: ElemAttrib
|
|
70
|
+
) -> EtreeElement:
|
|
71
|
+
"""Create an etree.SubElement, make every kwarg value a string.
|
|
72
|
+
|
|
73
|
+
:param parent: parent element
|
|
74
|
+
:param tag: element tag
|
|
75
|
+
:param attributes: element attribute names and values
|
|
76
|
+
:returns: new ``tag`` element
|
|
77
|
+
|
|
78
|
+
>>> parent = etree.Element('g')
|
|
79
|
+
>>> _ = new_sub_element(parent, 'rect')
|
|
80
|
+
>>> etree.tostring(parent)
|
|
81
|
+
b'<g><rect/></g>'
|
|
82
|
+
"""
|
|
83
|
+
elem = etree.SubElement(parent, tag)
|
|
84
|
+
set_attributes(elem, **attributes)
|
|
85
|
+
return elem
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def update_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
|
|
89
|
+
"""Update an existing etree.Element with additional params.
|
|
90
|
+
|
|
91
|
+
:param elem: at etree element
|
|
92
|
+
:param attributes: element attribute names and values
|
|
93
|
+
:returns: the element with updated attributes
|
|
94
|
+
|
|
95
|
+
This is to take advantage of the argument conversion in ``new_element``.
|
|
96
|
+
"""
|
|
97
|
+
set_attributes(elem, **attributes)
|
|
98
|
+
return elem
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def deepcopy_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
|
|
102
|
+
"""Create a deepcopy of an element. Optionally pass additional params.
|
|
103
|
+
|
|
104
|
+
:param elem: at etree element or list of elements
|
|
105
|
+
:param attributes: element attribute names and values
|
|
106
|
+
:returns: a deepcopy of the element with updated attributes
|
|
107
|
+
:raises DeprecationWarning:
|
|
108
|
+
"""
|
|
109
|
+
warnings.warn(
|
|
110
|
+
"deepcopy_element is deprecated. "
|
|
111
|
+
+ "Use copy.deepcopy from the standard library instead.",
|
|
112
|
+
category=DeprecationWarning,
|
|
113
|
+
stacklevel=1,
|
|
114
|
+
)
|
|
115
|
+
elem = copy.deepcopy(elem)
|
|
116
|
+
_ = update_element(elem, **attributes)
|
|
117
|
+
return elem
|