svg-ultralight 0.39.0__tar.gz → 0.40.0__tar.gz

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 (60) hide show
  1. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/.pre-commit-config.yaml +3 -1
  2. {svg_ultralight-0.39.0/src/svg_ultralight.egg-info → svg_ultralight-0.40.0}/PKG-INFO +4 -2
  3. svg_ultralight-0.40.0/dev-requirements.txt +110 -0
  4. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/pyproject.toml +11 -4
  5. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/__init__.py +7 -1
  6. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/animate.py +3 -3
  7. svg_ultralight-0.40.0/src/svg_ultralight/bounding_boxes/padded_text_initializers.py +186 -0
  8. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +1 -1
  9. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +1 -1
  10. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +1 -1
  11. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +14 -12
  12. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +81 -4
  13. svg_ultralight-0.40.0/src/svg_ultralight/font_tools/__init__.py +5 -0
  14. svg_ultralight-0.40.0/src/svg_ultralight/font_tools/comp_results.py +284 -0
  15. svg_ultralight-0.40.0/src/svg_ultralight/font_tools/font_css.py +82 -0
  16. svg_ultralight-0.40.0/src/svg_ultralight/font_tools/font_info.py +567 -0
  17. svg_ultralight-0.40.0/src/svg_ultralight/font_tools/globs.py +7 -0
  18. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/image_ops.py +4 -2
  19. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/inkscape.py +29 -18
  20. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/main.py +5 -3
  21. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/query.py +8 -37
  22. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/string_conversion.py +69 -1
  23. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/transformations.py +10 -3
  24. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0/src/svg_ultralight.egg-info}/PKG-INFO +4 -2
  25. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight.egg-info/SOURCES.txt +9 -0
  26. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight.egg-info/requires.txt +3 -1
  27. svg_ultralight-0.40.0/tests/conftest.py +32 -0
  28. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_bounding.py +3 -2
  29. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_inkscape.py +5 -12
  30. svg_ultralight-0.40.0/tests/test_padded_text_initializers.py +77 -0
  31. svg_ultralight-0.40.0/tests/test_padding.py +56 -0
  32. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_queries.py +15 -8
  33. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_string_conversion.py +40 -3
  34. svg_ultralight-0.39.0/tests/conftest.py +0 -16
  35. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/.gitignore +0 -0
  36. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/README.md +0 -0
  37. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/setup.cfg +0 -0
  38. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
  39. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py +0 -0
  40. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/constructors/__init__.py +0 -0
  41. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/constructors/new_element.py +0 -0
  42. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/layout.py +0 -0
  43. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/metadata.py +0 -0
  44. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/nsmap.py +0 -0
  45. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/py.typed +0 -0
  46. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/root_elements.py +0 -0
  47. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/strings/__init__.py +0 -0
  48. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
  49. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight/unit_conversion.py +0 -0
  50. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
  51. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
  52. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/__init__.py +0 -0
  53. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/resources/arrow.svg +0 -0
  54. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_layout.py +0 -0
  55. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_matrices.py +0 -0
  56. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_metadata.py +0 -0
  57. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_new_element.py +0 -0
  58. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_root_elements.py +0 -0
  59. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tests/test_svg_ultralight.py +0 -0
  60. {svg_ultralight-0.39.0 → svg_ultralight-0.40.0}/tox.ini +0 -0
@@ -92,6 +92,8 @@ repos:
92
92
  # S301 don't use pickle
93
93
  # B028 wants explicit stacklevel on warn
94
94
  # BLE001 Use of `except Exception:` detected
95
+ # ANN401 Any type disallowed
96
+ # FLY002 Consider f-string instead of string join
95
97
  rev: 'v0.11.11'
96
98
  hooks:
97
99
  - id: ruff
@@ -100,7 +102,7 @@ repos:
100
102
  args:
101
103
  - --target-version=py39
102
104
  - --select=ALL
103
- - --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001
105
+ - --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001,ANN401,FLY002
104
106
  - --fix
105
107
  - --fixable=RUF022
