|
19 | 19 | import io
|
20 | 20 | import inspect
|
21 | 21 | import mimetypes
|
| 22 | +import pathlib |
22 | 23 | import typing
|
23 | 24 | from typing import Any, Callable, Union
|
24 | 25 | from typing_extensions import TypedDict
|
|
30 | 31 |
|
31 | 32 | if typing.TYPE_CHECKING:
|
32 | 33 | import PIL.Image
|
33 |
| - import PIL.PngImagePlugin |
| 34 | + import PIL.ImageFile |
34 | 35 | import IPython.display
|
35 | 36 |
|
36 | 37 | IMAGE_TYPES = (PIL.Image.Image, IPython.display.Image)
|
37 | 38 | else:
|
38 | 39 | IMAGE_TYPES = ()
|
39 | 40 | try:
|
40 | 41 | import PIL.Image
|
41 |
| - import PIL.PngImagePlugin |
| 42 | + import PIL.ImageFile |
42 | 43 |
|
43 | 44 | IMAGE_TYPES = IMAGE_TYPES + (PIL.Image.Image,)
|
44 | 45 | except ImportError:
|
|
72 | 73 | ]
|
73 | 74 |
|
74 | 75 |
|
75 |
| -def pil_to_blob(img): |
76 |
| - # When you load an image with PIL you get a subclass of PIL.Image |
77 |
| - # The subclass knows what file type it was loaded from it has a `.format` class attribute |
78 |
| - # and the `get_format_mimetype` method. Convert these back to the same file type. |
79 |
| - # |
80 |
| - # The base image class doesn't know its file type, it just knows its mode. |
81 |
| - # RGBA converts to PNG easily, P[allet] converts to GIF, RGB to GIF. |
82 |
| - # But for anything else I'm not going to bother mapping it out (for now) let's just convert to RGB and send it. |
83 |
| - # |
84 |
| - # References: |
85 |
| - # - file formats: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html |
86 |
| - # - image modes: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes |
87 |
| - |
88 |
| - bytesio = io.BytesIO() |
89 |
| - |
90 |
| - get_mime = getattr(img, "get_format_mimetype", None) |
91 |
| - if get_mime is not None: |
92 |
| - # If the image is created from a file, convert back to the same file type. |
93 |
| - img.save(bytesio, format=img.format) |
94 |
| - mime_type = img.get_format_mimetype() |
95 |
| - elif img.mode == "RGBA": |
96 |
| - img.save(bytesio, format="PNG") |
97 |
| - mime_type = "image/png" |
98 |
| - elif img.mode == "P": |
99 |
| - img.save(bytesio, format="GIF") |
100 |
| - mime_type = "image/gif" |
101 |
| - else: |
102 |
| - if img.mode != "RGB": |
103 |
| - img = img.convert("RGB") |
104 |
| - img.save(bytesio, format="JPEG") |
105 |
| - mime_type = "image/jpeg" |
106 |
| - bytesio.seek(0) |
107 |
| - data = bytesio.read() |
108 |
| - return protos.Blob(mime_type=mime_type, data=data) |
| 76 | +def _pil_to_blob(image: PIL.Image.Image) -> protos.Blob: |
| 77 | + # If the image is a local file, return a file-based blob without any modification. |
| 78 | + # Otherwise, return a lossless WebP blob (same quality with optimized size). |
| 79 | + def file_blob(image: PIL.Image.Image) -> protos.Blob | None: |
| 80 | + if not isinstance(image, PIL.ImageFile.ImageFile) or image.filename is None: |
| 81 | + return None |
| 82 | + filename = str(image.filename) |
| 83 | + if not pathlib.Path(filename).is_file(): |
| 84 | + return None |
| 85 | + |
| 86 | + mime_type = image.get_format_mimetype() |
| 87 | + image_bytes = pathlib.Path(filename).read_bytes() |
| 88 | + |
| 89 | + return protos.Blob(mime_type=mime_type, data=image_bytes) |
| 90 | + |
| 91 | + def webp_blob(image: PIL.Image.Image) -> protos.Blob: |
| 92 | + # Reference: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp |
| 93 | + image_io = io.BytesIO() |
| 94 | + image.save(image_io, format="webp", lossless=True) |
| 95 | + image_io.seek(0) |
| 96 | + |
| 97 | + mime_type = "image/webp" |
| 98 | + image_bytes = image_io.read() |
| 99 | + |
| 100 | + return protos.Blob(mime_type=mime_type, data=image_bytes) |
| 101 | + |
| 102 | + return file_blob(image) or webp_blob(image) |
109 | 103 |
|
110 | 104 |
|
111 | 105 | def image_to_blob(image) -> protos.Blob:
|
112 | 106 | if PIL is not None:
|
113 | 107 | if isinstance(image, PIL.Image.Image):
|
114 |
| - return pil_to_blob(image) |
| 108 | + return _pil_to_blob(image) |
115 | 109 |
|
116 | 110 | if IPython is not None:
|
117 | 111 | if isinstance(image, IPython.display.Image):
|
|
0 commit comments