|
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