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