svg-ultralight 0.47.0__py3-none-any.whl → 0.50.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.
Potentially problematic release.
This version of svg-ultralight might be problematic. Click here for more details.
- svg_ultralight/__init__.py +108 -105
- svg_ultralight/animate.py +40 -40
- svg_ultralight/attrib_hints.py +13 -14
- svg_ultralight/bounding_boxes/__init__.py +5 -5
- svg_ultralight/bounding_boxes/bound_helpers.py +189 -201
- svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -206
- svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
- svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
- svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
- svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
- svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
- svg_ultralight/constructors/__init__.py +14 -14
- svg_ultralight/constructors/new_element.py +115 -115
- svg_ultralight/font_tools/__init__.py +5 -5
- svg_ultralight/font_tools/comp_results.py +295 -293
- svg_ultralight/font_tools/font_info.py +793 -784
- svg_ultralight/image_ops.py +156 -156
- svg_ultralight/inkscape.py +261 -261
- svg_ultralight/layout.py +290 -291
- svg_ultralight/main.py +183 -198
- svg_ultralight/metadata.py +122 -122
- svg_ultralight/nsmap.py +36 -36
- svg_ultralight/py.typed +5 -0
- svg_ultralight/query.py +254 -249
- svg_ultralight/read_svg.py +58 -0
- svg_ultralight/root_elements.py +87 -87
- svg_ultralight/string_conversion.py +244 -244
- svg_ultralight/strings/__init__.py +21 -13
- svg_ultralight/strings/svg_strings.py +106 -67
- svg_ultralight/transformations.py +140 -141
- svg_ultralight/unit_conversion.py +247 -248
- {svg_ultralight-0.47.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
- svg_ultralight-0.50.1.dist-info/RECORD +34 -0
- svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
- svg_ultralight-0.47.0.dist-info/RECORD +0 -34
- svg_ultralight-0.47.0.dist-info/WHEEL +0 -5
- svg_ultralight-0.47.0.dist-info/top_level.txt +0 -1
svg_ultralight/image_ops.py
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
"""Crop an image before converting to binary and including in the svg file.
|
|
2
|
-
|
|
3
|
-
This optional module requires the Pillow library. Create an svg image element with a
|
|
4
|
-
rasterized image positioned inside a bounding box.
|
|
5
|
-
|
|
6
|
-
:author: Shay Hill
|
|
7
|
-
:created: 2024-11-20
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
|
-
|
|
14
|
-
from paragraphs import par
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from PIL import Image
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from PIL.Image import Image as ImageType
|
|
21
|
-
except ImportError as err:
|
|
22
|
-
msg = par(
|
|
23
|
-
"""PIL is not installed. Install it using 'pip install Pillow' to use
|
|
24
|
-
svg_ultralight.image_ops module."""
|
|
25
|
-
)
|
|
26
|
-
raise ImportError(msg) from err
|
|
27
|
-
|
|
28
|
-
import base64
|
|
29
|
-
import io
|
|
30
|
-
|
|
31
|
-
from lxml import etree
|
|
32
|
-
|
|
33
|
-
from svg_ultralight import NSMAP
|
|
34
|
-
from svg_ultralight.bounding_boxes.bound_helpers import bbox_dict
|
|
35
|
-
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
36
|
-
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
37
|
-
from svg_ultralight.constructors import new_element
|
|
38
|
-
|
|
39
|
-
if TYPE_CHECKING:
|
|
40
|
-
import os
|
|
41
|
-
|
|
42
|
-
from lxml.etree import (
|
|
43
|
-
_Element as EtreeElement, # pyright: ignore [reportPrivateUsage]
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _symmetric_crop(
|
|
48
|
-
image: ImageType, center: tuple[float, float] | None = None
|
|
49
|
-
) -> ImageType:
|
|
50
|
-
"""Crop an image symmetrically around a center point.
|
|
51
|
-
|
|
52
|
-
:param image: PIL.Image instance
|
|
53
|
-
:param center: optional center point for cropping. Proportions of image with and
|
|
54
|
-
image height, so the default value, (0.5, 0.5), is the true center of the
|
|
55
|
-
image. (0.4, 0.5) would crop 20% off the right side of the image.
|
|
56
|
-
:return: PIL.Image instance
|
|
57
|
-
"""
|
|
58
|
-
if center is None:
|
|
59
|
-
return image
|
|
60
|
-
|
|
61
|
-
if not all(0 < x < 1 for x in center):
|
|
62
|
-
msg = "Center must be between (0, 0) and (1, 1)"
|
|
63
|
-
raise ValueError(msg)
|
|
64
|
-
|
|
65
|
-
xd, yd = (min(x, 1 - x) for x in center)
|
|
66
|
-
left, right = sorted(x * image.width for x in (center[0] - xd, center[0] + xd))
|
|
67
|
-
top, bottom = sorted(x * image.height for x in (center[1] - yd, center[1] + yd))
|
|
68
|
-
|
|
69
|
-
return image.crop((left, top, right, bottom))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _crop_image_to_bbox_ratio(
|
|
73
|
-
image: ImageType, bbox: BoundingBox, center: tuple[float, float] | None
|
|
74
|
-
) -> ImageType:
|
|
75
|
-
"""Crop an image to the ratio of a bounding box.
|
|
76
|
-
|
|
77
|
-
:param image: PIL.Image instance
|
|
78
|
-
:param bbox: BoundingBox instance
|
|
79
|
-
:param center: optional center point for cropping. Proportions of image with and
|
|
80
|
-
image height, so the default value, (0.5, 0.5), is the true center of the
|
|
81
|
-
image. (0.4, 0.5) would crop 20% off the right side of the image.
|
|
82
|
-
:return: PIL.Image instance
|
|
83
|
-
|
|
84
|
-
This crops the image to the specified ratio. It's not a resize, so it will cut
|
|
85
|
-
off the top and bottom or the sides of the image to fit the ratio.
|
|
86
|
-
"""
|
|
87
|
-
image = _symmetric_crop(image, center)
|
|
88
|
-
width, height = image.size
|
|
89
|
-
|
|
90
|
-
ratio = bbox.width / bbox.height
|
|
91
|
-
if width / height > ratio:
|
|
92
|
-
new_width = height * ratio
|
|
93
|
-
left = (width - new_width) / 2
|
|
94
|
-
right = width - left
|
|
95
|
-
return image.crop((left, 0, right, height))
|
|
96
|
-
new_height = width / ratio
|
|
97
|
-
top = (height - new_height) / 2
|
|
98
|
-
bottom = height - top
|
|
99
|
-
return image.crop((0, top, width, bottom))
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _get_svg_embedded_image_str(image: ImageType) -> str:
|
|
103
|
-
"""Return the string you'll need to embed an image in an svg.
|
|
104
|
-
|
|
105
|
-
:param image: PIL.Image instance
|
|
106
|
-
:return: argument for xlink:href
|
|
107
|
-
"""
|
|
108
|
-
in_mem_file = io.BytesIO()
|
|
109
|
-
image.save(in_mem_file, format="PNG")
|
|
110
|
-
_ = in_mem_file.seek(0)
|
|
111
|
-
img_bytes = in_mem_file.read()
|
|
112
|
-
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
|
113
|
-
base64_encoded_result_str = base64_encoded_result_bytes.decode("ascii")
|
|
114
|
-
return "data:image/png;base64," + base64_encoded_result_str
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def new_image_blem(
|
|
118
|
-
filename: str | os.PathLike[str],
|
|
119
|
-
bbox: BoundingBox | None = None,
|
|
120
|
-
center: tuple[float, float] | None = None,
|
|
121
|
-
) -> BoundElement:
|
|
122
|
-
"""Create a new svg image element inside a bounding box.
|
|
123
|
-
|
|
124
|
-
:param filename: filename of source image
|
|
125
|
-
:param bbox: bounding box for the image
|
|
126
|
-
:param center: center point for cropping. Proportions of image width and image
|
|
127
|
-
height, so the default value, (0.5, 0.5), is the true center of the image.
|
|
128
|
-
(0.4, 0.5) would crop 20% off the right side of the image.
|
|
129
|
-
:return: a BoundElement element with the cropped image embedded
|
|
130
|
-
"""
|
|
131
|
-
image = Image.open(filename)
|
|
132
|
-
if bbox is None:
|
|
133
|
-
bbox = BoundingBox(0, 0, image.width, image.height)
|
|
134
|
-
image = _crop_image_to_bbox_ratio(Image.open(filename), bbox, center)
|
|
135
|
-
svg_image = new_element("image", **bbox_dict(bbox))
|
|
136
|
-
svg_image.set(
|
|
137
|
-
etree.QName(NSMAP["xlink"], "href"), _get_svg_embedded_image_str(image)
|
|
138
|
-
)
|
|
139
|
-
return BoundElement(svg_image, bbox)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def new_image_elem_in_bbox(
|
|
143
|
-
filename: str | os.PathLike[str],
|
|
144
|
-
bbox: BoundingBox | None = None,
|
|
145
|
-
center: tuple[float, float] | None = None,
|
|
146
|
-
) -> EtreeElement:
|
|
147
|
-
"""Create a new svg image element inside a bounding box.
|
|
148
|
-
|
|
149
|
-
:param filename: filename of source image
|
|
150
|
-
:param bbox: bounding box for the image
|
|
151
|
-
:param center: center point for cropping. Proportions of image width and image
|
|
152
|
-
height, so the default value, (0.5, 0.5), is the true center of the image.
|
|
153
|
-
(0.4, 0.5) would crop 20% off the right side of the image.
|
|
154
|
-
:return: an etree image element with the cropped image embedded
|
|
155
|
-
"""
|
|
156
|
-
return new_image_blem(filename, bbox, center).elem
|
|
1
|
+
"""Crop an image before converting to binary and including in the svg file.
|
|
2
|
+
|
|
3
|
+
This optional module requires the Pillow library. Create an svg image element with a
|
|
4
|
+
rasterized image positioned inside a bounding box.
|
|
5
|
+
|
|
6
|
+
:author: Shay Hill
|
|
7
|
+
:created: 2024-11-20
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from paragraphs import par
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from PIL import Image
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from PIL.Image import Image as ImageType
|
|
21
|
+
except ImportError as err:
|
|
22
|
+
msg = par(
|
|
23
|
+
"""PIL is not installed. Install it using 'pip install Pillow' to use
|
|
24
|
+
svg_ultralight.image_ops module."""
|
|
25
|
+
)
|
|
26
|
+
raise ImportError(msg) from err
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import io
|
|
30
|
+
|
|
31
|
+
from lxml import etree
|
|
32
|
+
|
|
33
|
+
from svg_ultralight import NSMAP
|
|
34
|
+
from svg_ultralight.bounding_boxes.bound_helpers import bbox_dict
|
|
35
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
36
|
+
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
37
|
+
from svg_ultralight.constructors import new_element
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
from lxml.etree import (
|
|
43
|
+
_Element as EtreeElement, # pyright: ignore [reportPrivateUsage]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _symmetric_crop(
|
|
48
|
+
image: ImageType, center: tuple[float, float] | None = None
|
|
49
|
+
) -> ImageType:
|
|
50
|
+
"""Crop an image symmetrically around a center point.
|
|
51
|
+
|
|
52
|
+
:param image: PIL.Image instance
|
|
53
|
+
:param center: optional center point for cropping. Proportions of image with and
|
|
54
|
+
image height, so the default value, (0.5, 0.5), is the true center of the
|
|
55
|
+
image. (0.4, 0.5) would crop 20% off the right side of the image.
|
|
56
|
+
:return: PIL.Image instance
|
|
57
|
+
"""
|
|
58
|
+
if center is None:
|
|
59
|
+
return image
|
|
60
|
+
|
|
61
|
+
if not all(0 < x < 1 for x in center):
|
|
62
|
+
msg = "Center must be between (0, 0) and (1, 1)"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
|
|
65
|
+
xd, yd = (min(x, 1 - x) for x in center)
|
|
66
|
+
left, right = sorted(x * image.width for x in (center[0] - xd, center[0] + xd))
|
|
67
|
+
top, bottom = sorted(x * image.height for x in (center[1] - yd, center[1] + yd))
|
|
68
|
+
|
|
69
|
+
return image.crop((left, top, right, bottom))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _crop_image_to_bbox_ratio(
|
|
73
|
+
image: ImageType, bbox: BoundingBox, center: tuple[float, float] | None
|
|
74
|
+
) -> ImageType:
|
|
75
|
+
"""Crop an image to the ratio of a bounding box.
|
|
76
|
+
|
|
77
|
+
:param image: PIL.Image instance
|
|
78
|
+
:param bbox: BoundingBox instance
|
|
79
|
+
:param center: optional center point for cropping. Proportions of image with and
|
|
80
|
+
image height, so the default value, (0.5, 0.5), is the true center of the
|
|
81
|
+
image. (0.4, 0.5) would crop 20% off the right side of the image.
|
|
82
|
+
:return: PIL.Image instance
|
|
83
|
+
|
|
84
|
+
This crops the image to the specified ratio. It's not a resize, so it will cut
|
|
85
|
+
off the top and bottom or the sides of the image to fit the ratio.
|
|
86
|
+
"""
|
|
87
|
+
image = _symmetric_crop(image, center)
|
|
88
|
+
width, height = image.size
|
|
89
|
+
|
|
90
|
+
ratio = bbox.width / bbox.height
|
|
91
|
+
if width / height > ratio:
|
|
92
|
+
new_width = height * ratio
|
|
93
|
+
left = (width - new_width) / 2
|
|
94
|
+
right = width - left
|
|
95
|
+
return image.crop((left, 0, right, height))
|
|
96
|
+
new_height = width / ratio
|
|
97
|
+
top = (height - new_height) / 2
|
|
98
|
+
bottom = height - top
|
|
99
|
+
return image.crop((0, top, width, bottom))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_svg_embedded_image_str(image: ImageType) -> str:
|
|
103
|
+
"""Return the string you'll need to embed an image in an svg.
|
|
104
|
+
|
|
105
|
+
:param image: PIL.Image instance
|
|
106
|
+
:return: argument for xlink:href
|
|
107
|
+
"""
|
|
108
|
+
in_mem_file = io.BytesIO()
|
|
109
|
+
image.save(in_mem_file, format="PNG")
|
|
110
|
+
_ = in_mem_file.seek(0)
|
|
111
|
+
img_bytes = in_mem_file.read()
|
|
112
|
+
base64_encoded_result_bytes = base64.b64encode(img_bytes)
|
|
113
|
+
base64_encoded_result_str = base64_encoded_result_bytes.decode("ascii")
|
|
114
|
+
return "data:image/png;base64," + base64_encoded_result_str
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def new_image_blem(
|
|
118
|
+
filename: str | os.PathLike[str],
|
|
119
|
+
bbox: BoundingBox | None = None,
|
|
120
|
+
center: tuple[float, float] | None = None,
|
|
121
|
+
) -> BoundElement:
|
|
122
|
+
"""Create a new svg image element inside a bounding box.
|
|
123
|
+
|
|
124
|
+
:param filename: filename of source image
|
|
125
|
+
:param bbox: bounding box for the image
|
|
126
|
+
:param center: center point for cropping. Proportions of image width and image
|
|
127
|
+
height, so the default value, (0.5, 0.5), is the true center of the image.
|
|
128
|
+
(0.4, 0.5) would crop 20% off the right side of the image.
|
|
129
|
+
:return: a BoundElement element with the cropped image embedded
|
|
130
|
+
"""
|
|
131
|
+
image = Image.open(filename)
|
|
132
|
+
if bbox is None:
|
|
133
|
+
bbox = BoundingBox(0, 0, image.width, image.height)
|
|
134
|
+
image = _crop_image_to_bbox_ratio(Image.open(filename), bbox, center)
|
|
135
|
+
svg_image = new_element("image", **bbox_dict(bbox))
|
|
136
|
+
svg_image.set(
|
|
137
|
+
etree.QName(NSMAP["xlink"], "href"), _get_svg_embedded_image_str(image)
|
|
138
|
+
)
|
|
139
|
+
return BoundElement(svg_image, bbox)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def new_image_elem_in_bbox(
|
|
143
|
+
filename: str | os.PathLike[str],
|
|
144
|
+
bbox: BoundingBox | None = None,
|
|
145
|
+
center: tuple[float, float] | None = None,
|
|
146
|
+
) -> EtreeElement:
|
|
147
|
+
"""Create a new svg image element inside a bounding box.
|
|
148
|
+
|
|
149
|
+
:param filename: filename of source image
|
|
150
|
+
:param bbox: bounding box for the image
|
|
151
|
+
:param center: center point for cropping. Proportions of image width and image
|
|
152
|
+
height, so the default value, (0.5, 0.5), is the true center of the image.
|
|
153
|
+
(0.4, 0.5) would crop 20% off the right side of the image.
|
|
154
|
+
:return: an etree image element with the cropped image embedded
|
|
155
|
+
"""
|
|
156
|
+
return new_image_blem(filename, bbox, center).elem
|