106
108
  - id: ruff
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svg-ultralight
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
+ Requires-Dist: cssutils
10
+ Requires-Dist: fonttools
9
11
  Requires-Dist: lxml
10
- Requires-Dist: pillow
11
12
  Requires-Dist: paragraphs
13
+ Requires-Dist: pillow
12
14
  Requires-Dist: types-lxml
13
15
  Requires-Dist: typing-extensions
14
16
  Provides-Extra: dev
@@ -0,0 +1,110 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.11
3
+ # by the following command:
4
+ #
5
+ # pip-compile --extra=dev --output-file=dev-requirements.txt --strip-extras pyproject.toml
6
+ #
7
+ argcomplete==3.6.2
8
+ # via commitizen
9
+ beautifulsoup4==4.13.4
10
+ # via types-lxml
11
+ cachetools==6.0.0
12
+ # via tox
13
+ cfgv==3.4.0
14
+ # via pre-commit
15
+ chardet==5.2.0
16
+ # via tox
17
+ charset-normalizer==3.4.2
18
+ # via commitizen
19
+ colorama==0.4.6
20
+ # via
21
+ # commitizen
22
+ # pytest
23
+ # tox
24
+ commitizen==4.8.2
25
+ # via svg-ultralight (pyproject.toml)
26
+ cssselect==1.3.0
27
+ # via types-lxml
28
+ cssutils==2.11.1
29
+ # via svg-ultralight (pyproject.toml)
30
+ decli==0.6.3
31
+ # via commitizen
32
+ distlib==0.3.9
33
+ # via virtualenv
34
+ filelock==3.18.0
35
+ # via
36
+ # tox
37
+ # virtualenv
38
+ fonttools==4.58.1
39
+ # via svg-ultralight (pyproject.toml)
40
+ identify==2.6.12
41
+ # via pre-commit
42
+ iniconfig==2.1.0
43
+ # via pytest
44
+ jinja2==3.1.6
45
+ # via commitizen
46
+ lxml==5.4.0
47
+ # via svg-ultralight (pyproject.toml)
48
+ markupsafe==3.0.2
49
+ # via jinja2
50
+ more-itertools==10.7.0
51
+ # via cssutils
52
+ nodeenv==1.9.1
53
+ # via pre-commit
54
+ packaging==25.0
55
+ # via
56
+ # commitizen
57
+ # pyproject-api
58
+ # pytest
59
+ # tox
60
+ paragraphs==1.0.1
61
+ # via svg-ultralight (pyproject.toml)
62
+ pillow==11.2.1
63
+ # via svg-ultralight (pyproject.toml)
64
+ platformdirs==4.3.8
65
+ # via
66
+ # tox
67
+ # virtualenv
68
+ pluggy==1.6.0
69
+ # via
70
+ # pytest
71
+ # tox
72
+ pre-commit==4.2.0
73
+ # via svg-ultralight (pyproject.toml)
74
+ prompt-toolkit==3.0.51
75
+ # via questionary
76
+ pygments==2.19.1
77
+ # via pytest
78
+ pyproject-api==1.9.1
79
+ # via tox
80
+ pytest==8.4.0
81
+ # via svg-ultralight (pyproject.toml)
82
+ pyyaml==6.0.2
83
+ # via
84
+ # commitizen
85
+ # pre-commit
86
+ questionary==2.1.0
87
+ # via commitizen
88
+ soupsieve==2.7
89
+ # via beautifulsoup4
90
+ termcolor==2.5.0
91
+ # via commitizen
92
+ tomlkit==0.13.2
93
+ # via commitizen
94
+ tox==4.26.0
95
+ # via svg-ultralight (pyproject.toml)
96
+ types-html5lib==1.1.11.20250516
97
+ # via types-lxml
98
+ types-lxml==2025.3.30
99
+ # via svg-ultralight (pyproject.toml)
100
+ typing-extensions==4.14.0
101
+ # via
102
+ # beautifulsoup4
103
+ # svg-ultralight (pyproject.toml)
104
+ # types-lxml
105
+ virtualenv==20.31.2
106
+ # via
107
+ # pre-commit
108
+ # tox
109
+ wcwidth==0.2.13
110
+ # via prompt-toolkit
@@ -1,13 +1,20 @@
1
1
  [project]
