svg-ultralight 0.48.0__py3-none-any.whl → 0.50.2__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.

Files changed (37) hide show
  1. svg_ultralight/__init__.py +108 -105
  2. svg_ultralight/animate.py +40 -40
  3. svg_ultralight/attrib_hints.py +14 -14
  4. svg_ultralight/bounding_boxes/__init__.py +5 -5
  5. svg_ultralight/bounding_boxes/bound_helpers.py +189 -189
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
  7. svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
  9. svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
  11. svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
  12. svg_ultralight/constructors/__init__.py +14 -14
  13. svg_ultralight/constructors/new_element.py +120 -115
  14. svg_ultralight/font_tools/__init__.py +5 -5
  15. svg_ultralight/font_tools/comp_results.py +295 -293
  16. svg_ultralight/font_tools/font_info.py +793 -792
  17. svg_ultralight/image_ops.py +156 -156
  18. svg_ultralight/inkscape.py +261 -261
  19. svg_ultralight/layout.py +291 -291
  20. svg_ultralight/main.py +183 -198
  21. svg_ultralight/metadata.py +122 -122
  22. svg_ultralight/nsmap.py +36 -36
  23. svg_ultralight/py.typed +5 -0
  24. svg_ultralight/query.py +254 -249
  25. svg_ultralight/read_svg.py +58 -0
  26. svg_ultralight/root_elements.py +87 -87
  27. svg_ultralight/string_conversion.py +244 -244
  28. svg_ultralight/strings/__init__.py +21 -13
  29. svg_ultralight/strings/svg_strings.py +106 -67
  30. svg_ultralight/transformations.py +140 -141
  31. svg_ultralight/unit_conversion.py +247 -248
  32. {svg_ultralight-0.48.0.dist-info → svg_ultralight-0.50.2.dist-info}/METADATA +208 -214
  33. svg_ultralight-0.50.2.dist-info/RECORD +34 -0
  34. svg_ultralight-0.50.2.dist-info/WHEEL +4 -0
  35. svg_ultralight-0.48.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.48.0.dist-info/top_level.txt +0 -1
