1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4# styletest: ignore E261
5
6""" Module imageio/freeimage.py
7
8This module contains the wrapper code for the freeimage library.
9The functions defined in this module are relatively thin; just thin
10enough so that arguments and results are native Python/numpy data
11types.
12
13"""
14
15import os
16import sys
17import ctypes
18import threading
19import logging
20import numpy
21
22from ..core import (
23    get_remote_file,
24    load_lib,
25    Dict,
26    resource_dirs,
27    IS_PYPY,
28    get_platform,
29    InternetNotAllowedError,
30    NeedDownloadError,
31)
32
33logger = logging.getLogger(__name__)
34
35TEST_NUMPY_NO_STRIDES = False  # To test pypy fallback
36
37FNAME_PER_PLATFORM = {
38    "osx32": "libfreeimage-3.16.0-osx10.6.dylib",  # universal library
39    "osx64": "libfreeimage-3.16.0-osx10.6.dylib",
40    "win32": "FreeImage-3.15.4-win32.dll",
41    "win64": "FreeImage-3.15.1-win64.dll",
42    "linux32": "libfreeimage-3.16.0-linux32.so",
43    "linux64": "libfreeimage-3.16.0-linux64.so",
44}
45
46
47def download(directory=None, force_download=False):
48    """ Download the FreeImage library to your computer.
49
50    Parameters
51    ----------
52    directory : str | None
53        The directory where the file will be cached if a download was
54        required to obtain the file. By default, the appdata directory
55        is used. This is also the first directory that is checked for
56        a local version of the file.
57    force_download : bool | str
58        If True, the file will be downloaded even if a local copy exists
59        (and this copy will be overwritten). Can also be a YYYY-MM-DD date
60        to ensure a file is up-to-date (modified date of a file on disk,
61        if present, is checked).
62    """
63    plat = get_platform()
64    if plat and plat in FNAME_PER_PLATFORM:
65        fname = "freeimage/" + FNAME_PER_PLATFORM[plat]
66        get_remote_file(fname=fname, directory=directory, force_download=force_download)
67        fi._lib = None  # allow trying again (needed to make tests work)
68
69
70def get_freeimage_lib():
71    """ Ensure we have our version of the binary freeimage lib.
72    """
73
74    lib = os.getenv("IMAGEIO_FREEIMAGE_LIB", None)
75    if lib:  # pragma: no cover
76        return lib
77
78    # Get filename to load
79    # If we do not provide a binary, the system may still do ...
80    plat = get_platform()
81    if plat and plat in FNAME_PER_PLATFORM:
82        try:
83            return get_remote_file("freeimage/" + FNAME_PER_PLATFORM[plat], auto=False)
84        except InternetNotAllowedError:
85            pass
86        except NeedDownloadError:
87            raise NeedDownloadError(
88                "Need FreeImage library. "
89                "You can obtain it with either:\n"
90                "  - download using the command: "
91                "imageio_download_bin freeimage\n"
92                "  - download by calling (in Python): "
93                "imageio.plugins.freeimage.download()\n"
94            )
95        except RuntimeError as e:  # pragma: no cover
96            logger.warning(str(e))
97
98
99# Define function to encode a filename to bytes (for the current system)
100efn = lambda x: x.encode(sys.getfilesystemencoding())
101
102# 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255
103GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32)
104
105
106class FI_TYPES(object):
107    FIT_UNKNOWN = 0
108    FIT_BITMAP = 1
109    FIT_UINT16 = 2
110    FIT_INT16 = 3
111    FIT_UINT32 = 4
112    FIT_INT32 = 5
113    FIT_FLOAT = 6
114    FIT_DOUBLE = 7
115    FIT_COMPLEX = 8
116    FIT_RGB16 = 9
117    FIT_RGBA16 = 10
118    FIT_RGBF = 11
119    FIT_RGBAF = 12
120
121    dtypes = {
122        FIT_BITMAP: numpy.uint8,
123        FIT_UINT16: numpy.uint16,
124        FIT_INT16: numpy.int16,
125        FIT_UINT32: numpy.uint32,
126        FIT_INT32: numpy.int32,
127        FIT_FLOAT: numpy.float32,
128        FIT_DOUBLE: numpy.float64,
129        FIT_COMPLEX: numpy.complex128,
130        FIT_RGB16: numpy.uint16,
131        FIT_RGBA16: numpy.uint16,
132        FIT_RGBF: numpy.float32,
133        FIT_RGBAF: numpy.float32,
134    }
135
136    fi_types = {
137        (numpy.uint8, 1): FIT_BITMAP,
138        (numpy.uint8, 3): FIT_BITMAP,
139        (numpy.uint8, 4): FIT_BITMAP,
140        (numpy.uint16, 1): FIT_UINT16,
141        (numpy.int16, 1): FIT_INT16,
142        (numpy.uint32, 1): FIT_UINT32,
143        (numpy.int32, 1): FIT_INT32,
144        (numpy.float32, 1): FIT_FLOAT,
145        (numpy.float64, 1): FIT_DOUBLE,
146        (numpy.complex128, 1): FIT_COMPLEX,
147        (numpy.uint16, 3): FIT_RGB16,
148        (numpy.uint16, 4): FIT_RGBA16,
149        (numpy.float32, 3): FIT_RGBF,
150        (numpy.float32, 4): FIT_RGBAF,
151    }
152
153    extra_dims = {
154        FIT_UINT16: [],
155        FIT_INT16: [],
156        FIT_UINT32: [],
157        FIT_INT32: [],
158        FIT_FLOAT: [],
159        FIT_DOUBLE: [],
160        FIT_COMPLEX: [],
161        FIT_RGB16: [3],
162        FIT_RGBA16: [4],
163        FIT_RGBF: [3],
164        FIT_RGBAF: [4],
165    }
166
167
168class IO_FLAGS(object):
169    FIF_LOAD_NOPIXELS = 0x8000  # loading: load the image header only
170    #                          # (not supported by all plugins)
171    BMP_DEFAULT = 0
172    BMP_SAVE_RLE = 1
173    CUT_DEFAULT = 0
174    DDS_DEFAULT = 0
175    EXR_DEFAULT = 0  # save data as half with piz-based wavelet compression
176    EXR_FLOAT = 0x0001  # save data as float instead of half (not recommended)
177    EXR_NONE = 0x0002  # save with no compression
178    EXR_ZIP = 0x0004  # save with zlib compression, in blocks of 16 scan lines
179    EXR_PIZ = 0x0008  # save with piz-based wavelet compression
180    EXR_PXR24 = 0x0010  # save with lossy 24-bit float compression
181    EXR_B44 = 0x0020  # save with lossy 44% float compression
182    #                # - goes to 22% when combined with EXR_LC
183    EXR_LC = 0x0040  # save images with one luminance and two chroma channels,
184    #               # rather than as RGB (lossy compression)
185    FAXG3_DEFAULT = 0
186    GIF_DEFAULT = 0
187    GIF_LOAD256 = 1  # Load the image as a 256 color image with ununsed
188    #               # palette entries, if it's 16 or 2 color
189    GIF_PLAYBACK = 2  # 'Play' the GIF to generate each frame (as 32bpp)
190    #                # instead of returning raw frame data when loading
191    HDR_DEFAULT = 0
192    ICO_DEFAULT = 0
193    ICO_MAKEALPHA = 1  # convert to 32bpp and create an alpha channel from the
194    #                 # AND-mask when loading
195    IFF_DEFAULT = 0
196    J2K_DEFAULT = 0  # save with a 16:1 rate
197    JP2_DEFAULT = 0  # save with a 16:1 rate
198    JPEG_DEFAULT = 0  # loading (see JPEG_FAST);
199    #                # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420)
200    JPEG_FAST = 0x0001  # load the file as fast as possible,
201    #                  # sacrificing some quality
202    JPEG_ACCURATE = 0x0002  # load the file with the best quality,
203    #                      # sacrificing some speed
204    JPEG_CMYK = 0x0004  # load separated CMYK "as is"
205    #                  # (use | to combine with other load flags)
206    JPEG_EXIFROTATE = 0x0008  # load and rotate according to
207    #                        # Exif 'Orientation' tag if available
208    JPEG_QUALITYSUPERB = 0x80  # save with superb quality (100:1)
209    JPEG_QUALITYGOOD = 0x0100  # save with good quality (75:1)
210    JPEG_QUALITYNORMAL = 0x0200  # save with normal quality (50:1)
211    JPEG_QUALITYAVERAGE = 0x0400  # save with average quality (25:1)
212    JPEG_QUALITYBAD = 0x0800  # save with bad quality (10:1)
213    JPEG_PROGRESSIVE = 0x2000  # save as a progressive-JPEG
214    #                         # (use | to combine with other save flags)
215    JPEG_SUBSAMPLING_411 = 0x1000  # save with high 4x1 chroma
216    #                             # subsampling (4:1:1)
217    JPEG_SUBSAMPLING_420 = 0x4000  # save with medium 2x2 medium chroma
218    #                             # subsampling (4:2:0) - default value
219    JPEG_SUBSAMPLING_422 = 0x8000  # save /w low 2x1 chroma subsampling (4:2:2)
220    JPEG_SUBSAMPLING_444 = 0x10000  # save with no chroma subsampling (4:4:4)
221    JPEG_OPTIMIZE = 0x20000  # on saving, compute optimal Huffman coding tables
222    #                       # (can reduce a few percent of file size)
223    JPEG_BASELINE = 0x40000  # save basic JPEG, without metadata or any markers
224    KOALA_DEFAULT = 0
225    LBM_DEFAULT = 0
226    MNG_DEFAULT = 0
227    PCD_DEFAULT = 0
228    PCD_BASE = 1  # load the bitmap sized 768 x 512
229    PCD_BASEDIV4 = 2  # load the bitmap sized 384 x 256
230    PCD_BASEDIV16 = 3  # load the bitmap sized 192 x 128
231    PCX_DEFAULT = 0
232    PFM_DEFAULT = 0
233    PICT_DEFAULT = 0
234    PNG_DEFAULT = 0
235    PNG_IGNOREGAMMA = 1  # loading: avoid gamma correction
236    PNG_Z_BEST_SPEED = 0x0001  # save using ZLib level 1 compression flag
237    #                         # (default value is 6)
238    PNG_Z_DEFAULT_COMPRESSION = 0x0006  # save using ZLib level 6 compression
239    #                                  # flag (default recommended value)
240    PNG_Z_BEST_COMPRESSION = 0x0009  # save using ZLib level 9 compression flag
241    #                               # (default value is 6)
242    PNG_Z_NO_COMPRESSION = 0x0100  # save without ZLib compression
243    PNG_INTERLACED = 0x0200  # save using Adam7 interlacing (use | to combine
244    #                       # with other save flags)
245    PNM_DEFAULT = 0
246    PNM_SAVE_RAW = 0  # Writer saves in RAW format (i.e. P4, P5 or P6)
247    PNM_SAVE_ASCII = 1  # Writer saves in ASCII format (i.e. P1, P2 or P3)
248    PSD_DEFAULT = 0
249    PSD_CMYK = 1  # reads tags for separated CMYK (default is conversion to RGB)
250    PSD_LAB = 2  # reads tags for CIELab (default is conversion to RGB)
251    RAS_DEFAULT = 0
252    RAW_DEFAULT = 0  # load the file as linear RGB 48-bit
253    RAW_PREVIEW = 1  # try to load the embedded JPEG preview with included
254    #               # Exif Data or default to RGB 24-bit
255    RAW_DISPLAY = 2  # load the file as RGB 24-bit
256    SGI_DEFAULT = 0
257    TARGA_DEFAULT = 0
258    TARGA_LOAD_RGB888 = 1  # Convert RGB555 and ARGB8888 -> RGB888.
259    TARGA_SAVE_RLE = 2  # Save with RLE compression
260    TIFF_DEFAULT = 0
261    TIFF_CMYK = 0x0001  # reads/stores tags for separated CMYK
262    #                  # (use | to combine with compression flags)
263    TIFF_PACKBITS = 0x0100  # save using PACKBITS compression
264    TIFF_DEFLATE = 0x0200  # save using DEFLATE (a.k.a. ZLIB) compression
265    TIFF_ADOBE_DEFLATE = 0x0400  # save using ADOBE DEFLATE compression
266    TIFF_NONE = 0x0800  # save without any compression
267    TIFF_CCITTFAX3 = 0x1000  # save using CCITT Group 3 fax encoding
268    TIFF_CCITTFAX4 = 0x2000  # save using CCITT Group 4 fax encoding
269    TIFF_LZW = 0x4000  # save using LZW compression
270    TIFF_JPEG = 0x8000  # save using JPEG compression
271    TIFF_LOGLUV = 0x10000  # save using LogLuv compression
272    WBMP_DEFAULT = 0
273    XBM_DEFAULT = 0
274    XPM_DEFAULT = 0
275
276
277class METADATA_MODELS(object):
278    FIMD_COMMENTS = 0
279    FIMD_EXIF_MAIN = 1
280    FIMD_EXIF_EXIF = 2
281    FIMD_EXIF_GPS = 3
282    FIMD_EXIF_MAKERNOTE = 4
283    FIMD_EXIF_INTEROP = 5
284    FIMD_IPTC = 6
285    FIMD_XMP = 7
286    FIMD_GEOTIFF = 8
287    FIMD_ANIMATION = 9
288
289
290class METADATA_DATATYPE(object):
291    FIDT_BYTE = 1  # 8-bit unsigned integer
292    FIDT_ASCII = 2  # 8-bit bytes w/ last byte null
293    FIDT_SHORT = 3  # 16-bit unsigned integer
294    FIDT_LONG = 4  # 32-bit unsigned integer
295    FIDT_RATIONAL = 5  # 64-bit unsigned fraction
296    FIDT_SBYTE = 6  # 8-bit signed integer
297    FIDT_UNDEFINED = 7  # 8-bit untyped data
298    FIDT_SSHORT = 8  # 16-bit signed integer
299    FIDT_SLONG = 9  # 32-bit signed integer
300    FIDT_SRATIONAL = 10  # 64-bit signed fraction
301    FIDT_FLOAT = 11  # 32-bit IEEE floating point
302    FIDT_DOUBLE = 12  # 64-bit IEEE floating point
303    FIDT_IFD = 13  # 32-bit unsigned integer (offset)
304    FIDT_PALETTE = 14  # 32-bit RGBQUAD
305    FIDT_LONG8 = 16  # 64-bit unsigned integer
306    FIDT_SLONG8 = 17  # 64-bit signed integer
307    FIDT_IFD8 = 18  # 64-bit unsigned integer (offset)
308
309    dtypes = {
310        FIDT_BYTE: numpy.uint8,
311        FIDT_SHORT: numpy.uint16,
312        FIDT_LONG: numpy.uint32,
313        FIDT_RATIONAL: [("numerator", numpy.uint32), ("denominator", numpy.uint32)],
314        FIDT_LONG8: numpy.uint64,
315        FIDT_SLONG8: numpy.int64,
316        FIDT_IFD8: numpy.uint64,
317        FIDT_SBYTE: numpy.int8,
318        FIDT_UNDEFINED: numpy.uint8,
319        FIDT_SSHORT: numpy.int16,
320        FIDT_SLONG: numpy.int32,
321        FIDT_SRATIONAL: [("numerator", numpy.int32), ("denominator", numpy.int32)],
322        FIDT_FLOAT: numpy.float32,
323        FIDT_DOUBLE: numpy.float64,
324        FIDT_IFD: numpy.uint32,
325        FIDT_PALETTE: [
326            ("R", numpy.uint8),
327            ("G", numpy.uint8),
328            ("B", numpy.uint8),
329            ("A", numpy.uint8),
330        ],
331    }
332
333
334class Freeimage(object):
335    """ Class to represent an interface to the FreeImage library.
336    This class is relatively thin. It provides a Pythonic API that converts
337    Freeimage objects to Python objects, but that's about it.
338    The actual implementation should be provided by the plugins.
339
340    The recommended way to call into the Freeimage library (so that
341    errors and warnings show up in the right moment) is to use this
342    object as a context manager:
343    with imageio.fi as lib:
344        lib.FreeImage_GetPalette()
345
346    """
347
348    _API = {
349        # All we're doing here is telling ctypes that some of the
350        # FreeImage functions return pointers instead of integers. (On
351        # 64-bit systems, without this information the pointers get
352        # truncated and crashes result). There's no need to list
353        # functions that return ints, or the types of the parameters
354        # to these or other functions -- that's fine to do implicitly.
355        # Note that the ctypes immediately converts the returned void_p
356        # back to a python int again! This is really not helpful,
357        # because then passing it back to another library call will
358        # cause truncation-to-32-bits on 64-bit systems. Thanks, ctypes!
359        # So after these calls one must immediately re-wrap the int as
360        # a c_void_p if it is to be passed back into FreeImage.
361        "FreeImage_AllocateT": (ctypes.c_void_p, None),
362        "FreeImage_FindFirstMetadata": (ctypes.c_void_p, None),
363        "FreeImage_GetBits": (ctypes.c_void_p, None),
364        "FreeImage_GetPalette": (ctypes.c_void_p, None),
365        "FreeImage_GetTagKey": (ctypes.c_char_p, None),
366        "FreeImage_GetTagValue": (ctypes.c_void_p, None),
367        "FreeImage_CreateTag": (ctypes.c_void_p, None),
368        "FreeImage_Save": (ctypes.c_void_p, None),
369        "FreeImage_Load": (ctypes.c_void_p, None),
370        "FreeImage_LoadFromMemory": (ctypes.c_void_p, None),
371        "FreeImage_OpenMultiBitmap": (ctypes.c_void_p, None),
372        "FreeImage_LoadMultiBitmapFromMemory": (ctypes.c_void_p, None),
373        "FreeImage_LockPage": (ctypes.c_void_p, None),
374        "FreeImage_OpenMemory": (ctypes.c_void_p, None),
375        # 'FreeImage_ReadMemory': (ctypes.c_void_p, None),
376        # 'FreeImage_CloseMemory': (ctypes.c_void_p, None),
377        "FreeImage_GetVersion": (ctypes.c_char_p, None),
378        "FreeImage_GetFIFExtensionList": (ctypes.c_char_p, None),
379        "FreeImage_GetFormatFromFIF": (ctypes.c_char_p, None),
380        "FreeImage_GetFIFDescription": (ctypes.c_char_p, None),
381        "FreeImage_ColorQuantizeEx": (ctypes.c_void_p, None),
382        # Pypy wants some extra definitions, so here we go ...
383        "FreeImage_IsLittleEndian": (ctypes.c_int, None),
384        "FreeImage_SetOutputMessage": (ctypes.c_void_p, None),
385        "FreeImage_GetFIFCount": (ctypes.c_int, None),
386        "FreeImage_IsPluginEnabled": (ctypes.c_int, None),
387        "FreeImage_GetFileType": (ctypes.c_int, None),
388        #
389        "FreeImage_GetTagType": (ctypes.c_int, None),
390        "FreeImage_GetTagLength": (ctypes.c_int, None),
391        "FreeImage_FindNextMetadata": (ctypes.c_int, None),
392        "FreeImage_FindCloseMetadata": (ctypes.c_void_p, None),
393        #
394        "FreeImage_GetFIFFromFilename": (ctypes.c_int, None),
395        "FreeImage_FIFSupportsReading": (ctypes.c_int, None),
396        "FreeImage_FIFSupportsWriting": (ctypes.c_int, None),
397        "FreeImage_FIFSupportsExportType": (ctypes.c_int, None),
398        "FreeImage_FIFSupportsExportBPP": (ctypes.c_int, None),
399        "FreeImage_GetHeight": (ctypes.c_int, None),
400        "FreeImage_GetWidth": (ctypes.c_int, None),
401        "FreeImage_GetImageType": (ctypes.c_int, None),
402        "FreeImage_GetBPP": (ctypes.c_int, None),
403        "FreeImage_GetColorsUsed": (ctypes.c_int, None),
404        "FreeImage_ConvertTo32Bits": (ctypes.c_void_p, None),
405        "FreeImage_GetPitch": (ctypes.c_int, None),
406        "FreeImage_Unload": (ctypes.c_void_p, None),
407    }
408
409    def __init__(self):
410
411        # Initialize freeimage lib as None
412        self._lib = None
413
414        # A lock to create thread-safety
415        self._lock = threading.RLock()
416
417        # Init log messages lists
418        self._messages = []
419
420        # Select functype for error handler
421        if sys.platform.startswith("win"):
422            functype = ctypes.WINFUNCTYPE
423        else:
424            functype = ctypes.CFUNCTYPE
425
426        # Create output message handler
427        @functype(None, ctypes.c_int, ctypes.c_char_p)
428        def error_handler(fif, message):
429            message = message.decode("utf-8")
430            self._messages.append(message)
431            while (len(self._messages)) > 256:
432                self._messages.pop(0)
433
434        # Make sure to keep a ref to function
435        self._error_handler = error_handler
436
437    @property
438    def lib(self):
439        if self._lib is None:
440            try:
441                self.load_freeimage()
442            except OSError as err:
443                self._lib = "The freeimage library could not be loaded: "
444                self._lib += str(err)
445        if isinstance(self._lib, str):
446            raise RuntimeError(self._lib)
447        return self._lib
448
449    def has_lib(self):
450        try:
451            self.lib
452        except Exception:
453            return False
454        return True
455
456    def load_freeimage(self):
457        """ Try to load the freeimage lib from the system. If not successful,
458        try to download the imageio version and try again.
459        """
460        # Load library and register API
461        success = False
462        try:
463            # Try without forcing a download, but giving preference
464            # to the imageio-provided lib (if previously downloaded)
465            self._load_freeimage()
466            self._register_api()
467            if self.lib.FreeImage_GetVersion().decode("utf-8") >= "3.15":
468                success = True
469        except OSError:
470            pass
471
472        if not success:
473            # Ensure we have our own lib, try again
474            get_freeimage_lib()
475            self._load_freeimage()
476            self._register_api()
477
478        # Wrap up
479        self.lib.FreeImage_SetOutputMessage(self._error_handler)
480        self.lib_version = self.lib.FreeImage_GetVersion().decode("utf-8")
481
482    def _load_freeimage(self):
483
484        # Define names
485        lib_names = ["freeimage", "libfreeimage"]
486        exact_lib_names = [
487            "FreeImage",
488            "libfreeimage.dylib",
489            "libfreeimage.so",
490            "libfreeimage.so.3",
491        ]
492        # Add names of libraries that we provide (that file may not exist)
493        res_dirs = resource_dirs()
494        plat = get_platform()
495        if plat:  # Can be None on e.g. FreeBSD
496            fname = FNAME_PER_PLATFORM[plat]
497            for dir in res_dirs:
498                exact_lib_names.insert(0, os.path.join(dir, "freeimage", fname))
499
500        # Add the path specified with IMAGEIO_FREEIMAGE_LIB:
501        lib = os.getenv("IMAGEIO_FREEIMAGE_LIB", None)
502        if lib is not None:
503            exact_lib_names.insert(0, lib)
504
505        # Load
506        try:
507            lib, fname = load_lib(exact_lib_names, lib_names, res_dirs)
508        except OSError as err:  # pragma: no cover
509            err_msg = str(err) + "\nPlease install the FreeImage library."
510            raise OSError(err_msg)
511
512        # Store
513        self._lib = lib
514        self.lib_fname = fname
515
516    def _register_api(self):
517        # Albert's ctypes pattern
518        for f, (restype, argtypes) in self._API.items():
519            func = getattr(self.lib, f)
520            func.restype = restype
521            func.argtypes = argtypes
522
523    ## Handling of output messages
524
525    def __enter__(self):
526        self._lock.acquire()
527        return self.lib
528
529    def __exit__(self, *args):
530        self._show_any_warnings()
531        self._lock.release()
532
533    def _reset_log(self):
534        """ Reset the list of output messages. Call this before
535        loading or saving an image with the FreeImage API.
536        """
537        self._messages = []
538
539    def _get_error_message(self):
540        """ Get the output messages produced since the last reset as
541        one string. Returns 'No known reason.' if there are no messages.
542        Also resets the log.
543        """
544        if self._messages:
545            res = " ".join(self._messages)
546            self._reset_log()
547            return res
548        else:
549            return "No known reason."
550
551    def _show_any_warnings(self):
552        """ If there were any messages since the last reset, show them
553        as a warning. Otherwise do nothing. Also resets the messages.
554        """
555        if self._messages:
556            logger.warning("imageio.freeimage warning: " + self._get_error_message())
557            self._reset_log()
558
559    def get_output_log(self):
560        """ Return a list of the last 256 output messages
561        (warnings and errors) produced by the FreeImage library.
562        """
563        # This message log is not cleared/reset, but kept to 256 elements.
564        return [m for m in self._messages]
565
566    def getFIF(self, filename, mode, bb=None):
567        """ Get the freeimage Format (FIF) from a given filename.
568        If mode is 'r', will try to determine the format by reading
569        the file, otherwise only the filename is used.
570
571        This function also tests whether the format supports reading/writing.
572        """
573        with self as lib:
574
575            # Init
576            ftype = -1
577            if mode not in "rw":
578                raise ValueError('Invalid mode (must be "r" or "w").')
579
580            # Try getting format from the content. Note that some files
581            # do not have a header that allows reading the format from
582            # the file.
583            if mode == "r":
584                if bb is not None:
585                    fimemory = lib.FreeImage_OpenMemory(ctypes.c_char_p(bb), len(bb))
586                    ftype = lib.FreeImage_GetFileTypeFromMemory(
587                        ctypes.c_void_p(fimemory), len(bb)
588                    )
589                    lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
590                if (ftype == -1) and os.path.isfile(filename):
591                    ftype = lib.FreeImage_GetFileType(efn(filename), 0)
592            # Try getting the format from the extension
593            if ftype == -1:
594                ftype = lib.FreeImage_GetFIFFromFilename(efn(filename))
595
596            # Test if ok
597            if ftype == -1:
598                raise ValueError('Cannot determine format of file "%s"' % filename)
599            elif mode == "w" and not lib.FreeImage_FIFSupportsWriting(ftype):
600                raise ValueError('Cannot write the format of file "%s"' % filename)
601            elif mode == "r" and not lib.FreeImage_FIFSupportsReading(ftype):
602                raise ValueError('Cannot read the format of file "%s"' % filename)
603            return ftype
604
605    def create_bitmap(self, filename, ftype, flags=0):
606        """ create_bitmap(filename, ftype, flags=0)
607        Create a wrapped bitmap object.
608        """
609        return FIBitmap(self, filename, ftype, flags)
610
611    def create_multipage_bitmap(self, filename, ftype, flags=0):
612        """ create_multipage_bitmap(filename, ftype, flags=0)
613        Create a wrapped multipage bitmap object.
614        """
615        return FIMultipageBitmap(self, filename, ftype, flags)
616
617
618class FIBaseBitmap(object):
619    def __init__(self, fi, filename, ftype, flags):
620        self._fi = fi
621        self._filename = filename
622        self._ftype = ftype
623        self._flags = flags
624        self._bitmap = None
625        self._close_funcs = []
626
627    def __del__(self):
628        self.close()
629
630    def close(self):
631        if (self._bitmap is not None) and self._close_funcs:
632            for close_func in self._close_funcs:
633                try:
634                    with self._fi:
635                        fun = close_func[0]
636                        fun(*close_func[1:])
637                except Exception:  # pragma: no cover
638                    pass
639            self._close_funcs = []
640            self._bitmap = None
641
642    def _set_bitmap(self, bitmap, close_func=None):
643        """ Function to set the bitmap and specify the function to unload it.
644        """
645        if self._bitmap is not None:
646            pass  # bitmap is converted
647        if close_func is None:
648            close_func = self._fi.lib.FreeImage_Unload, bitmap
649
650        self._bitmap = bitmap
651        if close_func:
652            self._close_funcs.append(close_func)
653
654    def get_meta_data(self):
655
656        # todo: there is also FreeImage_TagToString, is that useful?
657        # and would that work well when reading and then saving?
658
659        # Create a list of (model_name, number) tuples
660        models = [
661            (name[5:], number)
662            for name, number in METADATA_MODELS.__dict__.items()
663            if name.startswith("FIMD_")
664        ]
665
666        # Prepare
667        metadata = Dict()
668        tag = ctypes.c_void_p()
669
670        with self._fi as lib:
671
672            # Iterate over all FreeImage meta models
673            for model_name, number in models:
674
675                # Find beginning, get search handle
676                mdhandle = lib.FreeImage_FindFirstMetadata(
677                    number, self._bitmap, ctypes.byref(tag)
678                )
679                mdhandle = ctypes.c_void_p(mdhandle)
680                if mdhandle:
681
682                    # Iterate over all tags in this model
683                    more = True
684                    while more:
685                        # Get info about tag
686                        tag_name = lib.FreeImage_GetTagKey(tag).decode("utf-8")
687                        tag_type = lib.FreeImage_GetTagType(tag)
688                        byte_size = lib.FreeImage_GetTagLength(tag)
689                        char_ptr = ctypes.c_char * byte_size
690                        data = char_ptr.from_address(lib.FreeImage_GetTagValue(tag))
691                        # Convert in a way compatible with Pypy
692                        tag_bytes = bytes(bytearray(data))
693                        # The default value is the raw bytes
694                        tag_val = tag_bytes
695                        # Convert to a Python value in the metadata dict
696                        if tag_type == METADATA_DATATYPE.FIDT_ASCII:
697                            tag_val = tag_bytes.decode("utf-8", "replace")
698                        elif tag_type in METADATA_DATATYPE.dtypes:
699                            dtype = METADATA_DATATYPE.dtypes[tag_type]
700                            if IS_PYPY and isinstance(dtype, (list, tuple)):
701                                pass  # pragma: no cover - or we get a segfault
702                            else:
703                                try:
704                                    tag_val = numpy.frombuffer(
705                                        tag_bytes, dtype=dtype
706                                    ).copy()
707                                    if len(tag_val) == 1:
708                                        tag_val = tag_val[0]
709                                except Exception:  # pragma: no cover
710                                    pass
711                        # Store data in dict
712                        subdict = metadata.setdefault(model_name, Dict())
713                        subdict[tag_name] = tag_val
714                        # Next
715                        more = lib.FreeImage_FindNextMetadata(
716                            mdhandle, ctypes.byref(tag)
717                        )
718
719                    # Close search handle for current meta model
720                    lib.FreeImage_FindCloseMetadata(mdhandle)
721
722            # Done
723            return metadata
724
725    def set_meta_data(self, metadata):
726
727        # Create a dict mapping model_name to number
728        models = {}
729        for name, number in METADATA_MODELS.__dict__.items():
730            if name.startswith("FIMD_"):
731                models[name[5:]] = number
732
733        # Create a mapping from numpy.dtype to METADATA_DATATYPE
734        def get_tag_type_number(dtype):
735            for number, numpy_dtype in METADATA_DATATYPE.dtypes.items():
736                if dtype == numpy_dtype:
737                    return number
738            else:
739                return None
740
741        with self._fi as lib:
742
743            for model_name, subdict in metadata.items():
744
745                # Get model number
746                number = models.get(model_name, None)
747                if number is None:
748                    continue  # Unknown model, silent ignore
749
750                for tag_name, tag_val in subdict.items():
751
752                    # Create new tag
753                    tag = lib.FreeImage_CreateTag()
754                    tag = ctypes.c_void_p(tag)
755
756                    try:
757                        # Convert Python value to FI type, val
758                        is_ascii = False
759                        if isinstance(tag_val, str):
760                            try:
761                                tag_bytes = tag_val.encode("ascii")
762                                is_ascii = True
763                            except UnicodeError:
764                                pass
765                        if is_ascii:
766                            tag_type = METADATA_DATATYPE.FIDT_ASCII
767                            tag_count = len(tag_bytes)
768                        else:
769                            if not hasattr(tag_val, "dtype"):
770                                tag_val = numpy.array([tag_val])
771                            tag_type = get_tag_type_number(tag_val.dtype)
772                            if tag_type is None:
773                                logger.warning(
774                                    "imageio.freeimage warning: Could not "
775                                    "determine tag type of %r." % tag_name
776                                )
777                                continue
778                            tag_bytes = tag_val.tostring()
779                            tag_count = tag_val.size
780                        # Set properties
781                        lib.FreeImage_SetTagKey(tag, tag_name.encode("utf-8"))
782                        lib.FreeImage_SetTagType(tag, tag_type)
783                        lib.FreeImage_SetTagCount(tag, tag_count)
784                        lib.FreeImage_SetTagLength(tag, len(tag_bytes))
785                        lib.FreeImage_SetTagValue(tag, tag_bytes)
786                        # Store tag
787                        tag_key = lib.FreeImage_GetTagKey(tag)
788                        lib.FreeImage_SetMetadata(number, self._bitmap, tag_key, tag)
789
790                    except Exception as err:  # pragma: no cover
791                        logger.warning(
792                            "imagio.freeimage warning: Could not set tag "
793                            "%r: %s, %s"
794                            % (tag_name, self._fi._get_error_message(), str(err))
795                        )
796                    finally:
797                        lib.FreeImage_DeleteTag(tag)
798
799
800class FIBitmap(FIBaseBitmap):
801    """ Wrapper for the FI bitmap object.
802    """
803
804    def allocate(self, array):
805
806        # Prepare array
807        assert isinstance(array, numpy.ndarray)
808        shape = array.shape
809        dtype = array.dtype
810
811        # Get shape and channel info
812        r, c = shape[:2]
813        if len(shape) == 2:
814            n_channels = 1
815        elif len(shape) == 3:
816            n_channels = shape[2]
817        else:
818            n_channels = shape[0]
819
820        # Get fi_type
821        try:
822            fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)]
823            self._fi_type = fi_type
824        except KeyError:
825            raise ValueError("Cannot write arrays of given type and shape.")
826
827        # Allocate bitmap
828        with self._fi as lib:
829            bpp = 8 * dtype.itemsize * n_channels
830            bitmap = lib.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0)
831            bitmap = ctypes.c_void_p(bitmap)
832
833            # Check and store
834            if not bitmap:  # pragma: no cover
835                raise RuntimeError(
836                    "Could not allocate bitmap for storage: %s"
837                    % self._fi._get_error_message()
838                )
839            self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
840
841    def load_from_filename(self, filename=None):
842        if filename is None:
843            filename = self._filename
844
845        with self._fi as lib:
846            # Create bitmap
847            bitmap = lib.FreeImage_Load(self._ftype, efn(filename), self._flags)
848            bitmap = ctypes.c_void_p(bitmap)
849
850            # Check and store
851            if not bitmap:  # pragma: no cover
852                raise ValueError(
853                    'Could not load bitmap "%s": %s'
854                    % (self._filename, self._fi._get_error_message())
855                )
856            self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
857
858    # def load_from_bytes(self, bb):
859    #     with self._fi as lib:
860    #         # Create bitmap
861    #         fimemory = lib.FreeImage_OpenMemory(
862    #                                         ctypes.c_char_p(bb), len(bb))
863    #         bitmap = lib.FreeImage_LoadFromMemory(
864    #                         self._ftype, ctypes.c_void_p(fimemory), self._flags)
865    #         bitmap = ctypes.c_void_p(bitmap)
866    #         lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
867    #
868    #         # Check
869    #         if not bitmap:
870    #             raise ValueError('Could not load bitmap "%s": %s'
871    #                         % (self._filename, self._fi._get_error_message()))
872    #         else:
873    #             self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
874
875    def save_to_filename(self, filename=None):
876        if filename is None:
877            filename = self._filename
878
879        ftype = self._ftype
880        bitmap = self._bitmap
881        fi_type = self._fi_type  # element type
882
883        with self._fi as lib:
884            # Check if can write
885            if fi_type == FI_TYPES.FIT_BITMAP:
886                can_write = lib.FreeImage_FIFSupportsExportBPP(
887                    ftype, lib.FreeImage_GetBPP(bitmap)
888                )
889            else:
890                can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
891            if not can_write:
892                raise TypeError("Cannot save image of this format " "to this file type")
893
894            # Save to file
895            res = lib.FreeImage_Save(ftype, bitmap, efn(filename), self._flags)
896            # Check
897            if not res:  # pragma: no cover, we do so many checks, this is rare
898                raise RuntimeError(
899                    'Could not save file "%s": %s'
900                    % (self._filename, self._fi._get_error_message())
901                )
902
903    # def save_to_bytes(self):
904    #     ftype = self._ftype
905    #     bitmap = self._bitmap
906    #     fi_type = self._fi_type # element type
907    #
908    #     with self._fi as lib:
909    #         # Check if can write
910    #         if fi_type == FI_TYPES.FIT_BITMAP:
911    #             can_write = lib.FreeImage_FIFSupportsExportBPP(ftype,
912    #                                     lib.FreeImage_GetBPP(bitmap))
913    #         else:
914    #             can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
915    #         if not can_write:
916    #             raise TypeError('Cannot save image of this format '
917    #                             'to this file type')
918    #
919    #         # Extract the bytes
920    #         fimemory = lib.FreeImage_OpenMemory(0, 0)
921    #         res = lib.FreeImage_SaveToMemory(ftype, bitmap,
922    #                                          ctypes.c_void_p(fimemory),
923    #                                          self._flags)
924    #         if res:
925    #             N = lib.FreeImage_TellMemory(ctypes.c_void_p(fimemory))
926    #             result = ctypes.create_string_buffer(N)
927    #             lib.FreeImage_SeekMemory(ctypes.c_void_p(fimemory), 0)
928    #             lib.FreeImage_ReadMemory(result, 1, N, ctypes.c_void_p(fimemory))
929    #             result = result.raw
930    #         lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
931    #
932    #         # Check
933    #         if not res:
934    #             raise RuntimeError('Could not save file "%s": %s'
935    #                     % (self._filename, self._fi._get_error_message()))
936    #
937    #     # Done
938    #     return result
939
940    def get_image_data(self):
941        dtype, shape, bpp = self._get_type_and_shape()
942        array = self._wrap_bitmap_bits_in_array(shape, dtype, False)
943        with self._fi as lib:
944            isle = lib.FreeImage_IsLittleEndian()
945
946        # swizzle the color components and flip the scanlines to go from
947        # FreeImage's BGR[A] and upside-down internal memory format to
948        # something more normal
949        def n(arr):
950            # return arr[..., ::-1].T  # Does not work on numpypy yet
951            if arr.ndim == 1:  # pragma: no cover
952                return arr[::-1].T
953            elif arr.ndim == 2:  # Always the case here ...
954                return arr[:, ::-1].T
955            elif arr.ndim == 3:  # pragma: no cover
956                return arr[:, :, ::-1].T
957            elif arr.ndim == 4:  # pragma: no cover
958                return arr[:, :, :, ::-1].T
959
960        if len(shape) == 3 and isle and dtype.type == numpy.uint8:
961            b = n(array[0])
962            g = n(array[1])
963            r = n(array[2])
964            if shape[0] == 3:
965                return numpy.dstack((r, g, b))
966            elif shape[0] == 4:
967                a = n(array[3])
968                return numpy.dstack((r, g, b, a))
969            else:  # pragma: no cover - we check this earlier
970                raise ValueError("Cannot handle images of shape %s" % shape)
971
972        # We need to copy because array does *not* own its memory
973        # after bitmap is freed.
974        a = n(array).copy()
975        return a
976
977    def set_image_data(self, array):
978
979        # Prepare array
980        assert isinstance(array, numpy.ndarray)
981        shape = array.shape
982        dtype = array.dtype
983        with self._fi as lib:
984            isle = lib.FreeImage_IsLittleEndian()
985
986        # Calculate shape and channels
987        r, c = shape[:2]
988        if len(shape) == 2:
989            n_channels = 1
990            w_shape = (c, r)
991        elif len(shape) == 3:
992            n_channels = shape[2]
993            w_shape = (n_channels, c, r)
994        else:
995            n_channels = shape[0]
996
997        def n(arr):  # normalise to freeimage's in-memory format
998            return arr[::-1].T
999
1000        wrapped_array = self._wrap_bitmap_bits_in_array(w_shape, dtype, True)
1001        # swizzle the color components and flip the scanlines to go to
1002        # FreeImage's BGR[A] and upside-down internal memory format
1003        # The BGR[A] order is only used for 8bits per channel images
1004        # on little endian machines. For everything else RGB[A] is
1005        # used.
1006        if len(shape) == 3 and isle and dtype.type == numpy.uint8:
1007            R = array[:, :, 0]
1008            G = array[:, :, 1]
1009            B = array[:, :, 2]
1010            wrapped_array[0] = n(B)
1011            wrapped_array[1] = n(G)
1012            wrapped_array[2] = n(R)
1013            if shape[2] == 4:
1014                A = array[:, :, 3]
1015                wrapped_array[3] = n(A)
1016        else:
1017            wrapped_array[:] = n(array)
1018        if self._need_finish:
1019            self._finish_wrapped_array(wrapped_array)
1020
1021        if len(shape) == 2 and dtype.type == numpy.uint8:
1022            with self._fi as lib:
1023                palette = lib.FreeImage_GetPalette(self._bitmap)
1024            palette = ctypes.c_void_p(palette)
1025            if not palette:
1026                raise RuntimeError("Could not get image palette")
1027            try:
1028                palette_data = GREY_PALETTE.ctypes.data
1029            except Exception:  # pragma: no cover - IS_PYPY
1030                palette_data = GREY_PALETTE.__array_interface__["data"][0]
1031            ctypes.memmove(palette, palette_data, 1024)
1032
1033    def _wrap_bitmap_bits_in_array(self, shape, dtype, save):
1034        """Return an ndarray view on the data in a FreeImage bitmap. Only
1035        valid for as long as the bitmap is loaded (if single page) / locked
1036        in memory (if multipage). This is used in loading data, but
1037        also during saving, to prepare a strided numpy array buffer.
1038
1039        """
1040        # Get bitmap info
1041        with self._fi as lib:
1042            pitch = lib.FreeImage_GetPitch(self._bitmap)
1043            bits = lib.FreeImage_GetBits(self._bitmap)
1044
1045        # Get more info
1046        height = shape[-1]
1047        byte_size = height * pitch
1048        itemsize = dtype.itemsize
1049
1050        # Get strides
1051        if len(shape) == 3:
1052            strides = (itemsize, shape[0] * itemsize, pitch)
1053        else:
1054            strides = (itemsize, pitch)
1055
1056        # Create numpy array and return
1057        data = (ctypes.c_char * byte_size).from_address(bits)
1058        try:
1059            self._need_finish = False
1060            if TEST_NUMPY_NO_STRIDES:
1061                raise NotImplementedError()
1062            return numpy.ndarray(shape, dtype=dtype, buffer=data, strides=strides)
1063        except NotImplementedError:
1064            # IS_PYPY - not very efficient. We create a C-contiguous
1065            # numpy array (because pypy does not support Fortran-order)
1066            # and shape it such that the rest of the code can remain.
1067            if save:
1068                self._need_finish = True  # Flag to use _finish_wrapped_array
1069                return numpy.zeros(shape, dtype=dtype)
1070            else:
1071                bb = bytes(bytearray(data))
1072                array = numpy.frombuffer(bb, dtype=dtype).copy()
1073                # Deal with strides
1074                if len(shape) == 3:
1075                    array.shape = shape[2], strides[-1] // shape[0], shape[0]
1076                    array2 = array[: shape[2], : shape[1], : shape[0]]
1077                    array = numpy.zeros(shape, dtype=array.dtype)
1078                    for i in range(shape[0]):
1079                        array[i] = array2[:, :, i].T
1080                else:
1081                    array.shape = shape[1], strides[-1]
1082                    array = array[: shape[1], : shape[0]].T
1083                return array
1084
1085    def _finish_wrapped_array(self, array):  # IS_PYPY
1086        """ Hardcore way to inject numpy array in bitmap.
1087        """
1088        # Get bitmap info
1089        with self._fi as lib:
1090            pitch = lib.FreeImage_GetPitch(self._bitmap)
1091            bits = lib.FreeImage_GetBits(self._bitmap)
1092            bpp = lib.FreeImage_GetBPP(self._bitmap)
1093        # Get channels and realwidth
1094        nchannels = bpp // 8 // array.itemsize
1095        realwidth = pitch // nchannels
1096        # Apply padding for pitch if necessary
1097        extra = realwidth - array.shape[-2]
1098        assert 0 <= extra < 10
1099        # Make sort of Fortran, also take padding (i.e. pitch) into account
1100        newshape = array.shape[-1], realwidth, nchannels
1101        array2 = numpy.zeros(newshape, array.dtype)
1102        if nchannels == 1:
1103            array2[:, : array.shape[-2], 0] = array.T
1104        else:
1105            for i in range(nchannels):
1106                array2[:, : array.shape[-2], i] = array[i, :, :].T
1107        # copy data
1108        data_ptr = array2.__array_interface__["data"][0]
1109        ctypes.memmove(bits, data_ptr, array2.nbytes)
1110        del array2
1111
1112    def _get_type_and_shape(self):
1113        bitmap = self._bitmap
1114
1115        # Get info on bitmap
1116        with self._fi as lib:
1117            w = lib.FreeImage_GetWidth(bitmap)
1118            h = lib.FreeImage_GetHeight(bitmap)
1119            self._fi_type = fi_type = lib.FreeImage_GetImageType(bitmap)
1120            if not fi_type:
1121                raise ValueError("Unknown image pixel type")
1122
1123        # Determine required props for numpy array
1124        bpp = None
1125        dtype = FI_TYPES.dtypes[fi_type]
1126
1127        if fi_type == FI_TYPES.FIT_BITMAP:
1128            with self._fi as lib:
1129                bpp = lib.FreeImage_GetBPP(bitmap)
1130                has_pallette = lib.FreeImage_GetColorsUsed(bitmap)
1131            if has_pallette:
1132                # Examine the palette. If it is grayscale, we return as such
1133                if has_pallette == 256:
1134                    palette = lib.FreeImage_GetPalette(bitmap)
1135                    palette = ctypes.c_void_p(palette)
1136                    p = (ctypes.c_uint8 * (256 * 4)).from_address(palette.value)
1137                    p = numpy.frombuffer(p, numpy.uint32).copy()
1138                    if (GREY_PALETTE == p).all():
1139                        extra_dims = []
1140                        return numpy.dtype(dtype), extra_dims + [w, h], bpp
1141                # Convert bitmap and call this method again
1142                newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
1143                newbitmap = ctypes.c_void_p(newbitmap)
1144                self._set_bitmap(newbitmap)
1145                return self._get_type_and_shape()
1146            elif bpp == 8:
1147                extra_dims = []
1148            elif bpp == 24:
1149                extra_dims = [3]
1150            elif bpp == 32:
1151                extra_dims = [4]
1152            else:  # pragma: no cover
1153                # raise ValueError('Cannot convert %d BPP bitmap' % bpp)
1154                # Convert bitmap and call this method again
1155                newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
1156                newbitmap = ctypes.c_void_p(newbitmap)
1157                self._set_bitmap(newbitmap)
1158                return self._get_type_and_shape()
1159        else:
1160            extra_dims = FI_TYPES.extra_dims[fi_type]
1161
1162        # Return dtype and shape
1163        return numpy.dtype(dtype), extra_dims + [w, h], bpp
1164
1165    def quantize(self, quantizer=0, palettesize=256):
1166        """ Quantize the bitmap to make it 8-bit (paletted). Returns a new
1167        FIBitmap object.
1168        Only for 24 bit images.
1169        """
1170        with self._fi as lib:
1171            # New bitmap
1172            bitmap = lib.FreeImage_ColorQuantizeEx(
1173                self._bitmap, quantizer, palettesize, 0, None
1174            )
1175            bitmap = ctypes.c_void_p(bitmap)
1176
1177            # Check and return
1178            if not bitmap:
1179                raise ValueError(
1180                    'Could not quantize bitmap "%s": %s'
1181                    % (self._filename, self._fi._get_error_message())
1182                )
1183
1184            new = FIBitmap(self._fi, self._filename, self._ftype, self._flags)
1185            new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
1186            new._fi_type = self._fi_type
1187            return new
1188
1189
1190# def convert_to_32bit(self):
1191#     """ Convert to 32bit image.
1192#     """
1193#     with self._fi as lib:
1194#         # New bitmap
1195#         bitmap = lib.FreeImage_ConvertTo32Bits(self._bitmap)
1196#         bitmap = ctypes.c_void_p(bitmap)
1197#
1198#         # Check and return
1199#         if not bitmap:
1200#             raise ValueError('Could not convert bitmap to 32bit "%s": %s' %
1201#                                 (self._filename,
1202#                                 self._fi._get_error_message()))
1203#         else:
1204#             new = FIBitmap(self._fi, self._filename, self._ftype,
1205#                             self._flags)
1206#             new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
1207#             new._fi_type = self._fi_type
1208#             return new
1209
1210
1211class FIMultipageBitmap(FIBaseBitmap):
1212    """ Wrapper for the multipage FI bitmap object.
1213    """
1214
1215    def load_from_filename(self, filename=None):
1216        if filename is None:  # pragma: no cover
1217            filename = self._filename
1218
1219        # Prepare
1220        create_new = False
1221        read_only = True
1222        keep_cache_in_memory = False
1223
1224        # Try opening
1225        with self._fi as lib:
1226
1227            # Create bitmap
1228            multibitmap = lib.FreeImage_OpenMultiBitmap(
1229                self._ftype,
1230                efn(filename),
1231                create_new,
1232                read_only,
1233                keep_cache_in_memory,
1234                self._flags,
1235            )
1236            multibitmap = ctypes.c_void_p(multibitmap)
1237
1238            # Check
1239            if not multibitmap:  # pragma: no cover
1240                err = self._fi._get_error_message()
1241                raise ValueError(
1242                    'Could not open file "%s" as multi-image: %s'
1243                    % (self._filename, err)
1244                )
1245            self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap))
1246
1247    # def load_from_bytes(self, bb):
1248    #     with self._fi as lib:
1249    #         # Create bitmap
1250    #         fimemory = lib.FreeImage_OpenMemory(
1251    #                                         ctypes.c_char_p(bb), len(bb))
1252    #         multibitmap = lib.FreeImage_LoadMultiBitmapFromMemory(
1253    #             self._ftype, ctypes.c_void_p(fimemory), self._flags)
1254    #         multibitmap = ctypes.c_void_p(multibitmap)
1255    #         #lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
1256    #         self._mem = fimemory
1257    #         self._bytes = bb
1258    #         # Check
1259    #         if not multibitmap:
1260    #             raise ValueError('Could not load multibitmap "%s": %s'
1261    #                         % (self._filename, self._fi._get_error_message()))
1262    #         else:
1263    #             self._set_bitmap(multibitmap,
1264    #                              (lib.FreeImage_CloseMultiBitmap, multibitmap))
1265
1266    def save_to_filename(self, filename=None):
1267        if filename is None:  # pragma: no cover
1268            filename = self._filename
1269
1270        # Prepare
1271        create_new = True
1272        read_only = False
1273        keep_cache_in_memory = False
1274
1275        # Open the file
1276        # todo: Set flags at close func
1277        with self._fi as lib:
1278            multibitmap = lib.FreeImage_OpenMultiBitmap(
1279                self._ftype,
1280                efn(filename),
1281                create_new,
1282                read_only,
1283                keep_cache_in_memory,
1284                0,
1285            )
1286            multibitmap = ctypes.c_void_p(multibitmap)
1287
1288            # Check
1289            if not multibitmap:  # pragma: no cover
1290                msg = 'Could not open file "%s" for writing multi-image: %s' % (
1291                    self._filename,
1292                    self._fi._get_error_message(),
1293                )
1294                raise ValueError(msg)
1295            self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap))
1296
1297    def __len__(self):
1298        with self._fi as lib:
1299            return lib.FreeImage_GetPageCount(self._bitmap)
1300
1301    def get_page(self, index):
1302        """ Return the sub-bitmap for the given page index.
1303        Please close the returned bitmap when done.
1304        """
1305        with self._fi as lib:
1306
1307            # Create low-level bitmap in freeimage
1308            bitmap = lib.FreeImage_LockPage(self._bitmap, index)
1309            bitmap = ctypes.c_void_p(bitmap)
1310            if not bitmap:  # pragma: no cover
1311                raise ValueError(
1312                    "Could not open sub-image %i in %r: %s"
1313                    % (index, self._filename, self._fi._get_error_message())
1314                )
1315
1316            # Get bitmap object to wrap this bitmap
1317            bm = FIBitmap(self._fi, self._filename, self._ftype, self._flags)
1318            bm._set_bitmap(
1319                bitmap, (lib.FreeImage_UnlockPage, self._bitmap, bitmap, False)
1320            )
1321            return bm
1322
1323    def append_bitmap(self, bitmap):
1324        """ Add a sub-bitmap to the multi-page bitmap.
1325        """
1326        with self._fi as lib:
1327            # no return value
1328            lib.FreeImage_AppendPage(self._bitmap, bitmap._bitmap)
1329
1330
1331# Create instance
1332fi = Freeimage()
1333