2
2
  name = "svg-ultralight"
3
- version = "0.39.0"
3
+ version = "0.40.0"
4
4
  description = "a sensible way to create svg files with Python"
5
5
  authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
6
6
  license = { text = "MIT" }
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.9"
9
- dependencies = ["lxml", "pillow", "paragraphs", "types-lxml", "typing-extensions"]
10
-
9
+ dependencies = [
10
+ "cssutils",
11
+ "fonttools",
12
+ "lxml",
13
+ "paragraphs",
14
+ "pillow",
15
+ "types-lxml",
16
+ "typing-extensions",
17
+ ]
11
18
  [project.optional-dependencies]
12
19
  dev = ["pytest", "commitizen", "pre-commit", "tox"]
13
20
  images = ["pillow"]
@@ -42,7 +49,7 @@ convention = "pep257"
42
49
 
43
50
  [tool.commitizen]
44
51
  name = "cz_conventional_commits"
45
- version = "0.39.0"
52
+ version = "0.40.0"
46
53
  tag_format = "$version"
47
54
  version_files = ["pyproject.toml:^version"]
48
55
  annotated_tag = true
@@ -14,6 +14,11 @@ from svg_ultralight.bounding_boxes.bound_helpers import (
14
14
  pad_bbox,
15
15
  parse_bound_element,
16
16
  )
17
+ from svg_ultralight.bounding_boxes.padded_text_initializers import (
18
+ pad_text,
19
+ pad_text_ft,
20
+ pad_text_mix,
21
+ )
17
22
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
18
23
  from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
19
24
  from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
@@ -39,7 +44,6 @@ from svg_ultralight.query import (
39
44
  clear_svg_ultralight_cache,
40
45
  get_bounding_box,
41
46
  get_bounding_boxes,
42
- pad_text,
43
47
  )
44
48
  from svg_ultralight.root_elements import new_svg_root_around_bounds
