4
from .typing import ImageType, Union
7
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'}
9
def to_image(image: ImageType, is_svg: bool = False) -> Image.Image:
11
Converts the input image to a PIL Image object.
14
image (Union[str, bytes, Image.Image]): The input image.
17
Image.Image: The converted PIL Image object.
23
raise RuntimeError('Install "cairosvg" package for open svg images')
24
if not isinstance(image, bytes):
27
cairosvg.svg2png(image, write_to=buffer)
28
image = Image.open(buffer)
29
if isinstance(image, str):
30
is_data_uri_an_image(image)
31
image = extract_data_uri(image)
32
if isinstance(image, bytes):
33
is_accepted_format(image)
34
image = Image.open(BytesIO(image))
35
elif not isinstance(image, Image.Image):
36
image = Image.open(image)
38
copy.format = image.format
42
def is_allowed_extension(filename: str) -> bool:
44
Checks if the given filename has an allowed extension.
47
filename (str): The filename to check.
50
bool: True if the extension is allowed, False otherwise.
52
return '.' in filename and \
53
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
55
def is_data_uri_an_image(data_uri: str) -> bool:
57
Checks if the given data URI represents an image.
60
data_uri (str): The data URI to check.
63
ValueError: If the data URI is invalid or the image format is not allowed.
65
# Check if the data URI starts with 'data:image' and contains an image format (e.g., jpeg, png, gif)
66
if not re.match(r'data:image/(\w+);base64,', data_uri):
67
raise ValueError("Invalid data URI image.")
68
# Extract the image format from the data URI
69
image_format = re.match(r'data:image/(\w+);base64,', data_uri).group(1)
70
# Check if the image format is one of the allowed formats (jpg, jpeg, png, gif)
71
if image_format.lower() not in ALLOWED_EXTENSIONS:
72
raise ValueError("Invalid image format (from mime file type).")
74
def is_accepted_format(binary_data: bytes) -> bool:
76
Checks if the given binary data represents an image with an accepted format.
79
binary_data (bytes): The binary data to check.
82
ValueError: If the image format is not allowed.
84
if binary_data.startswith(b'\xFF\xD8\xFF'):
85
pass # It's a JPEG image
86
elif binary_data.startswith(b'\x89PNG\r\n\x1a\n'):
87
pass # It's a PNG image
88
elif binary_data.startswith(b'GIF87a') or binary_data.startswith(b'GIF89a'):
89
pass # It's a GIF image
90
elif binary_data.startswith(b'\x89JFIF') or binary_data.startswith(b'JFIF\x00'):
91
pass # It's a JPEG image
92
elif binary_data.startswith(b'\xFF\xD8'):
93
pass # It's a JPEG image
94
elif binary_data.startswith(b'RIFF') and binary_data[8:12] == b'WEBP':
95
pass # It's a WebP image
97
raise ValueError("Invalid image format (from magic code).")
99
def extract_data_uri(data_uri: str) -> bytes:
101
Extracts the binary data from the given data URI.
104
data_uri (str): The data URI.
107
bytes: The extracted binary data.
109
data = data_uri.split(",")[1]
110
data = base64.b64decode(data)
113
def get_orientation(image: Image.Image) -> int:
115
Gets the orientation of the given image.
118
image (Image.Image): The image.
121
int: The orientation value.
123
exif_data = image.getexif() if hasattr(image, 'getexif') else image._getexif()
124
if exif_data is not None:
125
orientation = exif_data.get(274) # 274 corresponds to the orientation tag in EXIF
126
if orientation is not None:
129
def process_image(img: Image.Image, new_width: int, new_height: int) -> Image.Image:
131
Processes the given image by adjusting its orientation and resizing it.
134
img (Image.Image): The image to process.
135
new_width (int): The new width of the image.
136
new_height (int): The new height of the image.
139
Image.Image: The processed image.
141
orientation = get_orientation(img)
144
img = img.transpose(Image.FLIP_LEFT_RIGHT)
145
if orientation in [3, 4]:
146
img = img.transpose(Image.ROTATE_180)
147
if orientation in [5, 6]:
148
img = img.transpose(Image.ROTATE_270)
149
if orientation in [7, 8]:
150
img = img.transpose(Image.ROTATE_90)
151
img.thumbnail((new_width, new_height))
154
def to_base64(image: Image.Image, compression_rate: float) -> str:
156
Converts the given image to a base64-encoded string.
159
image (Image.Image): The image to convert.
160
compression_rate (float): The compression rate (0.0 to 1.0).
163
str: The base64-encoded image.
165
output_buffer = BytesIO()
166
if image.mode != "RGB":
167
image = image.convert('RGB')
168
image.save(output_buffer, format="JPEG", quality=int(compression_rate * 100))
169
return base64.b64encode(output_buffer.getvalue()).decode()
171
def format_images_markdown(images, alt: str, preview: str="{image}?w=200&h=200") -> str:
173
Formats the given images as a markdown string.
176
images: The images to format.
177
alt (str): The alt for the images.
178
preview (str, optional): The preview URL format. Defaults to "{image}?w=200&h=200".
181
str: The formatted markdown string.
183
if isinstance(images, str):
184
images = f"[![{alt}]({preview.replace('{image}', images)})]({images})"
186
images = [f"[![#{idx+1} {alt}]({preview.replace('{image}', image)})]({image})" for idx, image in enumerate(images)]
187
images = "\n".join(images)
188
start_flag = "<!-- generated images start -->\n"
189
end_flag = "<!-- generated images end -->\n"
190
return f"\n{start_flag}{images}\n{end_flag}\n"
192
def to_bytes(image: Image.Image) -> bytes:
194
Converts the given image to bytes.
197
image (Image.Image): The image to convert.
200
bytes: The image as bytes.
203
image.save(bytes_io, image.format)
205
return bytes_io.getvalue()
207
class ImageResponse():
210
images: Union[str, list],
216
self.options = options
218
def __str__(self) -> str:
219
return format_images_markdown(self.images, self.alt)
221
def get(self, key: str):
222
return self.options.get(key)