svg-ultralight 0.64.0__py3-none-any.whl → 0.73.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.
- svg_ultralight/__init__.py +8 -6
- svg_ultralight/bounding_boxes/bound_helpers.py +37 -50
- svg_ultralight/bounding_boxes/padded_text_initializers.py +148 -182
- svg_ultralight/bounding_boxes/type_bounding_box.py +31 -14
- svg_ultralight/bounding_boxes/type_padded_list.py +2 -7
- svg_ultralight/bounding_boxes/type_padded_text.py +240 -53
- svg_ultralight/constructors/new_element.py +37 -3
- svg_ultralight/font_tools/comp_results.py +18 -18
- svg_ultralight/font_tools/font_info.py +117 -36
- svg_ultralight/layout.py +37 -18
- svg_ultralight/main.py +26 -16
- svg_ultralight/root_elements.py +6 -4
- svg_ultralight/string_conversion.py +40 -8
- svg_ultralight/unit_conversion.py +104 -27
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/METADATA +1 -1
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/RECORD +17 -18
- svg_ultralight/read_svg.py +0 -58
- {svg_ultralight-0.64.0.dist-info → svg_ultralight-0.73.1.dist-info}/WHEEL +0 -0
svg_ultralight/__init__.py
CHANGED
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
from svg_ultralight.bounding_boxes.bound_helpers import (
|
|
8
8
|
bbox_dict,
|
|
9
9
|
cut_bbox,
|
|
10
|
+
get_bounding_box_from_root,
|
|
10
11
|
new_bbox_rect,
|
|
11
12
|
new_bbox_union,
|
|
12
13
|
new_bound_union,
|
|
13
|
-
new_element_union,
|
|
14
14
|
pad_bbox,
|
|
15
15
|
parse_bound_element,
|
|
16
16
|
)
|
|
17
17
|
from svg_ultralight.bounding_boxes.padded_text_initializers import (
|
|
18
|
+
join_tspans,
|
|
18
19
|
pad_text,
|
|
19
20
|
pad_text_ft,
|
|
20
|
-
|
|
21
|
+
pad_text_inkscape,
|
|
21
22
|
wrap_text_ft,
|
|
22
23
|
)
|
|
23
24
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
@@ -25,10 +26,11 @@ from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
|
|
|
25
26
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
26
27
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
27
28
|
from svg_ultralight.bounding_boxes.type_padded_list import PaddedList
|
|
28
|
-
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
29
|
+
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText, new_padded_union
|
|
29
30
|
from svg_ultralight.constructors.new_element import (
|
|
30
31
|
deepcopy_element,
|
|
31
32
|
new_element,
|
|
33
|
+
new_element_union,
|
|
32
34
|
new_sub_element,
|
|
33
35
|
update_element,
|
|
34
36
|
)
|
|
@@ -48,7 +50,6 @@ from svg_ultralight.query import (
|
|
|
48
50
|
get_bounding_box,
|
|
49
51
|
get_bounding_boxes,
|
|
50
52
|
)
|
|
51
|
-
from svg_ultralight.read_svg import get_bounding_box_from_root, parse
|
|
52
53
|
from svg_ultralight.root_elements import new_svg_root_around_bounds
|
|
53
54
|
from svg_ultralight.string_conversion import (
|
|
54
55
|
format_attr_dict,
|
|
@@ -81,6 +82,7 @@ __all__ = [
|
|
|
81
82
|
"get_bounding_box",
|
|
82
83
|
"get_bounding_box_from_root",
|
|
83
84
|
"get_bounding_boxes",
|
|
85
|
+
"join_tspans",
|
|
84
86
|
"mat_apply",
|
|
85
87
|
"mat_dot",
|
|
86
88
|
"mat_invert",
|
|
@@ -90,6 +92,7 @@ __all__ = [
|
|
|
90
92
|
"new_element",
|
|
91
93
|
"new_element_union",
|
|
92
94
|
"new_metadata",
|
|
95
|
+
"new_padded_union",
|
|
93
96
|
"new_qname",
|
|
94
97
|
"new_sub_element",
|
|
95
98
|
"new_svg_root",
|
|
@@ -97,8 +100,7 @@ __all__ = [
|
|
|
97
100
|
"pad_bbox",
|
|
98
101
|
"pad_text",
|
|
99
102
|
"pad_text_ft",
|
|
100
|
-
"
|
|
101
|
-
"parse",
|
|
103
|
+
"pad_text_inkscape",
|
|
102
104
|
"parse_bound_element",
|
|
103
105
|
"transform_element",
|
|
104
106
|
"update_element",
|
|
@@ -9,57 +9,28 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from lxml import etree
|
|
12
|
-
from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
|
|
13
12
|
|
|
14
13
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
15
14
|
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
16
15
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox, HasBoundingBox
|
|
17
|
-
from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
|
|
18
16
|
from svg_ultralight.constructors import new_element
|
|
17
|
+
from svg_ultralight.constructors.new_element import new_element_union
|
|
19
18
|
from svg_ultralight.layout import PadArg, expand_pad_arg
|
|
19
|
+
from svg_ultralight.unit_conversion import MeasurementArg, to_user_units
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
import os
|
|
23
23
|
|
|
24
|
+
from lxml.etree import (
|
|
25
|
+
_Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
|
|
26
|
+
)
|
|
27
|
+
|
|
24
28
|
from svg_ultralight.attrib_hints import ElemAttrib
|
|
25
29
|
from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
|
|
26
30
|
|
|
27
31
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
28
32
|
|
|
29
33
|
|
|
30
|
-
def new_element_union(
|
|
31
|
-
*elems: EtreeElement | SupportsBounds, **attributes: ElemAttrib
|
|
32
|
-
) -> EtreeElement:
|
|
33
|
-
"""Get the union of any elements found in the given arguments.
|
|
34
|
-
|
|
35
|
-
:param elems: BoundElements, PaddedTexts, or EtreeElements.
|
|
36
|
-
Other arguments will be ignored.
|
|
37
|
-
:return: a new group element containing all elements.
|
|
38
|
-
|
|
39
|
-
This does not support consolidating attributes. E.g., if all elements have the
|
|
40
|
-
same fill color, this will not be recognized and consilidated into a single
|
|
41
|
-
attribute for the group. Too many attributes change their behavior when applied
|
|
42
|
-
to a group.
|
|
43
|
-
"""
|
|
44
|
-
elements_found: list[EtreeElement] = []
|
|
45
|
-
for elem in elems:
|
|
46
|
-
if isinstance(elem, (BoundElement, PaddedText)):
|
|
47
|
-
elements_found.append(elem.elem)
|
|
48
|
-
elif isinstance(elem, EtreeElement):
|
|
49
|
-
elements_found.append(elem)
|
|
50
|
-
|
|
51
|
-
if not elements_found:
|
|
52
|
-
msg = (
|
|
53
|
-
"Cannot find any elements to union. "
|
|
54
|
-
+ "At least one argument must be a "
|
|
55
|
-
+ "BoundElement, PaddedText, or EtreeElement."
|
|
56
|
-
)
|
|
57
|
-
raise ValueError(msg)
|
|
58
|
-
group = new_element("g", **attributes)
|
|
59
|
-
group.extend(elements_found)
|
|
60
|
-
return group
|
|
61
|
-
|
|
62
|
-
|
|
63
34
|
def new_bbox_union(*blems: SupportsBounds | EtreeElement) -> BoundingBox:
|
|
64
35
|
"""Get the union of the bounding boxes of the given elements.
|
|
65
36
|
|
|
@@ -98,10 +69,10 @@ def new_bound_union(*blems: SupportsBounds | EtreeElement) -> BoundElement:
|
|
|
98
69
|
def cut_bbox(
|
|
99
70
|
bbox: SupportsBounds,
|
|
100
71
|
*,
|
|
101
|
-
x:
|
|
102
|
-
y:
|
|
103
|
-
x2:
|
|
104
|
-
y2:
|
|
72
|
+
x: MeasurementArg | None = None,
|
|
73
|
+
y: MeasurementArg | None = None,
|
|
74
|
+
x2: MeasurementArg | None = None,
|
|
75
|
+
y2: MeasurementArg | None = None,
|
|
105
76
|
) -> BoundingBox:
|
|
106
77
|
"""Return a new bounding box with updated limits.
|
|
107
78
|
|
|
@@ -112,10 +83,10 @@ def cut_bbox(
|
|
|
112
83
|
:param y2: the new y2-coordinate.
|
|
113
84
|
:return: a new bounding box with the updated limits.
|
|
114
85
|
"""
|
|
115
|
-
x = bbox.x if x is None else x
|
|
116
|
-
y = bbox.y if y is None else y
|
|
117
|
-
x2 = bbox.x2 if x2 is None else x2
|
|
118
|
-
y2 = bbox.y2 if y2 is None else y2
|
|
86
|
+
x = bbox.x if x is None else to_user_units(x)
|
|
87
|
+
y = bbox.y if y is None else to_user_units(y)
|
|
88
|
+
x2 = bbox.x2 if x2 is None else to_user_units(x2)
|
|
89
|
+
y2 = bbox.y2 if y2 is None else to_user_units(y2)
|
|
119
90
|
x, x2 = sorted((x, x2))
|
|
120
91
|
y, y2 = sorted((y, y2))
|
|
121
92
|
width = x2 - x
|
|
@@ -151,7 +122,7 @@ def bbox_dict(bbox: SupportsBounds) -> dict[str, float]:
|
|
|
151
122
|
return {"x": bbox.x, "y": bbox.y, "width": bbox.width, "height": bbox.height}
|
|
152
123
|
|
|
153
124
|
|
|
154
|
-
def new_bbox_rect(bbox: BoundingBox, **kwargs:
|
|
125
|
+
def new_bbox_rect(bbox: BoundingBox, **kwargs: ElemAttrib) -> EtreeElement:
|
|
155
126
|
"""Return a new rect element with the same dimensions as the bounding box.
|
|
156
127
|
|
|
157
128
|
:param bbox: the bounding box or bound element from which to extract dimensions.
|
|
@@ -160,7 +131,19 @@ def new_bbox_rect(bbox: BoundingBox, **kwargs: float | str) -> EtreeElement:
|
|
|
160
131
|
return new_element("rect", **bbox_dict(bbox), **kwargs)
|
|
161
132
|
|
|
162
133
|
|
|
163
|
-
def
|
|
134
|
+
def new_bound_rect(bbox: BoundingBox, **kwargs: ElemAttrib) -> BoundElement:
|
|
135
|
+
"""Return a new rect element with the same dimensions as the bounding box.
|
|
136
|
+
|
|
137
|
+
:param bbox: the bounding box or bound element from which to extract dimensions.
|
|
138
|
+
:param kwargs: additional attributes for the rect element.
|
|
139
|
+
"""
|
|
140
|
+
elem = new_bbox_rect(bbox, **kwargs)
|
|
141
|
+
return BoundElement(elem, bbox)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_bounding_box_from_root(
|
|
145
|
+
elem: EtreeElement,
|
|
146
|
+
) -> tuple[float, float, MeasurementArg, MeasurementArg]:
|
|
164
147
|
"""Return the view box of an element as a tuple of floats.
|
|
165
148
|
|
|
166
149
|
:param elem: the element from which to extract the view box.
|
|
@@ -170,11 +153,15 @@ def _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
|
|
|
170
153
|
files have a viewBox attribute.
|
|
171
154
|
"""
|
|
172
155
|
view_box = elem.get("viewBox")
|
|
173
|
-
if view_box
|
|
174
|
-
|
|
156
|
+
if view_box:
|
|
157
|
+
x, y, width, height = map(float, view_box.split())
|
|
158
|
+
return x, y, width, height
|
|
159
|
+
width = elem.get("width")
|
|
160
|
+
height = elem.get("height")
|
|
161
|
+
if width is None or height is None:
|
|
162
|
+
msg = "Cannot infer viewBox from element."
|
|
175
163
|
raise ValueError(msg)
|
|
176
|
-
|
|
177
|
-
return x, y, width, height
|
|
164
|
+
return 0, 0, to_user_units(width), to_user_units(height)
|
|
178
165
|
|
|
179
166
|
|
|
180
167
|
def parse_bound_element(svg_file: str | os.PathLike[str]) -> BoundElement:
|
|
@@ -196,5 +183,5 @@ def parse_bound_element(svg_file: str | os.PathLike[str]) -> BoundElement:
|
|
|
196
183
|
elem.extend(list(root))
|
|
197
184
|
if len(elem) == 1:
|
|
198
185
|
elem = elem[0]
|
|
199
|
-
bbox = BoundingBox(*
|
|
186
|
+
bbox = BoundingBox(*get_bounding_box_from_root(root))
|
|
200
187
|
return BoundElement(elem, bbox)
|
|
@@ -2,19 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Three variants:
|
|
4
4
|
|
|
5
|
-
- `
|
|
5
|
+
- `pad_text_inkscape`: uses Inkscape to measure text bounds
|
|
6
6
|
|
|
7
7
|
- `pad_text_ft`: uses fontTools to measure text bounds (faster, and you get line_gap)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
9
|
+
There is a default font size for pad_text_inkscape if an element is passed. There is
|
|
10
|
+
also a default for the other pad_text_ functions, but it taken from the font file and
|
|
11
|
+
is usually 1024, so it won't be easy to miss. The default for standard
|
|
12
|
+
pad_text_inkscape is to prevent surprises if Inksape defaults to font-size 12pt while
|
|
13
|
+
your browser defaults to 16px.
|
|
18
14
|
|
|
19
15
|
:author: Shay Hill
|
|
20
16
|
:created: 2025-06-09
|
|
@@ -22,16 +18,21 @@ to 16px.
|
|
|
22
18
|
|
|
23
19
|
from __future__ import annotations
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
import copy
|
|
22
|
+
import itertools as it
|
|
26
23
|
from typing import TYPE_CHECKING, overload
|
|
27
24
|
|
|
28
25
|
from svg_ultralight.attrib_hints import ElemAttrib
|
|
29
|
-
from svg_ultralight.bounding_boxes.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
from svg_ultralight.bounding_boxes.type_padded_text import (
|
|
27
|
+
FontMetrics,
|
|
28
|
+
PaddedText,
|
|
29
|
+
new_padded_union,
|
|
30
|
+
)
|
|
31
|
+
from svg_ultralight.constructors import update_element
|
|
33
32
|
from svg_ultralight.font_tools.font_info import (
|
|
33
|
+
DATA_TEXT_ESCAPE_CHARS,
|
|
34
34
|
FTFontInfo,
|
|
35
|
+
FTTextInfo,
|
|
35
36
|
get_padded_text_info,
|
|
36
37
|
get_svg_font_attributes,
|
|
37
38
|
)
|
|
@@ -49,12 +50,23 @@ if TYPE_CHECKING:
|
|
|
49
50
|
|
|
50
51
|
DEFAULT_Y_BOUNDS_REFERENCE = "{[|gjpqyf"
|
|
51
52
|
|
|
52
|
-
# A default font size for
|
|
53
|
-
# element.
|
|
54
|
-
DEFAULT_FONT_SIZE_FOR_PAD_TEXT = 12.0 # Default font size for
|
|
53
|
+
# A default font size for pad_text_inkscape if font-size is not specified in the
|
|
54
|
+
# reference element.
|
|
55
|
+
DEFAULT_FONT_SIZE_FOR_PAD_TEXT = 12.0 # Default font size for pad_text_inkscape
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
def
|
|
58
|
+
def _desanitize_svg_data_text(text: str) -> str:
|
|
59
|
+
"""Desanitize a string from an SVG data-text attribute.
|
|
60
|
+
|
|
61
|
+
:param text: The input string to desanitize.
|
|
62
|
+
:return: The desanitized string with XML characters unescaped.
|
|
63
|
+
"""
|
|
64
|
+
for char, escape_seq in DATA_TEXT_ESCAPE_CHARS.items():
|
|
65
|
+
text = text.replace(escape_seq, char)
|
|
66
|
+
return text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pad_text_inkscape(
|
|
58
70
|
inkscape: str | os.PathLike[str],
|
|
59
71
|
text_elem: EtreeElement,
|
|
60
72
|
y_bounds_reference: str | None = None,
|
|
@@ -77,7 +89,8 @@ def pad_text(
|
|
|
77
89
|
This is going to conflict with any font-family, font-style, or other
|
|
78
90
|
font-related attributes *except* font-size. You likely want to use
|
|
79
91
|
`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 `
|
|
92
|
+
use it here to compare results between `pad_text_inkscape` and
|
|
93
|
+
`new_padded_text`.
|
|
81
94
|
:return: a PaddedText instance
|
|
82
95
|
"""
|
|
83
96
|
if y_bounds_reference is None:
|
|
@@ -86,8 +99,8 @@ def pad_text(
|
|
|
86
99
|
_ = update_element(text_elem, **get_svg_font_attributes(font))
|
|
87
100
|
if "font-size" not in text_elem.attrib:
|
|
88
101
|
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)
|
|
102
|
+
rmargin_ref = copy.deepcopy(text_elem)
|
|
103
|
+
capline_ref = copy.deepcopy(text_elem)
|
|
91
104
|
_ = rmargin_ref.attrib.pop("id", None)
|
|
92
105
|
_ = capline_ref.attrib.pop("id", None)
|
|
93
106
|
rmargin_ref.attrib["text-anchor"] = "end"
|
|
@@ -107,113 +120,115 @@ def pad_text(
|
|
|
107
120
|
rpad,
|
|
108
121
|
bpad,
|
|
109
122
|
lpad,
|
|
110
|
-
font_size=float(text_elem.attrib["font-size"]),
|
|
111
123
|
)
|
|
112
124
|
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
def _remove_svg_font_attributes(attributes: dict[str, ElemAttrib]) -> dict[str, str]:
|
|
127
|
+
"""Remove svg font attributes from the attributes dict.
|
|
128
|
+
|
|
129
|
+
These are either not required when explicitly passing a font file, not relevant,
|
|
130
|
+
or not supported by fontTools.
|
|
131
|
+
"""
|
|
132
|
+
attributes_ = format_attr_dict(**attributes)
|
|
133
|
+
keys_to_remove = [
|
|
134
|
+
"font-size",
|
|
135
|
+
"font-family",
|
|
136
|
+
"font-style",
|
|
137
|
+
"font-weight",
|
|
138
|
+
"font-stretch",
|
|
139
|
+
]
|
|
140
|
+
return {k: v for k, v in attributes_.items() if k not in keys_to_remove}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def join_tspans(
|
|
116
144
|
font: str | os.PathLike[str],
|
|
145
|
+
tspans: list[PaddedText],
|
|
146
|
+
attrib: OptionalElemAttribMapping = None,
|
|
147
|
+
) -> PaddedText:
|
|
148
|
+
"""Join multiple PaddedText elements into a single BoundElement.
|
|
149
|
+
|
|
150
|
+
:param font: the one font file used for kerning.
|
|
151
|
+
:param tspans: list of tspan elements to join (each an output from pad_chars_ft).
|
|
152
|
+
|
|
153
|
+
This is limited and will not handle arbitrary text elements (only `g` elements
|
|
154
|
+
with a "data-text" attribute equal to the character(s) in the tspan). Will also
|
|
155
|
+
not handle scaled PaddedText instances. This is for joining tspans originally
|
|
156
|
+
after they are created and all using similar fonts.
|
|
157
|
+
"""
|
|
158
|
+
font_info = FTFontInfo(font)
|
|
159
|
+
for left, right in it.pairwise(tspans):
|
|
160
|
+
l_joint = _desanitize_svg_data_text(left.elem.attrib["data-text"])[-1]
|
|
161
|
+
r_joint = _desanitize_svg_data_text(right.elem.attrib["data-text"])[0]
|
|
162
|
+
l_name = font_info.get_glyph_name(l_joint)
|
|
163
|
+
r_name = font_info.get_glyph_name(r_joint)
|
|
164
|
+
kern = font_info.kern_table.get((l_name, r_name), 0)
|
|
165
|
+
right.x = left.x2 + kern
|
|
166
|
+
return new_padded_union(*tspans, **attrib or {})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@overload
|
|
170
|
+
def pad_text(
|
|
171
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
117
172
|
text: str,
|
|
118
173
|
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
174
|
**attributes: ElemAttrib,
|
|
125
|
-
) ->
|
|
175
|
+
) -> PaddedText: ...
|
|
126
176
|
|
|
127
177
|
|
|
128
178
|
@overload
|
|
129
|
-
def
|
|
130
|
-
font: str | os.PathLike[str],
|
|
179
|
+
def pad_text(
|
|
180
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
131
181
|
text: list[str],
|
|
132
182
|
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
183
|
**attributes: ElemAttrib,
|
|
139
|
-
) -> list[
|
|
184
|
+
) -> list[PaddedText]: ...
|
|
140
185
|
|
|
141
186
|
|
|
142
|
-
def
|
|
143
|
-
font: str | os.PathLike[str],
|
|
187
|
+
def pad_text(
|
|
188
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
144
189
|
text: str | list[str],
|
|
145
190
|
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
191
|
**attributes: ElemAttrib,
|
|
152
|
-
) ->
|
|
153
|
-
"""Create a
|
|
192
|
+
) -> PaddedText | list[PaddedText]:
|
|
193
|
+
"""Create a new PaddedText instance using fontTools.
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
:param font: path to a font file.
|
|
196
|
+
:param text: the text of the text element or a list of text strings.
|
|
197
|
+
:param font_size: the font size to use. Skip for default font size. This can
|
|
198
|
+
always be set later, but the argument is useful if you're working with fonts
|
|
199
|
+
that have different native font sizes (usually 1000, 1024, or 2048).
|
|
159
200
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
attributes_ = _remove_svg_font_attributes(attributes)
|
|
202
|
+
font = font if isinstance(font, FTFontInfo) else FTFontInfo(font)
|
|
203
|
+
metrics = FontMetrics(
|
|
204
|
+
font.units_per_em,
|
|
205
|
+
font.ascent,
|
|
206
|
+
font.descent,
|
|
207
|
+
font.line_gap,
|
|
208
|
+
font.cap_height,
|
|
209
|
+
font.x_height,
|
|
210
|
+
)
|
|
163
211
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
plems: list[PaddedText] = []
|
|
213
|
+
for t in [text] if isinstance(text, str) else text:
|
|
214
|
+
text_info = FTTextInfo(font, t)
|
|
215
|
+
elem = text_info.new_element(**attributes_)
|
|
216
|
+
plem = PaddedText(
|
|
217
|
+
elem, text_info.bbox, *text_info.padding, metrics=copy.copy(metrics)
|
|
218
|
+
)
|
|
219
|
+
if font_size:
|
|
220
|
+
plem.font_size = font_size
|
|
221
|
+
plems.append(plem)
|
|
222
|
+
font.maybe_close()
|
|
169
223
|
|
|
170
|
-
input_one_text_item = False
|
|
171
224
|
if isinstance(text, str):
|
|
172
|
-
|
|
173
|
-
|
|
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}
|
|
225
|
+
return plems[0]
|
|
226
|
+
return plems
|
|
212
227
|
|
|
213
228
|
|
|
214
229
|
@overload
|
|
215
230
|
def pad_text_ft(
|
|
216
|
-
font: str | os.PathLike[str],
|
|
231
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
217
232
|
text: str,
|
|
218
233
|
font_size: float | None = None,
|
|
219
234
|
ascent: float | None = None,
|
|
@@ -227,7 +242,7 @@ def pad_text_ft(
|
|
|
227
242
|
|
|
228
243
|
@overload
|
|
229
244
|
def pad_text_ft(
|
|
230
|
-
font: str | os.PathLike[str],
|
|
245
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
231
246
|
text: list[str],
|
|
232
247
|
font_size: float | None = None,
|
|
233
248
|
ascent: float | None = None,
|
|
@@ -240,7 +255,7 @@ def pad_text_ft(
|
|
|
240
255
|
|
|
241
256
|
|
|
242
257
|
def pad_text_ft(
|
|
243
|
-
font: str | os.PathLike[str],
|
|
258
|
+
font: str | os.PathLike[str] | FTFontInfo,
|
|
244
259
|
text: str | list[str],
|
|
245
260
|
font_size: float | None = None,
|
|
246
261
|
ascent: float | None = None,
|
|
@@ -262,8 +277,9 @@ def pad_text_ft(
|
|
|
262
277
|
:param y_bounds_reference: optional character or string to use as a reference
|
|
263
278
|
for the ascent and descent. If provided, the ascent and descent will be the y
|
|
264
279
|
extents of the capline reference. This argument is provided to mimic the
|
|
265
|
-
behavior of the query module's `
|
|
266
|
-
inspect font files and relies on Inkscape to
|
|
280
|
+
behavior of the query module's `pad_text_inkscape` function.
|
|
281
|
+
`pad_text_inkscape` does not inspect font files and relies on Inkscape to
|
|
282
|
+
measure reference characters.
|
|
267
283
|
:param attrib: optionally pass additional attributes as a mapping instead of as
|
|
268
284
|
anonymous kwargs. This is useful for pleasing the linter when unpacking a
|
|
269
285
|
dictionary into a function call.
|
|
@@ -282,23 +298,33 @@ def pad_text_ft(
|
|
|
282
298
|
text = [text]
|
|
283
299
|
|
|
284
300
|
font_info = FTFontInfo(font)
|
|
301
|
+
metrics = FontMetrics(
|
|
302
|
+
font_info.units_per_em,
|
|
303
|
+
font_info.ascent,
|
|
304
|
+
font_info.descent,
|
|
305
|
+
font_info.line_gap,
|
|
306
|
+
font_info.cap_height,
|
|
307
|
+
font_info.x_height,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
elems: list[PaddedText] = []
|
|
311
|
+
for text_item in text:
|
|
312
|
+
ti = get_padded_text_info(
|
|
313
|
+
font_info,
|
|
314
|
+
text_item,
|
|
315
|
+
None, # font_size
|
|
316
|
+
ascent,
|
|
317
|
+
descent,
|
|
318
|
+
y_bounds_reference=y_bounds_reference,
|
|
319
|
+
)
|
|
320
|
+
elem = ti.new_element(**attributes_)
|
|
321
|
+
plem = PaddedText(elem, ti.bbox, *ti.padding, metrics=copy.copy(metrics))
|
|
322
|
+
if font_size:
|
|
323
|
+
plem.font_size = font_size
|
|
324
|
+
elems.append(plem)
|
|
325
|
+
|
|
326
|
+
font_info.maybe_close()
|
|
285
327
|
|
|
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
328
|
if input_one_text_item:
|
|
303
329
|
return elems[0]
|
|
304
330
|
return elems
|
|
@@ -380,63 +406,3 @@ def wrap_text_ft(
|
|
|
380
406
|
if input_one_text_item:
|
|
381
407
|
return all_wrapped[0]
|
|
382
408
|
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
|
-
)
|