45
49
  from svg_ultralight.string_conversion import (
@@ -87,6 +91,8 @@ __all__ = [
87
91
  "new_svg_root_around_bounds",
88
92
  "pad_bbox",
89
93
  "pad_text",
94
+ "pad_text_ft",
95
+ "pad_text_mix",
90
96
  "parse_bound_element",
91
97
  "transform_element",
92
98
  "update_element",
@@ -16,13 +16,13 @@ except ModuleNotFoundError as exc:
16
16
  from typing import TYPE_CHECKING
17
17
 
18
18
  if TYPE_CHECKING:
19
+ import os
19
20
  from collections.abc import Iterable
20
- from pathlib import Path
21
21
 
22
22
 
23
23
  def write_gif(
24
- gif: str | Path,
25
- pngs: Iterable[str] | Iterable[Path] | Iterable[str | Path],
24
+ gif: str | os.PathLike[str],
25
+ pngs: Iterable[str] | Iterable[os.PathLike[str]] | Iterable[str | os.PathLike[str]],
26
26
  duration: float = 100,
27
27
  loop: int = 0,
28
28
  ) -> None:
@@ -0,0 +1,186 @@
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
+ :author: Shay Hill
14
+ :created: 2025-06-09
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from copy import deepcopy
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
24
+ from svg_ultralight.constructors import new_element, update_element
25
+ from svg_ultralight.font_tools.font_info import (
26
+ get_padded_text_info,
27
+ get_svg_font_attributes,
28
+ )
29
+ from svg_ultralight.font_tools.globs import DEFAULT_FONT_SIZE
30
+ from svg_ultralight.query import get_bounding_boxes
31
+ from svg_ultralight.string_conversion import (
32
+ encode_to_css_class_name,
33
+ format_attr_dict,
34
+ format_number,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ import os
39
+
40
+ from lxml.etree import (
41
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
42
+ )
43
+
44
+ DEFAULT_Y_BOUNDS_REFERENCE = "{[|gjpqyf"
45
+
46
+
47
+ def pad_text(
48
+ inkscape: str | os.PathLike[str],
49
+ text_elem: EtreeElement,
50
+ y_bounds_reference: str | None = None,
51
+ *,
52
+ font: str | os.PathLike[str] | None = None,
53
+ ) -> PaddedText:
54
+ r"""Create a PaddedText instance from a text element.
55
+
56
+ :param inkscape: path to an inkscape executable on your local file system
57
+ IMPORTANT: path cannot end with ``.exe``.
58
+ Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
59
+ :param text_elem: an etree element with a text tag
60
+ :param y_bounds_reference: an optional string to use to determine the ascent and
61
+ capline of the font. The default is a good choice, which approaches or even
62
+ meets the ascent of descent of most fonts without using utf-8 characters. You
63
+ might want to use a letter like "M" or even "x" if you are using an all-caps
64
+ string and want to center between the capline and baseline or if you'd like
65
+ to center between the baseline and x-line.
66
+ :param font: optionally add a path to a font file to use for the text element.
67
+ This is going to conflict with any font-family, font-style, or other
68
+ font-related attributes *except* font-size. You likely want to use
69
+ `font_tools.new_padded_text` if you're going to pass a font path, but you can
70
+ use it here to compare results between `pad_text` and `new_padded_text`.
71
+ :return: a PaddedText instance
72
+ """
73
+ if y_bounds_reference is None:
74
+ y_bounds_reference = DEFAULT_Y_BOUNDS_REFERENCE
75
+ if font is not None:
76
+ _ = update_element(text_elem, **get_svg_font_attributes(font))
77
+ if "font-size" not in text_elem.attrib:
78
+ text_elem.attrib["font-size"] = format_number(DEFAULT_FONT_SIZE)
79
+ rmargin_ref = deepcopy(text_elem)
80
+ capline_ref = deepcopy(text_elem)
81
+ _ = rmargin_ref.attrib.pop("id", None)
82
+ _ = capline_ref.attrib.pop("id", None)
83
+ rmargin_ref.attrib["text-anchor"] = "end"
84
+ capline_ref.text = y_bounds_reference
85
+
86
+ bboxes = get_bounding_boxes(inkscape, text_elem, rmargin_ref, capline_ref)
87
+ bbox, rmargin_bbox, capline_bbox = bboxes
88
+
89
+ tpad = bbox.y - capline_bbox.y
90
+ rpad = -rmargin_bbox.x2
91
+ bpad = capline_bbox.y2 - bbox.y2
92
+ lpad = bbox.x
93
+ return PaddedText(text_elem, bbox, tpad, rpad, bpad, lpad)
94
+
95
+
96
+ def pad_text_ft(
97
+ font: str | os.PathLike[str],
98
+ text: str,
99
+ font_size: float = DEFAULT_FONT_SIZE,
100
+ ascent: float | None = None,
101
+ descent: float | None = None,
102
+ *,
103
+ y_bounds_reference: str | None = None,
104
+ **attributes: str | float,
105
+ ) -> PaddedText:
106
+ """Create a new PaddedText instance using fontTools.
107
+
108
+ :param font: path to a font file.
109
+ :param text: the text of the text element.
110
+ :param font_size: the font size to use.
111
+ :param ascent: the ascent of the font. If not provided, it will be calculated
112
+ from the font file.
113
+ :param descent: the descent of the font. If not provided, it will be calculated
114
+ from the font file.
115
+ :param y_bounds_reference: optional character or string to use as a reference
116
+ for the ascent and descent. If provided, the ascent and descent will be the y
117
+ extents of the capline reference. This argument is provided to mimic the
118
+ behavior of the query module's `pad_text` function. `pad_text` does no
119
+ inspect font files and relies on Inkscape to measure reference characters.
120
+ :param attributes: additional attributes to set on the text element. There is a
121
+ chance these will cause the font element to exceed the BoundingBox of the
122
+ PaddedText instance.
123
+ :return: a PaddedText instance with a line_gap defined.
124
+ """
125
+ attributes_ = format_attr_dict(**attributes)
126
+ attributes_["font-size"] = attributes_.get("font-size", format_number(font_size))
127
+ attributes_["class"] = encode_to_css_class_name(Path(font).name)
128
+
129
+ elem = new_element("text", text=text, **attributes_)
130
+ info = get_padded_text_info(
131
+ font, text, font_size, ascent, descent, y_bounds_reference=y_bounds_reference
132
+ )
133
+ return PaddedText(elem, info.bbox, *info.padding, info.line_gap)
134
+
135
+
136
+ def pad_text_mix(
137
+ inkscape: str | os.PathLike[str],
138
+ font: str | os.PathLike[str],
139
+ text: str,
140
+ font_size: float = DEFAULT_FONT_SIZE,
141
+ ascent: float | None = None,
142
+ descent: float | None = None,
143
+ *,
144
+ y_bounds_reference: str | None = None,
145
+ **attributes: str | float,
146
+ ) -> PaddedText:
147
+ """Use Inkscape text bounds and fill missing with fontTools.
148
+
149
+ :param font: path to a font file.
150
+ :param text: the text of the text element.
151
+ :param font_size: the font size to use.
152
+ :param ascent: the ascent of the font. If not provided, it will be calculated
153
+ from the font file.
154
+ :param descent: the descent of the font. If not provided, it will be calculated
155
+ from the font file.
156
+ :param y_bounds_reference: optional character or string to use as a reference
157
+ for the ascent and descent. If provided, the ascent and descent will be the y
158
+ extents of the capline reference. This argument is provided to mimic the
159
+ behavior of the query module's `pad_text` function. `pad_text` does no
160
+ inspect font files and relies on Inkscape to measure reference characters.
161
+ :param attributes: additional attributes to set on the text element. There is a
162
+ chance these will cause the font element to exceed the BoundingBox of the
163
+ PaddedText instance.
164
+ :return: a PaddedText instance with a line_gap defined.
165
+ """
166
+ elem = new_element("text", text=text, **attributes)
167
+ padded_inkscape = pad_text(inkscape, elem, y_bounds_reference, font=font)
168
+ padded_fonttools = pad_text_ft(
169
+ font,
170
+ text,
171
+ font_size,
172
+ ascent,
173
+ descent,
174
+ y_bounds_reference=y_bounds_reference,
175
+ **attributes,
176
+ )
177
+ bbox = padded_inkscape.unpadded_bbox
178
+ rpad = padded_inkscape.rpad
179
+ lpad = padded_inkscape.lpad
180
+ if y_bounds_reference is None:
181
+ tpad = padded_fonttools.tpad
182
+ bpad = padded_fonttools.bpad
183
+ else:
184
+ tpad = padded_inkscape.tpad
185
+ bpad = padded_inkscape.bpad
186
+ return PaddedText(elem, bbox, tpad, rpad, bpad, lpad, padded_fonttools.line_gap)
@@ -49,7 +49,7 @@ class SupportsBounds(Protocol):
49
49
  self,
50
50
  transformation: _Matrix | None = None,
51
51
  *,
52
- scale: tuple[float, float] | None = None,
52
+ scale: tuple[float, float] | float | None = None,
53
53
  dx: float | None = None,
54
54
  dy: float | None = None,
55
55
  ):
@@ -44,7 +44,7 @@ class BoundCollection(HasBoundingBox):
44
44
  self,
45
45
  transformation: _Matrix | None = None,
46
46
  *,
47
- scale: tuple[float, float] | None = None,
47
+ scale: tuple[float, float] | float | None = None,
48
48
  dx: float | None = None,
49
49
  dy: float | None = None,
50
50
  ):
@@ -49,7 +49,7 @@ class BoundElement(HasBoundingBox):
49
49
  self,
50
50
  transformation: _Matrix | None = None,
51
51
  *,
52
- scale: tuple[float, float] | None = None,
52
+ scale: tuple[float, float] | float | None = None,
53
53
  dx: float | None = None,
54
54
  dy: float | None = None,
55
55
  ):
