freeimage.py 14 KB

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