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,442 @@
|
|
|
1
|
+
"""Functions that create PaddedText instances.
|
|
2
|
+
|
|
3
|
+
Three variants:
|
|
4
|
+
|
|
5
|
+
- `pad_text`: uses Inkscape to measure text bounds
|
|
6
|
+
|
|
7
|
+
- `pad_text_ft`: uses fontTools to measure text bounds (faster, and you get line_gap)
|
|
8
|
+
|
|
9
|
+
- `pad_text_mix`: uses Inkscape and fontTools to give true ascent, descent, and
|
|
10
|
+
line_gap while correcting some of the layout differences between fontTools and
|
|
11
|
+
Inkscape.
|
|
12
|
+
|
|
13
|
+
There is a default font size for pad_text if an element is passed. There is also a
|
|
14
|
+
default for the other pad_text_ functions, but it taken from the font file and is
|
|
15
|
+
usually 1024, so it won't be easy to miss. The default for standard pad_text is to
|
|
16
|
+
prevent surprises if Inksape defaults to font-size 12pt while your browser defaults
|
|
17
|
+
to 16px.
|
|
18
|
+
|
|
19
|
+
:author: Shay Hill
|
|
20
|
+
:created: 2025-06-09
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from copy import deepcopy
|
|
26
|
+
from typing import TYPE_CHECKING, overload
|
|
27
|
+
|
|
28
|
+
from svg_ultralight.attrib_hints import ElemAttrib
|
|
29
|
+
from svg_ultralight.bounding_boxes.bound_helpers import pad_bbox
|
|
30
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
31
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
32
|
+
from svg_ultralight.constructors import new_element, update_element
|
|
33
|
+
from svg_ultralight.font_tools.font_info import (
|
|
34
|
+
FTFontInfo,
|
|
35
|
+
get_padded_text_info,
|
|
36
|
+
get_svg_font_attributes,
|
|
37
|
+
)
|
|
38
|
+
from svg_ultralight.query import get_bounding_boxes
|
|
39
|
+
from svg_ultralight.string_conversion import format_attr_dict, format_number
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
from lxml.etree import (
|
|
45
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
|
|
49
|
+
|
|
50
|
+
DEFAULT_Y_BOUNDS_REFERENCE = "{[|gjpqyf"
|
|
51
|
+
|
|
52
|
+
# A default font size for pad_text if font-size is not specified in the reference
|
|
53
|
+
# element.
|
|
54
|
+
DEFAULT_FONT_SIZE_FOR_PAD_TEXT = 12.0 # Default font size for pad_text if not specified
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pad_text(
|
|
58
|
+
inkscape: str | os.PathLike[str],
|
|
59
|
+
text_elem: EtreeElement,
|
|
60
|
+
y_bounds_reference: str | None = None,
|
|
61
|
+
*,
|
|
62
|
+
font: str | os.PathLike[str] | None = None,
|
|
63
|
+
) -> PaddedText:
|
|
64
|
+
r"""Create a PaddedText instance from a text element.
|
|
65
|
+
|
|
66
|
+
:param inkscape: path to an inkscape executable on your local file system
|
|
67
|
+
IMPORTANT: path cannot end with ``.exe``.
|
|
68
|
+
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
69
|
+
:param text_elem: an etree element with a text tag
|
|
70
|
+
:param y_bounds_reference: an optional string to use to determine the ascent and
|
|
71
|
+
capline of the font. The default is a good choice, which approaches or even
|
|
72
|
+
meets the ascent of descent of most fonts without using utf-8 characters. You
|
|
73
|
+
might want to use a letter like "M" or even "x" if you are using an all-caps
|
|
74
|
+
string and want to center between the capline and baseline or if you'd like
|
|
75
|
+
to center between the baseline and x-line.
|
|
76
|
+
:param font: optionally add a path to a font file to use for the text element.
|
|
77
|
+
This is going to conflict with any font-family, font-style, or other
|
|
78
|
+
font-related attributes *except* font-size. You likely want to use
|
|
79
|
+
`font_tools.new_padded_text` if you're going to pass a font path, but you can
|
|
80
|
+
use it here to compare results between `pad_text` and `new_padded_text`.
|
|
81
|
+
:return: a PaddedText instance
|
|
82
|
+
"""
|
|
83
|
+
if y_bounds_reference is None:
|
|
84
|
+
y_bounds_reference = DEFAULT_Y_BOUNDS_REFERENCE
|
|
85
|
+
if font is not None:
|
|
86
|
+
_ = update_element(text_elem, **get_svg_font_attributes(font))
|
|
87
|
+
if "font-size" not in text_elem.attrib:
|
|
88
|
+
text_elem.attrib["font-size"] = format_number(DEFAULT_FONT_SIZE_FOR_PAD_TEXT)
|
|
89
|
+
rmargin_ref = deepcopy(text_elem)
|
|
90
|
+
capline_ref = deepcopy(text_elem)
|
|
91
|
+
_ = rmargin_ref.attrib.pop("id", None)
|
|
92
|
+
_ = capline_ref.attrib.pop("id", None)
|
|
93
|
+
rmargin_ref.attrib["text-anchor"] = "end"
|
|
94
|
+
capline_ref.text = y_bounds_reference
|
|
95
|
+
|
|
96
|
+
bboxes = get_bounding_boxes(inkscape, text_elem, rmargin_ref, capline_ref)
|
|
97
|
+
bbox, rmargin_bbox, capline_bbox = bboxes
|
|
98
|
+
|
|
99
|
+
tpad = bbox.y - capline_bbox.y
|
|
100
|
+
rpad = -rmargin_bbox.x2
|
|
101
|
+
bpad = capline_bbox.y2 - bbox.y2
|
|
102
|
+
lpad = bbox.x
|
|
103
|
+
return PaddedText(
|
|
104
|
+
text_elem,
|
|
105
|
+
bbox,
|
|
106
|
+
tpad,
|
|
107
|
+
rpad,
|
|
108
|
+
bpad,
|
|
109
|
+
lpad,
|
|
110
|
+
font_size=float(text_elem.attrib["font-size"]),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@overload
|
|
115
|
+
def pad_chars_ft(
|
|
116
|
+
font: str | os.PathLike[str],
|
|
117
|
+
text: str,
|
|
118
|
+
font_size: float | None = None,
|
|
119
|
+
ascent: float | None = None,
|
|
120
|
+
descent: float | None = None,
|
|
121
|
+
*,
|
|
122
|
+
y_bounds_reference: str | None = None,
|
|
123
|
+
attrib: OptionalElemAttribMapping = None,
|
|
124
|
+
**attributes: ElemAttrib,
|
|
125
|
+
) -> BoundElement: ...
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@overload
|
|
129
|
+
def pad_chars_ft(
|
|
130
|
+
font: str | os.PathLike[str],
|
|
131
|
+
text: list[str],
|
|
132
|
+
font_size: float | None = None,
|
|
133
|
+
ascent: float | None = None,
|
|
134
|
+
descent: float | None = None,
|
|
135
|
+
*,
|
|
136
|
+
y_bounds_reference: str | None = None,
|
|
137
|
+
attrib: OptionalElemAttribMapping = None,
|
|
138
|
+
**attributes: ElemAttrib,
|
|
139
|
+
) -> list[BoundElement]: ...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def pad_chars_ft(
|
|
143
|
+
font: str | os.PathLike[str],
|
|
144
|
+
text: str | list[str],
|
|
145
|
+
font_size: float | None = None,
|
|
146
|
+
ascent: float | None = None,
|
|
147
|
+
descent: float | None = None,
|
|
148
|
+
*,
|
|
149
|
+
y_bounds_reference: str | None = None,
|
|
150
|
+
attrib: OptionalElemAttribMapping = None,
|
|
151
|
+
**attributes: ElemAttrib,
|
|
152
|
+
) -> BoundElement | list[BoundElement]:
|
|
153
|
+
"""Create a bound group of paths for each character in the text.
|
|
154
|
+
|
|
155
|
+
Create a bound group of path elements, one for each character in the text. This
|
|
156
|
+
will provide less utility in most respects than `pad_text_ft`, but will be useful
|
|
157
|
+
for animations and other effects where individual characters need to be
|
|
158
|
+
addressed.
|
|
159
|
+
"""
|
|
160
|
+
attributes.update(attrib or {})
|
|
161
|
+
attributes_ = format_attr_dict(**attributes)
|
|
162
|
+
attributes_.update(get_svg_font_attributes(font))
|
|
163
|
+
|
|
164
|
+
_ = attributes_.pop("font-size", None)
|
|
165
|
+
_ = attributes_.pop("font-family", None)
|
|
166
|
+
_ = attributes_.pop("font-style", None)
|
|
167
|
+
_ = attributes_.pop("font-weight", None)
|
|
168
|
+
_ = attributes_.pop("font-stretch", None)
|
|
169
|
+
|
|
170
|
+
input_one_text_item = False
|
|
171
|
+
if isinstance(text, str):
|
|
172
|
+
input_one_text_item = True
|
|
173
|
+
text = [text]
|
|
174
|
+
elems: list[BoundElement] = []
|
|
175
|
+
|
|
176
|
+
font_info = FTFontInfo(font)
|
|
177
|
+
try:
|
|
178
|
+
for text_item in text:
|
|
179
|
+
text_info = get_padded_text_info(
|
|
180
|
+
font_info,
|
|
181
|
+
text_item,
|
|
182
|
+
font_size,
|
|
183
|
+
ascent,
|
|
184
|
+
descent,
|
|
185
|
+
y_bounds_reference=y_bounds_reference,
|
|
186
|
+
)
|
|
187
|
+
elem = text_info.new_chars_group_element(**attributes_)
|
|
188
|
+
bbox = pad_bbox(text_info.bbox, text_info.padding)
|
|
189
|
+
elems.append(BoundElement(elem, bbox))
|
|
190
|
+
finally:
|
|
191
|
+
font_info.font.close()
|
|
192
|
+
if input_one_text_item:
|
|
193
|
+
return elems[0]
|
|
194
|
+
return elems
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _remove_svg_font_attributes(attributes: dict[str, ElemAttrib]) -> dict[str, str]:
|
|
198
|
+
"""Remove svg font attributes from the attributes dict.
|
|
199
|
+
|
|
200
|
+
These are either not required when explicitly passing a font file, not relevant,
|
|
201
|
+
or not supported by fontTools.
|
|
202
|
+
"""
|
|
203
|
+
attributes_ = format_attr_dict(**attributes)
|
|
204
|
+
keys_to_remove = [
|
|
205
|
+
"font-size",
|
|
206
|
+
"font-family",
|
|
207
|
+
"font-style",
|
|
208
|
+
"font-weight",
|
|
209
|
+
"font-stretch",
|
|
210
|
+
]
|
|
211
|
+
return {k: v for k, v in attributes_.items() if k not in keys_to_remove}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@overload
|
|
215
|
+
def pad_text_ft(
|
|
216
|
+
font: str | os.PathLike[str],
|
|
217
|
+
text: str,
|
|
218
|
+
font_size: float | None = None,
|
|
219
|
+
ascent: float | None = None,
|
|
220
|
+
descent: float | None = None,
|
|
221
|
+
*,
|
|
222
|
+
y_bounds_reference: str | None = None,
|
|
223
|
+
attrib: OptionalElemAttribMapping = None,
|
|
224
|
+
**attributes: ElemAttrib,
|
|
225
|
+
) -> PaddedText: ...
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@overload
|
|
229
|
+
def pad_text_ft(
|
|
230
|
+
font: str | os.PathLike[str],
|
|
231
|
+
text: list[str],
|
|
232
|
+
font_size: float | None = None,
|
|
233
|
+
ascent: float | None = None,
|
|
234
|
+
descent: float | None = None,
|
|
235
|
+
*,
|
|
236
|
+
y_bounds_reference: str | None = None,
|
|
237
|
+
attrib: OptionalElemAttribMapping = None,
|
|
238
|
+
**attributes: ElemAttrib,
|
|
239
|
+
) -> list[PaddedText]: ...
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def pad_text_ft(
|
|
243
|
+
font: str | os.PathLike[str],
|
|
244
|
+
text: str | list[str],
|
|
245
|
+
font_size: float | None = None,
|
|
246
|
+
ascent: float | None = None,
|
|
247
|
+
descent: float | None = None,
|
|
248
|
+
*,
|
|
249
|
+
y_bounds_reference: str | None = None,
|
|
250
|
+
attrib: OptionalElemAttribMapping = None,
|
|
251
|
+
**attributes: ElemAttrib,
|
|
252
|
+
) -> PaddedText | list[PaddedText]:
|
|
253
|
+
"""Create a new PaddedText instance using fontTools.
|
|
254
|
+
|
|
255
|
+
:param font: path to a font file.
|
|
256
|
+
:param text: the text of the text element or a list of text strings.
|
|
257
|
+
:param font_size: the font size to use.
|
|
258
|
+
:param ascent: the ascent of the font. If not provided, it will be calculated
|
|
259
|
+
from the font file.
|
|
260
|
+
:param descent: the descent of the font. If not provided, it will be calculated
|
|
261
|
+
from the font file.
|
|
262
|
+
:param y_bounds_reference: optional character or string to use as a reference
|
|
263
|
+
for the ascent and descent. If provided, the ascent and descent will be the y
|
|
264
|
+
extents of the capline reference. This argument is provided to mimic the
|
|
265
|
+
behavior of the query module's `pad_text` function. `pad_text` does no
|
|
266
|
+
inspect font files and relies on Inkscape to measure reference characters.
|
|
267
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
268
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
269
|
+
dictionary into a function call.
|
|
270
|
+
:param attributes: additional attributes to set on the text element. There is a
|
|
271
|
+
chance these will cause the font element to exceed the BoundingBox of the
|
|
272
|
+
PaddedText instance.
|
|
273
|
+
:return: a PaddedText instance with a line_gap defined. If a list of strings is
|
|
274
|
+
given for parameter `text`, a list of PaddedText instances is returned.
|
|
275
|
+
"""
|
|
276
|
+
attributes.update(attrib or {})
|
|
277
|
+
attributes_ = _remove_svg_font_attributes(attributes)
|
|
278
|
+
|
|
279
|
+
input_one_text_item = False
|
|
280
|
+
if isinstance(text, str):
|
|
281
|
+
input_one_text_item = True
|
|
282
|
+
text = [text]
|
|
283
|
+
|
|
284
|
+
font_info = FTFontInfo(font)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
elems: list[PaddedText] = []
|
|
288
|
+
for text_item in text:
|
|
289
|
+
ti = get_padded_text_info(
|
|
290
|
+
font_info,
|
|
291
|
+
text_item,
|
|
292
|
+
font_size,
|
|
293
|
+
ascent,
|
|
294
|
+
descent,
|
|
295
|
+
y_bounds_reference=y_bounds_reference,
|
|
296
|
+
)
|
|
297
|
+
elem = ti.new_element(**attributes_)
|
|
298
|
+
plem = PaddedText(elem, ti.bbox, *ti.padding, ti.line_gap, ti.font_size)
|
|
299
|
+
elems.append(plem)
|
|
300
|
+
finally:
|
|
301
|
+
font_info.font.close()
|
|
302
|
+
if input_one_text_item:
|
|
303
|
+
return elems[0]
|
|
304
|
+
return elems
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@overload
|
|
308
|
+
def wrap_text_ft(
|
|
309
|
+
font: str | os.PathLike[str],
|
|
310
|
+
text: str,
|
|
311
|
+
width: float,
|
|
312
|
+
font_size: float | None = None,
|
|
313
|
+
ascent: float | None = None,
|
|
314
|
+
descent: float | None = None,
|
|
315
|
+
*,
|
|
316
|
+
y_bounds_reference: str | None = None,
|
|
317
|
+
) -> list[str]: ...
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@overload
|
|
321
|
+
def wrap_text_ft(
|
|
322
|
+
font: str | os.PathLike[str],
|
|
323
|
+
text: list[str],
|
|
324
|
+
width: float,
|
|
325
|
+
font_size: float | None = None,
|
|
326
|
+
ascent: float | None = None,
|
|
327
|
+
descent: float | None = None,
|
|
328
|
+
*,
|
|
329
|
+
y_bounds_reference: str | None = None,
|
|
330
|
+
) -> list[list[str]]: ...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def wrap_text_ft(
|
|
334
|
+
font: str | os.PathLike[str],
|
|
335
|
+
text: str | list[str],
|
|
336
|
+
width: float,
|
|
337
|
+
font_size: float | None = None,
|
|
338
|
+
ascent: float | None = None,
|
|
339
|
+
descent: float | None = None,
|
|
340
|
+
*,
|
|
341
|
+
y_bounds_reference: str | None = None,
|
|
342
|
+
) -> list[str] | list[list[str]]:
|
|
343
|
+
"""Wrap text to fit within the width of the font's bounding box."""
|
|
344
|
+
input_one_text_item = False
|
|
345
|
+
if isinstance(text, str):
|
|
346
|
+
input_one_text_item = True
|
|
347
|
+
text = [text]
|
|
348
|
+
|
|
349
|
+
all_wrapped: list[list[str]] = []
|
|
350
|
+
font_info = FTFontInfo(font)
|
|
351
|
+
|
|
352
|
+
def get_width(line: str) -> float:
|
|
353
|
+
ti = get_padded_text_info(
|
|
354
|
+
font_info,
|
|
355
|
+
line,
|
|
356
|
+
font_size,
|
|
357
|
+
ascent,
|
|
358
|
+
descent,
|
|
359
|
+
y_bounds_reference=y_bounds_reference,
|
|
360
|
+
)
|
|
361
|
+
return ti.bbox.width
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
for text_item in text:
|
|
365
|
+
words = text_item.split()
|
|
366
|
+
if not words:
|
|
367
|
+
all_wrapped.append([])
|
|
368
|
+
continue
|
|
369
|
+
wrapped: list[str] = [words.pop(0)]
|
|
370
|
+
while words:
|
|
371
|
+
next_word = words.pop(0)
|
|
372
|
+
test_line = f"{wrapped[-1]} {next_word}"
|
|
373
|
+
if get_width(test_line) <= width:
|
|
374
|
+
wrapped[-1] = test_line
|
|
375
|
+
else:
|
|
376
|
+
wrapped.append(next_word)
|
|
377
|
+
all_wrapped.append(wrapped)
|
|
378
|
+
finally:
|
|
379
|
+
font_info.font.close()
|
|
380
|
+
if input_one_text_item:
|
|
381
|
+
return all_wrapped[0]
|
|
382
|
+
return all_wrapped
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def pad_text_mix(
|
|
386
|
+
inkscape: str | os.PathLike[str],
|
|
387
|
+
font: str | os.PathLike[str],
|
|
388
|
+
text: str,
|
|
389
|
+
font_size: float | None = None,
|
|
390
|
+
ascent: float | None = None,
|
|
391
|
+
descent: float | None = None,
|
|
392
|
+
*,
|
|
393
|
+
y_bounds_reference: str | None = None,
|
|
394
|
+
attrib: OptionalElemAttribMapping = None,
|
|
395
|
+
**attributes: ElemAttrib,
|
|
396
|
+
) -> PaddedText:
|
|
397
|
+
"""Use Inkscape text bounds and fill missing with fontTools.
|
|
398
|
+
|
|
399
|
+
:param font: path to a font file.
|
|
400
|
+
:param text: the text of the text element.
|
|
401
|
+
:param font_size: the font size to use.
|
|
402
|
+
:param ascent: the ascent of the font. If not provided, it will be calculated
|
|
403
|
+
from the font file.
|
|
404
|
+
:param descent: the descent of the font. If not provided, it will be calculated
|
|
405
|
+
from the font file.
|
|
406
|
+
:param y_bounds_reference: optional character or string to use as a reference
|
|
407
|
+
for the ascent and descent. If provided, the ascent and descent will be the y
|
|
408
|
+
extents of the capline reference. This argument is provided to mimic the
|
|
409
|
+
behavior of the query module's `pad_text` function. `pad_text` does no
|
|
410
|
+
inspect font files and relies on Inkscape to measure reference characters.
|
|
411
|
+
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
412
|
+
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
413
|
+
dictionary into a function call.
|
|
414
|
+
:param attributes: additional attributes to set on the text element. There is a
|
|
415
|
+
chance these will cause the font element to exceed the BoundingBox of the
|
|
416
|
+
PaddedText instance.
|
|
417
|
+
:return: a PaddedText instance with a line_gap defined.
|
|
418
|
+
"""
|
|
419
|
+
attributes.update(attrib or {})
|
|
420
|
+
elem = new_element("text", text=text, **attributes)
|
|
421
|
+
padded_inkscape = pad_text(inkscape, elem, y_bounds_reference, font=font)
|
|
422
|
+
padded_fonttools = pad_text_ft(
|
|
423
|
+
font,
|
|
424
|
+
text,
|
|
425
|
+
font_size,
|
|
426
|
+
ascent,
|
|
427
|
+
descent,
|
|
428
|
+
y_bounds_reference=y_bounds_reference,
|
|
429
|
+
attrib=attributes,
|
|
430
|
+
)
|
|
431
|
+
bbox = padded_inkscape.tbox
|
|
432
|
+
rpad = padded_inkscape.rpad
|
|
433
|
+
lpad = padded_inkscape.lpad
|
|
434
|
+
if y_bounds_reference is None:
|
|
435
|
+
tpad = padded_fonttools.tpad
|
|
436
|
+
bpad = padded_fonttools.bpad
|
|
437
|
+
else:
|
|
438
|
+
tpad = padded_inkscape.tpad
|
|
439
|
+
bpad = padded_inkscape.bpad
|
|
440
|
+
return PaddedText(
|
|
441
|
+
elem, bbox, tpad, rpad, bpad, lpad, padded_fonttools.line_gap, font_size
|
|
442
|
+
)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""A protocol for objects that support bounds.
|
|
2
|
+
|
|
3
|
+
This module defines a protocol for objects that can have bounds. Existing and
|
|
4
|
+
future types like BoundingBox, BoundElement, and PaddedText can be arranged,
|
|
5
|
+
aligned, and uniformly scaled by reading and setting their bounds. This is the
|
|
6
|
+
interface needed to support such alignment.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
x (float): The minimum x coordinate. x2 (float): The maximum x coordinate.
|
|
10
|
+
cx (float): The center x coordinate. y (float): The minimum y coordinate. y2
|
|
11
|
+
(float): The maximum y coordinate. cy (float): The center y coordinate.
|
|
12
|
+
width (float): The width of the object. height (float): The height of the
|
|
13
|
+
object. scale (float): The scale of the object.
|
|
14
|
+
|
|
15
|
+
:author: Shay Hill
|
|
16
|
+
:created: 2023-02-15
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
23
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SupportsBounds(Protocol):
|
|
27
|
+
"""Protocol for objects that can have bounds.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
transformation (_Matrix): An svg-style transformation matrix.
|
|
31
|
+
transform (method): Apply a transformation to the object.
|
|
32
|
+
x (float): The minimum x coordinate.
|
|
33
|
+
x2 (float): The maximum x coordinate.
|
|
34
|
+
cx (float): The center x coordinate.
|
|
35
|
+
y (float): The minimum y coordinate.
|
|
36
|
+
y2 (float): The maximum y coordinate.
|
|
37
|
+
cy (float): The center y coordinate.
|
|
38
|
+
width (float): The width of the object.
|
|
39
|
+
height(float): The height of the object.
|
|
40
|
+
scale ((float, float)): The x and yx and y scale of the object.
|
|
41
|
+
|
|
42
|
+
There is no setter for scale. Scale is a function of width and height.
|
|
43
|
+
Setting scale would be ambiguous. because the typical implementation of
|
|
44
|
+
scale would modify the x and y coordinates. If you want to scale an object,
|
|
45
|
+
set width and height.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def transform(
|
|
49
|
+
self,
|
|
50
|
+
transformation: _Matrix | None = None,
|
|
51
|
+
*,
|
|
52
|
+
scale: tuple[float, float] | float | None = None,
|
|
53
|
+
dx: float | None = None,
|
|
54
|
+
dy: float | None = None,
|
|
55
|
+
reverse: bool = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Apply a transformation to the object."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def x(self) -> float:
|
|
62
|
+
"""Return minimum x coordinate."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@x.setter
|
|
66
|
+
def x(self, value: float) -> None:
|
|
67
|
+
"""Set minimum x coordinate.
|
|
68
|
+
|
|
69
|
+
:param value: The minimum x coordinate.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def x2(self) -> float:
|
|
74
|
+
"""Return maximum x coordinate."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
@x2.setter
|
|
78
|
+
def x2(self, value: float) -> None:
|
|
79
|
+
"""Set maximum x coordinate.
|
|
80
|
+
|
|
81
|
+
:param value: The maximum x coordinate.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def cx(self) -> float:
|
|
86
|
+
"""Return center x coordinate."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
@cx.setter
|
|
90
|
+
def cx(self, value: float) -> None:
|
|
91
|
+
"""Set center x coordinate.
|
|
92
|
+
|
|
93
|
+
:param value: The center x coordinate.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def y(self) -> float:
|
|
98
|
+
"""Return minimum y coordinate."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@y.setter
|
|
102
|
+
def y(self, value: float) -> None:
|
|
103
|
+
"""Set minimum y coordinate.
|
|
104
|
+
|
|
105
|
+
:param value: The minimum y coordinate.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def y2(self) -> float:
|
|
110
|
+
"""Return maximum y coordinate."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@y2.setter
|
|
114
|
+
def y2(self, value: float) -> None:
|
|
115
|
+
"""Set maximum y coordinate.
|
|
116
|
+
|
|
117
|
+
:param value: The maximum y coordinate.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def cy(self) -> float:
|
|
122
|
+
"""Return center y coordinate."""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
@cy.setter
|
|
126
|
+
def cy(self, value: float) -> None:
|
|
127
|
+
"""Set center y coordinate.
|
|
128
|
+
|
|
129
|
+
:param value: The center y coordinate.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def width(self) -> float:
|
|
134
|
+
"""Return width of the object."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
@width.setter
|
|
138
|
+
def width(self, value: float) -> None:
|
|
139
|
+
"""Set width of the object.
|
|
140
|
+
|
|
141
|
+
:param value: The width of the object.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def height(self) -> float:
|
|
146
|
+
"""Return height of the object."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
@height.setter
|
|
150
|
+
def height(self, value: float) -> None:
|
|
151
|
+
"""Set height of the object.
|
|
152
|
+
|
|
153
|
+
:param value: The height of the object.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def scale(self) -> tuple[float, float]:
|
|
158
|
+
"""Return scale of the object."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
@scale.setter
|
|
162
|
+
def scale(self, value: tuple[float, float]) -> None:
|
|
163
|
+
"""Return scale of the object.
|
|
164
|
+
|
|
165
|
+
:param value: The scale of the object.
|
|
166
|
+
"""
|
|
167
|
+
...
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
2
|
+
|
|
3
|
+
:author: Shay Hill
|
|
4
|
+
:created: 2024-05-05
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
13
|
+
|
|
14
|
+
from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_union
|
|
15
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import HasBoundingBox
|
|
16
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
20
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
21
|
+
|
|
22
|
+
_Matrix = tuple[float, float, float, float, float, float]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class BoundCollection(HasBoundingBox):
|
|
27
|
+
"""A class to hold a list of bound elements and transform them together.
|
|
28
|
+
|
|
29
|
+
This will transform the individual elements in place.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
blems: list[SupportsBounds | EtreeElement] = dataclasses.field(init=False)
|
|
33
|
+
bbox: BoundingBox = dataclasses.field(init=False)
|
|
34
|
+
|
|
35
|
+
def __init__(self, *blems: SupportsBounds | EtreeElement) -> None:
|
|
36
|
+
"""Initialize the bound collection.
|
|
37
|
+
|
|
38
|
+
:param blems: bound elements to be transformed together
|
|
39
|
+
"""
|
|
40
|
+
self.blems = list(blems)
|
|
41
|
+
self.bbox = new_bbox_union(*self.blems)
|
|
42
|
+
|
|
43
|
+
def transform(
|
|
44
|
+
self,
|
|
45
|
+
transformation: _Matrix | None = None,
|
|
46
|
+
*,
|
|
47
|
+
scale: tuple[float, float] | float | None = None,
|
|
48
|
+
dx: float | None = None,
|
|
49
|
+
dy: float | None = None,
|
|
50
|
+
reverse: bool = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Transform each bound element in self.blems.
|
|
53
|
+
|
|
54
|
+
:param transformation: 2D transformation matrix
|
|
55
|
+
:param scale: optional scale factor
|
|
56
|
+
:param dx: optional x translation
|
|
57
|
+
:param dy: optional y translation
|
|
58
|
+
:param reverse: Transform the element as if it were in a <g> element
|
|
59
|
+
transformed by tmat.
|
|
60
|
+
|
|
61
|
+
Keep track of all compounding transformations in order to have a value for
|
|
62
|
+
self.scale (required for members and to provide access to cumulative
|
|
63
|
+
transforms should this be useful for any reason. This means all
|
|
64
|
+
transformations must be applied to two bounding boxes: a persistant bbox to
|
|
65
|
+
keep track of the scale property and a temporary bbox to isolate each
|
|
66
|
+
transformation.
|
|
67
|
+
"""
|
|
68
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
69
|
+
self.bbox.transform(tmat)
|
|
70
|
+
for blem in self.blems:
|
|
71
|
+
if isinstance(blem, EtreeElement):
|
|
72
|
+
_ = transform_element(blem, tmat, reverse=reverse)
|
|
73
|
+
else:
|
|
74
|
+
blem.transform(tmat, reverse=reverse)
|