File: freeimage.py

package info (click to toggle)
python-imageio 2.4.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 4,824 kB
  • sloc: python: 18,306; makefile: 145
file content (484 lines) | stat: -rw-r--r-- 17,911 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# -*- coding: utf-8 -*-
# imageio is distributed under the terms of the (new) BSD License.

""" Plugin that wraps the freeimage lib. The wrapper for Freeimage is
part of the core of imageio, but it's functionality is exposed via
the plugin system (therefore this plugin is very thin).
"""

from __future__ import absolute_import, print_function, division

import numpy as np

from .. import formats
from ..core import Format, image_as_uint
from ._freeimage import fi, download, IO_FLAGS, FNAME_PER_PLATFORM  # noqa


# todo: support files with only meta data


class FreeimageFormat(Format):
    """ This is the default format used for FreeImage. Each Freeimage
    format has the 'flags' keyword argument. See the Freeimage
    documentation for more information.

    The freeimage plugin requires a `freeimage` binary. If this binary
    not available on the system, it can be downloaded manually from
    <https://github.com/imageio/imageio-binaries> by either
    
    - the command line script ``imageio_download_bin freeimage``
    - the Python method ``imageio.plugins.freeimage.download()``

    Parameters for reading
    ----------------------
    flags : int
        A freeimage-specific option. In most cases we provide explicit
        parameters for influencing image reading.
    
    Parameters for saving
    ----------------------
    flags : int
        A freeimage-specific option. In most cases we provide explicit
        parameters for influencing image saving.
    """

    _modes = "i"

    @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

    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 Exception:  # pragma: no cover
                    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
    <https://github.com/imageio/imageio-binaries> 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
    <https://github.com/imageio/imageio-binaries> 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
    <https://github.com/imageio/imageio-binaries> 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)


## Create the formats

SPECIAL_CLASSES = {
    "jpeg": FreeimageJpegFormat,
    "png": FreeimagePngFormat,
    "bmp": FreeimageBmpFormat,
    "gif": None,  # defined in freeimagemulti
    "ico": None,  # defined in freeimagemulti
    "mng": None,  # defined in freeimagemulti
}

# rename TIFF to make way for the tiffile plugin
NAME_MAP = {"TIFF": "FI_TIFF"}

# This is a dump of supported FreeImage formats on Linux fi verion 3.16.0
# > imageio.plugins.freeimage.create_freeimage_formats()
# > for i in sorted(imageio.plugins.freeimage.fiformats): print('%r,' % (i, ))
fiformats = [
    ("BMP", 0, "Windows or OS/2 Bitmap", "bmp"),
    ("CUT", 21, "Dr. Halo", "cut"),
    ("DDS", 24, "DirectX Surface", "dds"),
    ("EXR", 29, "ILM OpenEXR", "exr"),
    ("G3", 27, "Raw fax format CCITT G.3", "g3"),
    ("GIF", 25, "Graphics Interchange Format", "gif"),
    ("HDR", 26, "High Dynamic Range Image", "hdr"),
    ("ICO", 1, "Windows Icon", "ico"),
    ("IFF", 5, "IFF Interleaved Bitmap", "iff,lbm"),
    ("J2K", 30, "JPEG-2000 codestream", "j2k,j2c"),
    ("JNG", 3, "JPEG Network Graphics", "jng"),
    ("JP2", 31, "JPEG-2000 File Format", "jp2"),
    ("JPEG", 2, "JPEG - JFIF Compliant", "jpg,jif,jpeg,jpe"),
    ("JPEG-XR", 36, "JPEG XR image format", "jxr,wdp,hdp"),
    ("KOALA", 4, "C64 Koala Graphics", "koa"),
    ("MNG", 6, "Multiple-image Network Graphics", "mng"),
    ("PBM", 7, "Portable Bitmap (ASCII)", "pbm"),
    ("PBMRAW", 8, "Portable Bitmap (RAW)", "pbm"),
    ("PCD", 9, "Kodak PhotoCD", "pcd"),
    ("PCX", 10, "Zsoft Paintbrush", "pcx"),
    ("PFM", 32, "Portable floatmap", "pfm"),
    ("PGM", 11, "Portable Greymap (ASCII)", "pgm"),
    ("PGMRAW", 12, "Portable Greymap (RAW)", "pgm"),
    ("PICT", 33, "Macintosh PICT", "pct,pict,pic"),
    ("PNG", 13, "Portable Network Graphics", "png"),
    ("PPM", 14, "Portable Pixelmap (ASCII)", "ppm"),
    ("PPMRAW", 15, "Portable Pixelmap (RAW)", "ppm"),
    ("PSD", 20, "Adobe Photoshop", "psd"),
    ("RAS", 16, "Sun Raster Image", "ras"),
    (
        "RAW",
        34,
        "RAW camera image",
        "3fr,arw,bay,bmq,cap,cine,cr2,crw,cs1,dc2,"
        "dcr,drf,dsc,dng,erf,fff,ia,iiq,k25,kc2,kdc,mdc,mef,mos,mrw,nef,nrw,orf,"
        "pef,ptx,pxn,qtk,raf,raw,rdc,rw2,rwl,rwz,sr2,srf,srw,sti",
    ),
    ("SGI", 28, "SGI Image Format", "sgi,rgb,rgba,bw"),
    ("TARGA", 17, "Truevision Targa", "tga,targa"),
    ("TIFF", 18, "Tagged Image File Format", "tif,tiff"),
    ("WBMP", 19, "Wireless Bitmap", "wap,wbmp,wbm"),
    ("WebP", 35, "Google WebP image format", "webp"),
    ("XBM", 22, "X11 Bitmap Format", "xbm"),
    ("XPM", 23, "X11 Pixmap Format", "xpm"),
]


def _create_predefined_freeimage_formats():

    for name, i, des, ext in fiformats:
        # name = NAME_MAP.get(name, name)
        # Get class for format
        FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat)
        if FormatClass:
            # Create Format and add
            format = FormatClass(name + "-FI", des, ext, FormatClass._modes)
            format._fif = i
            formats.add_format(format)


def create_freeimage_formats():
    """ By default, imageio registers a list of predefined formats
    that freeimage can handle. If your version of imageio can handle
    more formats, you can call this function to register them.
    """
    fiformats[:] = []

    # Freeimage available?
    if fi is None:  # pragma: no cover
        return

    # Init
    lib = fi._lib

    # Create formats
    for i in range(lib.FreeImage_GetFIFCount()):
        if lib.FreeImage_IsPluginEnabled(i):
            # Get info
            name = lib.FreeImage_GetFormatFromFIF(i).decode("ascii")
            des = lib.FreeImage_GetFIFDescription(i).decode("ascii")
            ext = lib.FreeImage_GetFIFExtensionList(i).decode("ascii")
            fiformats.append((name, i, des, ext))
            # name = NAME_MAP.get(name, name)
            # Get class for format
            FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat)
            if not FormatClass:
                continue
            # Create Format and add
            format = FormatClass(name + "-FI", des, ext, FormatClass._modes)
            format._fif = i
            formats.add_format(format, overwrite=True)


_create_predefined_freeimage_formats()