@@ -41,6 +41,18 @@ class HasBoundingBox(SupportsBounds):
41
41
  y2 = y + self.bbox.base_height
42
42
  return (x, y), (x2, y), (x2, y2), (x, y2)
43
43
 
44
+ def values(self) -> tuple[float, float, float, float]:
45
+ """Get the values of the bounding box.
46
+
47
+ :return: x, y, width, height of the bounding box
48
+ """
49
+ return (
50
+ self.bbox.x,
51
+ self.bbox.y,
52
+ self.bbox.width,
53
+ self.bbox.height,
54
+ )
55
+
44
56
  def _get_transformed_corners(
45
57
  self,
46
58
  ) -> tuple[
@@ -59,21 +71,11 @@ class HasBoundingBox(SupportsBounds):
59
71
  )
60
72
  return c0, c1, c2, c3
61
73
 
62
- def _scale_scale_by_uniform_scalar(self, scalar: float) -> None:
63
- """Scale the bounding box uniformly by a factor.
64
-
65
- :param scale: scale factor
66
- Unlike self.scale, this does not set the scale, but scales the scale. So if
67
- the current scale is (2, 6), and you call this with a scalar of 2, the new
68
- scale will be (4, 12).
69
- """
70
- self.transform(scale=(scalar, scalar))
71
-
72
74
  def transform(
73
75
  self,
74
76
  transformation: _Matrix | None = None,
75
77
  *,
76
- scale: tuple[float, float] | None = None,
78
+ scale: tuple[float, float] | float | None = None,
77
79
  dx: float | None = None,
78
80
  dy: float | None = None,
79
81
  ):
@@ -241,7 +243,7 @@ class HasBoundingBox(SupportsBounds):
241
243
  """
242
244
  current_x = self.x
243
245
  current_y = self.y
244
- self._scale_scale_by_uniform_scalar(value / self.width)
246
+ self.transform(scale=value / self.width)
245
247
  self.x = current_x
246
248
  self.y = current_y
247
249
 
@@ -66,6 +66,8 @@ from __future__ import annotations
66
66
 
67
67
  from typing import TYPE_CHECKING
68
68
 
69
+ from paragraphs import par
70
+
69
71
  from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
70
72
  from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
71
73
  from svg_ultralight.transformations import new_transformation_matrix, transform_element
@@ -77,6 +79,14 @@ if TYPE_CHECKING:
77
79
 
78
80
  _Matrix = tuple[float, float, float, float, float, float]
79
81
 
82
+ _no_line_gap_msg = par(
83
+ """No line_gap defined. Line gap is an inherent font attribute defined within a
84
+ font file. If this PaddedText instance was created with `pad_text` from reference
85
+ elements, a line_gap was not defined. Reading line_gap from the font file
86
+ requires creating a PaddedText instance with `pad_text_ft` or `pad_text_mixed`.
87
+ You can set an arbitrary line_gap after init with `instance.line_gap = value`."""
88
+ )
89
+
80
90
 
81
91
  class PaddedText(BoundElement):
82
92
  """A line of text with a bounding box and padding."""
@@ -89,6 +99,7 @@ class PaddedText(BoundElement):
89
99
  rpad: float,
90
100
  bpad: float,
91
101
  lpad: float,
102
+ line_gap: float | None = None,
92
103
  ) -> None:
93
104
  """Initialize a PaddedText instance.
94
105
 
@@ -105,6 +116,7 @@ class PaddedText(BoundElement):
105
116
  self.rpad = rpad
106
117
  self.base_bpad = bpad
107
118
  self.lpad = lpad
119
+ self._line_gap = line_gap
108
120
 
109
121
  @property
110
122
  def bbox(self) -> BoundingBox:
@@ -122,7 +134,6 @@ class PaddedText(BoundElement):
122
134
  self.y,
123
135
  self.width,
124
136
  self.height,
125
- self.unpadded_bbox.transformation,
126
137
  )
127
138
 
128
139
  @bbox.setter
@@ -139,7 +150,7 @@ class PaddedText(BoundElement):
139
150
  self,
