1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4"""Read/Write images using FreeImage.
5
6Backend Library: `FreeImage <https://freeimage.sourceforge.io/>`_
7
8.. note::
9 To use this plugin you have to install its backend::
10
11 imageio_download_bin freeimage
12
13 or you can download the backend using the function::
14
15 imageio.plugins.freeimage.download()
16
17Each Freeimage format has the ``flags`` keyword argument. See the `Freeimage
18documentation <https://freeimage.sourceforge.io/>`_ for more information.
19
20Parameters
21----------
22flags : int
23 A freeimage-specific option. In most cases we provide explicit
24 parameters for influencing image reading.
25
26"""
27
28import numpy as np
29
30from ..core import Format, image_as_uint
31from ..core.request import RETURN_BYTES
32from ._freeimage import FNAME_PER_PLATFORM, IO_FLAGS, download, fi # noqa
33
34# todo: support files with only meta data
35
36
37class FreeimageFormat(Format):
38 """See :mod:`imageio.plugins.freeimage`"""
39
40 _modes = "i"
41
42 def __init__(self, name, description, extensions=None, modes=None, *, fif=None):
43 super().__init__(name, description, extensions=extensions, modes=modes)
44 self._fif = fif
45
46 @property
47 def fif(self):
48 return self._fif # Set when format is created
49
50 def _can_read(self, request):
51 # Ask freeimage if it can read it, maybe ext missing
52 if fi.has_lib():
53 if not hasattr(request, "_fif"):
54 try:
55 request._fif = fi.getFIF(request.filename, "r", request.firstbytes)
56 except Exception: # pragma: no cover
57 request._fif = -1
58 if request._fif == self.fif:
59 return True
60 elif request._fif == 7 and self.fif == 14:
61 # PPM gets identified as PBM and PPM can read PBM
62 # see: https://github.com/imageio/imageio/issues/677
63 return True
64
65 def _can_write(self, request):
66 # Ask freeimage, because we are not aware of all formats
67 if fi.has_lib():
68 if not hasattr(request, "_fif"):
69 try:
70 request._fif = fi.getFIF(request.filename, "w")
71 except ValueError: # pragma: no cover
72 if request.raw_uri == RETURN_BYTES:
73 request._fif = self.fif
74 else:
75 request._fif = -1
76 if request._fif is self.fif:
77 return True
78
79 # --
80
81 class Reader(Format.Reader):
82 def _get_length(self):
83 return 1
84
85 def _open(self, flags=0):
86 self._bm = fi.create_bitmap(self.request.filename, self.format.fif, flags)
87 self._bm.load_from_filename(self.request.get_local_filename())
88
89 def _close(self):
90 self._bm.close()
91
92 def _get_data(self, index):
93 if index != 0:
94 raise IndexError("This format only supports singleton images.")
95 return self._bm.get_image_data(), self._bm.get_meta_data()
96
97 def _get_meta_data(self, index):
98 if not (index is None or index == 0):
99 raise IndexError()
100 return self._bm.get_meta_data()
101
102 # --
103
104 class Writer(Format.Writer):
105 def _open(self, flags=0):
106 self._flags = flags # Store flags for later use
107 self._bm = None
108 self._is_set = False # To prevent appending more than one image
109 self._meta = {}
110
111 def _close(self):
112 # Set global meta data
113 self._bm.set_meta_data(self._meta)
114 # Write and close
115 self._bm.save_to_filename(self.request.get_local_filename())
116 self._bm.close()
117
118 def _append_data(self, im, meta):
119 # Check if set
120 if not self._is_set:
121 self._is_set = True
122 else:
123 raise RuntimeError(
124 "Singleton image; " "can only append image data once."
125 )
126 # Pop unit dimension for grayscale images
127 if im.ndim == 3 and im.shape[-1] == 1:
128 im = im[:, :, 0]
129 # Lazy instantaion of the bitmap, we need image data
130 if self._bm is None:
131 self._bm = fi.create_bitmap(
132 self.request.filename, self.format.fif, self._flags
133 )
134 self._bm.allocate(im)
135 # Set data
136 self._bm.set_image_data(im)
137 # There is no distinction between global and per-image meta data
138 # for singleton images
139 self._meta = meta
140
141 def _set_meta_data(self, meta):
142 self._meta = meta
143
144
145# Special plugins
146
147# todo: there is also FIF_LOAD_NOPIXELS,
148# but perhaps that should be used with get_meta_data.
149
150
151class FreeimageBmpFormat(FreeimageFormat):
152 """A BMP format based on the Freeimage library.
153
154 This format supports grayscale, RGB and RGBA images.
155
156 The freeimage plugin requires a `freeimage` binary. If this binary
157 not available on the system, it can be downloaded manually from
158 <https://github.com/imageio/imageio-binaries> by either
159
160 - the command line script ``imageio_download_bin freeimage``
161 - the Python method ``imageio.plugins.freeimage.download()``
162
163 Parameters for saving
164 ---------------------
165 compression : bool
166 Whether to compress the bitmap using RLE when saving. Default False.
167 It seems this does not always work, but who cares, you should use
168 PNG anyway.
169
170 """
171
172 class Writer(FreeimageFormat.Writer):
173 def _open(self, flags=0, compression=False):
174 # Build flags from kwargs
175 flags = int(flags)
176 if compression:
177 flags |= IO_FLAGS.BMP_SAVE_RLE
178 else:
179 flags |= IO_FLAGS.BMP_DEFAULT
180 # Act as usual, but with modified flags
181 return FreeimageFormat.Writer._open(self, flags)
182
183 def _append_data(self, im, meta):
184 im = image_as_uint(im, bitdepth=8)
185 return FreeimageFormat.Writer._append_data(self, im, meta)
186
187
188class FreeimagePngFormat(FreeimageFormat):
189 """A PNG format based on the Freeimage library.
190
191 This format supports grayscale, RGB and RGBA images.
192
193 The freeimage plugin requires a `freeimage` binary. If this binary
194 not available on the system, it can be downloaded manually from
195 <https://github.com/imageio/imageio-binaries> by either
196
197 - the command line script ``imageio_download_bin freeimage``
198 - the Python method ``imageio.plugins.freeimage.download()``
199
200 Parameters for reading
201 ----------------------
202 ignoregamma : bool
203 Avoid gamma correction. Default True.
204
205 Parameters for saving
206 ---------------------
207 compression : {0, 1, 6, 9}
208 The compression factor. Higher factors result in more
209 compression at the cost of speed. Note that PNG compression is
210 always lossless. Default 9.
211 quantize : int
212 If specified, turn the given RGB or RGBA image in a paletted image
213 for more efficient storage. The value should be between 2 and 256.
214 If the value of 0 the image is not quantized.
215 interlaced : bool
216 Save using Adam7 interlacing. Default False.
217 """
218
219 class Reader(FreeimageFormat.Reader):
220 def _open(self, flags=0, ignoregamma=True):
221 # Build flags from kwargs
222 flags = int(flags)
223 if ignoregamma:
224 flags |= IO_FLAGS.PNG_IGNOREGAMMA
225 # Enter as usual, with modified flags
226 return FreeimageFormat.Reader._open(self, flags)
227
228 # --
229
230 class Writer(FreeimageFormat.Writer):
231 def _open(self, flags=0, compression=9, quantize=0, interlaced=False):
232 compression_map = {
233 0: IO_FLAGS.PNG_Z_NO_COMPRESSION,
234 1: IO_FLAGS.PNG_Z_BEST_SPEED,
235 6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION,
236 9: IO_FLAGS.PNG_Z_BEST_COMPRESSION,
237 }
238 # Build flags from kwargs
239 flags = int(flags)
240 if interlaced:
241 flags |= IO_FLAGS.PNG_INTERLACED
242 try:
243 flags |= compression_map[compression]
244 except KeyError:
245 raise ValueError("Png compression must be 0, 1, 6, or 9.")
246 # Act as usual, but with modified flags
247 return FreeimageFormat.Writer._open(self, flags)
248
249 def _append_data(self, im, meta):
250 if str(im.dtype) == "uint16":
251 im = image_as_uint(im, bitdepth=16)
252 else:
253 im = image_as_uint(im, bitdepth=8)
254 FreeimageFormat.Writer._append_data(self, im, meta)
255 # Quantize?
256 q = int(self.request.kwargs.get("quantize", False))
257 if not q:
258 pass
259 elif not (im.ndim == 3 and im.shape[-1] == 3):
260 raise ValueError("Can only quantize RGB images")
261 elif q < 2 or q > 256:
262 raise ValueError("PNG quantize param must be 2..256")
263 else:
264 bm = self._bm.quantize(0, q)
265 self._bm.close()
266 self._bm = bm
267
268
269class FreeimageJpegFormat(FreeimageFormat):
270 """A JPEG format based on the Freeimage library.
271
272 This format supports grayscale and RGB images.
273
274 The freeimage plugin requires a `freeimage` binary. If this binary
275 not available on the system, it can be downloaded manually from
276 <https://github.com/imageio/imageio-binaries> by either
277
278 - the command line script ``imageio_download_bin freeimage``
279 - the Python method ``imageio.plugins.freeimage.download()``
280
281 Parameters for reading
282 ----------------------
283 exifrotate : bool
284 Automatically rotate the image according to the exif flag.
285 Default True. If 2 is given, do the rotation in Python instead
286 of freeimage.
287 quickread : bool
288 Read the image more quickly, at the expense of quality.
289 Default False.
290
291 Parameters for saving
292 ---------------------
293 quality : scalar
294 The compression factor of the saved image (1..100), higher
295 numbers result in higher quality but larger file size. Default 75.
296 progressive : bool
297 Save as a progressive JPEG file (e.g. for images on the web).
298 Default False.
299 optimize : bool
300 On saving, compute optimal Huffman coding tables (can reduce a
301 few percent of file size). Default False.
302 baseline : bool
303 Save basic JPEG, without metadata or any markers. Default False.
304
305 """
306
307 class Reader(FreeimageFormat.Reader):
308 def _open(self, flags=0, exifrotate=True, quickread=False):
309 # Build flags from kwargs
310 flags = int(flags)
311 if exifrotate and exifrotate != 2:
312 flags |= IO_FLAGS.JPEG_EXIFROTATE
313 if not quickread:
314 flags |= IO_FLAGS.JPEG_ACCURATE
315 # Enter as usual, with modified flags
316 return FreeimageFormat.Reader._open(self, flags)
317
318 def _get_data(self, index):
319 im, meta = FreeimageFormat.Reader._get_data(self, index)
320 im = self._rotate(im, meta)
321 return im, meta
322
323 def _rotate(self, im, meta):
324 """Use Orientation information from EXIF meta data to
325 orient the image correctly. Freeimage is also supposed to
326 support that, and I am pretty sure it once did, but now it
327 does not, so let's just do it in Python.
328 Edit: and now it works again, just leave in place as a fallback.
329 """
330 if self.request.kwargs.get("exifrotate", None) == 2:
331 try:
332 ori = meta["EXIF_MAIN"]["Orientation"]
333 except KeyError: # pragma: no cover
334 pass # Orientation not available
335 else: # pragma: no cover - we cannot touch all cases
336 # www.impulseadventure.com/photo/exif-orientation.html
337 if ori in [1, 2]:
338 pass
339 if ori in [3, 4]:
340 im = np.rot90(im, 2)
341 if ori in [5, 6]:
342 im = np.rot90(im, 3)
343 if ori in [7, 8]:
344 im = np.rot90(im)
345 if ori in [2, 4, 5, 7]: # Flipped cases (rare)
346 im = np.fliplr(im)
347 return im
348
349 # --
350
351 class Writer(FreeimageFormat.Writer):
352 def _open(
353 self, flags=0, quality=75, progressive=False, optimize=False, baseline=False
354 ):
355 # Test quality
356 quality = int(quality)
357 if quality < 1 or quality > 100:
358 raise ValueError("JPEG quality should be between 1 and 100.")
359 # Build flags from kwargs
360 flags = int(flags)
361 flags |= quality
362 if progressive:
363 flags |= IO_FLAGS.JPEG_PROGRESSIVE
364 if optimize:
365 flags |= IO_FLAGS.JPEG_OPTIMIZE
366 if baseline:
367 flags |= IO_FLAGS.JPEG_BASELINE
368 # Act as usual, but with modified flags
369 return FreeimageFormat.Writer._open(self, flags)
370
371 def _append_data(self, im, meta):
372 if im.ndim == 3 and im.shape[-1] == 4:
373 raise IOError("JPEG does not support alpha channel.")
374 im = image_as_uint(im, bitdepth=8)
375 return FreeimageFormat.Writer._append_data(self, im, meta)
376
377
378class FreeimagePnmFormat(FreeimageFormat):
379 """A PNM format based on the Freeimage library.
380
381 This format supports single bit (PBM), grayscale (PGM) and RGB (PPM)
382 images, even with ASCII or binary coding.
383
384 The freeimage plugin requires a `freeimage` binary. If this binary
385 not available on the system, it can be downloaded manually from
386 <https://github.com/imageio/imageio-binaries> by either
387
388 - the command line script ``imageio_download_bin freeimage``
389 - the Python method ``imageio.plugins.freeimage.download()``
390
391 Parameters for saving
392 ---------------------
393 use_ascii : bool
394 Save with ASCII coding. Default True.
395 """
396
397 class Writer(FreeimageFormat.Writer):
398 def _open(self, flags=0, use_ascii=True):
399 # Build flags from kwargs
400 flags = int(flags)
401 if use_ascii:
402 flags |= IO_FLAGS.PNM_SAVE_ASCII
403 # Act as usual, but with modified flags
404 return FreeimageFormat.Writer._open(self, flags)