# -*- coding: utf-8 -*- # imageio is distributed under the terms of the (new) BSD License. """Read/Write images using FreeImage. Backend Library: `FreeImage `_ .. note:: To use this plugin you have to install its backend:: imageio_download_bin freeimage or you can download the backend using the function:: imageio.plugins.freeimage.download() Each Freeimage format has the ``flags`` keyword argument. See the `Freeimage documentation `_ for more information. Parameters ---------- flags : int A freeimage-specific option. In most cases we provide explicit parameters for influencing image reading. """ import numpy as np from ..core import Format, image_as_uint from ..core.request import RETURN_BYTES from ._freeimage import FNAME_PER_PLATFORM, IO_FLAGS, download, fi # noqa # todo: support files with only meta data class FreeimageFormat(Format): """See :mod:`imageio.plugins.freeimage`""" _modes = "i" def __init__(self, name, description, extensions=None, modes=None, *, fif=None): super().__init__(name, description, extensions=extensions, modes=modes) self._fif = fif @property def fif(self): return self._fif # Set when format is created def _can_read(self, request): # Ask freeimage if it can read it, maybe ext missing if fi.has_lib(): if not hasattr(request, "_fif"): try: request._fif = fi.getFIF(request.filename, "r", request.firstbytes) except Exception: # pragma: no cover request._fif = -1 if request._fif == self.fif: return True elif request._fif == 7 and self.fif == 14: # PPM gets identified as PBM and PPM can read PBM # see: https://github.com/imageio/imageio/issues/677 return True def _can_write(self, request): # Ask freeimage, because we are not aware of all formats if fi.has_lib(): if not hasattr(request, "_fif"): try: request._fif = fi.getFIF(request.filename, "w") except ValueError: # pragma: no cover if request.raw_uri == RETURN_BYTES: request._fif = self.fif else: request._fif = -1 if request._fif is self.fif: return True # -- class Reader(Format.Reader): def _get_length(self): return 1 def _open(self, flags=0): self._bm = fi.create_bitmap(self.request.filename, self.format.fif, flags) self._bm.load_from_filename(self.request.get_local_filename()) def _close(self): self._bm.close() def _get_data(self, index): if index != 0: raise IndexError("This format only supports singleton images.") return self._bm.get_image_data(), self._bm.get_meta_data() def _get_meta_data(self, index): if not (index is None or index == 0): raise IndexError() return self._bm.get_meta_data() # -- class Writer(Format.Writer): def _open(self, flags=0): self._flags = flags # Store flags for later use self._bm = None self._is_set = False # To prevent appending more than one image self._meta = {} def _close(self): # Set global meta data self._bm.set_meta_data(self._meta) # Write and close self._bm.save_to_filename(self.request.get_local_filename()) self._bm.close() def _append_data(self, im, meta): # Check if set if not self._is_set: self._is_set = True else: raise RuntimeError( "Singleton image; " "can only append image data once." ) # Pop unit dimension for grayscale images if im.ndim == 3 and im.shape[-1] == 1: im = im[:, :, 0] # Lazy instantaion of the bitmap, we need image data if self._bm is None: self._bm = fi.create_bitmap( self.request.filename, self.format.fif, self._flags ) self._bm.allocate(im) # Set data self._bm.set_image_data(im) # There is no distinction between global and per-image meta data # for singleton images self._meta = meta def _set_meta_data(self, meta): self._meta = meta # Special plugins # todo: there is also FIF_LOAD_NOPIXELS, # but perhaps that should be used with get_meta_data. class FreeimageBmpFormat(FreeimageFormat): """A BMP format based on the Freeimage library. This format supports grayscale, RGB and RGBA images. The freeimage plugin requires a `freeimage` binary. If this binary not available on the system, it can be downloaded manually from by either - the command line script ``imageio_download_bin freeimage`` - the Python method ``imageio.plugins.freeimage.download()`` Parameters for saving --------------------- compression : bool Whether to compress the bitmap using RLE when saving. Default False. It seems this does not always work, but who cares, you should use PNG anyway. """ class Writer(FreeimageFormat.Writer): def _open(self, flags=0, compression=False): # Build flags from kwargs flags = int(flags) if compression: flags |= IO_FLAGS.BMP_SAVE_RLE else: flags |= IO_FLAGS.BMP_DEFAULT # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): im = image_as_uint(im, bitdepth=8) return FreeimageFormat.Writer._append_data(self, im, meta) class FreeimagePngFormat(FreeimageFormat): """A PNG format based on the Freeimage library. This format supports grayscale, RGB and RGBA images. The freeimage plugin requires a `freeimage` binary. If this binary not available on the system, it can be downloaded manually from by either - the command line script ``imageio_download_bin freeimage`` - the Python method ``imageio.plugins.freeimage.download()`` Parameters for reading ---------------------- ignoregamma : bool Avoid gamma correction. Default True. Parameters for saving --------------------- compression : {0, 1, 6, 9} The compression factor. Higher factors result in more compression at the cost of speed. Note that PNG compression is always lossless. Default 9. quantize : int If specified, turn the given RGB or RGBA image in a paletted image for more efficient storage. The value should be between 2 and 256. If the value of 0 the image is not quantized. interlaced : bool Save using Adam7 interlacing. Default False. """ class Reader(FreeimageFormat.Reader): def _open(self, flags=0, ignoregamma=True): # Build flags from kwargs flags = int(flags) if ignoregamma: flags |= IO_FLAGS.PNG_IGNOREGAMMA # Enter as usual, with modified flags return FreeimageFormat.Reader._open(self, flags) # -- class Writer(FreeimageFormat.Writer): def _open(self, flags=0, compression=9, quantize=0, interlaced=False): compression_map = { 0: IO_FLAGS.PNG_Z_NO_COMPRESSION, 1: IO_FLAGS.PNG_Z_BEST_SPEED, 6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION, 9: IO_FLAGS.PNG_Z_BEST_COMPRESSION, } # Build flags from kwargs flags = int(flags) if interlaced: flags |= IO_FLAGS.PNG_INTERLACED try: flags |= compression_map[compression] except KeyError: raise ValueError("Png compression must be 0, 1, 6, or 9.") # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): if str(im.dtype) == "uint16": im = image_as_uint(im, bitdepth=16) else: im = image_as_uint(im, bitdepth=8) FreeimageFormat.Writer._append_data(self, im, meta) # Quantize? q = int(self.request.kwargs.get("quantize", False)) if not q: pass elif not (im.ndim == 3 and im.shape[-1] == 3): raise ValueError("Can only quantize RGB images") elif q < 2 or q > 256: raise ValueError("PNG quantize param must be 2..256") else: bm = self._bm.quantize(0, q) self._bm.close() self._bm = bm class FreeimageJpegFormat(FreeimageFormat): """A JPEG format based on the Freeimage library. This format supports grayscale and RGB images. The freeimage plugin requires a `freeimage` binary. If this binary not available on the system, it can be downloaded manually from by either - the command line script ``imageio_download_bin freeimage`` - the Python method ``imageio.plugins.freeimage.download()`` Parameters for reading ---------------------- exifrotate : bool Automatically rotate the image according to the exif flag. Default True. If 2 is given, do the rotation in Python instead of freeimage. quickread : bool Read the image more quickly, at the expense of quality. Default False. Parameters for saving --------------------- quality : scalar The compression factor of the saved image (1..100), higher numbers result in higher quality but larger file size. Default 75. progressive : bool Save as a progressive JPEG file (e.g. for images on the web). Default False. optimize : bool On saving, compute optimal Huffman coding tables (can reduce a few percent of file size). Default False. baseline : bool Save basic JPEG, without metadata or any markers. Default False. """ class Reader(FreeimageFormat.Reader): def _open(self, flags=0, exifrotate=True, quickread=False): # Build flags from kwargs flags = int(flags) if exifrotate and exifrotate != 2: flags |= IO_FLAGS.JPEG_EXIFROTATE if not quickread: flags |= IO_FLAGS.JPEG_ACCURATE # Enter as usual, with modified flags return FreeimageFormat.Reader._open(self, flags) def _get_data(self, index): im, meta = FreeimageFormat.Reader._get_data(self, index) im = self._rotate(im, meta) return im, meta def _rotate(self, im, meta): """Use Orientation information from EXIF meta data to orient the image correctly. Freeimage is also supposed to support that, and I am pretty sure it once did, but now it does not, so let's just do it in Python. Edit: and now it works again, just leave in place as a fallback. """ if self.request.kwargs.get("exifrotate", None) == 2: try: ori = meta["EXIF_MAIN"]["Orientation"] except KeyError: # pragma: no cover pass # Orientation not available else: # pragma: no cover - we cannot touch all cases # www.impulseadventure.com/photo/exif-orientation.html if ori in [1, 2]: pass if ori in [3, 4]: im = np.rot90(im, 2) if ori in [5, 6]: im = np.rot90(im, 3) if ori in [7, 8]: im = np.rot90(im) if ori in [2, 4, 5, 7]: # Flipped cases (rare) im = np.fliplr(im) return im # -- class Writer(FreeimageFormat.Writer): def _open( self, flags=0, quality=75, progressive=False, optimize=False, baseline=False ): # Test quality quality = int(quality) if quality < 1 or quality > 100: raise ValueError("JPEG quality should be between 1 and 100.") # Build flags from kwargs flags = int(flags) flags |= quality if progressive: flags |= IO_FLAGS.JPEG_PROGRESSIVE if optimize: flags |= IO_FLAGS.JPEG_OPTIMIZE if baseline: flags |= IO_FLAGS.JPEG_BASELINE # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): if im.ndim == 3 and im.shape[-1] == 4: raise IOError("JPEG does not support alpha channel.") im = image_as_uint(im, bitdepth=8) return FreeimageFormat.Writer._append_data(self, im, meta) class FreeimagePnmFormat(FreeimageFormat): """A PNM format based on the Freeimage library. This format supports single bit (PBM), grayscale (PGM) and RGB (PPM) images, even with ASCII or binary coding. The freeimage plugin requires a `freeimage` binary. If this binary not available on the system, it can be downloaded manually from by either - the command line script ``imageio_download_bin freeimage`` - the Python method ``imageio.plugins.freeimage.download()`` Parameters for saving --------------------- use_ascii : bool Save with ASCII coding. Default True. """ class Writer(FreeimageFormat.Writer): def _open(self, flags=0, use_ascii=True): # Build flags from kwargs flags = int(flags) if use_ascii: flags |= IO_FLAGS.PNM_SAVE_ASCII # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags)