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.
- svg_ultralight/__init__.py +108 -105
- svg_ultralight/animate.py +40 -40
- svg_ultralight/attrib_hints.py +13 -14
- svg_ultralight/bounding_boxes/__init__.py +5 -5
- svg_ultralight/bounding_boxes/bound_helpers.py +189 -201
- svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -206
- svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
- svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
- svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
- svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
- svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
- svg_ultralight/constructors/__init__.py +14 -14
- svg_ultralight/constructors/new_element.py +115 -115
- svg_ultralight/font_tools/__init__.py +5 -5
- svg_ultralight/font_tools/comp_results.py +295 -293
- svg_ultralight/font_tools/font_info.py +793 -784
- svg_ultralight/image_ops.py +156 -156
- svg_ultralight/inkscape.py +261 -261
- svg_ultralight/layout.py +290 -291
- svg_ultralight/main.py +183 -198
- svg_ultralight/metadata.py +122 -122
- svg_ultralight/nsmap.py +36 -36
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -249
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +87 -87
- svg_ultralight/string_conversion.py +244 -244
- svg_ultralight/strings/__init__.py +21 -13
- svg_ultralight/strings/svg_strings.py +106 -67
- svg_ultralight/transformations.py +140 -141
- svg_ultralight/unit_conversion.py +247 -248
- {svg_ultralight-0.47.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
- svg_ultralight-0.50.1.dist-info/RECORD +34 -0
- svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
- svg_ultralight-0.47.0.dist-info/RECORD +0 -34
- svg_ultralight-0.47.0.dist-info/WHEEL +0 -5
- 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
|