140
151
  transformation: _Matrix | None = None,
141
152
  *,
142
- scale: tuple[float, float] | None = None,
153
+ scale: tuple[float, float] | float | None = None,
143
154
  dx: float | None = None,
144
155
  dy: float | None = None,
145
156
  ):
@@ -154,6 +165,32 @@ class PaddedText(BoundElement):
154
165
  self.unpadded_bbox.transform(tmat)
155
166
  _ = transform_element(self.elem, tmat)
156
167
 
168
+ @property
169
+ def line_gap(self) -> float:
170
+ """The line gap between this line of text and the next.
171
+
172
+ :return: The line gap between this line of text and the next.
173
+ """
174
+ if self._line_gap is None:
175
+ raise AttributeError(_no_line_gap_msg)
176
+ return self._line_gap
177
+
178
+ @line_gap.setter
179
+ def line_gap(self, value: float) -> None:
180
+ """Set the line gap between this line of text and the next.
181
+
182
+ :param value: The new line gap.
183
+ """
184
+ self._line_gap = value
185
+
186
+ @property
187
+ def leading(self) -> float:
188
+ """The leading of this line of text.
189
+
190
+ :return: The line gap plus the height of this line of text.
191
+ """
192
+ return self.height + self.line_gap
193
+
157
194
  @property
158
195
  def tpad(self) -> float:
159
196
  """The top padding of this line of text.
@@ -208,7 +245,12 @@ class PaddedText(BoundElement):
208
245
  *and* y2) when scaling.
209
246
  """
210
247
  y2 = self.y2
211
- self.unpadded_bbox.width = value - self.lpad - self.rpad
248
+
249
+ no_margins_old = self.unpadded_bbox.width
250
+ no_margins_new = value - self.lpad - self.rpad
251
+ scale = no_margins_new / no_margins_old
252
+ self.transform(scale=(scale, scale))
253
+
212
254
  self.y2 = y2
213
255
 
214
256
  @property
@@ -226,7 +268,10 @@ class PaddedText(BoundElement):
226
268
  :param height: The new height of this line of text.
227
269
  :effects: the text_element bounding box is scaled to height - tpad - bpad.
228
270
  """
229
- self.width *= value / self.height
271
+ y2 = self.y2
272
+ scale = value / self.height
273
+ self.transform(scale=(scale, scale))
274
+ self.y2 = y2
230
275
 
231
276
  @property
232
277
  def x(self) -> float:
@@ -244,6 +289,22 @@ class PaddedText(BoundElement):
244
289
  """
245
290
  self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
246
291
 
292
+ @property
293
+ def cx(self) -> float:
294
+ """The horizontal center of this line of text.
295
+
296
+ :return: The horizontal center of this line of text.
297
+ """
298
+ return self.x + self.width / 2
299
+
300
+ @cx.setter
301
+ def cx(self, value: float) -> None:
302
+ """Set the horizontal center of this line of text.
303
+
304
+ :param value: The horizontal center of this line of text.
305
+ """
306
+ self.x += value - self.cx
307
+
247
308
  @property
248
309
  def x2(self) -> float:
249
310
  """The right margin of this line of text.
@@ -276,6 +337,22 @@ class PaddedText(BoundElement):
276
337
  """
277
338
  self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
278
339
 
340
+ @property
341
+ def cy(self) -> float:
342
+ """The horizontal center of this line of text.
343
+
344
+ :return: The horizontal center of this line of text.
345
+ """
346
+ return self.y + self.height / 2
347
+
348
+ @cy.setter
349
+ def cy(self, value: float) -> None:
350
+ """Set the horizontal center of this line of text.
351
+
352
+ :param value: The horizontal center of this line of text.
353
+ """
354
+ self.y += value - self.cy
355
+
279
356
  @property
280
357
  def y2(self) -> float:
281
358
  """The bottom of this line of text.
@@ -0,0 +1,5 @@
1
+ """Mark font_tools as a package.
2
+
3
+ :author: Shay Hill
4
+ :created: 2025-06-04
5
+ """