@@ -1,293 +1,295 @@
1
- """Compare results between Inkscape and fontTools.
2
-
3
- Function `check_font_tools_alignment` will let you know if it's relatively safe to
4
- use `pad_text_mix` or `pad_text_ft`, which improve `pad_text` by assigning `line_gap`
5
- values to the resulting PaddedText instance and by aligning with the actual descent
6
- and ascent of a font instead of by attempting to infer these from a referenve string.
7
-
8
- See Enum `FontBboxError` for the possible error codes and their meanings returned by
9
- `check_font`.
10
-
11
- You can use `draw_comparison` to debug or explore differences between fontTools and
12
- Inkscape.
13
-
14
- :author: Shay Hill
15
- :created: 2025-06-08
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import enum
21
- import itertools as it
22
- import string
23
- import sys
24
- from pathlib import Path
25
- from typing import TYPE_CHECKING
26
-
27
- from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_rect, pad_bbox
28
- from svg_ultralight.bounding_boxes.padded_text_initializers import (
29
- DEFAULT_Y_BOUNDS_REFERENCE,
30
- pad_text,
31
- pad_text_ft,
32
- )
33
- from svg_ultralight.constructors import new_element
34
- from svg_ultralight.font_tools.font_info import FTFontInfo, get_svg_font_attributes
35
- from svg_ultralight.inkscape import write_root
36
- from svg_ultralight.root_elements import new_svg_root_around_bounds
37
-
38
- if TYPE_CHECKING:
39
- import os
40
- from collections.abc import Iterator
41
-
42
- from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
43
-
44
-
45
- class FontBboxError(enum.Enum):
46
- """Classify the type of error between Inkscape and fontTools bounding boxes.
47
-
48
- INIT: Use `pad_text`.
49
-
50
- FontTools failed to run. This can happen with fonts that, inentionally or
51
- not, do not have the required tables or character sets to build a bounding box
52
- around the TEXT_TEXT. You can only use the `pad_text` PaddedText constructor.
53
- This font may work with other or ascii-only text.
54
-
55
- ELEM_Y: Use `pad_text` or `pad_text_mix` with cautions.
56
-
57
- The y coordinate of the element bounding box is off by more than 1% of
58
- the height. This error matters, because the y coordinates are used by
59
- `pad_bbox_ft` and `pad_bbox_mix`. You can use either of these functions with a
60
- y_bounds_reference element and accept some potential error in `line_gap` or
61
- explicitly pass `ascent` and `descent` values to `pad_text_ft` or `pad_text_mix`.
62
-
63
- SAFE_ELEM_X: Use `pad_text_mix`.
64
-
65
- The y bounds are accurate, but the x coordinate of the element
66
- bounding box is off by more than 1%. This is called "safe" because it is not used
67
- by pad_bbox_mix, but you cannot use `pad_text_ft` without expecting BoundingBox
68
- inaccuracies.
69
-
70
- LINE_Y: Use `pad_text` or `pad_text_mix` with caution.
71
-
72
- All of the above match, but the y coordinate of the line bounding box
73
- (the padded bounding box) is off by more than 1% of the height. This error
74
- matters as does ELEM_Y, but it does not exist for any font on my system. Fonts
75
- without ELEM_Y errors should not have LINE_Y errors.
76
-
77
- SAFE_LINE_X: Use `pad_text_mix`.
78
-
79
- All of the above match, but the x coordinate of the line bounding
80
- box (the padded bounding box) is off by more than 1%. This is safe or unsafe as
81
- SAFE_ELEM_X, but also does not exist for any font on my system.
82
-
83
- NO_ERROR: Use `pad_text_ft`.
84
-
85
- No errors were found. The bounding boxes match within 1% of the height.
86
- You can use `pad_text_ft` to get the same result as `pad_text` or `pad_text_mix`
87
- without the delay caused by an Inkscape call.
88
- """
89
-
90
- INIT = enum.auto()
91
- ELEM_Y = enum.auto()
92
- SAFE_ELEM_X = enum.auto()
93
- LINE_Y = enum.auto()
94
- SAFE_LINE_X = enum.auto()
95
- NO_ERROR = enum.auto()
96
-
97
-
98
- # ===================================================================================
99
- # Produce some commonly used Western UTF-8 characters for test text.
100
- # ===================================================================================
101
-
102
-
103
- def _get_western_utf8() -> str:
104
- """Return a string of the commonly used Western UTF-8 character set."""
105
- western = " ".join(
106
- [
107
- string.ascii_lowercase,
108
- string.ascii_uppercase,
109
- string.digits,
110
- string.punctuation,
111
- "áÁéÉíÍóÓúÚñÑäÄëËïÏöÖüÜçÇàÀèÈìÌòÒùÙâÂêÊîÎôÔûÛãÃõÕåÅæÆøØœŒßÿŸ",
112
- ]
113
- )
114
- return western + " "
115
-
116
-
117
- DEFAULT_TEST_TEXT = _get_western_utf8()
118
-
119
-
120
- def _format_bbox_error(
121
- bbox_a: BoundingBox, bbox_b: BoundingBox
122
- ) -> tuple[int, int, int, int]:
123
- """Return the difference between two bounding boxes as a percentage of height."""
124
- width = bbox_a.width
125
- height = bbox_a.height
126
- diff = (
127
- bbox_b.x - bbox_a.x,
128
- bbox_b.y - bbox_a.y,
129
- bbox_b.width - bbox_a.width,
130
- bbox_b.height - bbox_a.height,
131
- )
132
- scaled_diff = (x / y for x, y in zip(diff, (height, height, width, height)))
133
- dx, dy, dw, dh = (int(x * 100) for x in scaled_diff)
134
- return dx, dy, dw, dh
135
-
136
-
137
- def check_font_tools_alignment(
138
- inkscape: str | os.PathLike[str],
139
- font: str | os.PathLike[str],
140
- text: str | None = None,
141
- ) -> tuple[FontBboxError, tuple[int, int, int, int] | None]:
142
- """Return an error code and the difference b/t Inkscape and fontTools bboxes.
143
-
144
- :param inkscape: path to an Inkscape executable
145
- :param font_path: path to the font file
146
- :return: a tuple of the error code and the percentage difference between the
147
- bounding boxes as a tuple of (dx, dy, dw, dh) or (error, None) if there was
148
- an error initializing fontTools.
149
- """
150
- if text is None:
151
- text = DEFAULT_TEST_TEXT
152
- try:
153
- svg_attribs = get_svg_font_attributes(font)
154
- text_elem = new_element("text", **svg_attribs, text=text)
155
- rslt_pt = pad_text(inkscape, text_elem)
156
- rslt_ft = pad_text_ft(
157
- font,
158
- text,
159
- y_bounds_reference=DEFAULT_Y_BOUNDS_REFERENCE,
160
- )
161
- except Exception:
162
- return FontBboxError.INIT, None
163
-
164
- error = _format_bbox_error(rslt_pt.unpadded_bbox, rslt_ft.unpadded_bbox)
165
- if error[1] or error[3]:
166
- return FontBboxError.ELEM_Y, error
167
- if error[0] or error[2]:
168
- return FontBboxError.SAFE_ELEM_X, error
169
-
170
- error = _format_bbox_error(rslt_pt.bbox, rslt_ft.bbox)
171
- if error[1] or error[3]:
172
- return FontBboxError.LINE_Y, error
173
- if error[0] or error[2]:
174
- return FontBboxError.SAFE_LINE_X, error
175
-
176
- return FontBboxError.NO_ERROR, None
177
-
178
-
179
- def draw_comparison(
180
- inkscape: str | os.PathLike[str],
181
- output: str | os.PathLike[str],
182
- font: str | os.PathLike[str],
183
- text: str | None = None,
184
- ) -> None:
185
- """Draw a font in Inkscape and fontTools.
186
-
187
- :param inkscape: path to an Inkscape executable
188
- :param output: path to the output SVG file
189
- :param font: path to the font file
190
- :param text: the text to render. If None, the font name will be used.
191
- :effect: Writes an SVG file to the output path.
192
-
193
- Compare the rendering and bounding boxes of a font in Inkscape and fontTools. The
194
- bounding boxes drawn will always be accurate, but some fonts will not render the
195
- Inkscape version in a browser. Conversely, Inskcape will not render the fontTools
196
- version in Inkscape, because Inkscape does not read locally linked fonts. It
197
- usually works, and it a good place to start if you'd like to compare fontTools
198
- and Inkscape results.
199
- """
200
- if text is None:
201
- text = Path(font).stem
202
- font_size = 12
203
- font_attributes = get_svg_font_attributes(font)
204
- text_elem = new_element(
205
- "text",
206
- text=text,
207
- **font_attributes,
208
- font_size=font_size,
209
- fill="none",
210
- stroke="green",
211
- stroke_width=0.1,
212
- )
213
- padded_pt = pad_text(inkscape, text_elem)
214
- padded_ft = pad_text_ft(
215
- font,
216
- text,
217
- font_size,
218
- y_bounds_reference=DEFAULT_Y_BOUNDS_REFERENCE,
219
- fill="none",
220
- stroke="orange",
221
- stroke_width=0.1,
222
- )
223
-
224
- root = new_svg_root_around_bounds(pad_bbox(padded_pt.bbox, 1))
225
- root.append(
226
- new_bbox_rect(
227
- padded_pt.unpadded_bbox, fill="none", stroke_width=0.07, stroke="red"
228
- )
229
- )
230
- root.append(
231
- new_bbox_rect(
232
- padded_ft.unpadded_bbox, fill="none", stroke_width=0.05, stroke="blue"
233
- )
234
- )
235
- root.append(padded_pt.elem)
236
- root.append(padded_ft.elem)
237
- _ = sys.stdout.write(f"{Path(font).stem} comparison drawn at {output}.\n")
238
- _ = write_root(inkscape, Path(output), root)
239
-
240
-
241
- def _iter_fonts(*fonts_dirs: Path) -> Iterator[Path]:
242
- """Yield a path to each ttf and otf file in the given directories.
243
-
244
- :param fonts_dir: directory to search for ttf and otf files, multiple ok
245
- :yield: paths to ttf and otf files in the given directories
246
-
247
- A helper function for _test_every_font_on_my_system.
248
- """
249
- if not fonts_dirs:
250
- return
251
- head, *tail = fonts_dirs
252
- ttf_files = head.glob("*.[tt][tt][ff]")
253
- otf_files = head.glob("*.[oO][tT][fF]")
254
- yield from it.chain(ttf_files, otf_files)
255
- yield from _iter_fonts(*tail)
256
-
257
-
258
- def _test_every_font_on_my_system(
259
- inkscape: str | os.PathLike[str],
260
- font_dirs: list[Path],
261
- text: str | None = None,
262
- ) -> None:
263
- """Test every font on my system."""
264
- if not Path(inkscape).with_suffix(".exe").exists():
265
- _ = sys.stdout.write(f"Inkscape not found at {inkscape}\n")
266
- return
267
- font_dirs = [x for x in font_dirs if x.exists()]
268
- if not font_dirs:
269
- _ = sys.stdout.write("No font directories found.\n")
270
- return
271
-
272
- counts = dict.fromkeys(FontBboxError, 0)
273
- for font_path in _iter_fonts(*font_dirs):
274
- error, diff = check_font_tools_alignment(inkscape, font_path, text)
275
- counts[error] += 1
276
- if error is not FontBboxError.NO_ERROR:
277
- _ = sys.stdout.write(f"Error with {font_path.name}: {error.name} {diff}\n")
278
- for k, v in counts.items():
279
- _ = sys.stdout.write(f"{k.name}: {v}\n")
280
-
281
-
282
- if __name__ == "__main__":
283
- _INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
284
- _FONT_DIRS = [
285
- Path(r"C:\Windows\Fonts"),
286
- Path(r"C:\Users\shaya\AppData\Local\Microsoft\Windows\Fonts"),
287
- ]
288
- _test_every_font_on_my_system(_INKSCAPE, _FONT_DIRS)
289
-
290
- font = Path(r"C:\Windows\Fonts\arial.ttf")
291
- font = Path("C:/Windows/Fonts/Aptos-Display-Bold.ttf")
292
- info = FTFontInfo(font)
293
- draw_comparison(_INKSCAPE, "temp.svg", font, "AApple")
1
+ """Compare results between Inkscape and fontTools.
2
+
3
+ Function `check_font_tools_alignment` will let you know if it's relatively safe to
4
+ use `pad_text_mix` or `pad_text_ft`, which improve `pad_text` by assigning `line_gap`
5
+ values to the resulting PaddedText instance and by aligning with the actual descent
6
+ and ascent of a font instead of by attempting to infer these from a referenve string.
7
+
8
+ See Enum `FontBboxError` for the possible error codes and their meanings returned by
9
+ `check_font`.
10
+
11
+ You can use `draw_comparison` to debug or explore differences between fontTools and
12
+ Inkscape.
13
+
14
+ :author: Shay Hill
15
+ :created: 2025-06-08
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import enum
21
+ import itertools as it
22
+ import string
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING
26
+
27
+ from svg_ultralight.bounding_boxes.bound_helpers import new_bbox_rect, pad_bbox
28
+ from svg_ultralight.bounding_boxes.padded_text_initializers import (
29
+ DEFAULT_Y_BOUNDS_REFERENCE,
30
+ pad_text,
31
+ pad_text_ft,
32
+ )
33
+ from svg_ultralight.constructors import new_element
34
+ from svg_ultralight.font_tools.font_info import FTFontInfo, get_svg_font_attributes
35
+ from svg_ultralight.inkscape import write_root
36
+ from svg_ultralight.root_elements import new_svg_root_around_bounds
37
+
38
+ if TYPE_CHECKING:
39
+ import os
40
+ from collections.abc import Iterator
41
+
42
+ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
43
+
44
+
45
+ class FontBboxError(enum.Enum):
46
+ """Classify the type of error between Inkscape and fontTools bounding boxes.
47
+
48
+ INIT: Use `pad_text`.
49
+
50
+ FontTools failed to run. This can happen with fonts that, inentionally or
51
+ not, do not have the required tables or character sets to build a bounding box
52
+ around the TEXT_TEXT. You can only use the `pad_text` PaddedText constructor.
53
+ This font may work with other or ascii-only text.
54
+
55
+ ELEM_Y: Use `pad_text` or `pad_text_mix` with cautions.
56
+
57
+ The y coordinate of the element bounding box is off by more than 1% of
58
+ the height. This error matters, because the y coordinates are used by
59
+ `pad_bbox_ft` and `pad_bbox_mix`. You can use either of these functions with a
60
+ y_bounds_reference element and accept some potential error in `line_gap` or
61
+ explicitly pass `ascent` and `descent` values to `pad_text_ft` or `pad_text_mix`.
62
+
63
+ SAFE_ELEM_X: Use `pad_text_mix`.
64
+
65
+ The y bounds are accurate, but the x coordinate of the element
66
+ bounding box is off by more than 1%. This is called "safe" because it is not used
67
+ by pad_bbox_mix, but you cannot use `pad_text_ft` without expecting BoundingBox
68
+ inaccuracies.
69
+
70
+ LINE_Y: Use `pad_text` or `pad_text_mix` with caution.
71
+
72
+ All of the above match, but the y coordinate of the line bounding box
73
+ (the padded bounding box) is off by more than 1% of the height. This error
74
+ matters as does ELEM_Y, but it does not exist for any font on my system. Fonts
75
+ without ELEM_Y errors should not have LINE_Y errors.
76
+
77
+ SAFE_LINE_X: Use `pad_text_mix`.
78
+
79
+ All of the above match, but the x coordinate of the line bounding
80
+ box (the padded bounding box) is off by more than 1%. This is safe or unsafe as
81
+ SAFE_ELEM_X, but also does not exist for any font on my system.
82
+
83
+ NO_ERROR: Use `pad_text_ft`.
84
+
85
+ No errors were found. The bounding boxes match within 1% of the height.
86
+ You can use `pad_text_ft` to get the same result as `pad_text` or `pad_text_mix`
87
+ without the delay caused by an Inkscape call.
88
+ """
89
+
90
+ INIT = enum.auto()
91
+ ELEM_Y = enum.auto()
92
+ SAFE_ELEM_X = enum.auto()
93
+ LINE_Y = enum.auto()
94
+ SAFE_LINE_X = enum.auto()
95
+ NO_ERROR = enum.auto()
96
+
97
+
98
+ # ===================================================================================
99
+ # Produce some commonly used Western UTF-8 characters for test text.
100
+ # ===================================================================================
101
+
102
+
103
+ def _get_western_utf8() -> str:
104
+ """Return a string of the commonly used Western UTF-8 character set."""
105
+ western = " ".join(
106
+ [
107
+ string.ascii_lowercase,
108
+ string.ascii_uppercase,
109
+ string.digits,
110
+ string.punctuation,
111
+ "áÁéÉíÍóÓúÚñÑäÄëËïÏöÖüÜçÇàÀèÈìÌòÒùÙâÂêÊîÎôÔûÛãÃõÕåÅæÆøØœŒßÿŸ",
112
+ ]
113
+ )
114
+ return western + " "
115
+
116
+
117
+ DEFAULT_TEST_TEXT = _get_western_utf8()
118
+
119
+
120
+ def _format_bbox_error(
121
+ bbox_a: BoundingBox, bbox_b: BoundingBox
122
+ ) -> tuple[int, int, int, int]:
123
+ """Return the difference between two bounding boxes as a percentage of height."""
124
+ width = bbox_a.width
125
+ height = bbox_a.height
126
+ diff = (
127
+ bbox_b.x - bbox_a.x,
128
+ bbox_b.y - bbox_a.y,
129
+ bbox_b.width - bbox_a.width,
130
+ bbox_b.height - bbox_a.height,
131
+ )
132
+ scaled_diff = (
133
+ x / y for x, y in zip(diff, (height, height, width, height), strict=True)
134
+ )
135
+ dx, dy, dw, dh = (int(x * 100) for x in scaled_diff)
136
+ return dx, dy, dw, dh
137
+
138
+
139
+ def check_font_tools_alignment(
140
+ inkscape: str | os.PathLike[str],
141
+ font: str | os.PathLike[str],
142
+ text: str | None = None,
143
+ ) -> tuple[FontBboxError, tuple[int, int, int, int] | None]:
144
+ """Return an error code and the difference b/t Inkscape and fontTools bboxes.
145
+
146
+ :param inkscape: path to an Inkscape executable
147
+ :param font_path: path to the font file
148
+ :return: a tuple of the error code and the percentage difference between the
149
+ bounding boxes as a tuple of (dx, dy, dw, dh) or (error, None) if there was
150
+ an error initializing fontTools.
151
+ """
152
+ if text is None:
153
+ text = DEFAULT_TEST_TEXT
154
+ try:
155
+ svg_attribs = get_svg_font_attributes(font)
156
+ text_elem = new_element("text", **svg_attribs, text=text)
157
+ rslt_pt = pad_text(inkscape, text_elem)
158
+ rslt_ft = pad_text_ft(
159
+ font,
160
+ text,
161
+ y_bounds_reference=DEFAULT_Y_BOUNDS_REFERENCE,
162
+ )
163
+ except Exception:
164
+ return FontBboxError.INIT, None
165
+
166
+ error = _format_bbox_error(rslt_pt.unpadded_bbox, rslt_ft.unpadded_bbox)
167
+ if error[1] or error[3]:
168
+ return FontBboxError.ELEM_Y, error
169
+ if error[0] or error[2]:
170
+ return FontBboxError.SAFE_ELEM_X, error
171
+
172
+ error = _format_bbox_error(rslt_pt.bbox, rslt_ft.bbox)
173
+ if error[1] or error[3]:
174
+ return FontBboxError.LINE_Y, error
175
+ if error[0] or error[2]:
176
+ return FontBboxError.SAFE_LINE_X, error
177
+
178
+ return FontBboxError.NO_ERROR, None
179
+
180
+
181
+ def draw_comparison(
182
+ inkscape: str | os.PathLike[str],
183
+ output: str | os.PathLike[str],
184
+ font: str | os.PathLike[str],
185
+ text: str | None = None,
186
+ ) -> None:
187
+ """Draw a font in Inkscape and fontTools.
188
+
189
+ :param inkscape: path to an Inkscape executable
190
+ :param output: path to the output SVG file
191
+ :param font: path to the font file
192
+ :param text: the text to render. If None, the font name will be used.
193
+ :effect: Writes an SVG file to the output path.
194
+
195
+ Compare the rendering and bounding boxes of a font in Inkscape and fontTools. The
196
+ bounding boxes drawn will always be accurate, but some fonts will not render the
197
+ Inkscape version in a browser. Conversely, Inskcape will not render the fontTools
198
+ version in Inkscape, because Inkscape does not read locally linked fonts. It
199
+ usually works, and it a good place to start if you'd like to compare fontTools
200
+ and Inkscape results.
201
+ """
202
+ if text is None:
203
+ text = Path(font).stem
204
+ font_size = 12
205
+ font_attributes = get_svg_font_attributes(font)
206
+ text_elem = new_element(
207
+ "text",
208
+ text=text,
209
+ **font_attributes,
210
+ font_size=font_size,
211
+ fill="none",
212
+ stroke="green",
213
+ stroke_width=0.1,
214
+ )
215
+ padded_pt = pad_text(inkscape, text_elem)
216
+ padded_ft = pad_text_ft(
217
+ font,
218
+ text,
219
+ font_size,
220
+ y_bounds_reference=DEFAULT_Y_BOUNDS_REFERENCE,
221
+ fill="none",
222
+ stroke="orange",
223
+ stroke_width=0.1,
224
+ )
225
+
226
+ root = new_svg_root_around_bounds(pad_bbox(padded_pt.bbox, 1))
227
+ root.append(
228
+ new_bbox_rect(
229
+ padded_pt.unpadded_bbox, fill="none", stroke_width=0.07, stroke="red"
230
+ )
231
+ )
232
+ root.append(
233
+ new_bbox_rect(
234
+ padded_ft.unpadded_bbox, fill="none", stroke_width=0.05, stroke="blue"
235
+ )
236
+ )
237
+ root.append(padded_pt.elem)
238
+ root.append(padded_ft.elem)
239
+ _ = sys.stdout.write(f"{Path(font).stem} comparison drawn at {output}.\n")
240
+ _ = write_root(inkscape, Path(output), root)
241
+
242
+
243
+ def _iter_fonts(*fonts_dirs: Path) -> Iterator[Path]:
244
+ """Yield a path to each ttf and otf file in the given directories.
245
+
246
+ :param fonts_dir: directory to search for ttf and otf files, multiple ok
247
+ :yield: paths to ttf and otf files in the given directories
248
+
249
+ A helper function for _test_every_font_on_my_system.
250
+ """
251
+ if not fonts_dirs:
252
+ return
253
+ head, *tail = fonts_dirs
254
+ ttf_files = head.glob("*.[tt][tt][ff]")
255
+ otf_files = head.glob("*.[oO][tT][fF]")
256
+ yield from it.chain(ttf_files, otf_files)
257
+ yield from _iter_fonts(*tail)
258
+
259
+
260
+ def _test_every_font_on_my_system(
261
+ inkscape: str | os.PathLike[str],
262
+ font_dirs: list[Path],
263
+ text: str | None = None,
264
+ ) -> None:
265
+ """Test every font on my system."""
266
+ if not Path(inkscape).with_suffix(".exe").exists():
267
+ _ = sys.stdout.write(f"Inkscape not found at {inkscape}\n")
268
+ return
269
+ font_dirs = [x for x in font_dirs if x.exists()]
270
+ if not font_dirs:
271
+ _ = sys.stdout.write("No font directories found.\n")
272
+ return
273
+
274
+ counts = dict.fromkeys(FontBboxError, 0)
275
+ for font_path in _iter_fonts(*font_dirs):
276
+ error, diff = check_font_tools_alignment(inkscape, font_path, text)
277
+ counts[error] += 1
278
+ if error is not FontBboxError.NO_ERROR:
279
+ _ = sys.stdout.write(f"Error with {font_path.name}: {error.name} {diff}\n")
280
+ for k, v in counts.items():
281
+ _ = sys.stdout.write(f"{k.name}: {v}\n")
282
+
283
+
284
+ if __name__ == "__main__":
285
+ _INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
286
+ _FONT_DIRS = [
287
+ Path(r"C:\Windows\Fonts"),
288
+ Path(r"C:\Users\shaya\AppData\Local\Microsoft\Windows\Fonts"),
289
+ ]
290
+ _test_every_font_on_my_system(_INKSCAPE, _FONT_DIRS)
291
+
292
+ font = Path(r"C:\Windows\Fonts\arial.ttf")
293
+ font = Path("C:/Windows/Fonts/Aptos-Display-Bold.ttf")
294
+ info = FTFontInfo(font)
295
+ draw_comparison(_INKSCAPE, "temp.svg", font, "AApple")