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