1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4""" Read/Write images using pillow/PIL (legacy).
5
6Backend Library: `Pillow <https://pillow.readthedocs.io/en/stable/>`_
7
8Pillow is a friendly fork of PIL (Python Image Library) and supports
9reading and writing of common formats (jpg, png, gif, tiff, ...). While
10these docs provide an overview of some of its features, pillow is
11constantly improving. Hence, the complete list of features can be found
12in pillows official docs (see the Backend Library link).
13
14Parameters for Reading
15----------------------
16pilmode : str
17 (Available for all formats except GIF-PIL)
18 From the Pillow documentation:
19
20 * 'L' (8-bit pixels, grayscale)
21 * 'P' (8-bit pixels, mapped to any other mode using a color palette)
22 * 'RGB' (3x8-bit pixels, true color)
23 * 'RGBA' (4x8-bit pixels, true color with transparency mask)
24 * 'CMYK' (4x8-bit pixels, color separation)
25 * 'YCbCr' (3x8-bit pixels, color video format)
26 * 'I' (32-bit signed integer pixels)
27 * 'F' (32-bit floating point pixels)
28
29 PIL also provides limited support for a few special modes, including
30 'LA' ('L' with alpha), 'RGBX' (true color with padding) and 'RGBa'
31 (true color with premultiplied alpha).
32
33 When translating a color image to grayscale (mode 'L', 'I' or 'F'),
34 the library uses the ITU-R 601-2 luma transform::
35
36 L = R * 299/1000 + G * 587/1000 + B * 114/1000
37as_gray : bool
38 (Available for all formats except GIF-PIL)
39 If True, the image is converted using mode 'F'. When `mode` is
40 not None and `as_gray` is True, the image is first converted
41 according to `mode`, and the result is then "flattened" using
42 mode 'F'.
43ignoregamma : bool
44 (Only available in PNG-PIL)
45 Avoid gamma correction. Default True.
46exifrotate : bool
47 (Only available in JPEG-PIL)
48 Automatically rotate the image according to exif flag. Default True.
49
50
51Parameters for saving
52---------------------
53optimize : bool
54 (Only available in PNG-PIL)
55 If present and true, instructs the PNG writer to make the output file
56 as small as possible. This includes extra processing in order to find
57 optimal encoder settings.
58transparency:
59 (Only available in PNG-PIL)
60 This option controls what color image to mark as transparent.
61dpi: tuple of two scalars
62 (Only available in PNG-PIL)
63 The desired dpi in each direction.
64pnginfo: PIL.PngImagePlugin.PngInfo
65 (Only available in PNG-PIL)
66 Object containing text tags.
67compress_level: int
68 (Only available in PNG-PIL)
69 ZLIB compression level, a number between 0 and 9: 1 gives best speed,
70 9 gives best compression, 0 gives no compression at all. Default is 9.
71 When ``optimize`` option is True ``compress_level`` has no effect
72 (it is set to 9 regardless of a value passed).
73compression: int
74 (Only available in PNG-PIL)
75 Compatibility with the freeimage PNG format. If given, it overrides
76 compress_level.
77icc_profile:
78 (Only available in PNG-PIL)
79 The ICC Profile to include in the saved file.
80bits (experimental): int
81 (Only available in PNG-PIL)
82 This option controls how many bits to store. If omitted,
83 the PNG writer uses 8 bits (256 colors).
84quantize:
85 (Only available in PNG-PIL)
86 Compatibility with the freeimage PNG format. If given, it overrides
87 bits. In this case, given as a number between 1-256.
88dictionary (experimental): dict
89 (Only available in PNG-PIL)
90 Set the ZLIB encoder dictionary.
91prefer_uint8: bool
92 (Only available in PNG-PIL)
93 Let the PNG writer truncate uint16 image arrays to uint8 if their values fall
94 within the range [0, 255]. Defaults to true for legacy compatibility, however
95 it is recommended to set this to false to avoid unexpected behavior when
96 saving e.g. weakly saturated images.
97
98quality : scalar
99 (Only available in JPEG-PIL)
100 The compression factor of the saved image (1..100), higher
101 numbers result in higher quality but larger file size. Default 75.
102progressive : bool
103 (Only available in JPEG-PIL)
104 Save as a progressive JPEG file (e.g. for images on the web).
105 Default False.
106optimize : bool
107 (Only available in JPEG-PIL)
108 On saving, compute optimal Huffman coding tables (can reduce a few
109 percent of file size). Default False.
110dpi : tuple of int
111 (Only available in JPEG-PIL)
112 The pixel density, ``(x,y)``.
113icc_profile : object
114 (Only available in JPEG-PIL)
115 If present and true, the image is stored with the provided ICC profile.
116 If this parameter is not provided, the image will be saved with no
117 profile attached.
118exif : dict
119 (Only available in JPEG-PIL)
120 If present, the image will be stored with the provided raw EXIF data.
121subsampling : str
122 (Only available in JPEG-PIL)
123 Sets the subsampling for the encoder. See Pillow docs for details.
124qtables : object
125 (Only available in JPEG-PIL)
126 Set the qtables for the encoder. See Pillow docs for details.
127quality_mode : str
128 (Only available in JPEG2000-PIL)
129 Either `"rates"` or `"dB"` depending on the units you want to use to
130 specify image quality.
131quality : float
132 (Only available in JPEG2000-PIL)
133 Approximate size reduction (if quality mode is `rates`) or a signal to noise ratio
134 in decibels (if quality mode is `dB`).
135loop : int
136 (Only available in GIF-PIL)
137 The number of iterations. Default 0 (meaning loop indefinitely).
138duration : {float, list}
139 (Only available in GIF-PIL)
140 The duration (in milliseconds) of each frame. Either specify one value
141 that is used for all frames, or one value for each frame.
142fps : float
143 (Only available in GIF-PIL)
144 The number of frames per second. If duration is not given, the
145 duration for each frame is set to 1/fps. Default 10.
146palettesize : int
147 (Only available in GIF-PIL)
148 The number of colors to quantize the image to. Is rounded to
149 the nearest power of two. Default 256.
150subrectangles : bool
151 (Only available in GIF-PIL)
152 If True, will try and optimize the GIF by storing only the
153 rectangular parts of each frame that change with respect to the
154 previous. Default False.
155
156Notes
157-----
158To enable JPEG 2000 support, you need to build and install the OpenJPEG library,
159version 2.0.0 or higher, before building the Python Imaging Library. Windows
160users can install the OpenJPEG binaries available on the OpenJPEG website, but
161must add them to their PATH in order to use PIL (if you fail to do this, you
162will get errors about not being able to load the ``_imaging`` DLL).
163
164GIF images read with this plugin are always RGBA. The alpha channel is ignored
165when saving RGB images.
166"""
167
168import logging
169import threading
170
171import numpy as np
172
173from ..core import Format, image_as_uint
174from ..core.request import URI_FILE, URI_BYTES
175
176
177logger = logging.getLogger(__name__)
178
179
180# todo: Pillow ImageGrab module supports grabbing the screen on Win and OSX.
181
182
183GENERIC_DOCS = """
184 Parameters for reading
185 ----------------------
186
187 pilmode : str
188 From the Pillow documentation:
189
190 * 'L' (8-bit pixels, grayscale)
191 * 'P' (8-bit pixels, mapped to any other mode using a color palette)
192 * 'RGB' (3x8-bit pixels, true color)
193 * 'RGBA' (4x8-bit pixels, true color with transparency mask)
194 * 'CMYK' (4x8-bit pixels, color separation)
195 * 'YCbCr' (3x8-bit pixels, color video format)
196 * 'I' (32-bit signed integer pixels)
197 * 'F' (32-bit floating point pixels)
198
199 PIL also provides limited support for a few special modes, including
200 'LA' ('L' with alpha), 'RGBX' (true color with padding) and 'RGBa'
201 (true color with premultiplied alpha).
202
203 When translating a color image to grayscale (mode 'L', 'I' or 'F'),
204 the library uses the ITU-R 601-2 luma transform::
205
206 L = R * 299/1000 + G * 587/1000 + B * 114/1000
207 as_gray : bool
208 If True, the image is converted using mode 'F'. When `mode` is
209 not None and `as_gray` is True, the image is first converted
210 according to `mode`, and the result is then "flattened" using
211 mode 'F'.
212"""
213
214
215class PillowFormat(Format):
216 """
217 Base format class for Pillow formats.
218 """
219
220 _pillow_imported = False
221 _Image = None
222 _modes = "i"
223 _description = ""
224
225 def __init__(self, *args, plugin_id: str = None, **kwargs):
226 super(PillowFormat, self).__init__(*args, **kwargs)
227 # Used to synchronize _init_pillow(), see #244
228 self._lock = threading.RLock()
229
230 self._plugin_id = plugin_id
231
232 @property
233 def plugin_id(self):
234 """The PIL plugin id."""
235 return self._plugin_id # Set when format is created
236
237 def _init_pillow(self):
238 with self._lock:
239 if not self._pillow_imported:
240 self._pillow_imported = True # more like tried to import
241 import PIL
242
243 if not hasattr(PIL, "__version__"): # pragma: no cover
244 raise ImportError(
245 "Imageio Pillow plugin requires " "Pillow, not PIL!"
246 )
247 from PIL import Image
248
249 self._Image = Image
250 elif self._Image is None: # pragma: no cover
251 raise RuntimeError("Imageio Pillow plugin requires " "Pillow lib.")
252 Image = self._Image
253
254 if self.plugin_id in ("PNG", "JPEG", "BMP", "GIF", "PPM"):
255 Image.preinit()
256 else:
257 Image.init()
258 return Image
259
260 def _can_read(self, request):
261 Image = self._init_pillow()
262 if self.plugin_id in Image.OPEN:
263 factory, accept = Image.OPEN[self.plugin_id]
264 if accept:
265 if request.firstbytes and accept(request.firstbytes):
266 return True
267
268 def _can_write(self, request):
269 Image = self._init_pillow()
270 if request.extension in self.extensions or request._uri_type in [
271 URI_FILE,
272 URI_BYTES,
273 ]:
274 if self.plugin_id in Image.SAVE:
275 return True
276
277 class Reader(Format.Reader):
278 def _open(self, pilmode=None, as_gray=False):
279 Image = self.format._init_pillow()
280 try:
281 factory, accept = Image.OPEN[self.format.plugin_id]
282 except KeyError:
283 raise RuntimeError("Format %s cannot read images." % self.format.name)
284 self._fp = self._get_file()
285 self._im = factory(self._fp, "")
286 if hasattr(Image, "_decompression_bomb_check"):
287 Image._decompression_bomb_check(self._im.size)
288 # Save the raw mode used by the palette for a BMP because it may not be the number of channels
289 # When the data is read, imageio hands the palette to PIL to handle and clears the rawmode argument
290 # However, there is a bug in PIL with handling animated GIFs with a different color palette on each frame.
291 # This issue is resolved by using the raw palette data but the rawmode information is now lost. So we
292 # store the raw mode for later use
293 if self._im.palette and self._im.palette.dirty:
294 self._im.palette.rawmode_saved = self._im.palette.rawmode
295 pil_try_read(self._im)
296 # Store args
297 self._kwargs = dict(
298 as_gray=as_gray, is_gray=_palette_is_grayscale(self._im)
299 )
300 # setting mode=None is not the same as just not providing it
301 if pilmode is not None:
302 self._kwargs["mode"] = pilmode
303 # Set length
304 self._length = 1
305 if hasattr(self._im, "n_frames"):
306 self._length = self._im.n_frames
307
308 def _get_file(self):
309 self._we_own_fp = False
310 return self.request.get_file()
311
312 def _close(self):
313 save_pillow_close(self._im)
314 if self._we_own_fp:
315 self._fp.close()
316 # else: request object handles closing the _fp
317
318 def _get_length(self):
319 return self._length
320
321 def _seek(self, index):
322 try:
323 self._im.seek(index)
324 except EOFError:
325 raise IndexError("Could not seek to index %i" % index)
326
327 def _get_data(self, index):
328 if index >= self._length:
329 raise IndexError("Image index %i > %i" % (index, self._length))
330 i = self._im.tell()
331 if i > index:
332 self._seek(index) # just try
333 else:
334 while i < index: # some formats need to be read in sequence
335 i += 1
336 self._seek(i)
337 if self._im.palette and self._im.palette.dirty:
338 self._im.palette.rawmode_saved = self._im.palette.rawmode
339 self._im.getdata()[0]
340 im = pil_get_frame(self._im, **self._kwargs)
341 return im, self._im.info
342
343 def _get_meta_data(self, index):
344 if not (index is None or index == 0):
345 raise IndexError()
346 return self._im.info
347
348 class Writer(Format.Writer):
349 def _open(self):
350 Image = self.format._init_pillow()
351 try:
352 self._save_func = Image.SAVE[self.format.plugin_id]
353 except KeyError:
354 raise RuntimeError("Format %s cannot write images." % self.format.name)
355 self._fp = self.request.get_file()
356 self._meta = {}
357 self._written = False
358
359 def _close(self):
360 pass # request object handled closing _fp
361
362 def _append_data(self, im, meta):
363 if self._written:
364 raise RuntimeError(
365 "Format %s only supports single images." % self.format.name
366 )
367 # Pop unit dimension for grayscale images
368 if im.ndim == 3 and im.shape[-1] == 1:
369 im = im[:, :, 0]
370 self._written = True
371 self._meta.update(meta)
372 img = ndarray_to_pil(
373 im, self.format.plugin_id, self._meta.pop("prefer_uint8", True)
374 )
375 if "bits" in self._meta:
376 img = img.quantize() # Make it a P image, so bits arg is used
377 img.save(self._fp, format=self.format.plugin_id, **self._meta)
378 save_pillow_close(img)
379
380 def set_meta_data(self, meta):
381 self._meta.update(meta)
382
383
384class PNGFormat(PillowFormat):
385 """See :mod:`imageio.plugins.pillow_legacy`"""
386
387 class Reader(PillowFormat.Reader):
388 def _open(self, pilmode=None, as_gray=False, ignoregamma=True):
389 return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
390
391 def _get_data(self, index):
392 im, info = PillowFormat.Reader._get_data(self, index)
393 if not self.request.kwargs.get("ignoregamma", True):
394 # The gamma value in the file represents the gamma factor for the
395 # hardware on the system where the file was created, and is meant
396 # to be able to match the colors with the system on which the
397 # image is shown. See also issue #366
398 try:
399 gamma = float(info["gamma"])
400 except (KeyError, ValueError):
401 pass
402 else:
403 scale = float(65536 if im.dtype == np.uint16 else 255)
404 gain = 1.0
405 im[:] = ((im / scale) ** gamma) * scale * gain + 0.4999
406 return im, info
407
408 # --
409
410 class Writer(PillowFormat.Writer):
411 def _open(self, compression=None, quantize=None, interlaced=False, **kwargs):
412 # Better default for compression
413 kwargs["compress_level"] = kwargs.get("compress_level", 9)
414
415 if compression is not None:
416 if compression < 0 or compression > 9:
417 raise ValueError("Invalid PNG compression level: %r" % compression)
418 kwargs["compress_level"] = compression
419 if quantize is not None:
420 for bits in range(1, 9):
421 if 2**bits == quantize:
422 break
423 else:
424 raise ValueError(
425 "PNG quantize must be power of two, " "not %r" % quantize
426 )
427 kwargs["bits"] = bits
428 if interlaced:
429 logger.warning("PIL PNG writer cannot produce interlaced images.")
430
431 ok_keys = (
432 "optimize",
433 "transparency",
434 "dpi",
435 "pnginfo",
436 "bits",
437 "compress_level",
438 "icc_profile",
439 "dictionary",
440 "prefer_uint8",
441 )
442 for key in kwargs:
443 if key not in ok_keys:
444 raise TypeError("Invalid arg for PNG writer: %r" % key)
445
446 PillowFormat.Writer._open(self)
447 self._meta.update(kwargs)
448
449 def _append_data(self, im, meta):
450 if str(im.dtype) == "uint16" and (im.ndim == 2 or im.shape[-1] == 1):
451 im = image_as_uint(im, bitdepth=16)
452 else:
453 im = image_as_uint(im, bitdepth=8)
454 PillowFormat.Writer._append_data(self, im, meta)
455
456
457class JPEGFormat(PillowFormat):
458 """See :mod:`imageio.plugins.pillow_legacy`"""
459
460 class Reader(PillowFormat.Reader):
461 def _open(self, pilmode=None, as_gray=False, exifrotate=True):
462 return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
463
464 def _get_file(self):
465 # Pillow uses seek for JPG, so we cannot directly stream from web
466 if self.request.filename.startswith(
467 ("http://", "https://")
468 ) or ".zip/" in self.request.filename.replace("\\", "/"):
469 self._we_own_fp = True
470 return open(self.request.get_local_filename(), "rb")
471 else:
472 self._we_own_fp = False
473 return self.request.get_file()
474
475 def _get_data(self, index):
476 im, info = PillowFormat.Reader._get_data(self, index)
477
478 # Handle exif
479 if "exif" in info:
480 from PIL.ExifTags import TAGS
481
482 info["EXIF_MAIN"] = {}
483 for tag, value in self._im._getexif().items():
484 decoded = TAGS.get(tag, tag)
485 info["EXIF_MAIN"][decoded] = value
486
487 im = self._rotate(im, info)
488 return im, info
489
490 def _rotate(self, im, meta):
491 """Use Orientation information from EXIF meta data to
492 orient the image correctly. Similar code as in FreeImage plugin.
493 """
494 if self.request.kwargs.get("exifrotate", True):
495 try:
496 ori = meta["EXIF_MAIN"]["Orientation"]
497 except KeyError: # pragma: no cover
498 pass # Orientation not available
499 else: # pragma: no cover - we cannot touch all cases
500 # www.impulseadventure.com/photo/exif-orientation.html
501 if ori in [1, 2]:
502 pass
503 if ori in [3, 4]:
504 im = np.rot90(im, 2)
505 if ori in [5, 6]:
506 im = np.rot90(im, 3)
507 if ori in [7, 8]:
508 im = np.rot90(im)
509 if ori in [2, 4, 5, 7]: # Flipped cases (rare)
510 im = np.fliplr(im)
511 return im
512
513 # --
514
515 class Writer(PillowFormat.Writer):
516 def _open(self, quality=75, progressive=False, optimize=False, **kwargs):
517 # The JPEG quality can be between 0 (worst) and 100 (best)
518 quality = int(quality)
519 if quality < 0 or quality > 100:
520 raise ValueError("JPEG quality should be between 0 and 100.")
521
522 kwargs["quality"] = quality
523 kwargs["progressive"] = bool(progressive)
524 kwargs["optimize"] = bool(progressive)
525
526 PillowFormat.Writer._open(self)
527 self._meta.update(kwargs)
528
529 def _append_data(self, im, meta):
530 if im.ndim == 3 and im.shape[-1] == 4:
531 raise IOError("JPEG does not support alpha channel.")
532 im = image_as_uint(im, bitdepth=8)
533 PillowFormat.Writer._append_data(self, im, meta)
534 return
535
536
537class JPEG2000Format(PillowFormat):
538 """See :mod:`imageio.plugins.pillow_legacy`"""
539
540 class Reader(PillowFormat.Reader):
541 def _open(self, pilmode=None, as_gray=False):
542 return PillowFormat.Reader._open(self, pilmode=pilmode, as_gray=as_gray)
543
544 def _get_file(self):
545 # Pillow uses seek for JPG, so we cannot directly stream from web
546 if self.request.filename.startswith(
547 ("http://", "https://")
548 ) or ".zip/" in self.request.filename.replace("\\", "/"):
549 self._we_own_fp = True
550 return open(self.request.get_local_filename(), "rb")
551 else:
552 self._we_own_fp = False
553 return self.request.get_file()
554
555 def _get_data(self, index):
556 im, info = PillowFormat.Reader._get_data(self, index)
557
558 # Handle exif
559 if "exif" in info:
560 from PIL.ExifTags import TAGS
561
562 info["EXIF_MAIN"] = {}
563 for tag, value in self._im._getexif().items():
564 decoded = TAGS.get(tag, tag)
565 info["EXIF_MAIN"][decoded] = value
566
567 im = self._rotate(im, info)
568 return im, info
569
570 def _rotate(self, im, meta):
571 """Use Orientation information from EXIF meta data to
572 orient the image correctly. Similar code as in FreeImage plugin.
573 """
574 if self.request.kwargs.get("exifrotate", True):
575 try:
576 ori = meta["EXIF_MAIN"]["Orientation"]
577 except KeyError: # pragma: no cover
578 pass # Orientation not available
579 else: # pragma: no cover - we cannot touch all cases
580 # www.impulseadventure.com/photo/exif-orientation.html
581 if ori in [1, 2]:
582 pass
583 if ori in [3, 4]:
584 im = np.rot90(im, 2)
585 if ori in [5, 6]:
586 im = np.rot90(im, 3)
587 if ori in [7, 8]:
588 im = np.rot90(im)
589 if ori in [2, 4, 5, 7]: # Flipped cases (rare)
590 im = np.fliplr(im)
591 return im
592
593 # --
594
595 class Writer(PillowFormat.Writer):
596 def _open(self, quality_mode="rates", quality=5, **kwargs):
597 # Check quality - in Pillow it should be no higher than 95
598 if quality_mode not in {"rates", "dB"}:
599 raise ValueError("Quality mode should be either 'rates' or 'dB'")
600
601 quality = float(quality)
602
603 if quality_mode == "rates" and (quality < 1 or quality > 1000):
604 raise ValueError(
605 "The quality value {} seems to be an invalid rate!".format(quality)
606 )
607 elif quality_mode == "dB" and (quality < 15 or quality > 100):
608 raise ValueError(
609 "The quality value {} seems to be an invalid PSNR!".format(quality)
610 )
611
612 kwargs["quality_mode"] = quality_mode
613 kwargs["quality_layers"] = [quality]
614
615 PillowFormat.Writer._open(self)
616 self._meta.update(kwargs)
617
618 def _append_data(self, im, meta):
619 if im.ndim == 3 and im.shape[-1] == 4:
620 raise IOError(
621 "The current implementation of JPEG 2000 does not support alpha channel."
622 )
623 im = image_as_uint(im, bitdepth=8)
624 PillowFormat.Writer._append_data(self, im, meta)
625 return
626
627
628def save_pillow_close(im):
629 # see issue #216 and #300
630 if hasattr(im, "close"):
631 if hasattr(getattr(im, "fp", None), "close"):
632 im.close()
633
634
635# Func from skimage
636
637# This cells contains code from scikit-image, in particular from
638# http://github.com/scikit-image/scikit-image/blob/master/
639# skimage/io/_plugins/pil_plugin.py
640# The scikit-image license applies.
641
642
643def pil_try_read(im):
644 try:
645 # this will raise an IOError if the file is not readable
646 im.getdata()[0]
647 except IOError as e:
648 site = "http://pillow.readthedocs.io/en/latest/installation.html"
649 site += "#external-libraries"
650 pillow_error_message = str(e)
651 error_message = (
652 'Could not load "%s" \n'
653 'Reason: "%s"\n'
654 "Please see documentation at: %s"
655 % (im.filename, pillow_error_message, site)
656 )
657 raise ValueError(error_message)
658
659
660def _palette_is_grayscale(pil_image):
661 if pil_image.mode != "P":
662 return False
663 elif pil_image.info.get("transparency", None): # see issue #475
664 return False
665 # get palette as an array with R, G, B columns
666 # Note: starting in pillow 9.1 palettes may have less than 256 entries
667 palette = np.asarray(pil_image.getpalette()).reshape((-1, 3))
668 # Not all palette colors are used; unused colors have junk values.
669 start, stop = pil_image.getextrema()
670 valid_palette = palette[start : stop + 1]
671 # Image is grayscale if channel differences (R - G and G - B)
672 # are all zero.
673 return np.allclose(np.diff(valid_palette), 0)
674
675
676def pil_get_frame(im, is_gray=None, as_gray=None, mode=None, dtype=None):
677 """
678 is_gray: Whether the image *is* gray (by inspecting its palette).
679 as_gray: Whether the resulting image must be converted to gaey.
680 mode: The mode to convert to.
681 """
682
683 if is_gray is None:
684 is_gray = _palette_is_grayscale(im)
685
686 frame = im
687
688 # Convert ...
689 if mode is not None:
690 # Mode is explicitly given ...
691 if mode != im.mode:
692 frame = im.convert(mode)
693 elif as_gray:
694 pass # don't do any auto-conversions (but do the explicit one above)
695 elif im.mode == "P" and is_gray:
696 # Paletted images that are already gray by their palette
697 # are converted so that the resulting numpy array is 2D.
698 frame = im.convert("L")
699 elif im.mode == "P":
700 # Paletted images are converted to RGB/RGBA. We jump some loops to make
701 # this work well.
702 if im.info.get("transparency", None) is not None:
703 # Let Pillow apply the transparency, see issue #210 and #246
704 frame = im.convert("RGBA")
705 elif im.palette.mode in ("RGB", "RGBA"):
706 # We can do this ourselves. Pillow seems to sometimes screw
707 # this up if a multi-gif has a palette for each frame ...
708 # Create palette array
709 p = np.frombuffer(im.palette.getdata()[1], np.uint8)
710 # Restore the raw mode that was saved to be used to parse the palette
711 if hasattr(im.palette, "rawmode_saved"):
712 im.palette.rawmode = im.palette.rawmode_saved
713 mode = im.palette.rawmode if im.palette.rawmode else im.palette.mode
714 nchannels = len(mode)
715 # Shape it.
716 p.shape = -1, nchannels
717 if p.shape[1] == 3 or (p.shape[1] == 4 and mode[-1] == "X"):
718 p = np.column_stack((p[:, :3], 255 * np.ones(p.shape[0], p.dtype)))
719 # Swap the axes if the mode is in BGR and not RGB
720 if mode.startswith("BGR"):
721 p = p[:, [2, 1, 0]] if p.shape[1] == 3 else p[:, [2, 1, 0, 3]]
722 # Apply palette
723 frame_paletted = np.array(im, np.uint8)
724 try:
725 frame = p[frame_paletted]
726 except Exception:
727 # Ok, let PIL do it. The introduction of the branch that
728 # tests `im.info['transparency']` should make this happen
729 # much less often, but let's keep it, to be safe.
730 frame = im.convert("RGBA")
731 else:
732 # Let Pillow do it. Unlinke skimage, we always convert
733 # to RGBA; palettes can be RGBA.
734 if True: # im.format == 'PNG' and 'transparency' in im.info:
735 frame = im.convert("RGBA")
736 else:
737 frame = im.convert("RGB")
738 elif "A" in im.mode:
739 frame = im.convert("RGBA")
740 elif im.mode == "CMYK":
741 frame = im.convert("RGB")
742 elif im.format == "GIF" and im.mode == "RGB":
743 # pillow9 returns RGBA images for subsequent frames so that it can deal
744 # with multi-frame GIF that use frame-level palettes and don't dispose
745 # all areas.
746
747 # For backwards compatibility, we promote everything to RGBA.
748 frame = im.convert("RGBA")
749
750 # Apply a post-convert if necessary
751 if as_gray:
752 frame = frame.convert("F") # Scipy compat
753 elif not isinstance(frame, np.ndarray) and frame.mode == "1":
754 # Workaround for crash in PIL. When im is 1-bit, the call array(im)
755 # can cause a segfault, or generate garbage. See
756 # https://github.com/scipy/scipy/issues/2138 and
757 # https://github.com/python-pillow/Pillow/issues/350.
758 #
759 # This converts im from a 1-bit image to an 8-bit image.
760 frame = frame.convert("L")
761
762 # Convert to numpy array
763 if im.mode.startswith("I;16"):
764 # e.g. in16 PNG's
765 shape = im.size
766 dtype = ">u2" if im.mode.endswith("B") else "<u2"
767 if "S" in im.mode:
768 dtype = dtype.replace("u", "i")
769 frame = np.frombuffer(frame.tobytes(), dtype).copy()
770 frame.shape = shape[::-1]
771 else:
772 # Use uint16 for PNG's in mode I
773 if im.format == "PNG" and im.mode == "I" and dtype is None:
774 dtype = "uint16"
775 frame = np.array(frame, dtype=dtype)
776
777 return frame
778
779
780def ndarray_to_pil(arr, format_str=None, prefer_uint8=True):
781 from PIL import Image
782
783 if arr.ndim == 3:
784 arr = image_as_uint(arr, bitdepth=8)
785 mode = {3: "RGB", 4: "RGBA"}[arr.shape[2]]
786
787 elif format_str in ["png", "PNG"]:
788 mode = "I;16"
789 mode_base = "I"
790
791 if arr.dtype.kind == "f":
792 arr = image_as_uint(arr)
793
794 elif prefer_uint8 and arr.max() < 256 and arr.min() >= 0:
795 arr = arr.astype(np.uint8)
796 mode = mode_base = "L"
797
798 else:
799 arr = image_as_uint(arr, bitdepth=16)
800
801 else:
802 arr = image_as_uint(arr, bitdepth=8)
803 mode = "L"
804 mode_base = "L"
805
806 if mode == "I;16" and int(getattr(Image, "__version__", "0").split(".")[0]) < 6:
807 # Pillow < v6.0.0 has limited support for the "I;16" mode,
808 # requiring us to fall back to this expensive workaround.
809 # tobytes actually creates a copy of the image, which is costly.
810 array_buffer = arr.tobytes()
811 if arr.ndim == 2:
812 im = Image.new(mode_base, arr.T.shape)
813 im.frombytes(array_buffer, "raw", mode)
814 else:
815 image_shape = (arr.shape[1], arr.shape[0])
816 im = Image.frombytes(mode, image_shape, array_buffer)
817 return im
818 else:
819 return Image.fromarray(arr, mode)
820
821
822# imported for backwards compatibility
823from .pillowmulti import GIFFormat, TIFFFormat # noqa: E402, F401