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.
@@ -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
- pad_text_mix,
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
- "pad_text_mix",
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: float | None = None,
102
- y: float | None = None,
103
- x2: float | None = None,
104
- y2: float | None = None,
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: float | str) -> EtreeElement:
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 _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
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 is None:
174
- msg = "Element does not have a viewBox attribute."
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
- x, y, width, height = map(float, view_box.split())
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(*_get_view_box(root))
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
- - `pad_text`: uses Inkscape to measure text bounds
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
- - `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.
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
- from copy import deepcopy
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.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
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 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
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 pad_text(
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 `pad_text` and `new_padded_text`.
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
- @overload
115
- def pad_chars_ft(
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
- ) -> BoundElement: ...
175
+ ) -> PaddedText: ...
126
176
 
127
177
 
128
178
  @overload
129
- def pad_chars_ft(
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[BoundElement]: ...
184
+ ) -> list[PaddedText]: ...
140
185
 
141
186
 
142
- def pad_chars_ft(
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
- ) -> BoundElement | list[BoundElement]:
153
- """Create a bound group of paths for each character in the text.
192
+ ) -> PaddedText | list[PaddedText]:
193
+ """Create a new PaddedText instance using fontTools.
154
194
 
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.
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
- attributes.update(attrib or {})
161
- attributes_ = format_attr_dict(**attributes)
162
- attributes_.update(get_svg_font_attributes(font))
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
- _ = 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)
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
- 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}
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 `pad_text` function. `pad_text` does no
266
- inspect font files and relies on Inkscape to measure reference characters.
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
- )