[imageio] 01/02: Imported Upstream version 1.0
Ghislain Vaillant
ghisvail-guest at moszumanska.debian.org
Mon Nov 17 09:47:59 UTC 2014
This is an automated email from the git hooks/post-receive script.
ghisvail-guest pushed a commit to branch master
in repository imageio.
commit 36afed046826462166675051852218b96dd3d737
Author: Ghislain Antony Vaillant <ghisvail at gmail.com>
Date: Mon Nov 17 09:41:19 2014 +0000
Imported Upstream version 1.0
---
PKG-INFO | 59 ++
imageio/__init__.py | 34 +
imageio/core/__init__.py | 16 +
imageio/core/fetching.py | 178 ++++++
imageio/core/findlib.py | 171 +++++
imageio/core/format.py | 601 ++++++++++++++++++
imageio/core/functions.py | 438 +++++++++++++
imageio/core/request.py | 422 +++++++++++++
imageio/core/util.py | 434 +++++++++++++
imageio/freeze.py | 17 +
imageio/plugins/__init__.py | 88 +++
imageio/plugins/_freeimage.py | 1249 +++++++++++++++++++++++++++++++++++++
imageio/plugins/_swf.py | 925 +++++++++++++++++++++++++++
imageio/plugins/avbin.py | 442 +++++++++++++
imageio/plugins/dicom.py | 1097 ++++++++++++++++++++++++++++++++
imageio/plugins/example.py | 143 +++++
imageio/plugins/ffmpeg.py | 659 +++++++++++++++++++
imageio/plugins/freeimage.py | 380 +++++++++++
imageio/plugins/freeimagemulti.py | 303 +++++++++
imageio/plugins/npz.py | 103 +++
imageio/plugins/swf.py | 333 ++++++++++
imageio/testing.py | 255 ++++++++
setup.py | 126 ++++
23 files changed, 8473 insertions(+)
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..20b9124
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,59 @@
+Metadata-Version: 1.1
+Name: imageio
+Version: 1.0
+Summary: Library for reading and writing a wide range of image formats.
+Home-page: http://imageio.github.io/
+Author: imageio contributors
+Author-email: almar.klein at gmail.com
+License: (new) BSD
+Download-URL: http://pypi.python.org/pypi/imageio
+Description:
+ .. image:: https://travis-ci.org/imageio/imageio.svg?branch=master
+ :target: https://travis-ci.org/imageio/imageio'
+
+ .. image:: https://coveralls.io/repos/imageio/imageio/badge.png?branch=master
+ :target: https://coveralls.io/r/imageio/imageio?branch=master
+
+
+ Imageio is a Python library that provides an easy interface to read and
+ write a wide range of image data, including animated images, volumetric
+ data, and scientific formats. It is cross-platform, runs on Python 2.x
+ and 3.x, and is easy to install.
+
+ Main website: http://imageio.github.io
+
+
+ Release notes: http://imageio.readthedocs.org/en/latest/releasenotes.html
+
+ Example:
+
+ .. code-block:: python:
+
+ >>> import imageio
+ >>> im = imageio.imread('astronaut.png')
+ >>> im.shape # im is a numpy array
+ (512, 512, 3)
+ >>> imageio.imsave('astronaut-gray.jpg', im[:, :, 0])
+
+ See the `user API <http://imageio.readthedocs.org/en/latest/userapi.html>`_
+ or `examples <http://imageio.readthedocs.org/en/latest/examples.html>`_
+ for more information.
+
+Keywords: image imread imsave io animation volume FreeImage ffmpeg
+Platform: any
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Science/Research
+Classifier: Intended Audience :: Education
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Requires: numpy
+Provides: imageio
diff --git a/imageio/__init__.py b/imageio/__init__.py
new file mode 100644
index 0000000..b0db1f5
--- /dev/null
+++ b/imageio/__init__.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+# This docstring is used at the index of the documentation pages, and
+# gets inserted into a slightly larger description (in setup.py) for
+# the page on Pypi:
+"""
+Imageio is a Python library that provides an easy interface to read and
+write a wide range of image data, including animated images, volumetric
+data, and scientific formats. It is cross-platform, runs on Python 2.x
+and 3.x, and is easy to install.
+
+Main website: http://imageio.github.io
+"""
+
+__version__ = '1.0'
+
+# Load some bits from core
+from .core import FormatManager, RETURN_BYTES # noqa
+
+# Instantiate format manager
+formats = FormatManager()
+
+# Load the functions
+from .core.functions import help # noqa
+from .core.functions import read, imread, mimread, volread, mvolread # noqa
+from .core.functions import save, imsave, mimsave, volsave, mvolsave # noqa
+
+# Load all the plugins
+from . import plugins # noqa
+
+# Clean up some names
+del FormatManager
diff --git a/imageio/core/__init__.py b/imageio/core/__init__.py
new file mode 100644
index 0000000..ae6b62f
--- /dev/null
+++ b/imageio/core/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" This subpackage provides the core functionality of imageio
+(everything but the plugins).
+"""
+
+from .util import Image, Dict, asarray, appdata_dir, urlopen # noqa
+from .util import BaseProgressIndicator, StdoutProgressIndicator # noqa
+from .util import string_types, text_type, binary_type, IS_PYPY # noqa
+from .util import get_platform # noqa
+from .findlib import load_lib # noqa
+from .fetching import get_remote_file # noqa
+from .request import Request, read_n_bytes, RETURN_BYTES # noqa
+from .format import Format, FormatManager # noqa
diff --git a/imageio/core/fetching.py b/imageio/core/fetching.py
new file mode 100644
index 0000000..459ae80
--- /dev/null
+++ b/imageio/core/fetching.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# Based on code from the vispy project
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""Data downloading and reading functions
+"""
+
+from __future__ import absolute_import, print_function, division
+
+from math import log
+import os
+from os import path as op
+import sys
+import shutil
+import time
+
+from . import appdata_dir, StdoutProgressIndicator, string_types, urlopen
+
+
+def get_remote_file(fname, directory=None, force_download=False):
+ """ Get a the filename for the local version of a file from the web
+
+ Parameters
+ ----------
+ fname : str
+ The filename on the remote data repository to download. These
+ correspond to paths on
+ ``https://github.com/imageio/imageio-binaries/``.
+ directory : str | None
+ Directory to use to save the file. By default, the appdata
+ directory is used.
+ force_download : bool | str
+ If True, the file will be downloaded even if a local copy exists
+ (and this copy will be overwritten). Can also be a YYYY-MM-DD date
+ to ensure a file is up-to-date (modified date of a file on disk,
+ if present, is checked).
+
+ Returns
+ -------
+ fname : str
+ The path to the file on the local system.
+ """
+ _url_root = 'https://github.com/imageio/imageio-binaries/raw/master/'
+ url = _url_root + fname
+ directory = directory or appdata_dir('imageio')
+
+ fname = op.join(directory, op.normcase(fname)) # convert to native
+ if op.isfile(fname):
+ if not force_download: # we're done
+ return fname
+ if isinstance(force_download, string_types):
+ ntime = time.strptime(force_download, '%Y-%m-%d')
+ ftime = time.gmtime(op.getctime(fname))
+ if ftime >= ntime:
+ return fname
+ else:
+ print('File older than %s, updating...' % force_download)
+ if not op.isdir(op.dirname(fname)):
+ os.makedirs(op.abspath(op.dirname(fname)))
+ # let's go get the file
+ if os.getenv('CONTINUOUS_INTEGRATION', False): # pragma: no cover
+ # On Travis, we retry a few times ...
+ for i in range(2):
+ try:
+ _fetch_file(url, fname)
+ return fname
+ except IOError:
+ time.sleep(0.5)
+ else:
+ _fetch_file(url, fname)
+ return fname
+ else: # pragma: no cover
+ _fetch_file(url, fname)
+ return fname
+
+
+def _fetch_file(url, file_name, print_destination=True):
+ """Load requested file, downloading it if needed or requested
+
+ Parameters
+ ----------
+ url: string
+ The url of file to be downloaded.
+ file_name: string
+ Name, along with the path, of where downloaded file will be saved.
+ print_destination: bool, optional
+ If true, destination of where file was saved will be printed after
+ download finishes.
+ resume: bool, optional
+ If true, try to resume partially downloaded files.
+ """
+ # Adapted from NISL:
+ # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py
+
+ temp_file_name = file_name + ".part"
+ local_file = None
+ initial_size = 0
+ try:
+ # Checking file size and displaying it alongside the download url
+ remote_file = urlopen(url, timeout=5.)
+ file_size = int(remote_file.headers['Content-Length'].strip())
+ print('Downloading data from %s (%s)' % (url, _sizeof_fmt(file_size)))
+ # Downloading data (can be extended to resume if need be)
+ local_file = open(temp_file_name, "wb")
+ _chunk_read(remote_file, local_file, initial_size=initial_size)
+ # temp file must be closed prior to the move
+ if not local_file.closed:
+ local_file.close()
+ shutil.move(temp_file_name, file_name)
+ if print_destination is True:
+ sys.stdout.write('File saved as %s.\n' % file_name)
+ except Exception as e:
+ raise IOError('Error while fetching file %s.\n'
+ 'Dataset fetching aborted (%s)' % (url, e))
+ finally:
+ if local_file is not None:
+ if not local_file.closed:
+ local_file.close()
+
+
+def _chunk_read(response, local_file, chunk_size=8192, initial_size=0):
+ """Download a file chunk by chunk and show advancement
+
+ Can also be used when resuming downloads over http.
+
+ Parameters
+ ----------
+ response: urllib.response.addinfourl
+ Response to the download request in order to get file size.
+ local_file: file
+ Hard disk file where data should be written.
+ chunk_size: integer, optional
+ Size of downloaded chunks. Default: 8192
+ initial_size: int, optional
+ If resuming, indicate the initial size of the file.
+ """
+ # Adapted from NISL:
+ # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py
+
+ bytes_so_far = initial_size
+ # Returns only amount left to download when resuming, not the size of the
+ # entire file
+ total_size = int(response.headers['Content-Length'].strip())
+ total_size += initial_size
+
+ progress = StdoutProgressIndicator('Downloading')
+ progress.start('', 'bytes', total_size)
+
+ while True:
+ chunk = response.read(chunk_size)
+ bytes_so_far += len(chunk)
+ if not chunk:
+ break
+ _chunk_write(chunk, local_file, progress)
+ progress.finish('Done')
+
+
+def _chunk_write(chunk, local_file, progress):
+ """Write a chunk to file and update the progress bar"""
+ local_file.write(chunk)
+ progress.increase_progress(len(chunk))
+ time.sleep(0.0001)
+
+
+def _sizeof_fmt(num):
+ """Turn number of bytes into human-readable str"""
+ units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']
+ decimals = [0, 0, 1, 2, 2, 2]
+ """Human friendly file size"""
+ if num > 1:
+ exponent = min(int(log(num, 1024)), len(units) - 1)
+ quotient = float(num) / 1024 ** exponent
+ unit = units[exponent]
+ num_decimals = decimals[exponent]
+ format_string = '{0:.%sf} {1}' % (num_decimals)
+ return format_string.format(quotient, unit)
+ return '0 bytes' if num == 0 else '1 byte'
diff --git a/imageio/core/findlib.py b/imageio/core/findlib.py
new file mode 100644
index 0000000..1f303da
--- /dev/null
+++ b/imageio/core/findlib.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# Copyright (C) 2013, Zach Pincus, Almar Klein and others
+
+""" This module contains generic code to find and load a dynamic library.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import os
+import sys
+import ctypes
+
+
+LOCALDIR = os.path.abspath(os.path.dirname(__file__))
+
+
+# More generic:
+# def get_local_lib_dirs(*libdirs):
+# """ Get a list of existing directories that end with one of the given
+# subdirs, and that are in the (sub)package that this modules is part of.
+# """
+# dirs = []
+# parts = __name__.split('.')
+# for i in reversed(range(len(parts))):
+# package_name = '.'.join(parts[:i])
+# package = sys.modules.get(package_name, None)
+# if package:
+# dirs.append(os.path.abspath(os.path.dirname(package.__file__)))
+# dirs = [os.path.join(d, sub) for sub in libdirs for d in dirs]
+# return [d for d in dirs if os.path.isdir(d)]
+
+
+def looks_lib(fname):
+ """ Returns True if the given filename looks like a dynamic library.
+ Based on extension, but cross-platform and more flexible.
+ """
+ fname = fname.lower()
+ if sys.platform.startswith('win'):
+ return fname.endswith('.dll')
+ elif sys.platform.startswith('darwin'):
+ return fname.endswith('.dylib')
+ else:
+ return fname.endswith('.so') or '.so.' in fname
+
+
+def generate_candidate_libs(lib_names, lib_dirs=None):
+ """ Generate a list of candidate filenames of what might be the dynamic
+ library corresponding with the given list of names.
+ Returns (lib_dirs, lib_paths)
+ """
+ lib_dirs = lib_dirs or []
+
+ # Get system dirs to search
+ sys_lib_dirs = ['/lib',
+ '/usr/lib',
+ '/usr/lib/x86_64-linux-gnu',
+ '/usr/local/lib',
+ '/opt/local/lib', ]
+
+ # Get Python dirs to search (shared if for Pyzo)
+ py_sub_dirs = ['lib', 'DLLs', 'shared']
+ py_lib_dirs = [os.path.join(sys.prefix, d) for d in py_sub_dirs]
+ if hasattr(sys, 'base_prefix'):
+ py_lib_dirs += [os.path.join(sys.base_prefix, d) for d in py_sub_dirs]
+
+ # Get user dirs to search (i.e. HOME)
+ home_dir = os.path.expanduser('~')
+ user_lib_dirs = [os.path.join(home_dir, d) for d in ['lib']]
+
+ # Select only the dirs for which a directory exists, and remove duplicates
+ potential_lib_dirs = lib_dirs + sys_lib_dirs + py_lib_dirs + user_lib_dirs
+ lib_dirs = []
+ for ld in potential_lib_dirs:
+ if os.path.isdir(ld) and ld not in lib_dirs:
+ lib_dirs.append(ld)
+
+ # Now attempt to find libraries of that name in the given directory
+ # (case-insensitive)
+ lib_paths = []
+ for lib_dir in lib_dirs:
+ # Get files, prefer short names, last version
+ files = os.listdir(lib_dir)
+ files = reversed(sorted(files))
+ files = sorted(files, key=len)
+ for lib_name in lib_names:
+ # Test all filenames for name and ext
+ for fname in files:
+ if fname.lower().startswith(lib_name) and looks_lib(fname):
+ lib_paths.append(os.path.join(lib_dir, fname))
+
+ # Return (only the items which are files)
+ lib_paths = [lp for lp in lib_paths if os.path.isfile(lp)]
+ return lib_dirs, lib_paths
+
+
+def load_lib(exact_lib_names, lib_names, lib_dirs=None):
+ """ load_lib(exact_lib_names, lib_names, lib_dirs=None)
+
+ Load a dynamic library.
+
+ This function first tries to load the library from the given exact
+ names. When that fails, it tries to find the library in common
+ locations. It searches for files that start with one of the names
+ given in lib_names (case insensitive). The search is performed in
+ the given lib_dirs and a set of common library dirs.
+
+ Returns ``(ctypes_library, library_path)``
+ """
+
+ # Checks
+ assert isinstance(exact_lib_names, list)
+ assert isinstance(lib_names, list)
+ if lib_dirs is not None:
+ assert isinstance(lib_dirs, list)
+ exact_lib_names = [n for n in exact_lib_names if n]
+ lib_names = [n for n in lib_names if n]
+
+ # Get reference name (for better messages)
+ if lib_names:
+ the_lib_name = lib_names[0]
+ elif exact_lib_names:
+ the_lib_name = exact_lib_names[0]
+ else:
+ raise ValueError("No library name given.")
+
+ # Collect filenames of potential libraries
+ # First try a few bare library names that ctypes might be able to find
+ # in the default locations for each platform.
+ lib_dirs, lib_paths = generate_candidate_libs(lib_names, lib_dirs)
+ lib_paths = exact_lib_names + lib_paths
+
+ # Select loader
+ if sys.platform.startswith('win'):
+ loader = ctypes.windll
+ else:
+ loader = ctypes.cdll
+
+ # Try to load until success
+ the_lib = None
+ errors = []
+ for fname in lib_paths:
+ try:
+ the_lib = loader.LoadLibrary(fname)
+ break
+ except Exception:
+ # Don't record errors when it couldn't load the library from an
+ # exact name -- this fails often, and doesn't provide any useful
+ # debugging information anyway, beyond "couldn't find library..."
+ if fname not in exact_lib_names:
+ # Get exception instance in Python 2.x/3.x compatible manner
+ e_type, e_value, e_tb = sys.exc_info()
+ del e_tb
+ errors.append((fname, e_value))
+
+ # No success ...
+ if the_lib is None:
+ if errors:
+ # No library loaded, and load-errors reported for some
+ # candidate libs
+ err_txt = ['%s:\n%s' % (l, str(e)) for l, e in errors]
+ msg = ('One or more %s libraries were found, but ' +
+ 'could not be loaded due to the following errors:\n%s')
+ raise OSError(msg % (the_lib_name, '\n\n'.join(err_txt)))
+ else:
+ # No errors, because no potential libraries found at all!
+ msg = 'Could not find a %s library in any of:\n%s'
+ raise OSError(msg % (the_lib_name, '\n'.join(lib_dirs)))
+
+ # Done
+ return the_lib, fname
diff --git a/imageio/core/format.py b/imageio/core/format.py
new file mode 100644
index 0000000..185d6b8
--- /dev/null
+++ b/imageio/core/format.py
@@ -0,0 +1,601 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+"""
+
+.. note::
+ imageio is under construction, some details with regard to the
+ Reader and Writer classes may change.
+
+These are the main classes of imageio. They expose an interface for
+advanced users and plugin developers. A brief overview:
+
+ * imageio.FormatManager - for keeping track of registered formats.
+ * imageio.Format - representation of a file format reader/writer
+ * imageio.Format.Reader - object used during the reading of a file.
+ * imageio.Format.Writer - object used during saving a file.
+ * imageio.Request - used to store the filename and other info.
+
+Plugins need to implement a Format class and register
+a format object using ``imageio.formats.add_format()``.
+
+"""
+
+from __future__ import absolute_import, print_function, division
+
+# todo: do we even use the known extensions?
+
+# Some notes:
+#
+# The classes in this module use the Request object to pass filename and
+# related info around. This request object is instantiated in imageio.read
+# and imageio.save.
+#
+# We use the verbs read and save throughout imageio. However, for the
+# associated classes we use the nouns "reader" and "writer", since
+# "saver" feels so awkward.
+
+from __future__ import with_statement
+
+import os
+
+import numpy as np
+
+from . import Image, asarray
+from . import string_types, text_type, binary_type # noqa
+
+
+class Format:
+ """ Represents an implementation to read/save a particular file format
+
+ A format instance is responsible for 1) providing information about
+ a format; 2) determining whether a certain file can be read/saved
+ with this format; 3) providing a reader/writer class.
+
+ Generally, imageio will select the right format and use that to
+ read/save an image. A format can also be explicitly chosen in all
+ read/save functios. Use ``print(format)``, or ``help(format_name)``
+ to see its documentation.
+
+ To implement a specific format, one should create a subclass of
+ Format and the Format.Reader and Format.Writer classes. see
+ :doc:`plugins` for details.
+
+ Parameters
+ ----------
+ name : str
+ A short name of this format. Users can select a format using its name.
+ description : str
+ A one-line description of the format.
+ extensions : str | list | None
+ List of filename extensions that this format supports. If a
+ string is passed it should be space or comma separated. The
+ extensions are used in the documentation and to allow users to
+ select a format by file extension. It is not used to determine
+ what format to use for reading/saving a file.
+ modes : str
+ A string containing the modes that this format can handle ('iIvV').
+ This attribute is used in the documentation and to select the
+ formats when reading/saving a file.
+ """
+
+ def __init__(self, name, description, extensions=None, modes=None):
+
+ # Store name and description
+ self._name = name.upper()
+ self._description = description
+
+ # Store extensions, do some effort to normalize them.
+ # They are stored as a list of lowercase strings without leading dots.
+ if extensions is None:
+ extensions = []
+ elif isinstance(extensions, string_types):
+ extensions = extensions.replace(',', ' ').split(' ')
+ #
+ if isinstance(extensions, (tuple, list)):
+ self._extensions = [e.strip('.').lower() for e in extensions if e]
+ else:
+ raise ValueError('Invalid value for extensions given.')
+
+ # Store mode
+ self._modes = modes or ''
+ if not isinstance(self._modes, string_types):
+ raise ValueError('Invalid value for modes given.')
+ for m in self._modes:
+ if m not in 'iIvV?':
+ raise ValueError('Invalid value for mode given.')
+
+ def __repr__(self):
+ # Short description
+ return '<Format %s - %s>' % (self.name, self.description)
+
+ def __str__(self):
+ return self.doc
+
+ @property
+ def doc(self):
+ """ The documentation for this format (name + description + docstring).
+ """
+ # Our docsring is assumed to be indented by four spaces. The
+ # first line needs special attention.
+ return '%s - %s\n\n %s\n' % (self.name, self.description,
+ self.__doc__.strip())
+
+ @property
+ def name(self):
+ """ The name of this format.
+ """
+ return self._name
+
+ @property
+ def description(self):
+ """ A short description of this format.
+ """
+ return self._description
+
+ @property
+ def extensions(self):
+ """ A list of file extensions supported by this plugin.
+ These are all lowercase without a leading dot.
+ """
+ return self._extensions
+
+ @property
+ def modes(self):
+ """ A string specifying the modes that this format can handle.
+ """
+ return self._modes
+
+ def read(self, request):
+ """ read(request)
+
+ Return a reader object that can be used to read data and info
+ from the given file. Users are encouraged to use imageio.read()
+ instead.
+ """
+ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''
+ if select_mode not in self.modes:
+ raise RuntimeError('Format %s cannot read in mode %r' %
+ (self.name, select_mode))
+ return self.Reader(self, request)
+
+ def save(self, request):
+ """ save(request)
+
+ Return a writer object that can be used to save data and info
+ to the given file. Users are encouraged to use imageio.save() instead.
+ """
+ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''
+ if select_mode not in self.modes:
+ raise RuntimeError('Format %s cannot save in mode %r' %
+ (self.name, select_mode))
+ return self.Writer(self, request)
+
+ def can_read(self, request):
+ """ can_read(request)
+
+ Get whether this format can read data from the specified uri.
+ """
+ return self._can_read(request)
+
+ def can_save(self, request):
+ """ can_save(request)
+
+ Get whether this format can save data to the speciefed uri.
+ """
+ return self._can_save(request)
+
+ def _can_read(self, request):
+ return None # Plugins must implement this
+
+ def _can_save(self, request):
+ return None # Plugins must implement this
+
+ # -----
+
+ class _BaseReaderWriter(object):
+ """ Base class for the Reader and Writer class to implement common
+ functionality. It implements a similar approach for opening/closing
+ and context management as Python's file objects.
+ """
+
+ def __init__(self, format, request):
+ self.__closed = False
+ self._BaseReaderWriter_last_index = -1
+ self._format = format
+ self._request = request
+ # Open the reader/writer
+ self._open(**self.request.kwargs.copy())
+
+ @property
+ def format(self):
+ """ The :class:`.Format` object corresponding to the current
+ read/save operation.
+ """
+ return self._format
+
+ @property
+ def request(self):
+ """ The :class:`.Request` object corresponding to the
+ current read/save operation.
+ """
+ return self._request
+
+ def __enter__(self):
+ self._checkClosed()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ if value is None:
+ # Otherwise error in close hide the real error.
+ self.close()
+
+ def __del__(self):
+ try:
+ self.close()
+ except Exception: # pragma: no cover
+ pass # Supress noise when called during interpreter shutdown
+
+ def close(self):
+ """ Flush and close the reader/writer.
+ This method has no effect if it is already closed.
+ """
+ if self.__closed:
+ return
+ self.__closed = True
+ self._close()
+ # Process results and clean request object
+ self.request.finish()
+
+ @property
+ def closed(self):
+ """ Whether the reader/writer is closed.
+ """
+ return self.__closed
+
+ def _checkClosed(self, msg=None):
+ """Internal: raise an ValueError if reader/writer is closed
+ """
+ if self.closed:
+ what = self.__class__.__name__
+ msg = msg or ("I/O operation on closed %s." % what)
+ raise RuntimeError(msg)
+
+ # To implement
+
+ def _open(self, **kwargs):
+ """ _open(**kwargs)
+
+ Plugins should probably implement this.
+
+ It is called when reader/writer is created. Here the
+ plugin can do its initialization. The given keyword arguments
+ are those that were given by the user at imageio.read() or
+ imageio.write().
+ """
+ raise NotImplementedError()
+
+ def _close(self):
+ """ _close()
+
+ Plugins should probably implement this.
+
+ It is called when the reader/writer is closed. Here the plugin
+ can do a cleanup, flush, etc.
+
+ """
+ raise NotImplementedError()
+
+ # -----
+
+ class Reader(_BaseReaderWriter):
+ """
+ The purpose of a reader object is to read data from an image
+ resource, and should be obtained by calling :func:`.read`.
+
+ A reader can be used as an iterator to read multiple images,
+ and (if the format permits) only reads data from the file when
+ new data is requested (i.e. streaming). A reader can also be
+ used as a context manager so that it is automatically closed.
+
+ Plugins implement Reader's for different formats. Though rare,
+ plugins may provide additional functionality (beyond what is
+ provided by the base reader class).
+ """
+
+ def get_length(self):
+ """ get_length()
+
+ Get the number of images in the file. (Note: you can also
+ use ``len(reader_object)``.)
+
+ The result can be:
+ * 0 for files that only have meta data
+ * 1 for singleton images (e.g. in PNG, JPEG, etc.)
+ * N for image series
+ * inf for streams (series of unknown length)
+ """
+ return self._get_length()
+
+ def get_data(self, index, **kwargs):
+ """ get_data(index, **kwargs)
+
+ Read image data from the file, using the image index. The
+ returned image has a 'meta' attribute with the meta data.
+
+ Some formats may support additional keyword arguments. These are
+ listed in the documentation of those formats.
+ """
+ self._checkClosed()
+ self._BaseReaderWriter_last_index = index
+ im, meta = self._get_data(index, **kwargs)
+ return Image(im, meta) # Image tests im and meta
+
+ def get_next_data(self, **kwargs):
+ """ get_next_data(**kwargs)
+
+ Read the next image from the series.
+
+ Some formats may support additional keyword arguments. These are
+ listed in the documentation of those formats.
+ """
+ return self.get_data(self._BaseReaderWriter_last_index+1, **kwargs)
+
+ def get_meta_data(self, index=None):
+ """ get_meta_data(index=None)
+
+ Read meta data from the file. using the image index. If the
+ index is omitted or None, return the file's (global) meta data.
+
+ Note that ``get_data`` also provides the meta data for the returned
+ image as an atrribute of that image.
+
+ The meta data is a dict, which shape depends on the format.
+ E.g. for JPEG, the dict maps group names to subdicts and each
+ group is a dict with name-value pairs. The groups represent
+ the different metadata formats (EXIF, XMP, etc.).
+ """
+ self._checkClosed()
+ meta = self._get_meta_data(index)
+ if not isinstance(meta, dict):
+ raise ValueError('Meta data must be a dict, not %r' %
+ meta.__class__.__name__)
+ return meta
+
+ def iter_data(self):
+ """ iter_data()
+
+ Iterate over all images in the series. (Note: you can also
+ iterate over the reader object.)
+
+ """
+ self._checkClosed()
+ i, n = 0, self.get_length()
+ while i < n:
+ try:
+ im, meta = self._get_data(i)
+ except IndexError:
+ if n == float('inf'):
+ return
+ raise
+ yield Image(im, meta)
+ i += 1
+
+ # Compatibility
+
+ def __iter__(self):
+ return self.iter_data()
+
+ def __len__(self):
+ return self.get_length()
+
+ # To implement
+
+ def _get_length(self):
+ """ _get_length()
+
+ Plugins must implement this.
+
+ The retured scalar specifies the number of images in the series.
+ See Reader.get_length for more information.
+ """
+ raise NotImplementedError()
+
+ def _get_data(self, index):
+ """ _get_data()
+
+ Plugins must implement this, but may raise an IndexError in
+ case the plugin does not support random access.
+
+ It should return the image and meta data: (ndarray, dict).
+ """
+ raise NotImplementedError()
+
+ def _get_meta_data(self, index):
+ """ _get_meta_data(index)
+
+ Plugins must implement this.
+
+ It should return the meta data as a dict, corresponding to the
+ given index, or to the file's (global) meta data if index is
+ None.
+ """
+ raise NotImplementedError()
+
+ # -----
+
+ class Writer(_BaseReaderWriter):
+ """
+ The purpose of a writer object is to save data to an image
+ resource, and should be obtained by calling :func:`.save`.
+
+ A writer will (if the format permits) write data to the file
+ as soon as new data is provided (i.e. streaming). A writer can
+ also be used as a context manager so that it is automatically
+ closed.
+
+ Plugins implement Writer's for different formats. Though rare,
+ plugins may provide additional functionality (beyond what is
+ provided by the base writer class).
+ """
+
+ def append_data(self, im, meta=None):
+ """ append_data(im, meta={})
+
+ Append an image (and meta data) to the file. The final meta
+ data that is used consists of the meta data on the given
+ image (if applicable), updated with the given meta data.
+ """
+ self._checkClosed()
+ # Check image data
+ if not isinstance(im, np.ndarray):
+ raise ValueError('append_data requires ndarray as first arg')
+ # Get total meta dict
+ total_meta = {}
+ if hasattr(im, 'meta') and isinstance(im.meta, dict):
+ total_meta.update(im.meta)
+ if meta is None:
+ pass
+ elif not isinstance(meta, dict):
+ raise ValueError('Meta must be a dict.')
+ else:
+ total_meta.update(meta)
+
+ # Decouple meta info
+ im = asarray(im)
+ # Call
+ return self._append_data(im, total_meta)
+
+ def set_meta_data(self, meta):
+ """ set_meta_data(meta)
+
+ Sets the file's (global) meta data. The meta data is a dict which
+ shape depends on the format. E.g. for JPEG the dict maps
+ group names to subdicts, and each group is a dict with
+ name-value pairs. The groups represents the different
+ metadata formats (EXIF, XMP, etc.).
+
+ Note that some meta formats may not be supported for
+ writing, and individual fields may be ignored without
+ warning if they are invalid.
+ """
+ self._checkClosed()
+ if not isinstance(meta, dict):
+ raise ValueError('Meta must be a dict.')
+ else:
+ return self._set_meta_data(meta)
+
+ # To implement
+
+ def _append_data(self, im, meta):
+ # Plugins must implement this
+ raise NotImplementedError()
+
+ def _set_meta_data(self, meta):
+ # Plugins must implement this
+ raise NotImplementedError()
+
+
+class FormatManager:
+ """
+ There is exactly one FormatManager object in imageio: ``imageio.formats``.
+ Its purpose it to keep track of the registered formats.
+
+ The format manager supports getting a format object using indexing (by
+ format name or extension). When used as an iterator, this object
+ yields all registered format objects.
+
+ See also :func:`.help`.
+ """
+
+ def __init__(self):
+ self._formats = []
+
+ def __repr__(self):
+ return '<imageio.FormatManager with %i registered formats>' % len(self)
+
+ def __iter__(self):
+ return iter(self._formats)
+
+ def __len__(self):
+ return len(self._formats)
+
+ def __str__(self):
+ ss = []
+ for format in self._formats:
+ ext = ', '.join(format.extensions)
+ s = '%s - %s [%s]' % (format.name, format.description, ext)
+ ss.append(s)
+ return '\n'.join(ss)
+
+ def __getitem__(self, name):
+ # Check
+ if not isinstance(name, string_types):
+ raise ValueError('Looking up a format should be done by name '
+ 'or by extension.')
+
+ # Test if name is existing file
+ if os.path.isfile(name):
+ from . import Request
+ format = self.search_read_format(Request(name, 'r?'))
+ if format is not None:
+ return format
+
+ if '.' in name:
+ # Look for extension
+ e1, e2 = os.path.splitext(name)
+ name = e2 or e1
+ # Search for format that supports this extension
+ name = name.lower()[1:]
+ for format in self._formats:
+ if name in format.extensions:
+ return format
+ else:
+ # Look for name
+ name = name.upper()
+ for format in self._formats:
+ if name == format.name:
+ return format
+ else:
+ # Maybe the user meant to specify an extension
+ return self['.'+name.lower()]
+
+ # Nothing found ...
+ raise IndexError('No format known by name %s.' % name)
+
+ def add_format(self, format):
+ """ add_formar(format)
+
+ Register a format, so that imageio can use it.
+ """
+ if not isinstance(format, Format):
+ raise ValueError('add_format needs argument to be a Format object')
+ elif format in self._formats:
+ raise ValueError('Given Format instance is already registered')
+ else:
+ self._formats.append(format)
+
+ def search_read_format(self, request):
+ """ search_read_format(request)
+
+ Search a format that can read a file according to the given request.
+ Returns None if no appropriate format was found. (used internally)
+ """
+ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''
+ for format in self._formats:
+ if select_mode in format.modes:
+ if format.can_read(request):
+ return format
+
+ def search_save_format(self, request):
+ """ search_save_format(request)
+
+ Search a format that can save a file according to the given request.
+ Returns None if no appropriate format was found. (used internally)
+ """
+ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''
+ for format in self._formats:
+ if select_mode in format.modes:
+ if format.can_save(request):
+ return format
diff --git a/imageio/core/functions.py b/imageio/core/functions.py
new file mode 100644
index 0000000..c676fe2
--- /dev/null
+++ b/imageio/core/functions.py
@@ -0,0 +1,438 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+"""
+These functions represent imageio's main interface for the user. They
+provide a common API to read and save image data for a large
+variety of formats. All read and save functions accept keyword
+arguments, which are passed on to the format that does the actual work.
+To see what keyword arguments are supported by a specific format, use
+the :func:`.help` function.
+
+Functions for reading:
+
+ * :func:`.imread` - read an image from the specified uri
+ * :func:`.mimread` - read a series of images from the specified uri
+ * :func:`.volread` - read a volume from the specified uri
+ * :func:`.mvolsave` - save a series of volumes to the specified uri
+
+Functions for saving:
+
+ * :func:`.imsave` - save an image to the specified uri
+ * :func:`.mimsave` - save a series of images to the specified uri
+ * :func:`.volsave` - save a volume to the specified uri
+ * :func:`.mvolread` - read a series of volumes from the specified uri
+
+More control:
+
+For a larger degree of control, imageio provides the functions
+:func:`.read` and :func:`.save`. They respectively return an
+:class:`.Reader` and an :class:`.Writer` object, which can
+be used to read/save data and meta data in a more controlled manner.
+This also allows specific scientific formats to be exposed in a way
+that best suits that file-format.
+
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+
+from . import Request
+from .. import formats
+
+
+def help(name=None):
+ """ help(name=None)
+
+ Print the documentation of the format specified by name, or a list
+ of supported formats if name is omitted.
+
+ Parameters
+ ----------
+ name : str
+ Can be the name of a format, a filename extension, or a full
+ filename. See also the :doc:`formats page <formats>`.
+ """
+ if not name:
+ print(formats)
+ else:
+ print(formats[name])
+
+
+## Base functions that return a reader/writer
+
+def read(uri, format=None, mode='?', **kwargs):
+ """ read(uri, format=None, mode='?', **kwargs)
+
+ Returns a :class:`.Reader` object which can be used to read data
+ and meta data from the specified file.
+
+ Parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the image from. This can be a normal
+ filename, a file in a zipfile, an http/ftp address, a file
+ object, or the raw bytes.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ mode : {'i', 'I', 'v', 'V', '?'}
+ Used to give the reader a hint on what the user expects (default "?"):
+ "i" for an image, "I" for multiple images, "v" for a volume,
+ "V" for multiple volumes, "?" for don't care.
+ kwargs : ...
+ Further keyword arguments are passed to the reader. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Create request object
+ request = Request(uri, 'r' + mode, **kwargs)
+
+ # Get format
+ if format is not None:
+ format = formats[format]
+ else:
+ format = formats.search_read_format(request)
+ if format is None:
+ raise ValueError('Could not find a format to read the specified file '
+ 'in mode %r' % mode)
+
+ # Return its reader object
+ return format.read(request)
+
+
+def save(uri, format=None, mode='?', **kwargs):
+ """ save(uri, format=None, mode='?', **kwargs)
+
+ Returns a :class:`.Writer` object which can be used to save data
+ and meta data to the specified file.
+
+ Parameters
+ ----------
+ uri : {str, file}
+ The resource to save the image to. This can be a normal
+ filename, a file in a zipfile, a file object, or
+ ``imageio.RETURN_BYTES``, in which case the raw bytes are
+ returned.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename.
+ mode : {'i', 'I', 'v', 'V', '?'}
+ Used to give the writer a hint on what the user expects (default '?'):
+ "i" for an image, "I" for multiple images, "v" for a volume,
+ "V" for multiple volumes, "?" for don't care.
+ kwargs : ...
+ Further keyword arguments are passed to the writer. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Create request object
+ request = Request(uri, 'w' + mode, **kwargs)
+
+ # Get format
+ if format is not None:
+ format = formats[format]
+ else:
+ format = formats.search_save_format(request)
+ if format is None:
+ raise ValueError('Could not find a format to save the specified file '
+ 'in mode %r' % mode)
+
+ # Return its writer object
+ return format.save(request)
+
+
+## Images
+
+def imread(uri, format=None, **kwargs):
+ """ imread(uri, format=None, **kwargs)
+
+ Reads an image from the specified file. Returns a numpy array, which
+ comes with a dict of meta data at its 'meta' attribute.
+
+ Parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the image from. This can be a normal
+ filename, a file in a zipfile, an http/ftp address, a file
+ object, or the raw bytes.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the reader. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get reader and read first
+ reader = read(uri, format, 'i', **kwargs)
+ with reader:
+ return reader.get_data(0)
+
+
+def imsave(uri, im, format=None, **kwargs):
+ """ imsave(uri, im, format=None, **kwargs)
+
+ Save an image to the specified file.
+
+ Parameters
+ ----------
+ uri : {str, file}
+ The resource to save the image to. This can be a normal
+ filename, a file in a zipfile, a file object, or
+ ``imageio.RETURN_BYTES``, in which case the raw bytes are
+ returned.
+ im : numpy.ndarray
+ The image data. Must be NxM, NxMx3 or NxMx4.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the writer. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Test image
+ if isinstance(im, np.ndarray):
+ if im.ndim == 2:
+ pass
+ elif im.ndim == 3 and im.shape[2] in [1, 3, 4]:
+ pass
+ else:
+ raise ValueError('Image must be 2D (grayscale, RGB, or RGBA).')
+ else:
+ raise ValueError('Image must be a numpy array.')
+
+ # Get writer and write first
+ writer = save(uri, format, 'i', **kwargs)
+ with writer:
+ writer.append_data(im)
+
+ # Return a result if there is any
+ return writer.request.get_result()
+
+
+## Multiple images
+
+def mimread(uri, format=None, **kwargs):
+ """ mimread(uri, format=None, **kwargs)
+
+ Reads multiple images from the specified file. Returns a list of
+ numpy arrays, each with a dict of meta data at its 'meta' attribute.
+
+ Parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the images from. This can be a normal
+ filename, a file in a zipfile, an http/ftp address, a file
+ object, or the raw bytes.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the reader. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get reader and read all
+ reader = read(uri, format, 'I', **kwargs)
+ with reader:
+ return [im for im in reader]
+
+
+def mimsave(uri, ims, format=None, **kwargs):
+ """ mimsave(uri, ims, format=None, **kwargs)
+
+ Save multiple images to the specified file.
+
+ Parameters
+ ----------
+ uri : {str, file}
+ The resource to save the images to. This can be a normal
+ filename, a file in a zipfile, a file object, or
+ ``imageio.RETURN_BYTES``, in which case the raw bytes are
+ returned.
+ ims : sequence of numpy arrays
+ The image data. Each array must be NxM, NxMx3 or NxMx4.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the writer. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get writer
+ writer = save(uri, format, 'I', **kwargs)
+ with writer:
+
+ # Iterate over images (ims may be a generator)
+ for im in ims:
+
+ # Test image
+ if isinstance(im, np.ndarray):
+ if im.ndim == 2:
+ pass
+ elif im.ndim == 3 and im.shape[2] in [1, 3, 4]:
+ pass
+ else:
+ raise ValueError('Image must be 2D '
+ '(grayscale, RGB, or RGBA).')
+ else:
+ raise ValueError('Image must be a numpy array.')
+
+ # Add image
+ writer.append_data(im)
+
+ # Return a result if there is any
+ return writer.request.get_result()
+
+
+## Volumes
+
+def volread(uri, format=None, **kwargs):
+ """ volread(uri, format=None, **kwargs)
+
+ Reads a volume from the specified file. Returns a numpy array, which
+ comes with a dict of meta data at its 'meta' attribute.
+
+ Parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the volume from. This can be a normal
+ filename, a file in a zipfile, an http/ftp address, a file
+ object, or the raw bytes.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the reader. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get reader and read first
+ reader = read(uri, format, 'v', **kwargs)
+ with reader:
+ return reader.get_data(0)
+
+
+def volsave(uri, im, format=None, **kwargs):
+ """ volsave(uri, vol, format=None, **kwargs)
+
+ Save a volume to the specified file.
+
+ Parameters
+ ----------
+ uri : {str, file}
+ The resource to save the image to. This can be a normal
+ filename, a file in a zipfile, a file object, or
+ ``imageio.RETURN_BYTES``, in which case the raw bytes are
+ returned.
+ vol : numpy.ndarray
+ The image data. Must be NxMxL (or NxMxLxK if each voxel is a tuple).
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the writer. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Test image
+ if isinstance(im, np.ndarray):
+ if im.ndim == 3:
+ pass
+ elif im.ndim == 4 and im.shape[3] < 32: # How large can a tuple be?
+ pass
+ else:
+ raise ValueError('Image must be 3D, or 4D if each voxel is '
+ 'a tuple.')
+ else:
+ raise ValueError('Image must be a numpy array.')
+
+ # Get writer and write first
+ writer = save(uri, format, 'v', **kwargs)
+ with writer:
+ writer.append_data(im)
+
+ # Return a result if there is any
+ return writer.request.get_result()
+
+
+## Multiple volumes
+
+def mvolread(uri, format=None, **kwargs):
+ """ mvolread(uri, format=None, **kwargs)
+
+ Reads multiple volumes from the specified file. Returns a list of
+ numpy arrays, each with a dict of meta data at its 'meta' attribute.
+
+ Parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the volumes from. This can be a normal
+ filename, a file in a zipfile, an http/ftp address, a file
+ object, or the raw bytes.
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the reader. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get reader and read all
+ reader = read(uri, format, 'V', **kwargs)
+ with reader:
+ return [im for im in reader]
+
+
+def mvolsave(uri, ims, format=None, **kwargs):
+ """ mvolsave(uri, vols, format=None, **kwargs)
+
+ Save multiple volumes to the specified file.
+
+ Parameters
+ ----------
+ uri : {str, file}
+ The resource to save the volumes to. This can be a normal
+ filename, a file in a zipfile, a file object, or
+ ``imageio.RETURN_BYTES``, in which case the raw bytes are
+ returned.
+ ims : sequence of numpy arrays
+ The image data. Each array must be NxMxL (or NxMxLxK if each
+ voxel is a tuple).
+ format : str
+ The format to use to read the file. By default imageio selects
+ the appropriate for you based on the filename and its contents.
+ kwargs : ...
+ Further keyword arguments are passed to the writer. See :func:`.help`
+ to see what arguments are available for a particular format.
+ """
+
+ # Get writer
+ writer = save(uri, format, 'V', **kwargs)
+ with writer:
+
+ # Iterate over images (ims may be a generator)
+ for im in ims:
+
+ # Test image
+ if isinstance(im, np.ndarray):
+ if im.ndim == 3:
+ pass
+ elif im.ndim == 4 and im.shape[3] < 32:
+ pass # How large can a tuple be?
+ else:
+ raise ValueError('Image must be 3D, or 4D if each voxel is'
+ 'a tuple.')
+ else:
+ raise ValueError('Image must be a numpy array.')
+
+ # Add image
+ writer.append_data(im)
+
+ # Return a result if there is any
+ return writer.request.get_result()
diff --git a/imageio/core/request.py b/imageio/core/request.py
new file mode 100644
index 0000000..66bb0ee
--- /dev/null
+++ b/imageio/core/request.py
@@ -0,0 +1,422 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+"""
+Definition of the Request object, which acts as a kind of bridge between
+what the user wants and what the plugins can.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import sys
+import os
+from io import BytesIO
+import zipfile
+import tempfile
+import shutil
+
+from imageio.core import string_types, binary_type, urlopen, get_remote_file
+
+# URI types
+URI_BYTES = 1
+URI_FILE = 2
+URI_FILENAME = 3
+URI_ZIPPED = 4
+URI_HTTP = 5
+URI_FTP = 6
+
+# The user can use this string in a write call to get the data back as bytes.
+RETURN_BYTES = '<bytes>'
+
+# Example images that will be auto-downloaded
+EXAMPLE_IMAGES = {
+ 'astronaut.png': 'Image of the astronaut Eileen Collins',
+ 'camera.png': 'Classic grayscale image of a photographer',
+ 'checkerboard.png': 'Black and white image of a chekerboard',
+ 'clock.png': 'Photo of a clock with motion blur (Stefan van der Walt)',
+ 'coffee.png': 'Image of a cup of coffee (Rachel Michetti)',
+
+ 'chelsea.png': 'Image of Stefan\'s cat',
+ 'wikkie.png': 'Image of Almar\'s cat',
+
+ 'coins.png': 'Image showing greek coins from Pompeii',
+ 'horse.png': 'Image showing the silhouette of a horse (Andreas Preuss)',
+ 'hubble_deep_field.png': 'Photograph taken by Hubble telescope (NASA)',
+ 'immunohistochemistry.png': 'Immunohistochemical (IHC) staining',
+ 'lena.png': 'Classic but sometimes controversioal Lena test image',
+ 'moon.png': 'Image showing a portion of the surface of the moon',
+ 'page.png': 'A scanned page of text',
+ 'text.png': 'A photograph of handdrawn text',
+
+ 'chelsea.zip': 'The chelsea.png in a zipfile (for testing)',
+ 'newtonscradle.gif': 'Animated GIF of a newton\'s cradle',
+ 'cockatoo.mp4': 'Video file of a cockatoo',
+ 'stent.npz': 'Volumetric image showing a stented abdominal aorta',
+}
+
+
+class Request(object):
+ """ Request(uri, mode, **kwargs)
+
+ Represents a request for reading or saving an image resource. This
+ object wraps information to that request and acts as an interface
+ for the plugins to several resources; it allows the user to read
+ from filenames, files, http, zipfiles, raw bytes, etc., but offer
+ a simple interface to the plugins via ``get_file()`` and
+ ``get_local_filename()``.
+
+ For each read/save operation a single Request instance is used and passed
+ to the can_read/can_save method of a format, and subsequently to
+ the Reader/Writer class. This allows rudimentary passing of
+ information between different formats and between a format and
+ associated reader/writer.
+
+ parameters
+ ----------
+ uri : {str, bytes, file}
+ The resource to load the image from.
+ mode : str
+ The first character is "r" or "w", indicating a read or write
+ request. The second character is used to indicate the kind of data:
+ "i" for an image, "I" for multiple images, "v" for a volume,
+ "V" for multiple volumes, "?" for don't care.
+ """
+
+ def __init__(self, uri, mode, **kwargs):
+
+ # General
+ self._uri_type = None
+ self._filename = None
+ self._kwargs = kwargs
+ self._result = None # Some write actions may have a result
+
+ # To handle the user-side
+ self._filename_zip = None # not None if a zipfile is used
+ self._bytes = None # Incoming bytes
+ self._zipfile = None # To store a zipfile instance (if used)
+
+ # To handle the plugin side
+ self._file = None # To store the file instance
+ self._filename_local = None # not None if using tempfile on this FS
+ self._firstbytes = None # For easy header parsing
+
+ # To store formats that may be able to fulfil this request
+ #self._potential_formats = []
+
+ # Check mode
+ self._mode = mode
+ if not isinstance(mode, string_types):
+ raise ValueError('Request requires mode must be a string')
+ if not len(mode) == 2:
+ raise ValueError('Request requires mode to have two chars')
+ if mode[0] not in 'rw':
+ raise ValueError('Request requires mode[0] to be "r" or "w"')
+ if mode[1] not in 'iIvV?':
+ raise ValueError('Request requires mode[1] to be in "iIvV?"')
+
+ # Parse what was given
+ self._parse_uri(uri)
+
+ def _parse_uri(self, uri):
+ """ Try to figure our what we were given
+ """
+ py3k = sys.version_info[0] == 3
+ is_read_request = self.mode[0] == 'r'
+ is_write_request = self.mode[0] == 'w'
+
+ if isinstance(uri, string_types):
+ # Explicit
+ if uri.startswith('http://') or uri.startswith('https://'):
+ self._uri_type = URI_HTTP
+ self._filename = uri
+ elif uri.startswith('ftp://') or uri.startswith('ftps://'):
+ self._uri_type = URI_FTP
+ self._filename = uri
+ elif uri.startswith('file://'):
+ self._uri_type = URI_FILENAME
+ self._filename = uri[7:]
+ elif uri.startswith('<video') and is_read_request:
+ self._uri_type = URI_BYTES
+ self._filename = uri
+ elif uri == RETURN_BYTES and is_write_request:
+ self._uri_type = URI_BYTES
+ self._filename = '<bytes>'
+ # Less explicit (particularly on py 2.x)
+ elif py3k:
+ self._uri_type = URI_FILENAME
+ self._filename = uri
+ else: # pragma: no cover - our ref for coverage is py3k
+ try:
+ isfile = os.path.isfile(uri)
+ except Exception:
+ isfile = False # If checking does not even work ...
+ if isfile:
+ self._uri_type = URI_FILENAME
+ self._filename = uri
+ elif len(uri) < 256: # Can go wrong with veeery tiny images
+ self._uri_type = URI_FILENAME
+ self._filename = uri
+ elif isinstance(uri, binary_type) and is_read_request:
+ self._uri_type = URI_BYTES
+ self._filename = '<bytes>'
+ self._bytes = uri
+ else:
+ self._uri_type = URI_FILENAME
+ self._filename = uri
+ elif py3k and isinstance(uri, binary_type) and is_read_request:
+ self._uri_type = URI_BYTES
+ self._filename = '<bytes>'
+ self._bytes = uri
+ # Files
+ elif is_read_request:
+ if hasattr(uri, 'read') and hasattr(uri, 'close'):
+ self._uri_type = URI_FILE
+ self._filename = '<file>'
+ self._file = uri
+ elif is_write_request:
+ if hasattr(uri, 'write') and hasattr(uri, 'close'):
+ self._uri_type = URI_FILE
+ self._filename = '<file>'
+ self._file = uri
+
+ # Expand user dir
+ if self._uri_type == URI_FILENAME and self._filename.startswith('~'):
+ self._filename = os.path.expanduser(self._filename)
+
+ # Check if a zipfile
+ if self._uri_type == URI_FILENAME:
+ # Search for zip extension followed by a path separater
+ for needle in ['.zip/', '.zip\\']:
+ zip_i = self._filename.lower().find(needle)
+ if zip_i > 0:
+ zip_i += 4
+ self._uri_type = URI_ZIPPED
+ self._filename_zip = (self._filename[:zip_i],
+ self._filename[zip_i:].lstrip('/\\'))
+ break
+
+ # Check if we could read it
+ if self._uri_type is None:
+ uri_r = repr(uri)
+ if len(uri_r) > 60:
+ uri_r = uri_r[:57] + '...'
+ raise IOError("Cannot understand given URI: %s." % uri_r)
+
+ # Check if this is supported
+ noWriting = [URI_HTTP, URI_FTP]
+ if is_write_request and self._uri_type in noWriting:
+ raise IOError('imageio does not support writing to http/ftp.')
+
+ # Check if file exists. If not, it might be an example image
+ if is_read_request:
+ if self._uri_type in [URI_FILENAME, URI_ZIPPED]:
+ fn = self._filename
+ if self._filename_zip:
+ fn = self._filename_zip[0]
+ if not os.path.exists(fn):
+ if fn in EXAMPLE_IMAGES:
+ fn = get_remote_file('images/' + fn)
+ self._filename = fn
+ if self._filename_zip:
+ self._filename_zip = fn, self._filename_zip[1]
+ self._filename = fn + '/' + self._filename_zip[1]
+ else:
+ raise IOError("No such file: '%s'" % fn)
+
+ @property
+ def filename(self):
+ """ The uri for which reading/saving was requested. This
+ can be a filename, an http address, or other resource
+ identifier. Do not rely on the filename to obtain the data,
+ but use ``get_file()`` or ``get_local_filename()`` instead.
+ """
+ return self._filename
+
+ @property
+ def mode(self):
+ """ The mode of the request. The first character is "r" or "w",
+ indicating a read or write request. The second character is
+ used to indicate the kind of data:
+ "i" for an image, "I" for multiple images, "v" for a volume,
+ "V" for multiple volumes, "?" for don't care.
+ """
+ return self._mode
+
+ @property
+ def kwargs(self):
+ """ The dict of keyword arguments supplied by the user.
+ """
+ return self._kwargs
+
+ ## For obtaining data
+
+ def get_file(self):
+ """ get_file()
+ Get a file object for the resource associated with this request.
+ If this is a reading request, the file is in read mode,
+ otherwise in write mode. This method is not thread safe. Plugins
+ do not need to close the file when done.
+
+ This is the preferred way to read/write the data. But if a
+ format cannot handle file-like objects, they should use
+ ``get_local_filename()``.
+ """
+ want_to_write = self.mode[0] == 'w'
+
+ # Is there already a file?
+ # Either _uri_type == URI_FILE, or we already opened the file,
+ # e.g. by using firstbytes
+ if self._file is not None:
+ self._file.seek(0)
+ return self._file
+
+ if self._uri_type == URI_BYTES:
+ if want_to_write:
+ self._file = BytesIO()
+ else:
+ self._file = BytesIO(self._bytes)
+
+ elif self._uri_type == URI_FILENAME:
+ if want_to_write:
+ self._file = open(self.filename, 'wb')
+ else:
+ self._file = open(self.filename, 'rb')
+
+ elif self._uri_type == URI_ZIPPED:
+ # Get the correct filename
+ filename, name = self._filename_zip
+ if want_to_write:
+ # Create new file object, we catch the bytes in finish()
+ self._file = BytesIO()
+ else:
+ # Open zipfile and open new file object for specific file
+ self._zipfile = zipfile.ZipFile(filename, 'r')
+ self._file = self._zipfile.open(name, 'r')
+
+ elif self._uri_type in [URI_HTTP or URI_FTP]:
+ assert not want_to_write # This should have been tested in init
+ self._file = urlopen(self.filename, timeout=5)
+
+ return self._file
+
+ def get_local_filename(self):
+ """ get_local_filename()
+ If the filename is an existing file on this filesystem, return
+ that. Otherwise a temporary file is created on the local file
+ system which can be used by the format to read from or write to.
+ """
+
+ if self._uri_type == URI_FILENAME:
+ return self._filename
+ else:
+ # Get filename
+ ext = os.path.splitext(self._filename)[1]
+ self._filename_local = tempfile.mktemp(ext, 'imageio_')
+ # Write stuff to it?
+ if self.mode[0] == 'r':
+ with open(self._filename_local, 'wb') as file:
+ shutil.copyfileobj(self.get_file(), file)
+ return self._filename_local
+
+ def finish(self):
+ """ finish()
+ For internal use (called when the context of the reader/writer
+ exits). Finishes this request. Close open files and process
+ results.
+ """
+
+ # Init
+ bytes = None
+
+ # Collect bytes from temp file
+ if self.mode[0] == 'w' and self._filename_local:
+ bytes = open(self._filename_local, 'rb').read()
+
+ # Collect bytes from BytesIO file object.
+ written = (self.mode[0] == 'w') and self._file
+ if written and self._uri_type in [URI_BYTES, URI_ZIPPED]:
+ bytes = self._file.getvalue()
+
+ # Close open files that we know of (and are responsible for)
+ if self._file and self._uri_type != URI_FILE:
+ self._file.close()
+ self._file = None
+ if self._zipfile:
+ self._zipfile.close()
+ self._zipfile = None
+ # Remove temp file
+ if self._filename_local:
+ try:
+ os.remove(self._filename_local)
+ except Exception: # pragma: no cover
+ pass
+ self._filename_local = None
+
+ # Handle bytes that we collected
+ if bytes is not None:
+ if self._uri_type == URI_BYTES:
+ self._result = bytes # Picked up by imread function
+ elif self._uri_type == URI_ZIPPED:
+ zf = zipfile.ZipFile(self._filename_zip[0], 'a')
+ zf.writestr(self._filename_zip[1], bytes)
+ zf.close()
+
+ # Detach so gc can clean even if a reference of self lingers
+ self._bytes = None
+
+ def get_result(self):
+ """ For internal use. In some situations a write action can have
+ a result (bytes data). That is obtained with this function.
+ """
+ self._result, res = None, self._result
+ return res
+
+ @property
+ def firstbytes(self):
+ """ The first 256 bytes of the file. These can be used to
+ parse the header to determine the file-format.
+ """
+ if self._firstbytes is None:
+ self._read_first_bytes()
+ return self._firstbytes
+
+ def _read_first_bytes(self, N=256):
+ if self._bytes is not None:
+ self._firstbytes = self._bytes[:N]
+ else:
+ # Prepare
+ f = self.get_file()
+ try:
+ i = f.tell()
+ except Exception:
+ i = None
+ # Read
+ self._firstbytes = read_n_bytes(f, N)
+ # Set back
+ try:
+ if i is None:
+ raise Exception('cannot seek with None')
+ f.seek(i)
+ except Exception:
+ # Prevent get_file() from reusing the file
+ self._file = None
+ # If the given URI was a file object, we have a problem,
+ # but that should be tested in get_file(), because we
+ # seek() there.
+ assert self._uri_type != URI_FILE
+
+
+def read_n_bytes(f, N):
+ """ read_n_bytes(file, n)
+
+ Read n bytes from the given file, or less if the file has less
+ bytes. Returns zero bytes if the file is closed.
+ """
+ bb = binary_type()
+ while len(bb) < N:
+ extra_bytes = f.read(N-len(bb))
+ if not extra_bytes:
+ break
+ bb += extra_bytes
+ return bb
diff --git a/imageio/core/util.py b/imageio/core/util.py
new file mode 100644
index 0000000..7df725a
--- /dev/null
+++ b/imageio/core/util.py
@@ -0,0 +1,434 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+"""
+Various utilities for imageio
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import re
+import os
+import sys
+import time
+import struct
+
+import numpy as np
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+# Taken from six.py
+PY3 = sys.version_info[0] == 3
+if PY3:
+ string_types = str,
+ text_type = str
+ binary_type = bytes
+else: # pragma: no cover
+ string_types = basestring, # noqa
+ text_type = unicode # noqa
+ binary_type = str
+
+
+def urlopen(*args, **kwargs):
+ """ Compatibility function for the urlopen function. Raises an
+ RuntimeError if urlopen could not be imported (which can occur in
+ frozen applications.
+ """
+ try:
+ from urllib2 import urlopen
+ except ImportError:
+ try:
+ from urllib.request import urlopen # Py3k
+ except ImportError:
+ raise RuntimeError('Could not import urlopen.')
+ return urlopen(*args, **kwargs)
+
+
+# currently not used ... the only use it to easly provide the global meta info
+class ImageList(list):
+ def __init__(self, meta=None):
+ list.__init__(self)
+ # Check
+ if not (meta is None or isinstance(meta, dict)):
+ raise ValueError('ImageList expects meta data to be a dict.')
+ # Convert and return
+ self._meta = meta if meta is not None else {}
+
+ @property
+ def meta(self):
+ """ The dict with the meta data of this image.
+ """
+ return self._meta
+
+
+class Image(np.ndarray):
+ """ Image(array, meta=None)
+
+ A subclass of np.ndarray that has a meta attribute.
+ Following scikit-image, we leave this as a normal numpy array as much
+ as we can.
+ """
+
+ def __new__(cls, array, meta=None):
+ # Check
+ if not isinstance(array, np.ndarray):
+ raise ValueError('Image expects a numpy array.')
+ if not (meta is None or isinstance(meta, dict)):
+ raise ValueError('Image expects meta data to be a dict.')
+ # Convert and return
+ meta = meta if meta is not None else {}
+ try:
+ ob = array.view(cls)
+ except AttributeError: # pragma: no cover
+ # Just return the original; no metadata on the array in Pypy!
+ return array
+ ob._copy_meta(meta)
+ return ob
+
+ def _copy_meta(self, meta):
+ """ Make a 2-level deep copy of the meta dictionary.
+ """
+ self._meta = Dict()
+ for key, val in meta.items():
+ if isinstance(val, dict):
+ val = Dict(val) # Copy this level
+ self._meta[key] = val
+
+ @property
+ def meta(self):
+ """ The dict with the meta data of this image.
+ """
+ return self._meta
+
+ def __array_finalize__(self, ob):
+ """ So the meta info is maintained when doing calculations with
+ the array.
+ """
+ if isinstance(ob, Image):
+ self._copy_meta(ob.meta)
+ else:
+ self._copy_meta({})
+
+ def __array_wrap__(self, out, context=None):
+ """ So that we return a native numpy array (or scalar) when a
+ reducting ufunc is applied (such as sum(), std(), etc.)
+ """
+ if not out.shape:
+ return out.dtype.type(out) # Scalar
+ elif out.shape != self.shape:
+ return out.view(type=np.ndarray)
+ else:
+ return out # Type Image
+
+
+def asarray(a):
+ """ Pypy-safe version of np.asarray. Pypy's np.asarray consumes a
+ *lot* of memory if the given array is an ndarray subclass. This
+ function does not.
+ """
+ if isinstance(a, np.ndarray):
+ if IS_PYPY:
+ a = a.copy() # pypy has issues with base views
+ plain = a.view(type=np.ndarray)
+ return plain
+ return np.asarray(a)
+
+
+try:
+ from collections import OrderedDict as _dict
+except ImportError:
+ _dict = dict
+
+
+class Dict(_dict):
+ """ A dict in which the keys can be get and set as if they were
+ attributes. Very convenient in combination with autocompletion.
+
+ This Dict still behaves as much as possible as a normal dict, and
+ keys can be anything that are otherwise valid keys. However,
+ keys that are not valid identifiers or that are names of the dict
+ class (such as 'items' and 'copy') cannot be get/set as attributes.
+ """
+
+ __reserved_names__ = dir(_dict()) # Also from OrderedDict
+ __pure_names__ = dir(dict())
+
+ def __getattribute__(self, key):
+ try:
+ return object.__getattribute__(self, key)
+ except AttributeError:
+ if key in self:
+ return self[key]
+ else:
+ raise
+
+ def __setattr__(self, key, val):
+ if key in Dict.__reserved_names__:
+ # Either let OrderedDict do its work, or disallow
+ if key not in Dict.__pure_names__:
+ return _dict.__setattr__(self, key, val)
+ else:
+ raise AttributeError('Reserved name, this key can only ' +
+ 'be set via ``d[%r] = X``' % key)
+ else:
+ # if isinstance(val, dict): val = Dict(val) -> no, makes a copy!
+ self[key] = val
+
+ def __dir__(self):
+ isidentifier = lambda x: bool(re.match(r'[a-z_]\w*$', x, re.I))
+ names = [k for k in self.keys() if
+ (isinstance(k, string_types) and isidentifier(k))]
+ return Dict.__reserved_names__ + names
+
+
+class BaseProgressIndicator:
+ """ BaseProgressIndicator(name)
+
+ A progress indicator helps display the progres of a task to the
+ user. Progress can be pending, running, finished or failed.
+
+ Each task has:
+ * a name - a short description of what needs to be done.
+ * an action - the current action in performing the task (e.g. a subtask)
+ * progress - how far the task is completed
+ * max - max number of progress units. If 0, the progress is indefinite
+ * unit - the units in which the progress is counted
+ * status - 0: pending, 1: in progress, 2: finished, 3: failed
+
+ This class defines an abstract interface. Subclasses should implement
+ _start, _stop, _update_progress(progressText), _write(message).
+ """
+
+ def __init__(self, name):
+ self._name = name
+ self._action = ''
+ self._unit = ''
+ self._max = 0
+ self._status = 0
+ self._last_progress_update = 0
+
+ def start(self, action='', unit='', max=0):
+ """ start(action='', unit='', max=0)
+
+ Start the progress. Optionally specify an action, a unit,
+ and a maxium progress value.
+ """
+ if self._status == 1:
+ self.finish()
+ self._action = action
+ self._unit = unit
+ self._max = max
+ #
+ self._progress = 0
+ self._status = 1
+ self._start()
+
+ def status(self):
+ """ status()
+
+ Get the status of the progress - 0: pending, 1: in progress,
+ 2: finished, 3: failed
+ """
+ return self._status
+
+ def set_progress(self, progress=0, force=False):
+ """ set_progress(progress=0, force=False)
+
+ Set the current progress. To avoid unnecessary progress updates
+ this will only have a visual effect if the time since the last
+ update is > 0.1 seconds, or if force is True.
+ """
+ self._progress = progress
+ # Update or not?
+ if not (force or (time.time() - self._last_progress_update > 0.1)):
+ return
+ self._last_progress_update = time.time()
+ # Compose new string
+ unit = self._unit or ''
+ progressText = ''
+ if unit == '%':
+ progressText = '%2.1f%%' % progress
+ elif self._max > 0:
+ percent = 100 * float(progress) / self._max
+ progressText = '%i/%i %s (%2.1f%%)' % (progress, self._max, unit,
+ percent)
+ elif progress > 0:
+ if isinstance(progress, float):
+ progressText = '%0.4g %s' % (progress, unit)
+ else:
+ progressText = '%i %s' % (progress, unit)
+ # Update
+ self._update_progress(progressText)
+
+ def increase_progress(self, extra_progress):
+ """ increase_progress(extra_progress)
+
+ Increase the progress by a certain amount.
+ """
+ self.set_progress(self._progress + extra_progress)
+
+ def finish(self, message=None):
+ """ finish(message=None)
+
+ Finish the progress, optionally specifying a message. This will
+ not set the progress to the maximum.
+ """
+ self.set_progress(self._progress, True) # fore update
+ self._status = 2
+ self._stop()
+ if message is not None:
+ self._write(message)
+
+ def fail(self, message=None):
+ """ fail(message=None)
+
+ Stop the progress with a failure, optionally specifying a message.
+ """
+ self.set_progress(self._progress, True) # fore update
+ self._status = 3
+ self._stop()
+ message = 'FAIL ' + (message or '')
+ self._write(message)
+
+ def write(self, message):
+ """ write(message)
+
+ Write a message during progress (such as a warning).
+ """
+ if self.__class__ == BaseProgressIndicator:
+ # When this class is used as a dummy, print explicit message
+ print(message)
+ else:
+ return self._write(message)
+
+ # Implementing classes should implement these
+
+ def _start(self):
+ pass
+
+ def _stop(self):
+ pass
+
+ def _update_progress(self, progressText):
+ pass
+
+ def _write(self, message):
+ pass
+
+
+class StdoutProgressIndicator(BaseProgressIndicator):
+ """ StdoutProgressIndicator(name)
+
+ A progress indicator that shows the progress in stdout. It
+ assumes that the tty can appropriately deal with backspace
+ characters.
+ """
+ def _start(self):
+ self._chars_prefix, self._chars = '', ''
+ # Write message
+ if self._action:
+ self._chars_prefix = '%s (%s): ' % (self._name, self._action)
+ else:
+ self._chars_prefix = '%s: ' % self._name
+ sys.stdout.write(self._chars_prefix)
+ sys.stdout.flush()
+
+ def _update_progress(self, progressText):
+ # If progress is unknown, at least make something move
+ if not progressText:
+ i1, i2, i3, i4 = '-\\|/'
+ M = {i1: i2, i2: i3, i3: i4, i4: i1}
+ progressText = M.get(self._chars, i1)
+ # Store new string and write
+ delChars = '\b'*len(self._chars)
+ self._chars = progressText
+ sys.stdout.write(delChars+self._chars)
+ sys.stdout.flush()
+
+ def _stop(self):
+ self._chars = self._chars_prefix = ''
+ sys.stdout.write('\n')
+ sys.stdout.flush()
+
+ def _write(self, message):
+ # Write message
+ delChars = '\b'*len(self._chars_prefix+self._chars)
+ sys.stdout.write(delChars+' '+message+'\n')
+ # Reprint progress text
+ sys.stdout.write(self._chars_prefix+self._chars)
+ sys.stdout.flush()
+
+
+# From pyzolib/paths.py (https://bitbucket.org/pyzo/pyzolib/src/tip/paths.py)
+def appdata_dir(appname=None, roaming=False):
+ """ appdata_dir(appname=None, roaming=False)
+
+ Get the path to the application directory, where applications are allowed
+ to write user specific files (e.g. configurations). For non-user specific
+ data, consider using common_appdata_dir().
+ If appname is given, a subdir is appended (and created if necessary).
+ If roaming is True, will prefer a roaming directory (Windows Vista/7).
+ """
+
+ # Define default user directory
+ userDir = os.path.expanduser('~')
+
+ # Get system app data dir
+ path = None
+ if sys.platform.startswith('win'):
+ path1, path2 = os.getenv('LOCALAPPDATA'), os.getenv('APPDATA')
+ path = (path2 or path1) if roaming else (path1 or path2)
+ elif sys.platform.startswith('darwin'):
+ path = os.path.join(userDir, 'Library', 'Application Support')
+ # On Linux and as fallback
+ if not (path and os.path.isdir(path)):
+ path = userDir
+
+ # Maybe we should store things local to the executable (in case of a
+ # portable distro or a frozen application that wants to be portable)
+ prefix = sys.prefix
+ if getattr(sys, 'frozen', None):
+ prefix = os.path.abspath(os.path.dirname(sys.path[0]))
+ for reldir in ('settings', '../settings'):
+ localpath = os.path.abspath(os.path.join(prefix, reldir))
+ if os.path.isdir(localpath): # pragma: no cover
+ try:
+ open(os.path.join(localpath, 'test.write'), 'wb').close()
+ os.remove(os.path.join(localpath, 'test.write'))
+ except IOError:
+ pass # We cannot write in this directory
+ else:
+ path = localpath
+ break
+
+ # Get path specific for this app
+ if appname:
+ if path == userDir:
+ appname = '.' + appname.lstrip('.') # Make it a hidden directory
+ path = os.path.join(path, appname)
+ if not os.path.isdir(path): # pragma: no cover
+ os.mkdir(path)
+
+ # Done
+ return path
+
+
+def get_platform():
+ """ get_platform()
+
+ Get a string that specifies the platform more specific than
+ sys.platform does. The result can be: linux32, linux64, win32,
+ win64, osx32, osx64. Other platforms may be added in the future.
+ """
+ # Get platform
+ if sys.platform.startswith('linux'):
+ plat = 'linux%i'
+ elif sys.platform.startswith('win'):
+ plat = 'win%i'
+ elif sys.platform.startswith('darwin'):
+ plat = 'osx%i'
+ else: # pragma: no cover
+ return None
+
+ return plat % (struct.calcsize('P') * 8) # 32 or 64 bits
diff --git a/imageio/freeze.py b/imageio/freeze.py
new file mode 100644
index 0000000..6d0fbb7
--- /dev/null
+++ b/imageio/freeze.py
@@ -0,0 +1,17 @@
+"""
+Helper functions for freezing imageio.
+"""
+
+import sys
+
+
+def get_includes():
+ if sys.version_info[0] == 3:
+ urllib = ['email', 'urllib.request', ]
+ else:
+ urllib = ['urllib2']
+ return urllib + ['numpy', 'zipfile', 'io']
+
+
+def get_excludes():
+ return []
diff --git a/imageio/plugins/__init__.py b/imageio/plugins/__init__.py
new file mode 100644
index 0000000..b2dfab5
--- /dev/null
+++ b/imageio/plugins/__init__.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+"""
+
+Imagio is plugin-based. Every supported format is provided with a
+plugin. You can write your own plugins to make imageio support
+additional formats. And we would be interested in adding such code to the
+imageio codebase!
+
+
+What is a plugin
+----------------
+
+In imageio, a plugin provides one or more :class:`.Format` objects, and
+corresponding :class:`.Reader` and :class:`.Writer` classes.
+Each Format object represents an implementation to read/save a
+particular file format. Its Reader and Writer classes do the actual
+reading/saving.
+
+The reader and writer objects have a ``request`` attribute that can be
+used to obtain information about the read or save :class:`.Request`, such as
+user-provided keyword arguments, as well get access to the raw image
+data.
+
+
+Registering
+-----------
+
+Strictly speaking a format can be used stand alone. However, to allow
+imageio to automatically select it for a specific file, the format must
+be registered using ``imageio.formats.add_format()``.
+
+Note that a plugin is not required to be part of the imageio package; as
+long as a format is registered, imageio can use it. This makes imageio very
+easy to extend.
+
+
+What methods to implement
+--------------------------
+
+Imageio is designed such that plugins only need to implement a few
+private methods. The public API is implemented by the base classes.
+In effect, the public methods can be given a descent docstring which
+does not have to be repeated at the plugins.
+
+For the Format class, the following needs to be implemented/specified:
+
+ * The format needs a short name, a description, and a list of file
+ extensions that are common for the file-format in question.
+ These ase set when instantiation the Format object.
+ * Use a docstring to provide more detailed information about the
+ format/plugin, such as parameters for reading and saving that the user
+ can supply via keyword arguments.
+ * Implement ``_can_read(request)``, return a bool.
+ See also the :class:`.Request` class.
+ * Implement ``_can_save(request)``, dito.
+
+For the Format.Reader class:
+
+ * Implement ``_open(**kwargs)`` to initialize the reader. Deal with the
+ user-provided keyword arguments here.
+ * Implement ``_close()`` to clean up.
+ * Implement ``_get_length()`` to provide a suitable length based on what
+ the user expects. Can be ``inf`` for streaming data.
+ * Implement ``_get_data(index)`` to return an array and a meta-data dict.
+ * Implement ``_get_meta_data(index)`` to return a meta-data dict. If index
+ is None, it should return the 'global' meta-data.
+
+For the Format.Writer class:
+
+ * Implement ``_open(**kwargs)`` to initialize the writer. Deal with the
+ user-provided keyword arguments here.
+ * Implement ``_close()`` to clean up.
+ * Implement ``_append_data(im, meta)`` to add data (and meta-data).
+ * Implement ``_set_meta_data(meta)`` to set the global meta-data.
+
+"""
+
+from . import freeimage # noqa
+from . import freeimagemulti # noqa
+from . import example # noqa
+from . import dicom # noqa
+from . import avbin # noqa
+from . import ffmpeg # noqa
+from . import npz # noqa
+from . import swf # noqa
diff --git a/imageio/plugins/_freeimage.py b/imageio/plugins/_freeimage.py
new file mode 100644
index 0000000..4ef9a23
--- /dev/null
+++ b/imageio/plugins/_freeimage.py
@@ -0,0 +1,1249 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+# styletest: ignore E261
+
+""" Module imageio/freeimage.py
+
+This module contains the wrapper code for the freeimage library.
+The functions defined in this module are relatively thin; just thin
+enough so that arguments and results are native Python/numpy data
+types.
+
+"""
+
+from __future__ import absolute_import, print_function, with_statement
+
+import os
+import sys
+import ctypes
+import threading
+import numpy
+
+from imageio.core import get_remote_file, load_lib, Dict, appdata_dir
+from imageio.core import string_types, binary_type, IS_PYPY, get_platform
+
+TEST_NUMPY_NO_STRIDES = False # To test pypy fallback
+
+
+def get_freeimage_lib():
+ """ Ensure we have our version of the binary freeimage lib.
+ """
+
+ LIBRARIES = {
+ 'osx32': 'libfreeimage-3.16.0-osx10.6.dylib', # universal library
+ 'osx64': 'libfreeimage-3.16.0-osx10.6.dylib',
+ 'win32': 'FreeImage-3.15.4-win32.dll',
+ 'win64': 'FreeImage-3.15.1-win64.dll',
+ 'linux32': 'libfreeimage-3.16.0-linux32.so',
+ 'linux64': 'libfreeimage-3.16.0-linux64.so',
+ }
+
+ # Get filename to load
+ # If we do not provide a binary, the system may still do ...
+ plat = get_platform()
+ if plat:
+ try:
+ return get_remote_file('freeimage/' + LIBRARIES[plat])
+ except RuntimeError as e: # pragma: no cover
+ print(str(e))
+
+
+# Define function to encode a filename to bytes (for the current system)
+efn = lambda x: x.encode(sys.getfilesystemencoding())
+
+# 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255
+GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32)
+
+
+class FI_TYPES(object):
+ FIT_UNKNOWN = 0
+ FIT_BITMAP = 1
+ FIT_UINT16 = 2
+ FIT_INT16 = 3
+ FIT_UINT32 = 4
+ FIT_INT32 = 5
+ FIT_FLOAT = 6
+ FIT_DOUBLE = 7
+ FIT_COMPLEX = 8
+ FIT_RGB16 = 9
+ FIT_RGBA16 = 10
+ FIT_RGBF = 11
+ FIT_RGBAF = 12
+
+ dtypes = {
+ FIT_BITMAP: numpy.uint8,
+ FIT_UINT16: numpy.uint16,
+ FIT_INT16: numpy.int16,
+ FIT_UINT32: numpy.uint32,
+ FIT_INT32: numpy.int32,
+ FIT_FLOAT: numpy.float32,
+ FIT_DOUBLE: numpy.float64,
+ FIT_COMPLEX: numpy.complex128,
+ FIT_RGB16: numpy.uint16,
+ FIT_RGBA16: numpy.uint16,
+ FIT_RGBF: numpy.float32,
+ FIT_RGBAF: numpy.float32
+ }
+
+ fi_types = {
+ (numpy.uint8, 1): FIT_BITMAP,
+ (numpy.uint8, 3): FIT_BITMAP,
+ (numpy.uint8, 4): FIT_BITMAP,
+ (numpy.uint16, 1): FIT_UINT16,
+ (numpy.int16, 1): FIT_INT16,
+ (numpy.uint32, 1): FIT_UINT32,
+ (numpy.int32, 1): FIT_INT32,
+ (numpy.float32, 1): FIT_FLOAT,
+ (numpy.float64, 1): FIT_DOUBLE,
+ (numpy.complex128, 1): FIT_COMPLEX,
+ (numpy.uint16, 3): FIT_RGB16,
+ (numpy.uint16, 4): FIT_RGBA16,
+ (numpy.float32, 3): FIT_RGBF,
+ (numpy.float32, 4): FIT_RGBAF
+ }
+
+ extra_dims = {
+ FIT_UINT16: [],
+ FIT_INT16: [],
+ FIT_UINT32: [],
+ FIT_INT32: [],
+ FIT_FLOAT: [],
+ FIT_DOUBLE: [],
+ FIT_COMPLEX: [],
+ FIT_RGB16: [3],
+ FIT_RGBA16: [4],
+ FIT_RGBF: [3],
+ FIT_RGBAF: [4]
+ }
+
+
+class IO_FLAGS(object):
+ FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only
+ # # (not supported by all plugins)
+ BMP_DEFAULT = 0
+ BMP_SAVE_RLE = 1
+ CUT_DEFAULT = 0
+ DDS_DEFAULT = 0
+ EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression
+ EXR_FLOAT = 0x0001 # save data as float instead of half (not recommended)
+ EXR_NONE = 0x0002 # save with no compression
+ EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines
+ EXR_PIZ = 0x0008 # save with piz-based wavelet compression
+ EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression
+ EXR_B44 = 0x0020 # save with lossy 44% float compression
+ # # - goes to 22% when combined with EXR_LC
+ EXR_LC = 0x0040 # save images with one luminance and two chroma channels,
+ # # rather than as RGB (lossy compression)
+ FAXG3_DEFAULT = 0
+ GIF_DEFAULT = 0
+ GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed
+ # # palette entries, if it's 16 or 2 color
+ GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp)
+ # # instead of returning raw frame data when loading
+ HDR_DEFAULT = 0
+ ICO_DEFAULT = 0
+ ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the
+ # # AND-mask when loading
+ IFF_DEFAULT = 0
+ J2K_DEFAULT = 0 # save with a 16:1 rate
+ JP2_DEFAULT = 0 # save with a 16:1 rate
+ JPEG_DEFAULT = 0 # loading (see JPEG_FAST);
+ # # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420)
+ JPEG_FAST = 0x0001 # load the file as fast as possible,
+ # # sacrificing some quality
+ JPEG_ACCURATE = 0x0002 # load the file with the best quality,
+ # # sacrificing some speed
+ JPEG_CMYK = 0x0004 # load separated CMYK "as is"
+ # # (use | to combine with other load flags)
+ JPEG_EXIFROTATE = 0x0008 # load and rotate according to
+ # # Exif 'Orientation' tag if available
+ JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1)
+ JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1)
+ JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1)
+ JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1)
+ JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1)
+ JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG
+ # # (use | to combine with other save flags)
+ JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma
+ # # subsampling (4:1:1)
+ JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma
+ # # subsampling (4:2:0) - default value
+ JPEG_SUBSAMPLING_422 = 0x8000 # save /w low 2x1 chroma subsampling (4:2:2)
+ JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4)
+ JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables
+ # # (can reduce a few percent of file size)
+ JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers
+ KOALA_DEFAULT = 0
+ LBM_DEFAULT = 0
+ MNG_DEFAULT = 0
+ PCD_DEFAULT = 0
+ PCD_BASE = 1 # load the bitmap sized 768 x 512
+ PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256
+ PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128
+ PCX_DEFAULT = 0
+ PFM_DEFAULT = 0
+ PICT_DEFAULT = 0
+ PNG_DEFAULT = 0
+ PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction
+ PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag
+ # # (default value is 6)
+ PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression
+ # # flag (default recommended value)
+ PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag
+ # # (default value is 6)
+ PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression
+ PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine
+ # # with other save flags)
+ PNM_DEFAULT = 0
+ PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6)
+ PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3)
+ PSD_DEFAULT = 0
+ PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB)
+ PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB)
+ RAS_DEFAULT = 0
+ RAW_DEFAULT = 0 # load the file as linear RGB 48-bit
+ RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included
+ # # Exif Data or default to RGB 24-bit
+ RAW_DISPLAY = 2 # load the file as RGB 24-bit
+ SGI_DEFAULT = 0
+ TARGA_DEFAULT = 0
+ TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888.
+ TARGA_SAVE_RLE = 2 # Save with RLE compression
+ TIFF_DEFAULT = 0
+ TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK
+ # # (use | to combine with compression flags)
+ TIFF_PACKBITS = 0x0100 # save using PACKBITS compression
+ TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression
+ TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression
+ TIFF_NONE = 0x0800 # save without any compression
+ TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding
+ TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding
+ TIFF_LZW = 0x4000 # save using LZW compression
+ TIFF_JPEG = 0x8000 # save using JPEG compression
+ TIFF_LOGLUV = 0x10000 # save using LogLuv compression
+ WBMP_DEFAULT = 0
+ XBM_DEFAULT = 0
+ XPM_DEFAULT = 0
+
+
+class METADATA_MODELS(object):
+ FIMD_COMMENTS = 0
+ FIMD_EXIF_MAIN = 1
+ FIMD_EXIF_EXIF = 2
+ FIMD_EXIF_GPS = 3
+ FIMD_EXIF_MAKERNOTE = 4
+ FIMD_EXIF_INTEROP = 5
+ FIMD_IPTC = 6
+ FIMD_XMP = 7
+ FIMD_GEOTIFF = 8
+ FIMD_ANIMATION = 9
+
+
+class METADATA_DATATYPE(object):
+ FIDT_BYTE = 1 # 8-bit unsigned integer
+ FIDT_ASCII = 2 # 8-bit bytes w/ last byte null
+ FIDT_SHORT = 3 # 16-bit unsigned integer
+ FIDT_LONG = 4 # 32-bit unsigned integer
+ FIDT_RATIONAL = 5 # 64-bit unsigned fraction
+ FIDT_SBYTE = 6 # 8-bit signed integer
+ FIDT_UNDEFINED = 7 # 8-bit untyped data
+ FIDT_SSHORT = 8 # 16-bit signed integer
+ FIDT_SLONG = 9 # 32-bit signed integer
+ FIDT_SRATIONAL = 10 # 64-bit signed fraction
+ FIDT_FLOAT = 11 # 32-bit IEEE floating point
+ FIDT_DOUBLE = 12 # 64-bit IEEE floating point
+ FIDT_IFD = 13 # 32-bit unsigned integer (offset)
+ FIDT_PALETTE = 14 # 32-bit RGBQUAD
+ FIDT_LONG8 = 16 # 64-bit unsigned integer
+ FIDT_SLONG8 = 17 # 64-bit signed integer
+ FIDT_IFD8 = 18 # 64-bit unsigned integer (offset)
+
+ dtypes = {
+ FIDT_BYTE: numpy.uint8,
+ FIDT_SHORT: numpy.uint16,
+ FIDT_LONG: numpy.uint32,
+ FIDT_RATIONAL: [('numerator', numpy.uint32),
+ ('denominator', numpy.uint32)],
+ FIDT_LONG8: numpy.uint64,
+ FIDT_SLONG8: numpy.int64,
+ FIDT_IFD8: numpy.uint64,
+ FIDT_SBYTE: numpy.int8,
+ FIDT_UNDEFINED: numpy.uint8,
+ FIDT_SSHORT: numpy.int16,
+ FIDT_SLONG: numpy.int32,
+ FIDT_SRATIONAL: [('numerator', numpy.int32),
+ ('denominator', numpy.int32)],
+ FIDT_FLOAT: numpy.float32,
+ FIDT_DOUBLE: numpy.float64,
+ FIDT_IFD: numpy.uint32,
+ FIDT_PALETTE: [('R', numpy.uint8), ('G', numpy.uint8),
+ ('B', numpy.uint8), ('A', numpy.uint8)],
+ }
+
+
+class Freeimage(object):
+ """ Class to represent an interface to the FreeImage library.
+ This class is relatively thin. It provides a Pythonic API that converts
+ Freeimage objects to Python objects, but that's about it.
+ The actual implementation should be provided by the plugins.
+
+ The recommended way to call into the Freeimage library (so that
+ errors and warnings show up in the right moment) is to use this
+ object as a context manager:
+ with imageio.fi as lib:
+ lib.FreeImage_GetPalette()
+
+ """
+
+ _API = {
+ # All we're doing here is telling ctypes that some of the
+ # FreeImage functions return pointers instead of integers. (On
+ # 64-bit systems, without this information the pointers get
+ # truncated and crashes result). There's no need to list
+ # functions that return ints, or the types of the parameters
+ # to these or other functions -- that's fine to do implicitly.
+
+ # Note that the ctypes immediately converts the returned void_p
+ # back to a python int again! This is really not helpful,
+ # because then passing it back to another library call will
+ # cause truncation-to-32-bits on 64-bit systems. Thanks, ctypes!
+ # So after these calls one must immediately re-wrap the int as
+ # a c_void_p if it is to be passed back into FreeImage.
+ 'FreeImage_AllocateT': (ctypes.c_void_p, None),
+ 'FreeImage_FindFirstMetadata': (ctypes.c_void_p, None),
+ 'FreeImage_GetBits': (ctypes.c_void_p, None),
+ 'FreeImage_GetPalette': (ctypes.c_void_p, None),
+ 'FreeImage_GetTagKey': (ctypes.c_char_p, None),
+ 'FreeImage_GetTagValue': (ctypes.c_void_p, None),
+
+ 'FreeImage_Save': (ctypes.c_void_p, None),
+ 'FreeImage_Load': (ctypes.c_void_p, None),
+ 'FreeImage_LoadFromMemory': (ctypes.c_void_p, None),
+
+ 'FreeImage_OpenMultiBitmap': (ctypes.c_void_p, None),
+ 'FreeImage_LoadMultiBitmapFromMemory': (ctypes.c_void_p, None),
+ 'FreeImage_LockPage': (ctypes.c_void_p, None),
+
+ 'FreeImage_OpenMemory': (ctypes.c_void_p, None),
+ #'FreeImage_ReadMemory': (ctypes.c_void_p, None),
+ #'FreeImage_CloseMemory': (ctypes.c_void_p, None),
+
+ 'FreeImage_GetVersion': (ctypes.c_char_p, None),
+ 'FreeImage_GetFIFExtensionList': (ctypes.c_char_p, None),
+ 'FreeImage_GetFormatFromFIF': (ctypes.c_char_p, None),
+ 'FreeImage_GetFIFDescription': (ctypes.c_char_p, None),
+
+ # Pypy wants some extra definitions, so here we go ...
+ 'FreeImage_IsLittleEndian': (ctypes.c_int, None),
+ 'FreeImage_SetOutputMessage': (ctypes.c_void_p, None),
+ 'FreeImage_GetFIFCount': (ctypes.c_int, None),
+ 'FreeImage_IsPluginEnabled': (ctypes.c_int, None),
+ 'FreeImage_GetFileType': (ctypes.c_int, None),
+ #
+ 'FreeImage_GetTagType': (ctypes.c_int, None),
+ 'FreeImage_GetTagLength': (ctypes.c_int, None),
+ 'FreeImage_FindNextMetadata': (ctypes.c_int, None),
+ 'FreeImage_FindCloseMetadata': (ctypes.c_void_p, None),
+ #
+ 'FreeImage_GetFIFFromFilename': (ctypes.c_int, None),
+ 'FreeImage_FIFSupportsReading': (ctypes.c_int, None),
+ 'FreeImage_FIFSupportsWriting': (ctypes.c_int, None),
+ 'FreeImage_FIFSupportsExportType': (ctypes.c_int, None),
+ 'FreeImage_FIFSupportsExportBPP': (ctypes.c_int, None),
+ 'FreeImage_GetHeight': (ctypes.c_int, None),
+ 'FreeImage_GetWidth': (ctypes.c_int, None),
+ 'FreeImage_GetImageType': (ctypes.c_int, None),
+ 'FreeImage_GetBPP': (ctypes.c_int, None),
+ 'FreeImage_GetColorsUsed': (ctypes.c_int, None),
+ 'FreeImage_ConvertTo32Bits': (ctypes.c_void_p, None),
+ 'FreeImage_GetPitch': (ctypes.c_int, None),
+ 'FreeImage_Unload': (ctypes.c_void_p, None),
+ }
+
+ def __init__(self):
+
+ # Initialize freeimage lib as None
+ self._lib = None
+
+ # A lock to create thread-safety
+ self._lock = threading.RLock()
+
+ # Init log messages lists
+ self._messages = []
+
+ # Select functype for error handler
+ if sys.platform.startswith('win'):
+ functype = ctypes.WINFUNCTYPE
+ else:
+ functype = ctypes.CFUNCTYPE
+
+ # Create output message handler
+ @functype(None, ctypes.c_int, ctypes.c_char_p)
+ def error_handler(fif, message):
+ message = message.decode('utf-8')
+ self._messages.append(message)
+ while (len(self._messages)) > 256:
+ self._messages.pop(0)
+
+ # Make sure to keep a ref to function
+ self._error_handler = error_handler
+
+ # Load library and register API
+ self._load_freeimage()
+ self._register_api()
+
+ # Register logger for output messages
+ self._lib.FreeImage_SetOutputMessage(self._error_handler)
+
+ # Store version
+ self._lib_version = self._lib.FreeImage_GetVersion().decode('utf-8')
+
+ def _load_freeimage(self):
+
+ # todo: we want to load from location relative to exe in frozen apps
+ # Get lib dirs
+ lib_dirs = [appdata_dir('imageio')]
+
+ # Make sure that we have our binary version of the libary
+ lib_filename = get_freeimage_lib() or 'notavalidlibname'
+
+ # Load library
+ lib_names = ['freeimage', 'libfreeimage']
+ exact_lib_names = [lib_filename, 'FreeImage', 'libfreeimage.dylib',
+ 'libfreeimage.so', 'libfreeimage.so.3']
+ try:
+ lib, fname = load_lib(exact_lib_names, lib_names, lib_dirs)
+ except OSError: # pragma: no cover
+ # Could not load. Get why
+ e_type, e_value, e_tb = sys.exc_info()
+ del e_tb
+ load_error = str(e_value)
+ err_msg = load_error + '\nPlease install the FreeImage library.'
+ raise OSError(err_msg)
+
+ # Store
+ self._lib = lib
+ self._lib_fname = fname
+
+ def _register_api(self):
+ # Albert's ctypes pattern
+ for f, (restype, argtypes) in self._API.items():
+ func = getattr(self._lib, f)
+ func.restype = restype
+ func.argtypes = argtypes
+
+ ## Handling of output messages
+
+ def __enter__(self):
+ self._lock.acquire()
+ return self._lib
+
+ def __exit__(self, *args):
+ self._show_any_warnings()
+ self._lock.release()
+
+ def _reset_log(self):
+ """ Reset the list of output messages. Call this before
+ loading or saving an image with the FreeImage API.
+ """
+ self._messages = []
+
+ def _get_error_message(self):
+ """ Get the output messages produced since the last reset as
+ one string. Returns 'No known reason.' if there are no messages.
+ Also resets the log.
+ """
+ if self._messages:
+ res = ' '.join(self._messages)
+ self._reset_log()
+ return res
+ else:
+ return 'No known reason.'
+
+ def _show_any_warnings(self):
+ """ If there were any messages since the last reset, show them
+ as a warning. Otherwise do nothing. Also resets the messages.
+ """
+ if self._messages:
+ print('imageio.freeimage warning: ' + self._get_error_message())
+ self._reset_log()
+
+ def get_output_log(self):
+ """ Return a list of the last 256 output messages
+ (warnings and errors) produced by the FreeImage library.
+ """
+ # This message log is not cleared/reset, but kept to 256 elements.
+ return [m for m in self._messages]
+
+ def getFIF(self, filename, mode, bytes=None):
+ """ Get the freeimage Format (FIF) from a given filename.
+ If mode is 'r', will try to determine the format by reading
+ the file, otherwise only the filename is used.
+
+ This function also tests whether the format supports reading/writing.
+ """
+ with self as lib:
+
+ # Init
+ ftype = -1
+ if mode not in 'rw':
+ raise ValueError('Invalid mode (must be "r" or "w").')
+
+ # Try getting format from the content. Note that some files
+ # do not have a header that allows reading the format from
+ # the file.
+ if mode == 'r':
+ if bytes is not None:
+ fimemory = lib.FreeImage_OpenMemory(
+ ctypes.c_char_p(bytes), len(bytes))
+ ftype = lib.FreeImage_GetFileTypeFromMemory(
+ ctypes.c_void_p(fimemory), len(bytes))
+ lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
+ if (ftype == -1) and os.path.isfile(filename):
+ ftype = lib.FreeImage_GetFileType(efn(filename), 0)
+ # Try getting the format from the extension
+ if ftype == -1:
+ ftype = lib.FreeImage_GetFIFFromFilename(efn(filename))
+
+ # Test if ok
+ if ftype == -1:
+ raise ValueError('Cannot determine format of file "%s"' %
+ filename)
+ elif mode == 'w' and not lib.FreeImage_FIFSupportsWriting(ftype):
+ raise ValueError('Cannot write the format of file "%s"' %
+ filename)
+ elif mode == 'r' and not lib.FreeImage_FIFSupportsReading(ftype):
+ raise ValueError('Cannot read the format of file "%s"' %
+ filename)
+ else:
+ return ftype
+
+ def create_bitmap(self, filename, ftype, flags=0):
+ """ create_bitmap(filename, ftype, flags=0)
+ Create a wrapped bitmap object.
+ """
+ return FIBitmap(self, filename, ftype, flags)
+
+ def create_multipage_bitmap(self, filename, ftype, flags=0):
+ """ create_multipage_bitmap(filename, ftype, flags=0)
+ Create a wrapped multipage bitmap object.
+ """
+ return FIMultipageBitmap(self, filename, ftype, flags)
+
+
+class FIBaseBitmap(object):
+ def __init__(self, fi, filename, ftype, flags):
+ self._fi = fi
+ self._filename = filename
+ self._ftype = ftype
+ self._flags = flags
+ self._bitmap = None
+ self._close_funcs = []
+
+ def __del__(self):
+ self.close()
+
+ def close(self):
+ if (self._bitmap is not None) and self._close_funcs:
+ for close_func in self._close_funcs:
+ try:
+ with self._fi:
+ fun = close_func[0]
+ fun(*close_func[1:])
+ except Exception: # pragma: no cover
+ pass
+ self._close_funcs = []
+ self._bitmap = None
+
+ def _set_bitmap(self, bitmap, close_func=None):
+ """ Function to set the bitmap and specify the function to unload it.
+ """
+ if self._bitmap is not None:
+ pass # bitmap is converted
+ if close_func is None:
+ close_func = self._fi._lib.FreeImage_Unload, bitmap
+
+ self._bitmap = bitmap
+ if close_func:
+ self._close_funcs.append(close_func)
+
+ def get_meta_data(self):
+
+ # todo: there is also FreeImage_TagToString, is that useful?
+ # and would that work well when reading and then saving?
+
+ # Create a list of (model_name, number) tuples
+ models = [(name[5:], number) for name, number in
+ METADATA_MODELS.__dict__.items() if name.startswith('FIMD_')]
+
+ # Prepare
+ metadata = Dict()
+ tag = ctypes.c_void_p()
+
+ with self._fi as lib:
+
+ # Iterate over all FreeImage meta models
+ for model_name, number in models:
+
+ # Find beginning, get search handle
+ mdhandle = lib.FreeImage_FindFirstMetadata(number,
+ self._bitmap,
+ ctypes.byref(tag))
+ mdhandle = ctypes.c_void_p(mdhandle)
+ if mdhandle:
+
+ # Iterate over all tags in this model
+ more = True
+ while more:
+ # Get info about tag
+ tag_name = lib.FreeImage_GetTagKey(tag).decode('utf-8')
+ tag_type = lib.FreeImage_GetTagType(tag)
+ byte_size = lib.FreeImage_GetTagLength(tag)
+ char_ptr = ctypes.c_char * byte_size
+ data = char_ptr.from_address(
+ lib.FreeImage_GetTagValue(tag))
+ # Convert in a way compatible with Pypy
+ tag_bytes = binary_type(bytearray(data))
+ # The default value is the raw bytes
+ tag_val = tag_bytes
+ # Convert to a Python value in the metadata dict
+ if tag_type == METADATA_DATATYPE.FIDT_ASCII:
+ tag_val = tag_bytes.decode('utf-8', 'replace')
+ elif tag_type in METADATA_DATATYPE.dtypes:
+ dtype = METADATA_DATATYPE.dtypes[tag_type]
+ if IS_PYPY and isinstance(dtype, (list, tuple)):
+ pass # pragma: no cover - or we get a segfault
+ else:
+ try:
+ tag_val = numpy.fromstring(tag_bytes,
+ dtype=dtype)
+ if len(tag_val) == 1:
+ tag_val = tag_val[0]
+ except Exception: # pragma: no cover
+ pass
+ # Store data in dict
+ subdict = metadata.setdefault(model_name, Dict())
+ subdict[tag_name] = tag_val
+ # Next
+ more = lib.FreeImage_FindNextMetadata(
+ mdhandle, ctypes.byref(tag))
+
+ # Close search handle for current meta model
+ lib.FreeImage_FindCloseMetadata(mdhandle)
+
+ # Done
+ return metadata
+
+ def set_meta_data(self, metadata):
+
+ # Create a dict mapping model_name to number
+ models = {}
+ for name, number in METADATA_MODELS.__dict__.items():
+ if name.startswith('FIMD_'):
+ models[name[5:]] = number
+
+ # Create a mapping from numpy.dtype to METADATA_DATATYPE
+ def get_tag_type_number(dtype):
+ for number, numpy_dtype in METADATA_DATATYPE.dtypes.items():
+ if dtype == numpy_dtype:
+ return number
+ else:
+ return None
+
+ with self._fi as lib:
+
+ for model_name, subdict in metadata.items():
+
+ # Get model number
+ number = models.get(model_name, None)
+ if number is None:
+ continue # Unknown model, silent ignore
+
+ for tag_name, tag_val in subdict.items():
+
+ # Create new tag
+ tag = lib.FreeImage_CreateTag()
+ tag = ctypes.c_void_p(tag)
+
+ try:
+ # Convert Python value to FI type, val
+ is_ascii = False
+ if isinstance(tag_val, string_types):
+ try:
+ tag_bytes = tag_val.encode('ascii')
+ is_ascii = True
+ except UnicodeError:
+ pass
+ if is_ascii:
+ tag_type = METADATA_DATATYPE.FIDT_ASCII
+ tag_count = len(tag_bytes)
+ else:
+ if not hasattr(tag_val, 'dtype'):
+ tag_val = numpy.array([tag_val])
+ tag_type = get_tag_type_number(tag_val.dtype)
+ if tag_type is None:
+ print('imageio.freeimage warning: Could not '
+ 'determine tag type of %r.' % tag_name)
+ continue
+ tag_bytes = tag_val.tostring()
+ tag_count = tag_val.size
+ # Set properties
+ lib.FreeImage_SetTagKey(tag, tag_name.encode('utf-8'))
+ lib.FreeImage_SetTagType(tag, tag_type)
+ lib.FreeImage_SetTagCount(tag, tag_count)
+ lib.FreeImage_SetTagLength(tag, len(tag_bytes))
+ lib.FreeImage_SetTagValue(tag, tag_bytes)
+ # Store tag
+ tag_key = lib.FreeImage_GetTagKey(tag)
+ lib.FreeImage_SetMetadata(number, self._bitmap,
+ tag_key, tag)
+
+ except Exception: # pragma: no cover
+ # Could not load. Get why
+ e_type, e_value, e_tb = sys.exc_info()
+ del e_tb
+ load_error = str(e_value)
+ print('imagio.freeimage warning: Could not set tag '
+ '%r: %s, %s' % (tag_name,
+ self._fi._get_error_message(),
+ load_error))
+ finally:
+ lib.FreeImage_DeleteTag(tag)
+
+
+class FIBitmap(FIBaseBitmap):
+ """ Wrapper for the FI bitmap object.
+ """
+
+ def allocate(self, array):
+
+ # Prepare array
+ assert isinstance(array, numpy.ndarray)
+ shape = array.shape
+ dtype = array.dtype
+
+ # Get shape and channel info
+ r, c = shape[:2]
+ if len(shape) == 2:
+ n_channels = 1
+ elif len(shape) == 3:
+ n_channels = shape[2]
+ else:
+ n_channels = shape[0]
+
+ # Get fi_type
+ try:
+ fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)]
+ self._fi_type = fi_type
+ except KeyError:
+ raise ValueError('Cannot write arrays of given type and shape.')
+
+ # Allocate bitmap
+ with self._fi as lib:
+ bpp = 8 * dtype.itemsize * n_channels
+ bitmap = lib.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0)
+ bitmap = ctypes.c_void_p(bitmap)
+
+ # Check and store
+ if not bitmap: # pragma: no cover
+ raise RuntimeError('Could not allocate bitmap for storage: %s'
+ % self._fi._get_error_message())
+ else:
+ self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
+
+ def load_from_filename(self, filename=None):
+ if filename is None:
+ filename = self._filename
+
+ with self._fi as lib:
+ # Create bitmap
+ bitmap = lib.FreeImage_Load(self._ftype, efn(filename),
+ self._flags)
+ bitmap = ctypes.c_void_p(bitmap)
+
+ # Check and store
+ if not bitmap: # pragma: no cover
+ raise ValueError('Could not load bitmap "%s": %s' %
+ (self._filename,
+ self._fi._get_error_message()))
+ else:
+ self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
+
+# def load_from_bytes(self, bytes):
+# with self._fi as lib:
+# # Create bitmap
+# fimemory = lib.FreeImage_OpenMemory(
+# ctypes.c_char_p(bytes), len(bytes))
+# bitmap = lib.FreeImage_LoadFromMemory(
+# self._ftype, ctypes.c_void_p(fimemory), self._flags)
+# bitmap = ctypes.c_void_p(bitmap)
+# lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
+#
+# # Check
+# if not bitmap:
+# raise ValueError('Could not load bitmap "%s": %s'
+# % (self._filename, self._fi._get_error_message()))
+# else:
+# self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
+
+ def save_to_filename(self, filename=None):
+ if filename is None:
+ filename = self._filename
+
+ ftype = self._ftype
+ bitmap = self._bitmap
+ fi_type = self._fi_type # element type
+
+ with self._fi as lib:
+ # Check if can write
+ if fi_type == FI_TYPES.FIT_BITMAP:
+ can_write = lib.FreeImage_FIFSupportsExportBPP(
+ ftype, lib.FreeImage_GetBPP(bitmap))
+ else:
+ can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
+ if not can_write:
+ raise TypeError('Cannot save image of this format '
+ 'to this file type')
+
+ # Save to file
+ res = lib.FreeImage_Save(ftype, bitmap, efn(filename), self._flags)
+ # Check
+ if not res: # pragma: no cover, we do so many checks, this is rare
+ raise RuntimeError('Could not save file "%s": %s' %
+ (self._filename,
+ self._fi._get_error_message()))
+
+# def save_to_bytes(self):
+# ftype = self._ftype
+# bitmap = self._bitmap
+# fi_type = self._fi_type # element type
+#
+# with self._fi as lib:
+# # Check if can write
+# if fi_type == FI_TYPES.FIT_BITMAP:
+# can_write = lib.FreeImage_FIFSupportsExportBPP(ftype,
+# lib.FreeImage_GetBPP(bitmap))
+# else:
+# can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)
+# if not can_write:
+# raise TypeError('Cannot save image of this format '
+# 'to this file type')
+#
+# # Extract the bytes
+# fimemory = lib.FreeImage_OpenMemory(0, 0)
+# res = lib.FreeImage_SaveToMemory(ftype, bitmap,
+# ctypes.c_void_p(fimemory),
+# self._flags)
+# if res:
+# N = lib.FreeImage_TellMemory(ctypes.c_void_p(fimemory))
+# result = ctypes.create_string_buffer(N)
+# lib.FreeImage_SeekMemory(ctypes.c_void_p(fimemory), 0)
+# lib.FreeImage_ReadMemory(result, 1, N, ctypes.c_void_p(fimemory))
+# result = result.raw
+# lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
+#
+# # Check
+# if not res:
+# raise RuntimeError('Could not save file "%s": %s'
+# % (self._filename, self._fi._get_error_message()))
+#
+# # Done
+# return result
+
+ def get_image_data(self):
+ dtype, shape, bpp = self._get_type_and_shape()
+ array = self._wrap_bitmap_bits_in_array(shape, dtype, False)
+ with self._fi as lib:
+ isle = lib.FreeImage_IsLittleEndian()
+
+ # swizzle the color components and flip the scanlines to go from
+ # FreeImage's BGR[A] and upside-down internal memory format to
+ # something more normal
+ def n(arr):
+ #return arr[..., ::-1].T # Does not work on numpypy yet
+ if arr.ndim == 1: # pragma: no cover
+ return arr[::-1].T
+ elif arr.ndim == 2: # Always the case here ...
+ return arr[:, ::-1].T
+ elif arr.ndim == 3: # pragma: no cover
+ return arr[:, :, ::-1].T
+ elif arr.ndim == 4: # pragma: no cover
+ return arr[:, :, :, ::-1].T
+
+ if len(shape) == 3 and isle and dtype.type == numpy.uint8:
+ b = n(array[0])
+ g = n(array[1])
+ r = n(array[2])
+ if shape[0] == 3:
+ return numpy.dstack((r, g, b))
+ elif shape[0] == 4:
+ a = n(array[3])
+ return numpy.dstack((r, g, b, a))
+ else: # pragma: no cover - we check this earlier
+ raise ValueError('Cannot handle images of shape %s' % shape)
+
+ # We need to copy because array does *not* own its memory
+ # after bitmap is freed.
+ a = n(array).copy()
+ return a
+
+ def set_image_data(self, array):
+
+ # Prepare array
+ assert isinstance(array, numpy.ndarray)
+ shape = array.shape
+ dtype = array.dtype
+ with self._fi as lib:
+ isle = lib.FreeImage_IsLittleEndian()
+
+ # Calculate shape and channels
+ r, c = shape[:2]
+ if len(shape) == 2:
+ n_channels = 1
+ w_shape = (c, r)
+ elif len(shape) == 3:
+ n_channels = shape[2]
+ w_shape = (n_channels, c, r)
+ else:
+ n_channels = shape[0]
+
+ def n(arr): # normalise to freeimage's in-memory format
+ return arr.T[:, ::-1]
+ wrapped_array = self._wrap_bitmap_bits_in_array(w_shape, dtype, True)
+ # swizzle the color components and flip the scanlines to go to
+ # FreeImage's BGR[A] and upside-down internal memory format
+ if len(shape) == 3:
+ R = array[:, :, 0]
+ G = array[:, :, 1]
+ B = array[:, :, 2]
+
+ if isle:
+ if dtype.type == numpy.uint8:
+ wrapped_array[0] = n(B)
+ wrapped_array[1] = n(G)
+ wrapped_array[2] = n(R)
+ elif dtype.type == numpy.uint16:
+ wrapped_array[0] = n(R)
+ wrapped_array[1] = n(G)
+ wrapped_array[2] = n(B)
+ #
+ if shape[2] == 4:
+ A = array[:, :, 3]
+ wrapped_array[3] = n(A)
+ else:
+ wrapped_array[:] = n(array)
+ if self._need_finish:
+ self._finish_wrapped_array(wrapped_array)
+
+ if len(shape) == 2 and dtype.type == numpy.uint8:
+ with self._fi as lib:
+ palette = lib.FreeImage_GetPalette(self._bitmap)
+ palette = ctypes.c_void_p(palette)
+ if not palette:
+ raise RuntimeError('Could not get image palette')
+ try:
+ palette_data = GREY_PALETTE.ctypes.data
+ except Exception: # pragma: no cover - IS_PYPY
+ palette_data = GREY_PALETTE.__array_interface__['data'][0]
+ ctypes.memmove(palette, palette_data, 1024)
+
+ def _wrap_bitmap_bits_in_array(self, shape, dtype, save):
+ """Return an ndarray view on the data in a FreeImage bitmap. Only
+ valid for as long as the bitmap is loaded (if single page) / locked
+ in memory (if multipage). This is used in loading data, but
+ also during saving, to prepare a strided numpy array buffer.
+
+ """
+ # Get bitmap info
+ with self._fi as lib:
+ pitch = lib.FreeImage_GetPitch(self._bitmap)
+ bits = lib.FreeImage_GetBits(self._bitmap)
+
+ # Get more info
+ height = shape[-1]
+ byte_size = height * pitch
+ itemsize = dtype.itemsize
+
+ # Get strides
+ if len(shape) == 3:
+ strides = (itemsize, shape[0]*itemsize, pitch)
+ else:
+ strides = (itemsize, pitch)
+
+ # Create numpy array and return
+ data = (ctypes.c_char*byte_size).from_address(bits)
+ try:
+ self._need_finish = False
+ if TEST_NUMPY_NO_STRIDES:
+ raise NotImplementedError()
+ return numpy.ndarray(shape, dtype=dtype, buffer=data,
+ strides=strides)
+ except NotImplementedError:
+ # IS_PYPY - not very efficient. We create a C-contiguous
+ # numpy array (because pypy does not support Fortran-order)
+ # and shape it such that the rest of the code can remain.
+ if save:
+ self._need_finish = True # Flag to use _finish_wrapped_array
+ return numpy.zeros(shape, dtype=dtype)
+ else:
+ bytes = binary_type(bytearray(data))
+ array = numpy.fromstring(bytes, dtype=dtype)
+ # Deal with strides
+ if len(shape) == 3:
+ array.shape = shape[2], strides[-1]/shape[0], shape[0]
+ array2 = array[:shape[2], :shape[1], :shape[0]]
+ array = numpy.zeros(shape, dtype=array.dtype)
+ for i in range(shape[0]):
+ array[i] = array2[:, :, i].T
+ else:
+ array.shape = shape[1], strides[-1]
+ array = array[:shape[1], :shape[0]].T
+ return array
+
+ def _finish_wrapped_array(self, array): # IS_PYPY
+ """ Hardcore way to inject numpy array in bitmap.
+ """
+ # Get bitmap info
+ with self._fi as lib:
+ pitch = lib.FreeImage_GetPitch(self._bitmap)
+ bits = lib.FreeImage_GetBits(self._bitmap)
+ bpp = lib.FreeImage_GetBPP(self._bitmap)
+ # Get channels and realwidth
+ nchannels = bpp // 8 // array.itemsize
+ realwidth = pitch // nchannels
+ # Apply padding for pitch if necessary
+ extra = realwidth - array.shape[-2]
+ assert extra >= 0 and extra < 10
+ # Make sort of Fortran, also take padding (i.e. pitch) into account
+ newshape = array.shape[-1], realwidth, nchannels
+ array2 = numpy.zeros(newshape, array.dtype)
+ if nchannels == 1:
+ array2[:, :array.shape[-2], 0] = array.T
+ else:
+ for i in range(nchannels):
+ array2[:, :array.shape[-2], i] = array[i, :, :].T
+ # copy data
+ data_ptr = array2.__array_interface__['data'][0]
+ ctypes.memmove(bits, data_ptr, array2.nbytes)
+ del array2
+
+ def _get_type_and_shape(self):
+ bitmap = self._bitmap
+
+ # Get info on bitmap
+ with self._fi as lib:
+ w = lib.FreeImage_GetWidth(bitmap)
+ h = lib.FreeImage_GetHeight(bitmap)
+ self._fi_type = fi_type = lib.FreeImage_GetImageType(bitmap)
+ if not fi_type:
+ raise ValueError('Unknown image pixel type')
+
+ # Determine required props for numpy array
+ bpp = None
+ dtype = FI_TYPES.dtypes[fi_type]
+
+ if fi_type == FI_TYPES.FIT_BITMAP:
+ with self._fi as lib:
+ bpp = lib.FreeImage_GetBPP(bitmap)
+ has_pallette = lib.FreeImage_GetColorsUsed(bitmap)
+ if has_pallette:
+ # Examine the palette. If it is grayscale, we return as such
+ if has_pallette == 256:
+ palette = lib.FreeImage_GetPalette(bitmap)
+ palette = ctypes.c_void_p(palette)
+ p = (ctypes.c_uint8*(256*4)).from_address(palette.value)
+ p = numpy.frombuffer(p, numpy.uint32)
+ if (GREY_PALETTE == p).all():
+ extra_dims = []
+ return numpy.dtype(dtype), extra_dims + [w, h], bpp
+ # Convert bitmap and call this method again
+ newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
+ newbitmap = ctypes.c_void_p(newbitmap)
+ self._set_bitmap(newbitmap)
+ return self._get_type_and_shape()
+ elif bpp == 8:
+ extra_dims = []
+ elif bpp == 24:
+ extra_dims = [3]
+ elif bpp == 32:
+ extra_dims = [4]
+ else: # pragma: no cover
+ #raise ValueError('Cannot convert %d BPP bitmap' % bpp)
+ # Convert bitmap and call this method again
+ newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)
+ newbitmap = ctypes.c_void_p(newbitmap)
+ self._set_bitmap(newbitmap)
+ return self._get_type_and_shape()
+ else:
+ extra_dims = FI_TYPES.extra_dims[fi_type]
+
+ # Return dtype and shape
+ return numpy.dtype(dtype), extra_dims + [w, h], bpp
+
+ def quantize(self, quantizer=0, palettesize=256):
+ """ Quantize the bitmap to make it 8-bit (paletted). Returns a new
+ FIBitmap object.
+ Only for 24 bit images.
+ """
+ with self._fi as lib:
+ # New bitmap
+ bitmap = lib.FreeImage_ColorQuantizeEx(self._bitmap, quantizer,
+ palettesize, 0, None)
+ bitmap = ctypes.c_void_p(bitmap)
+
+ # Check and return
+ if not bitmap:
+ raise ValueError('Could not quantize bitmap "%s": %s' %
+ (self._filename,
+ self._fi._get_error_message()))
+ else:
+ new = FIBitmap(self._fi, self._filename, self._ftype,
+ self._flags)
+ new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
+ new._fi_type = self._fi_type
+ return new
+
+# def convert_to_32bit(self):
+# """ Convert to 32bit image.
+# """
+# with self._fi as lib:
+# # New bitmap
+# bitmap = lib.FreeImage_ConvertTo32Bits(self._bitmap)
+# bitmap = ctypes.c_void_p(bitmap)
+#
+# # Check and return
+# if not bitmap:
+# raise ValueError('Could not convert bitmap to 32bit "%s": %s' %
+# (self._filename,
+# self._fi._get_error_message()))
+# else:
+# new = FIBitmap(self._fi, self._filename, self._ftype,
+# self._flags)
+# new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))
+# new._fi_type = self._fi_type
+# return new
+
+
+class FIMultipageBitmap(FIBaseBitmap):
+ """ Wrapper for the multipage FI bitmap object.
+ """
+
+ def load_from_filename(self, filename=None):
+ if filename is None: # pragma: no cover
+ filename = self._filename
+
+ # Prepare
+ create_new = False
+ read_only = True
+ keep_cache_in_memory = False
+
+ # Try opening
+ with self._fi as lib:
+
+ # Create bitmap
+ multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype,
+ efn(filename),
+ create_new, read_only,
+ keep_cache_in_memory,
+ self._flags)
+ multibitmap = ctypes.c_void_p(multibitmap)
+
+ # Check
+ if not multibitmap: # pragma: no cover
+ err = self._fi._get_error_message()
+ raise ValueError('Could not open file "%s" as multi-image: %s'
+ % (self._filename, err))
+ else:
+ self._set_bitmap(multibitmap,
+ (lib.FreeImage_CloseMultiBitmap, multibitmap))
+
+# def load_from_bytes(self, bytes):
+# with self._fi as lib:
+# # Create bitmap
+# fimemory = lib.FreeImage_OpenMemory(
+# ctypes.c_char_p(bytes), len(bytes))
+# multibitmap = lib.FreeImage_LoadMultiBitmapFromMemory(
+# self._ftype, ctypes.c_void_p(fimemory), self._flags)
+# multibitmap = ctypes.c_void_p(multibitmap)
+# #lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))
+# self._mem = fimemory
+# self._bytes = bytes
+# # Check
+# if not multibitmap:
+# raise ValueError('Could not load multibitmap "%s": %s'
+# % (self._filename, self._fi._get_error_message()))
+# else:
+# self._set_bitmap(multibitmap,
+# (lib.FreeImage_CloseMultiBitmap, multibitmap))
+
+ def save_to_filename(self, filename=None):
+ if filename is None: # pragma: no cover
+ filename = self._filename
+
+ # Prepare
+ create_new = True
+ read_only = False
+ keep_cache_in_memory = False
+
+ # Open the file
+ # todo: Set flags at close func
+ with self._fi as lib:
+ multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype,
+ efn(filename),
+ create_new, read_only,
+ keep_cache_in_memory,
+ 0)
+ multibitmap = ctypes.c_void_p(multibitmap)
+
+ # Check
+ if not multibitmap: # pragma: no cover
+ msg = ('Could not open file "%s" for writing multi-image: %s'
+ % (self._filename, self._fi._get_error_message()))
+ raise ValueError(msg)
+ else:
+ self._set_bitmap(multibitmap,
+ (lib.FreeImage_CloseMultiBitmap, multibitmap))
+
+ def __len__(self):
+ with self._fi as lib:
+ return lib.FreeImage_GetPageCount(self._bitmap)
+
+ def get_page(self, index):
+ """ Return the sub-bitmap for the given page index.
+ Please close the returned bitmap when done.
+ """
+ with self._fi as lib:
+
+ # Create low-level bitmap in freeimage
+ bitmap = lib.FreeImage_LockPage(self._bitmap, index)
+ bitmap = ctypes.c_void_p(bitmap)
+ if not bitmap: # pragma: no cover
+ raise ValueError('Could not open sub-image %i in %r: %s' %
+ (index, self._filename,
+ self._fi._get_error_message()))
+
+ # Get bitmap object to wrap this bitmap
+ bm = FIBitmap(self._fi, self._filename, self._ftype, self._flags)
+ bm._set_bitmap(bitmap, (lib.FreeImage_UnlockPage, self._bitmap,
+ bitmap, False))
+ return bm
+
+ def append_bitmap(self, bitmap):
+ """ Add a sub-bitmap to the multi-page bitmap.
+ """
+ with self._fi as lib:
+ # no return value
+ lib.FreeImage_AppendPage(self._bitmap, bitmap._bitmap)
+
+
+# Create instance
+try:
+ fi = Freeimage()
+except OSError: # pragma: no cover
+ print('Warning: the freeimage wrapper of imageio could not be loaded:')
+ e_type, e_value, e_tb = sys.exc_info()
+ del e_tb
+ print(str(e_value))
+ fi = None
diff --git a/imageio/plugins/_swf.py b/imageio/plugins/_swf.py
new file mode 100644
index 0000000..430f0e7
--- /dev/null
+++ b/imageio/plugins/_swf.py
@@ -0,0 +1,925 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+# This code was taken from visvis/vvmovy/images2swf.py
+
+# styletest: ignore E261
+
+"""
+Provides a function (write_swf) to store a series of numpy arrays in an
+SWF movie, that can be played on a wide range of OS's.
+
+In desperation of wanting to share animated images, and then lacking a good
+writer for animated gif or .avi, I decided to look into SWF. This format
+is very well documented.
+
+This is a pure python module to create an SWF file that shows a series
+of images. The images are stored using the DEFLATE algorithm (same as
+PNG and ZIP and which is included in the standard Python distribution).
+As this compression algorithm is much more effective than that used in
+GIF images, we obtain better quality (24 bit colors + alpha channel)
+while still producesing smaller files (a test showed ~75%). Although
+SWF also allows for JPEG compression, doing so would probably require
+a third party library for the JPEG encoding/decoding, we could
+potentially do this via freeimage.
+
+This module requires Python 2.x / 3,x and numpy.
+
+sources and tools:
+
+- SWF on wikipedia
+- Adobes "SWF File Format Specification" version 10
+ (http://www.adobe.com/devnet/swf/pdf/swf_file_format_spec_v10.pdf)
+- swftools (swfdump in specific) for debugging
+- iwisoft swf2avi can be used to convert swf to avi/mpg/flv with really
+ good quality, while file size is reduced with factors 20-100.
+ A good program in my opinion. The free version has the limitation
+ of a watermark in the upper left corner.
+
+"""
+
+import os
+import sys
+import zlib
+import time # noqa
+
+import numpy as np
+
+from imageio.core import string_types, binary_type
+
+PY3 = sys.version_info >= (3, )
+
+# todo: use FreeImage to support reading JPEG images from SWF?
+
+
+## Base functions and classes
+
+
+class BitArray:
+ """ Dynamic array of bits that automatically resizes
+ with factors of two.
+ Append bits using .append() or +=
+ You can reverse bits using .reverse()
+ """
+
+ def __init__(self, initvalue=None):
+ self.data = np.zeros((16,), dtype=np.uint8)
+ self._len = 0
+ if initvalue is not None:
+ self.append(initvalue)
+
+ def __len__(self):
+ return self._len # self.data.shape[0]
+
+ def __repr__(self):
+ return self.data[:self._len].tostring().decode('ascii')
+
+ def _checkSize(self):
+ # check length... grow if necessary
+ arraylen = self.data.shape[0]
+ if self._len >= arraylen:
+ tmp = np.zeros((arraylen*2,), dtype=np.uint8)
+ tmp[:self._len] = self.data[:self._len]
+ self.data = tmp
+
+ def __add__(self, value):
+ self.append(value)
+ return self
+
+ def append(self, bits):
+
+ # check input
+ if isinstance(bits, BitArray):
+ bits = str(bits)
+ if isinstance(bits, int): # pragma: no cover - we dont use it
+ bits = str(bits)
+ if not isinstance(bits, string_types): # pragma: no cover
+ raise ValueError("Append bits as strings or integers!")
+
+ # add bits
+ for bit in bits:
+ self.data[self._len] = ord(bit)
+ self._len += 1
+ self._checkSize()
+
+ def reverse(self):
+ """ In-place reverse. """
+ tmp = self.data[:self._len].copy()
+ self.data[:self._len] = tmp[::-1]
+
+ def tobytes(self):
+ """ Convert to bytes. If necessary,
+ zeros are padded to the end (right side).
+ """
+ bits = str(self)
+
+ # determine number of bytes
+ nbytes = 0
+ while nbytes * 8 < len(bits):
+ nbytes += 1
+ # pad
+ bits = bits.ljust(nbytes * 8, '0')
+
+ # go from bits to bytes
+ bb = binary_type()
+ for i in range(nbytes):
+ tmp = int(bits[i * 8: (i + 1) * 8], 2)
+ bb += int2uint8(tmp)
+
+ # done
+ return bb
+
+
+if PY3:
+ def int2uint32(i):
+ return int(i).to_bytes(4, 'little')
+
+ def int2uint16(i):
+ return int(i).to_bytes(2, 'little')
+
+ def int2uint8(i):
+ return int(i).to_bytes(1, 'little')
+else: # pragma: no cover
+ def int2uint32(i):
+ number = int(i)
+ n1, n2, n3, n4 = 1, 256, 256 * 256, 256 * 256 * 256 # noqa
+ b4, number = number // n4, number % n4
+ b3, number = number // n3, number % n3
+ b2, number = number // n2, number % n2
+ b1 = number
+ return chr(b1) + chr(b2) + chr(b3) + chr(b4)
+
+ def int2uint16(i):
+ i = int(i)
+ # devide in two parts (bytes)
+ i1 = i % 256
+ i2 = int(i // 256)
+ # make string (little endian)
+ return chr(i1) + chr(i2)
+
+ def int2uint8(i):
+ return chr(int(i))
+
+
+def int2bits(i, n=None):
+ """ convert int to a string of bits (0's and 1's in a string),
+ pad to n elements. Convert back using int(ss,2). """
+ ii = i
+
+ # make bits
+ bb = BitArray()
+ while ii > 0:
+ bb += str(ii % 2)
+ ii = ii >> 1
+ bb.reverse()
+
+ # justify
+ if n is not None:
+ if len(bb) > n: # pragma: no cover
+ raise ValueError("int2bits fail: len larger than padlength.")
+ bb = str(bb).rjust(n, '0')
+
+ # done
+ return BitArray(bb)
+
+
+def bits2int(bb, n=8):
+ # Init
+ value = ''
+
+ # Get value in bits
+ for i in range(len(bb)):
+ b = bb[i:i+1]
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8, '0') + value
+
+ # Make decimal
+ return(int(value[:n], 2))
+
+
+def get_type_and_len(bb):
+ """ bb should be 6 bytes at least
+ Return (type, length, length_of_full_tag)
+ """
+ # Init
+ value = ''
+
+ # Get first 16 bits
+ for i in range(2):
+ b = bb[i:i + 1]
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8, '0') + value
+
+ # Get type and length
+ type = int(value[:10], 2)
+ L = int(value[10:], 2)
+ L2 = L + 2
+
+ # Long tag header?
+ if L == 63: # '111111'
+ value = ''
+ for i in range(2, 6):
+ b = bb[i:i + 1] # becomes a single-byte bytes() on both PY3 & PY2
+ tmp = bin(ord(b))[2:]
+ #value += tmp.rjust(8,'0')
+ value = tmp.rjust(8, '0') + value
+ L = int(value, 2)
+ L2 = L + 6
+
+ # Done
+ return type, L, L2
+
+
+def signedint2bits(i, n=None):
+ """ convert signed int to a string of bits (0's and 1's in a string),
+ pad to n elements. Negative numbers are stored in 2's complement bit
+ patterns, thus positive numbers always start with a 0.
+ """
+
+ # negative number?
+ ii = i
+ if i < 0:
+ # A negative number, -n, is represented as the bitwise opposite of
+ ii = abs(ii) - 1 # the positive-zero number n-1.
+
+ # make bits
+ bb = BitArray()
+ while ii > 0:
+ bb += str(ii % 2)
+ ii = ii >> 1
+ bb.reverse()
+
+ # justify
+ bb = '0' + str(bb) # always need the sign bit in front
+ if n is not None:
+ if len(bb) > n: # pragma: no cover
+ raise ValueError("signedint2bits fail: len larger than padlength.")
+ bb = bb.rjust(n, '0')
+
+ # was it negative? (then opposite bits)
+ if i < 0:
+ bb = bb.replace('0', 'x').replace('1', '0').replace('x', '1')
+
+ # done
+ return BitArray(bb)
+
+
+def twits2bits(arr):
+ """ Given a few (signed) numbers, store them
+ as compactly as possible in the wat specifief by the swf format.
+ The numbers are multiplied by 20, assuming they
+ are twits.
+ Can be used to make the RECT record.
+ """
+
+ # first determine length using non justified bit strings
+ maxlen = 1
+ for i in arr:
+ tmp = len(signedint2bits(i*20))
+ if tmp > maxlen:
+ maxlen = tmp
+
+ # build array
+ bits = int2bits(maxlen, 5)
+ for i in arr:
+ bits += signedint2bits(i * 20, maxlen)
+
+ return bits
+
+
+def floats2bits(arr):
+ """ Given a few (signed) numbers, convert them to bits,
+ stored as FB (float bit values). We always use 16.16.
+ Negative numbers are not (yet) possible, because I don't
+ know how the're implemented (ambiguity).
+ """
+ bits = int2bits(31, 5) # 32 does not fit in 5 bits!
+ for i in arr:
+ if i < 0: # pragma: no cover
+ raise ValueError("Dit not implement negative floats!")
+ i1 = int(i)
+ i2 = i - i1
+ bits += int2bits(i1, 15)
+ bits += int2bits(i2 * 2 ** 16, 16)
+ return bits
+
+
+## Base Tag
+
+class Tag:
+
+ def __init__(self):
+ self.bytes = binary_type()
+ self.tagtype = -1
+
+ def process_tag(self):
+ """ Implement this to create the tag. """
+ raise NotImplementedError()
+
+ def get_tag(self):
+ """ Calls processTag and attaches the header. """
+ self.process_tag()
+
+ # tag to binary
+ bits = int2bits(self.tagtype, 10)
+
+ # complete header uint16 thing
+ bits += '1' * 6 # = 63 = 0x3f
+ # make uint16
+ bb = int2uint16(int(str(bits), 2))
+
+ # now add 32bit length descriptor
+ bb += int2uint32(len(self.bytes))
+
+ # done, attach and return
+ bb += self.bytes
+ return bb
+
+ def make_rect_record(self, xmin, xmax, ymin, ymax):
+ """ Simply uses makeCompactArray to produce
+ a RECT Record. """
+ return twits2bits([xmin, xmax, ymin, ymax])
+
+ def make_matrix_record(self, scale_xy=None, rot_xy=None, trans_xy=None):
+
+ # empty matrix?
+ if scale_xy is None and rot_xy is None and trans_xy is None:
+ return "0"*8
+
+ # init
+ bits = BitArray()
+
+ # scale
+ if scale_xy:
+ bits += '1'
+ bits += floats2bits([scale_xy[0], scale_xy[1]])
+ else:
+ bits += '0'
+
+ # rotation
+ if rot_xy:
+ bits += '1'
+ bits += floats2bits([rot_xy[0], rot_xy[1]])
+ else:
+ bits += '0'
+
+ # translation (no flag here)
+ if trans_xy:
+ bits += twits2bits([trans_xy[0], trans_xy[1]])
+ else:
+ bits += twits2bits([0, 0])
+
+ # done
+ return bits
+
+
+## Control tags
+
+class ControlTag(Tag):
+ def __init__(self):
+ Tag.__init__(self)
+
+
+class FileAttributesTag(ControlTag):
+ def __init__(self):
+ ControlTag.__init__(self)
+ self.tagtype = 69
+
+ def process_tag(self):
+ self.bytes = '\x00'.encode('ascii') * (1+3)
+
+
+class ShowFrameTag(ControlTag):
+ def __init__(self):
+ ControlTag.__init__(self)
+ self.tagtype = 1
+
+ def process_tag(self):
+ self.bytes = binary_type()
+
+
+class SetBackgroundTag(ControlTag):
+ """ Set the color in 0-255, or 0-1 (if floats given). """
+ def __init__(self, *rgb):
+ self.tagtype = 9
+ if len(rgb) == 1:
+ rgb = rgb[0]
+ self.rgb = rgb
+
+ def process_tag(self):
+ bb = binary_type()
+ for i in range(3):
+ clr = self.rgb[i]
+ if isinstance(clr, float): # pragma: no cover - not used
+ clr = clr * 255
+ bb += int2uint8(clr)
+ self.bytes = bb
+
+
+class DoActionTag(Tag):
+ def __init__(self, action='stop'):
+ Tag.__init__(self)
+ self.tagtype = 12
+ self.actions = [action]
+
+ def append(self, action): # pragma: no cover - not used
+ self.actions.append(action)
+
+ def process_tag(self):
+ bb = binary_type()
+
+ for action in self.actions:
+ action = action.lower()
+ if action == 'stop':
+ bb += '\x07'.encode('ascii')
+ elif action == 'play': # pragma: no cover - not used
+ bb += '\x06'.encode('ascii')
+ else: # pragma: no cover
+ print("warning, unkown action: %s" % action)
+
+ bb += int2uint8(0)
+ self.bytes = bb
+
+
+## Definition tags
+class DefinitionTag(Tag):
+ counter = 0 # to give automatically id's
+
+ def __init__(self):
+ Tag.__init__(self)
+ DefinitionTag.counter += 1
+ self.id = DefinitionTag.counter # id in dictionary
+
+
+class BitmapTag(DefinitionTag):
+
+ def __init__(self, im):
+ DefinitionTag.__init__(self)
+ self.tagtype = 36 # DefineBitsLossless2
+
+ # convert image (note that format is ARGB)
+ # even a grayscale image is stored in ARGB, nevertheless,
+ # the fabilous deflate compression will make it that not much
+ # more data is required for storing (25% or so, and less than 10%
+ # when storing RGB as ARGB).
+
+ if len(im.shape) == 3:
+ if im.shape[2] in [3, 4]:
+ tmp = np.ones((im.shape[0], im.shape[1], 4),
+ dtype=np.uint8) * 255
+ for i in range(3):
+ tmp[:, :, i + 1] = im[:, :, i]
+ if im.shape[2] == 4:
+ tmp[:, :, 0] = im[:, :, 3] # swap channel where alpha is
+ else: # pragma: no cover
+ raise ValueError("Invalid shape to be an image.")
+
+ elif len(im.shape) == 2:
+ tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8)*255
+ for i in range(3):
+ tmp[:, :, i + 1] = im[:, :]
+ else: # pragma: no cover
+ raise ValueError("Invalid shape to be an image.")
+
+ # we changed the image to uint8 4 channels.
+ # now compress!
+ self._data = zlib.compress(tmp.tostring(), zlib.DEFLATED)
+ self.imshape = im.shape
+
+ def process_tag(self):
+
+ # build tag
+ bb = binary_type()
+ bb += int2uint16(self.id) # CharacterID
+ bb += int2uint8(5) # BitmapFormat
+ bb += int2uint16(self.imshape[1]) # BitmapWidth
+ bb += int2uint16(self.imshape[0]) # BitmapHeight
+ bb += self._data # ZlibBitmapData
+
+ self.bytes = bb
+
+
+class PlaceObjectTag(ControlTag):
+ def __init__(self, depth, idToPlace=None, xy=(0, 0), move=False):
+ ControlTag.__init__(self)
+ self.tagtype = 26
+ self.depth = depth
+ self.idToPlace = idToPlace
+ self.xy = xy
+ self.move = move
+
+ def process_tag(self):
+ # retrieve stuff
+ depth = self.depth
+ xy = self.xy
+ id = self.idToPlace
+
+ # build PlaceObject2
+ bb = binary_type()
+ if self.move:
+ bb += '\x07'.encode('ascii')
+ else:
+ # (8 bit flags): 4:matrix, 2:character, 1:move
+ bb += '\x06'.encode('ascii')
+ bb += int2uint16(depth) # Depth
+ bb += int2uint16(id) # character id
+ bb += self.make_matrix_record(trans_xy=xy).tobytes() # MATRIX record
+ self.bytes = bb
+
+
+class ShapeTag(DefinitionTag):
+ def __init__(self, bitmapId, xy, wh):
+ DefinitionTag.__init__(self)
+ self.tagtype = 2
+ self.bitmapId = bitmapId
+ self.xy = xy
+ self.wh = wh
+
+ def process_tag(self):
+ """ Returns a defineshape tag. with a bitmap fill """
+
+ bb = binary_type()
+ bb += int2uint16(self.id)
+ xy, wh = self.xy, self.wh
+ tmp = self.make_rect_record(xy[0], wh[0], xy[1], wh[1]) # ShapeBounds
+ bb += tmp.tobytes()
+
+ # make SHAPEWITHSTYLE structure
+
+ # first entry: FILLSTYLEARRAY with in it a single fill style
+ bb += int2uint8(1) # FillStyleCount
+ bb += '\x41'.encode('ascii') # FillStyleType (0x41 or 0x43 unsmoothed)
+ bb += int2uint16(self.bitmapId) # BitmapId
+ #bb += '\x00' # BitmapMatrix (empty matrix with leftover bits filled)
+ bb += self.make_matrix_record(scale_xy=(20, 20)).tobytes()
+
+# # first entry: FILLSTYLEARRAY with in it a single fill style
+# bb += int2uint8(1) # FillStyleCount
+# bb += '\x00' # solid fill
+# bb += '\x00\x00\xff' # color
+
+ # second entry: LINESTYLEARRAY with a single line style
+ bb += int2uint8(0) # LineStyleCount
+ #bb += int2uint16(0*20) # Width
+ #bb += '\x00\xff\x00' # Color
+
+ # third and fourth entry: NumFillBits and NumLineBits (4 bits each)
+ # I each give them four bits, so 16 styles possible.
+ bb += '\x44'.encode('ascii')
+
+ self.bytes = bb
+
+ # last entries: SHAPERECORDs ... (individual shape records not aligned)
+ # STYLECHANGERECORD
+ bits = BitArray()
+ bits += self.make_style_change_record(0, 1,
+ moveTo=(self.wh[0], self.wh[1]))
+ # STRAIGHTEDGERECORD 4x
+ bits += self.make_straight_edge_record(-self.wh[0], 0)
+ bits += self.make_straight_edge_record(0, -self.wh[1])
+ bits += self.make_straight_edge_record(self.wh[0], 0)
+ bits += self.make_straight_edge_record(0, self.wh[1])
+
+ # ENDSHAPRECORD
+ bits += self.make_end_shape_record()
+
+ self.bytes += bits.tobytes()
+
+ # done
+ #self.bytes = bb
+
+ def make_style_change_record(self, lineStyle=None, fillStyle=None,
+ moveTo=None):
+
+ # first 6 flags
+ # Note that we use FillStyle1. If we don't flash (at least 8) does not
+ # recognize the frames properly when importing to library.
+
+ bits = BitArray()
+ bits += '0' # TypeFlag (not an edge record)
+ bits += '0' # StateNewStyles (only for DefineShape2 and Defineshape3)
+ if lineStyle:
+ bits += '1' # StateLineStyle
+ else:
+ bits += '0'
+ if fillStyle:
+ bits += '1' # StateFillStyle1
+ else:
+ bits += '0'
+ bits += '0' # StateFillStyle0
+ if moveTo:
+ bits += '1' # StateMoveTo
+ else:
+ bits += '0'
+
+ # give information
+ # todo: nbits for fillStyle and lineStyle is hard coded.
+
+ if moveTo:
+ bits += twits2bits([moveTo[0], moveTo[1]])
+ if fillStyle:
+ bits += int2bits(fillStyle, 4)
+ if lineStyle:
+ bits += int2bits(lineStyle, 4)
+
+ return bits
+
+ def make_straight_edge_record(self, *dxdy):
+ if len(dxdy) == 1:
+ dxdy = dxdy[0]
+
+ # determine required number of bits
+ xbits = signedint2bits(dxdy[0] * 20)
+ ybits = signedint2bits(dxdy[1] * 20)
+ nbits = max([len(xbits), len(ybits)])
+
+ bits = BitArray()
+ bits += '11' # TypeFlag and StraightFlag
+ bits += int2bits(nbits-2, 4)
+ bits += '1' # GeneralLineFlag
+ bits += signedint2bits(dxdy[0] * 20, nbits)
+ bits += signedint2bits(dxdy[1] * 20, nbits)
+
+ # note: I do not make use of vertical/horizontal only lines...
+
+ return bits
+
+ def make_end_shape_record(self):
+ bits = BitArray()
+ bits += "0" # TypeFlag: no edge
+ bits += "0"*5 # EndOfShape
+ return bits
+
+
+def read_pixels(bb, i, tagType, L1):
+ """ With pf's seed after the recordheader, reads the pixeldata.
+ """
+
+ # Get info
+ charId = bb[i:i + 2] # noqa
+ i += 2
+ format = ord(bb[i:i + 1])
+ i += 1
+ width = bits2int(bb[i:i + 2], 16)
+ i += 2
+ height = bits2int(bb[i:i + 2], 16)
+ i += 2
+
+ # If we can, get pixeldata and make numpy array
+ if format != 5:
+ print("Can only read 24bit or 32bit RGB(A) lossless images.")
+ else:
+ # Read byte data
+ offset = 2 + 1 + 2 + 2 # all the info bits
+ bb2 = bb[i:i+(L1-offset)]
+
+ # Decompress and make numpy array
+ data = zlib.decompress(bb2)
+ a = np.frombuffer(data, dtype=np.uint8)
+
+ # Set shape
+ if tagType == 20:
+ # DefineBitsLossless - RGB data
+ try:
+ a.shape = height, width, 3
+ except Exception:
+ # Byte align stuff might cause troubles
+ print("Cannot read image due to byte alignment")
+ if tagType == 36:
+ # DefineBitsLossless2 - ARGB data
+ a.shape = height, width, 4
+ # Swap alpha channel to make RGBA
+ b = a
+ a = np.zeros_like(a)
+ a[:, :, 0] = b[:, :, 1]
+ a[:, :, 1] = b[:, :, 2]
+ a[:, :, 2] = b[:, :, 3]
+ a[:, :, 3] = b[:, :, 0]
+
+ return a
+
+
+## Last few functions
+
+
+# These are the original public functions, we don't use them, but we
+# keep it so that in principle this module can be used stand-alone.
+
+def checkImages(images): # pragma: no cover
+ """ checkImages(images)
+ Check numpy images and correct intensity range etc.
+ The same for all movie formats.
+ """
+ # Init results
+ images2 = []
+
+ for im in images:
+ if isinstance(im, np.ndarray):
+ # Check and convert dtype
+ if im.dtype == np.uint8:
+ images2.append(im) # Ok
+ elif im.dtype in [np.float32, np.float64]:
+ theMax = im.max()
+ if theMax > 128 and theMax < 300:
+ pass # assume 0:255
+ else:
+ im = im.copy()
+ im[im < 0] = 0
+ im[im > 1] = 1
+ im *= 255
+ images2.append(im.astype(np.uint8))
+ else:
+ im = im.astype(np.uint8)
+ images2.append(im)
+ # Check size
+ if im.ndim == 2:
+ pass # ok
+ elif im.ndim == 3:
+ if im.shape[2] not in [3, 4]:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('This array can not represent an image.')
+ else:
+ raise ValueError('Invalid image type: ' + str(type(im)))
+
+ # Done
+ return images2
+
+
+def build_file(fp, taglist, nframes=1, framesize=(500, 500), fps=10,
+ version=8): # pragma: no cover
+ """ Give the given file (as bytes) a header. """
+
+ # compose header
+ bb = binary_type()
+ bb += 'F'.encode('ascii') # uncompressed
+ bb += 'WS'.encode('ascii') # signature bytes
+ bb += int2uint8(version) # version
+ bb += '0000'.encode('ascii') # FileLength (leave open for now)
+ bb += Tag().make_rect_record(0, framesize[0], 0, framesize[1]).tobytes()
+ bb += int2uint8(0) + int2uint8(fps) # FrameRate
+ bb += int2uint16(nframes)
+ fp.write(bb)
+
+ # produce all tags
+ for tag in taglist:
+ fp.write(tag.get_tag())
+
+ # finish with end tag
+ fp.write('\x00\x00'.encode('ascii'))
+
+ # set size
+ sze = fp.tell()
+ fp.seek(4)
+ fp.write(int2uint32(sze))
+
+
+def write_swf(filename, images, duration=0.1, repeat=True): # pragma: no cover
+ """Write an swf-file from the specified images. If repeat is False,
+ the movie is finished with a stop action. Duration may also
+ be a list with durations for each frame (note that the duration
+ for each frame is always an integer amount of the minimum duration.)
+
+ Images should be a list consisting numpy arrays with values between
+ 0 and 255 for integer types, and between 0 and 1 for float types.
+
+ """
+
+ # Check images
+ images2 = checkImages(images)
+
+ # Init
+ taglist = [FileAttributesTag(), SetBackgroundTag(0, 0, 0)]
+
+ # Check duration
+ if hasattr(duration, '__len__'):
+ if len(duration) == len(images2):
+ duration = [d for d in duration]
+ else:
+ raise ValueError("len(duration) doesn't match amount of images.")
+ else:
+ duration = [duration for im in images2]
+
+ # Build delays list
+ minDuration = float(min(duration))
+ delays = [round(d/minDuration) for d in duration]
+ delays = [max(1, int(d)) for d in delays]
+
+ # Get FPS
+ fps = 1.0/minDuration
+
+ # Produce series of tags for each image
+ #t0 = time.time()
+ nframes = 0
+ for im in images2:
+ bm = BitmapTag(im)
+ wh = (im.shape[1], im.shape[0])
+ sh = ShapeTag(bm.id, (0, 0), wh)
+ po = PlaceObjectTag(1, sh.id, move=nframes > 0)
+ taglist.extend([bm, sh, po])
+ for i in range(delays[nframes]):
+ taglist.append(ShowFrameTag())
+ nframes += 1
+
+ if not repeat:
+ taglist.append(DoActionTag('stop'))
+
+ # Build file
+ #t1 = time.time()
+ fp = open(filename, 'wb')
+ try:
+ build_file(fp, taglist, nframes=nframes, framesize=wh, fps=fps)
+ except Exception:
+ raise
+ finally:
+ fp.close()
+ #t2 = time.time()
+
+ #print("Writing SWF took %1.2f and %1.2f seconds" % (t1-t0, t2-t1) )
+
+
+def read_swf(filename): # pragma: no cover
+ """Read all images from an SWF (shockwave flash) file. Returns a list
+ of numpy arrays.
+
+ Limitation: only read the PNG encoded images (not the JPG encoded ones).
+ """
+
+ # Check whether it exists
+ if not os.path.isfile(filename):
+ raise IOError('File not found: '+str(filename))
+
+ # Init images
+ images = []
+
+ # Open file and read all
+ fp = open(filename, 'rb')
+ bb = fp.read()
+
+ try:
+ # Check opening tag
+ tmp = bb[0:3].decode('ascii', 'ignore')
+ if tmp.upper() == 'FWS':
+ pass # ok
+ elif tmp.upper() == 'CWS':
+ # Decompress movie
+ bb = bb[:8] + zlib.decompress(bb[8:])
+ else:
+ raise IOError('Not a valid SWF file: ' + str(filename))
+
+ # Set filepointer at first tag (skipping framesize RECT and two uin16's
+ i = 8
+ nbits = bits2int(bb[i: i + 1], 5) # skip FrameSize
+ nbits = 5 + nbits * 4
+ Lrect = nbits / 8.0
+ if Lrect % 1:
+ Lrect += 1
+ Lrect = int(Lrect)
+ i += Lrect+4
+
+ # Iterate over the tags
+ counter = 0
+ while True:
+ counter += 1
+
+ # Get tag header
+ head = bb[i:i+6]
+ if not head:
+ break # Done (we missed end tag)
+
+ # Determine type and length
+ T, L1, L2 = get_type_and_len(head)
+ if not L2:
+ print('Invalid tag length, could not proceed')
+ break
+ #print(T, L2)
+
+ # Read image if we can
+ if T in [20, 36]:
+ im = read_pixels(bb, i+6, T, L1)
+ if im is not None:
+ images.append(im)
+ elif T in [6, 21, 35, 90]:
+ print('Ignoring JPEG image: cannot read JPEG.')
+ else:
+ pass # Not an image tag
+
+ # Detect end tag
+ if T == 0:
+ break
+
+ # Next tag!
+ i += L2
+
+ finally:
+ fp.close()
+
+ # Done
+ return images
+
+
+# Backward compatibility; same public names as when this was images2swf.
+writeSwf = write_swf
+readSwf = read_swf
diff --git a/imageio/plugins/avbin.py b/imageio/plugins/avbin.py
new file mode 100644
index 0000000..081d46f
--- /dev/null
+++ b/imageio/plugins/avbin.py
@@ -0,0 +1,442 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Plugin for reading videos via AvBin
+
+Would be nice if we could capture webcam with this, but unfortunately,
+avbin does not currently support this.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+import ctypes
+import sys
+
+from imageio import formats
+from imageio.core import Format, get_platform, get_remote_file
+
+
+AVBIN_RESULT_ERROR = -1
+AVBIN_RESULT_OK = 0
+#AVbinResult = ctypes.c_int
+
+
+def AVbinResult(x):
+ if x != AVBIN_RESULT_OK:
+ raise RuntimeError('AVBin returned error code %i' % x)
+ return x
+
+AVBIN_STREAM_TYPE_UNKNOWN = 0
+AVBIN_STREAM_TYPE_VIDEO = 1
+AVBIN_STREAM_TYPE_AUDIO = 2
+AVbinStreamType = ctypes.c_int
+
+AVBIN_SAMPLE_FORMAT_U8 = 0
+AVBIN_SAMPLE_FORMAT_S16 = 1
+AVBIN_SAMPLE_FORMAT_S24 = 2
+AVBIN_SAMPLE_FORMAT_S32 = 3
+AVBIN_SAMPLE_FORMAT_FLOAT = 4
+AVbinSampleFormat = ctypes.c_int
+
+AVBIN_LOG_QUIET = -8
+AVBIN_LOG_PANIC = 0
+AVBIN_LOG_FATAL = 8
+AVBIN_LOG_ERROR = 16
+AVBIN_LOG_WARNING = 24
+AVBIN_LOG_INFO = 32
+AVBIN_LOG_VERBOSE = 40
+AVBIN_LOG_DEBUG = 48
+AVbinLogLevel = ctypes.c_int
+
+AVbinFileP = ctypes.c_void_p
+AVbinStreamP = ctypes.c_void_p
+
+Timestamp = ctypes.c_int64
+
+
+class AVbinFileInfo(ctypes.Structure):
+ _fields_ = [
+ ('structure_size', ctypes.c_size_t),
+ ('n_streams', ctypes.c_int),
+ ('start_time', Timestamp),
+ ('duration', Timestamp),
+ ('title', ctypes.c_char * 512),
+ ('author', ctypes.c_char * 512),
+ ('copyright', ctypes.c_char * 512),
+ ('comment', ctypes.c_char * 512),
+ ('album', ctypes.c_char * 512),
+ ('year', ctypes.c_int),
+ ('track', ctypes.c_int),
+ ('genre', ctypes.c_char * 32),
+ ]
+
+
+class _AVbinStreamInfoVideo8(ctypes.Structure):
+ _fields_ = [
+ ('width', ctypes.c_uint),
+ ('height', ctypes.c_uint),
+ ('sample_aspect_num', ctypes.c_uint),
+ ('sample_aspect_den', ctypes.c_uint),
+ ('frame_rate_num', ctypes.c_uint),
+ ('frame_rate_den', ctypes.c_uint),
+ ]
+
+
+class _AVbinStreamInfoAudio8(ctypes.Structure):
+ _fields_ = [
+ ('sample_format', ctypes.c_int),
+ ('sample_rate', ctypes.c_uint),
+ ('sample_bits', ctypes.c_uint),
+ ('channels', ctypes.c_uint),
+ ]
+
+
+class _AVbinStreamInfoUnion8(ctypes.Union):
+ _fields_ = [
+ ('video', _AVbinStreamInfoVideo8),
+ ('audio', _AVbinStreamInfoAudio8),
+ ]
+
+
+class AVbinStreamInfo8(ctypes.Structure):
+ _fields_ = [
+ ('structure_size', ctypes.c_size_t),
+ ('type', ctypes.c_int),
+ ('u', _AVbinStreamInfoUnion8)
+ ]
+
+
+class AVbinPacket(ctypes.Structure):
+ _fields_ = [
+ ('structure_size', ctypes.c_size_t),
+ ('timestamp', Timestamp),
+ ('stream_index', ctypes.c_int),
+ ('data', ctypes.POINTER(ctypes.c_uint8)),
+ ('size', ctypes.c_size_t),
+ ]
+
+
+AVbinLogCallback = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int,
+ ctypes.c_char_p)
+
+
+def timestamp_from_avbin(timestamp):
+ return float(timestamp) / 1000000
+
+
+def get_avbin_lib():
+ """ Get avbin .dll/.dylib/.so
+ """
+
+ LIBRARIES = {
+ 'osx64': 'libavbin-11alpha4-osx.dylib',
+ 'win32': 'avbin-10-win32.dll',
+ 'win64': 'avbin-10-win64.dll',
+ 'linux32': 'libavbin-10-linux32.so',
+ 'linux64': 'libavbin-10-linux64.so',
+ }
+
+ platform = get_platform()
+
+ try:
+ lib = LIBRARIES[platform]
+ except KeyError: # pragma: no cover
+ raise RuntimeError('Avbin plugin is not supported on platform %s' %
+ platform)
+
+ return get_remote_file('avbin/' + lib)
+
+
+class AvBinFormat(Format):
+ """
+ The AvBinFormat uses the AvBin library (based on libav) to read
+ video files.
+
+ This plugin is more efficient than the ffmpeg plugin, because it
+ uses ctypes (rather than a pipe like the ffmpeg plugin does).
+ Further, it supports reading images into a given numpy array.
+
+ The limitations of this plugin are that seeking, writing and camera
+ feeds are not supported. See the ffmpeg format for these features.
+
+ Parameters for reading
+ ----------------------
+ loop : bool
+ If True, the video will rewind as soon as a frame is requested
+ beyond the last frame. Otherwise, IndexError is raised. Default False.
+ stream : int
+ Specifies which video stream to read. Default 0.
+ videoformat : str | None
+ Specifies the video format (e.g. 'avi', or 'mp4'). If this is None
+ (default) the format is auto-detected.
+
+ Parameters for get_data
+ -----------------------
+ out : np.ndarray
+ destination for the data retrieved. This can be used to save
+ time-consuming memory allocations when reading multiple image
+ sequntially. The shape of out must be (width, height, 3), the
+ dtype must be np.uint8 and it must be C-contiguous.
+
+ Use the create_empty_image() method of the reader object
+ to create an array that is suitable for get_data.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self._avbin = None
+ Format.__init__(self, *args, **kwargs)
+
+ def _can_read(self, request):
+ # This method is called when the format manager is searching
+ # for a format to read a certain image. Return True if this format
+ # can do it.
+ #
+ # The format manager is aware of the extensions and the modes
+ # that each format can handle. However, the ability to read a
+ # format could be more subtle. Also, the format would ideally
+ # check the request.firstbytes and look for a header of some
+ # kind. Further, the extension might not always be known.
+ #
+ # The request object has:
+ # request.filename: a representation of the source (only for reporing)
+ # request.firstbytes: the first 256 bytes of the file.
+ # request.mode[0]: read or write mode
+ # request.mode[1]: what kind of data the user expects: one of 'iIvV?'
+
+ if request.mode[1] in (self.modes + '?'):
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ def _can_save(self, request):
+ return False # AvBin does not support writing videos
+
+ def avbinlib(self, libpath=None):
+ if self._avbin is not None and libpath is None:
+ # Already loaded
+ return self._avbin
+
+ if libpath is None:
+ libpath = get_avbin_lib()
+
+ self._avbin = avbin = ctypes.cdll.LoadLibrary(libpath)
+
+ avbin.avbin_get_version.restype = ctypes.c_int
+ avbin.avbin_get_ffmpeg_revision.restype = ctypes.c_int
+ avbin.avbin_get_audio_buffer_size.restype = ctypes.c_size_t
+ avbin.avbin_have_feature.restype = ctypes.c_int
+ avbin.avbin_have_feature.argtypes = [ctypes.c_char_p]
+
+ avbin.avbin_init.restype = AVbinResult
+ avbin.avbin_set_log_level.restype = AVbinResult
+ avbin.avbin_set_log_level.argtypes = [AVbinLogLevel]
+ avbin.avbin_set_log_callback.argtypes = [AVbinLogCallback]
+
+ avbin.avbin_open_filename.restype = AVbinFileP
+ avbin.avbin_open_filename.argtypes = [ctypes.c_char_p]
+ avbin.avbin_open_filename_with_format.restype = AVbinFileP
+ avbin.avbin_open_filename_with_format.argtypes = [ctypes.c_char_p,
+ ctypes.c_char_p]
+ avbin.avbin_close_file.argtypes = [AVbinFileP]
+ avbin.avbin_seek_file.argtypes = [AVbinFileP, Timestamp]
+ avbin.avbin_file_info.argtypes = [AVbinFileP,
+ ctypes.POINTER(AVbinFileInfo)]
+ avbin.avbin_stream_info.argtypes = [AVbinFileP, ctypes.c_int,
+ ctypes.POINTER(AVbinStreamInfo8)]
+
+ avbin.avbin_open_stream.restype = ctypes.c_void_p
+ avbin.avbin_open_stream.argtypes = [AVbinFileP, ctypes.c_int]
+ avbin.avbin_close_stream.argtypes = [AVbinStreamP]
+
+ avbin.avbin_read.argtypes = [AVbinFileP, ctypes.POINTER(AVbinPacket)]
+ avbin.avbin_read.restype = AVbinResult
+ avbin.avbin_decode_audio.restype = ctypes.c_int
+ avbin.avbin_decode_audio.argtypes = [AVbinStreamP, ctypes.c_void_p,
+ ctypes.c_size_t, ctypes.c_void_p,
+ ctypes.POINTER(ctypes.c_int)]
+ avbin.avbin_decode_video.restype = ctypes.c_int
+ avbin.avbin_decode_video.argtypes = [AVbinStreamP, ctypes.c_void_p,
+ ctypes.c_size_t, ctypes.c_void_p]
+
+ avbin.avbin_init()
+ avbin.avbin_set_log_level(AVBIN_LOG_QUIET)
+
+ return self._avbin
+
+ # -- reader
+
+ class Reader(Format.Reader):
+
+ def _open(self, loop=False, stream=0, videoformat=None,
+ skipempty=False):
+
+ # Init args
+ self._arg_loop = bool(loop)
+ self._arg_stream = int(stream)
+ self._arg_videoformat = videoformat
+ self._arg_skipempty = bool(skipempty)
+
+ # Init other variables
+ self._filename = self.request.get_local_filename()
+ self._file = None
+ self._meta = {'plugin': 'avbin'}
+
+ self._init_video()
+
+ def _init_video(self):
+
+ avbin = self.format.avbinlib()
+ filename_bytes = self._filename.encode(sys.getfilesystemencoding())
+
+ # Open file
+ if self._arg_videoformat is not None:
+ self._file = avbin.avbin_open_filename_with_format(
+ filename_bytes, self._arg_videoformat.encode('ascii'))
+ else:
+ self._file = avbin.avbin_open_filename(filename_bytes)
+ if not self._file:
+ raise IOError('Could not open "%s"' % self._filename)
+
+ # Get info
+ self._info = AVbinFileInfo()
+ self._info.structure_size = ctypes.sizeof(self._info)
+ avbin.avbin_file_info(self._file, ctypes.byref(self._info))
+
+ # Store some info in meta dict
+ self._meta['avbin_version'] = str(avbin.avbin_get_version())
+ self._meta['title'] = self._info.title.decode('utf-8')
+ self._meta['author'] = self._info.author.decode('utf-8')
+ # The reported duration is different from what we get from ffmpeg,
+ # and using it as is will yielf a wrong nframes. We correct below
+ self._meta['duration'] = timestamp_from_avbin(self._info.duration)
+
+ # Parse through the available streams in the file and find
+ # the video stream specified by stream
+
+ video_stream_counter = 0
+
+ for i in range(self._info.n_streams):
+ info = AVbinStreamInfo8()
+ info.structure_size = ctypes.sizeof(info)
+ avbin.avbin_stream_info(self._file, i, info)
+
+ if info.type != AVBIN_STREAM_TYPE_VIDEO:
+ continue
+
+ if video_stream_counter != self._arg_stream:
+ video_stream_counter += 1
+ continue
+
+ # We have the n-th (n=stream number specified) video stream
+ self._stream = avbin.avbin_open_stream(self._file, i)
+
+ # Store info specific to this stream
+ self._stream_info = info
+ self._width = info.u.video.width
+ self._height = info.u.video.height
+ # Store meta info
+ self._meta['size'] = self._width, self._height
+ self._meta['source_size'] = self._width, self._height
+ self._meta['fps'] = (float(info.u.video.frame_rate_num) /
+ float(info.u.video.frame_rate_den))
+ self._meta['duration'] -= 1.0 / self._meta['fps'] # correct
+ self._meta['nframes'] = int(self._meta['duration'] *
+ self._meta['fps'])
+
+ self._stream_index = i
+ break
+ else:
+ raise IOError('Stream #%d not found in %r' %
+ (self._arg_stream, self._filename))
+
+ self._packet = AVbinPacket()
+ self._packet.structure_size = ctypes.sizeof(self._packet)
+
+ self._framecounter = 0
+
+ def _close(self):
+ if self._file is not None:
+ avbin = self.format.avbinlib()
+ avbin.avbin_close_file(self._file)
+ self._file = None
+
+ def _get_length(self):
+ # Return the number of images. Can be np.inf
+ # Note that nframes is an estimate that can be a few frames off
+ # for very large video files
+ return self._meta['nframes']
+
+ def create_empty_image(self):
+ return np.zeros((self._height, self._width, 3), dtype=np.uint8)
+
+ def _get_data(self, index, out=None):
+ avbin = self.format.avbinlib()
+
+ # Modulo index (for looping)
+ if self._meta['nframes'] and self._meta['nframes'] < float('inf'):
+ if self._arg_loop:
+ index = index % self._meta['nframes']
+
+ # Check index
+ if index < 0:
+ raise IndexError('Frame index must be > 0')
+ elif index >= self._meta['nframes']:
+ raise IndexError('Reached end of video')
+ elif index != self._framecounter:
+ if index == 0: # Rewind
+ self._close()
+ self._init_video()
+ return self._get_data(0)
+ raise IndexError('Avbin format cannot seek')
+
+ self._framecounter += 1
+
+ if out is None:
+ out = self.create_empty_image()
+
+ assert (out.dtype == np.uint8 and out.flags.c_contiguous and
+ out.shape == (self._height, self._width, 3))
+
+ # Read from the file until the next packet of our video
+ # stream is found
+ while True:
+ try:
+ avbin.avbin_read(self._file, ctypes.byref(self._packet))
+ except RuntimeError: # todo: I hope we can fix this ...
+ raise IndexError('Reached end of video too soon')
+ if self._packet.stream_index != self._stream_index:
+ continue
+
+ # Decode the image, storing data in the out array
+ try:
+ ptr = out.ctypes.data
+ except Exception: # pragma: no cover - IS_PYPY
+ ptr = out.__array_interface__['data'][0]
+ result = avbin.avbin_decode_video(self._stream,
+ self._packet.data,
+ self._packet.size,
+ ptr)
+
+ # Check for success. If not, continue reading the file stream
+ # AK: disabled for now, because this will make the file
+ # shorter; you're just dropping frames! We need to think
+ # of a better solution ...
+ if (not self._arg_skipempty) or result != -1:
+ break
+
+ # Return array and dummy meta data
+ return out, dict(timestamp=self._packet.timestamp)
+
+ def _get_meta_data(self, index):
+ return self._meta
+
+
+# Register. You register an *instance* of a Format class. Here specify:
+format = AvBinFormat('avbin', # short name
+ 'Many video formats (via AvBin, i.e. libav library)',
+ 'mov avi mp4 mpg mpeg', # list of extensions
+ 'I' # modes, characters in iIvV
+ )
+formats.add_format(format)
diff --git a/imageio/plugins/dicom.py b/imageio/plugins/dicom.py
new file mode 100644
index 0000000..12d162d
--- /dev/null
+++ b/imageio/plugins/dicom.py
@@ -0,0 +1,1097 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Plugin for reading DICOM files.
+"""
+
+# todo: Use pydicom:
+# * Note: is not py3k ready yet
+# * Allow reading the full meta info
+# I think we can more or less replace the SimpleDicomReader with a
+# pydicom.Dataset For series, only ned to read the full info from one
+# file: speed still high
+# * Perhaps allow writing?
+
+from __future__ import absolute_import, print_function, division
+
+import sys
+import os
+import struct
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format, BaseProgressIndicator, StdoutProgressIndicator
+from imageio.core import string_types, read_n_bytes
+
+
+# Determine endianity of system
+sys_is_little_endian = (sys.byteorder == 'little')
+
+
+class DicomFormat(Format):
+ """ A format for reading DICOM images: a common format used to store
+ medical image data, such as X-ray, CT and MRI.
+
+ This format borrows some code (and ideas) from the pydicom project,
+ and (to the best of our knowledge) has the same limitations as
+ pydicom with regard to the type of files that it can handle. However,
+ only a predefined subset of tags are extracted from the file. This allows
+ for great simplifications allowing us to make a stand-alone reader, and
+ also results in a much faster read time. We plan to allow reading all
+ tags in the future (by using pydicom).
+
+ This format provides functionality to group images of the same
+ series together, thus extracting volumes (and multiple volumes).
+ Using volread will attempt to yield a volume. If multiple volumes
+ are present, the first one is given. Using mimread will simply yield
+ all images in the given directory (not taking series into account).
+
+ Parameters for reading
+ ----------------------
+ progress : {True, False, BaseProgressIndicator}
+ Whether to show progress when reading from multiple files.
+ Default True. By passing an object that inherits from
+ BaseProgressIndicator, the way in which progress is reported
+ can be costumized.
+
+ """
+
+ def _can_read(self, request):
+ # If user URI was a directory, we check whether it has a DICOM file
+ if os.path.isdir(request.filename):
+ files = os.listdir(request.filename)
+ files.sort() # Make it consistent
+ if files:
+ with open(os.path.join(request.filename, files[0]), 'rb') as f:
+ first_bytes = read_n_bytes(f, 140)
+ return first_bytes[128:132] == b'DICM'
+ else:
+ return False
+ # Check
+ return request.firstbytes[128:132] == b'DICM'
+
+ def _can_save(self, request):
+ # We cannot save yet. May be possible if we will used pydicom as
+ # a backend.
+ return False
+
+ # --
+
+ class Reader(Format.Reader):
+
+ def _open(self, progress=True):
+
+ if os.path.isdir(self.request.filename):
+ # A dir can be given if the user used the format explicitly
+ self._info = {}
+ self._data = None
+ else:
+ # Read the given dataset now ...
+ dcm = SimpleDicomReader(self.request.get_file())
+ self._info = dcm._info
+ self._data = dcm.get_numpy_array()
+
+ # Initialize series, list of DicomSeries objects
+ self._series = None # only created if needed
+
+ # Set progress indicator
+ if isinstance(progress, BaseProgressIndicator):
+ self._progressIndicator = progress
+ elif progress is True:
+ p = StdoutProgressIndicator('Reading DICOM')
+ self._progressIndicator = p
+ elif progress in (None, False):
+ self._progressIndicator = BaseProgressIndicator('Dummy')
+ else:
+ raise ValueError('Invalid value for progress.')
+
+ def _close(self):
+ # Clean up
+ self._info = None
+ self._data = None
+ self._series = None
+
+ @property
+ def series(self):
+ if self._series is None:
+ self._series = process_directory(self.request,
+ self._progressIndicator)
+ return self._series
+
+ def _get_length(self):
+ if self._data is None:
+ dcm = self.series[0][0]
+ self._info = dcm._info
+ self._data = dcm.get_numpy_array()
+
+ nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
+
+ if self.request.mode[1] == 'i':
+ # User expects one, but lets be honest about this file
+ return nslices
+ elif self.request.mode[1] == 'I':
+ # User expects multiple, if this file has multiple slices, ok.
+ # Otherwise we have to check the series.
+ if nslices > 1:
+ return nslices
+ else:
+ return sum([len(serie) for serie in self.series])
+ elif self.request.mode[1] == 'v':
+ # User expects a volume, if this file has one, ok.
+ # Otherwise we have to check the series
+ if nslices > 1:
+ return 1
+ else:
+ return len(self.series) # We assume one volume per series
+ elif self.request.mode[1] == 'V':
+ # User expects multiple volumes. We have to check the series
+ return len(self.series) # We assume one volume per series
+ else:
+ raise RuntimeError('DICOM plugin should know what to expect.')
+
+ def _get_data(self, index):
+ if self._data is None:
+ dcm = self.series[0][0]
+ self._info = dcm._info
+ self._data = dcm.get_numpy_array()
+
+ nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
+
+ if self.request.mode[1] == 'i':
+ # Allow index >1 only if this file contains >1
+ if nslices > 1:
+ return self._data[index], self._info
+ elif index == 0:
+ return self._data, self._info
+ else:
+ raise IndexError('Dicom file contains only one slice.')
+ elif self.request.mode[1] == 'I':
+ # Return slice from volume, or return item from series
+ if index == 0 and nslices > 1:
+ return self._data[index], self._info
+ else:
+ L = []
+ for serie in self.series:
+ L.extend([dcm_ for dcm_ in serie])
+ return L[index].get_numpy_array(), L[index].info
+ elif self.request.mode[1] in 'vV':
+ # Return volume or series
+ if index == 0 and nslices > 1:
+ return self._data, self._info
+ else:
+ return (self.series[index].get_numpy_array(),
+ self.series[index].info)
+ else: # pragma: no cover
+ raise ValueError('DICOM plugin should know what to expect.')
+
+ def _get_meta_data(self, index):
+ if self._data is None:
+ dcm = self.series[0][0]
+ self._info = dcm._info
+ self._data = dcm.get_numpy_array()
+
+ nslices = self._data.shape[0] if (self._data.ndim == 3) else 1
+
+ # Default is the meta data of the given file, or the "first" file.
+ if index is None:
+ return self._info
+
+ if self.request.mode[1] == 'i':
+ return self._info
+ elif self.request.mode[1] == 'I':
+ # Return slice from volume, or return item from series
+ if index == 0 and nslices > 1:
+ return self._info
+ else:
+ L = []
+ for serie in self.series:
+ L.extend([dcm_ for dcm_ in serie])
+ return L[index].info
+ elif self.request.mode[1] in 'vV':
+ # Return volume or series
+ if index == 0 and nslices > 1:
+ return self._info
+ else:
+ return self.series[index].info
+ else: # pragma: no cover
+ raise ValueError('DICOM plugin should know what to expect.')
+
+
+# Add this format
+formats.add_format(DicomFormat(
+ 'DICOM',
+ 'Digital Imaging and Communications in Medicine',
+ '.dcm .ct .mri', 'iIvV')) # Often DICOM files have weird or no extensions
+
+
+# Define a dictionary that contains the tags that we would like to know
+MINIDICT = {
+ (0x7FE0, 0x0010): ('PixelData', 'OB'),
+ # Date and time
+ (0x0008, 0x0020): ('StudyDate', 'DA'),
+ (0x0008, 0x0021): ('SeriesDate', 'DA'),
+ (0x0008, 0x0022): ('AcquisitionDate', 'DA'),
+ (0x0008, 0x0023): ('ContentDate', 'DA'),
+ (0x0008, 0x0030): ('StudyTime', 'TM'),
+ (0x0008, 0x0031): ('SeriesTime', 'TM'),
+ (0x0008, 0x0032): ('AcquisitionTime', 'TM'),
+ (0x0008, 0x0033): ('ContentTime', 'TM'),
+ # With what, where, by whom?
+ (0x0008, 0x0060): ('Modality', 'CS'),
+ (0x0008, 0x0070): ('Manufacturer', 'LO'),
+ (0x0008, 0x0080): ('InstitutionName', 'LO'),
+ # Descriptions
+ (0x0008, 0x1030): ('StudyDescription', 'LO'),
+ (0x0008, 0x103E): ('SeriesDescription', 'LO'),
+ # UID's
+ (0x0020, 0x0016): ('SOPClassUID', 'UI'),
+ (0x0020, 0x0018): ('SOPInstanceUID', 'UI'),
+ (0x0020, 0x000D): ('StudyInstanceUID', 'UI'),
+ (0x0020, 0x000E): ('SeriesInstanceUID', 'UI'),
+ (0x0008, 0x0117): ('ContextUID', 'UI'),
+ # Numbers
+ (0x0020, 0x0011): ('SeriesNumber', 'IS'),
+ (0x0020, 0x0012): ('AcquisitionNumber', 'IS'),
+ (0x0020, 0x0013): ('InstanceNumber', 'IS'),
+ (0x0020, 0x0014): ('IsotopeNumber', 'IS'),
+ (0x0020, 0x0015): ('PhaseNumber', 'IS'),
+ (0x0020, 0x0016): ('IntervalNumber', 'IS'),
+ (0x0020, 0x0017): ('TimeSlotNumber', 'IS'),
+ (0x0020, 0x0018): ('AngleNumber', 'IS'),
+ (0x0020, 0x0019): ('ItemNumber', 'IS'),
+ (0x0020, 0x0020): ('PatientOrientation', 'CS'),
+ (0x0020, 0x0030): ('ImagePosition', 'CS'),
+ (0x0020, 0x0032): ('ImagePositionPatient', 'CS'),
+ (0x0020, 0x0035): ('ImageOrientation', 'CS'),
+ (0x0020, 0x0037): ('ImageOrientationPatient', 'CS'),
+ # Patient infotmation
+ (0x0010, 0x0010): ('PatientName', 'PN'),
+ (0x0010, 0x0020): ('PatientID', 'LO'),
+ (0x0010, 0x0030): ('PatientBirthDate', 'DA'),
+ (0x0010, 0x0040): ('PatientSex', 'CS'),
+ (0x0010, 0x1010): ('PatientAge', 'AS'),
+ (0x0010, 0x1020): ('PatientSize', 'DS'),
+ (0x0010, 0x1030): ('PatientWeight', 'DS'),
+ # Image specific (required to construct numpy array)
+ (0x0028, 0x0002): ('SamplesPerPixel', 'US'),
+ (0x0028, 0x0008): ('NumberOfFrames', 'IS'),
+ (0x0028, 0x0100): ('BitsAllocated', 'US'),
+ (0x0028, 0x0101): ('BitsStored', 'US'),
+ (0x0028, 0x0102): ('HighBit', 'US'),
+ (0x0028, 0x0103): ('PixelRepresentation', 'US'),
+ (0x0028, 0x0010): ('Rows', 'US'),
+ (0x0028, 0x0011): ('Columns', 'US'),
+ (0x0028, 0x1052): ('RescaleIntercept', 'DS'),
+ (0x0028, 0x1053): ('RescaleSlope', 'DS'),
+ # Image specific (for the user)
+ (0x0028, 0x0030): ('PixelSpacing', 'DS'),
+ (0x0018, 0x0088): ('SliceSpacing', 'DS'),
+}
+
+# Define some special tags:
+# See PS 3.5-2008 section 7.5 (p.40)
+ItemTag = (0xFFFE, 0xE000) # start of Sequence Item
+ItemDelimiterTag = (0xFFFE, 0xE00D) # end of Sequence Item
+SequenceDelimiterTag = (0xFFFE, 0xE0DD) # end of Sequence of undefined length
+
+# Define set of groups that we're interested in (so we can quickly skip others)
+GROUPS = set([key[0] for key in MINIDICT.keys()])
+VRS = set([val[1] for val in MINIDICT.values()])
+
+
+class NotADicomFile(Exception):
+ pass
+
+
+class SimpleDicomReader(object):
+ """
+ This class provides reading of pixel data from DICOM files. It is
+ focussed on getting the pixel data, not the meta info.
+
+ To use, first create an instance of this class (giving it
+ a file object or filename). Next use the info attribute to
+ get a dict of the meta data. The loading of pixel data is
+ deferred until get_numpy_array() is called.
+
+ Comparison with Pydicom
+ -----------------------
+
+ This code focusses on getting the pixel data out, which allows some
+ shortcuts, resulting in the code being much smaller.
+
+ Since the processing of data elements is much cheaper (it skips a lot
+ of tags), this code is about 3x faster than pydicom (except for the
+ deflated DICOM files).
+
+ This class does borrow some code (and ideas) from the pydicom
+ project, and (to the best of our knowledge) has the same limitations
+ as pydicom with regard to the type of files that it can handle.
+
+ Limitations
+ -----------
+
+ For more advanced DICOM processing, please check out pydicom.
+
+ * Only a predefined subset of data elements (meta information) is read.
+ * This is a reader; it can not write DICOM files.
+ * (just like pydicom) it can handle none of the compressed DICOM
+ formats except for "Deflated Explicit VR Little Endian"
+ (1.2.840.10008.1.2.1.99).
+
+ """
+
+ def __init__(self, file):
+ # Open file if filename given
+ if isinstance(file, string_types):
+ self._filename = file
+ self._file = open(file, 'rb')
+ else:
+ self._filename = '<unknown file>'
+ self._file = file
+ # Init variable to store position and size of pixel data
+ self._pixel_data_loc = None
+ # The meta header is always explicit and little endian
+ self.is_implicit_VR = False
+ self.is_little_endian = True
+ self._unpackPrefix = '<'
+ # Dict to store data elements of interest in
+ self._info = {}
+ # VR Conversion
+ self._converters = {
+ # Numbers
+ 'US': lambda x: self._unpack('H', x),
+ 'UL': lambda x: self._unpack('L', x),
+ # Numbers encoded as strings
+ 'DS': lambda x: self._splitValues(x, float, '\\'),
+ 'IS': lambda x: self._splitValues(x, int, '\\'),
+ # strings
+ 'AS': lambda x: x.decode('ascii').strip('\x00'),
+ 'DA': lambda x: x.decode('ascii').strip('\x00'),
+ 'TM': lambda x: x.decode('ascii').strip('\x00'),
+ 'UI': lambda x: x.decode('ascii').strip('\x00'),
+ 'LO': lambda x: x.decode('utf-8').strip('\x00').rstrip(),
+ 'CS': lambda x: self._splitValues(x, float, '\\'),
+ 'PN': lambda x: x.decode('utf-8').strip('\x00').rstrip(),
+ }
+
+ # Initiate reading
+ self._read()
+
+ @property
+ def info(self):
+ return self._info
+
+ def _splitValues(self, x, type, splitter):
+ s = x.decode('ascii').strip('\x00')
+ try:
+ if splitter in s:
+ return tuple([type(v) for v in s.split(splitter) if v.strip()])
+ else:
+ return type(s)
+ except ValueError:
+ return s
+
+ def _unpack(self, fmt, value):
+ return struct.unpack(self._unpackPrefix+fmt, value)[0]
+
+ # Really only so we need minimal changes to _pixel_data_numpy
+ def __iter__(self):
+ return iter(self._info.keys())
+
+ def __getattr__(self, key):
+ info = object.__getattribute__(self, '_info')
+ if key in info:
+ return info[key]
+ return object.__getattribute__(self, key) # pragma: no cover
+
+ def _read(self):
+ f = self._file
+ # Check prefix after peamble
+ f.seek(128)
+ if f.read(4) != b'DICM':
+ raise NotADicomFile('Not a valid DICOM file.')
+ # Read
+ self._read_header()
+ self._read_data_elements()
+ self._get_shape_and_sampling()
+ # Close if done, reopen if necessary to read pixel data
+ if os.path.isfile(self._filename):
+ self._file.close()
+ self._file = None
+
+ def _readDataElement(self):
+ f = self._file
+ # Get group and element
+ group = self._unpack('H', f.read(2))
+ element = self._unpack('H', f.read(2))
+ # Get value length
+ if self.is_implicit_VR:
+ vl = self._unpack('I', f.read(4))
+ else:
+ vr = f.read(2)
+ if vr in (b'OB', b'OW', b'SQ', b'UN'):
+ reserved = f.read(2) # noqa
+ vl = self._unpack('I', f.read(4))
+ else:
+ vl = self._unpack('H', f.read(2))
+ # Get value
+ if group == 0x7FE0 and element == 0x0010:
+ here = f.tell()
+ self._pixel_data_loc = here, vl
+ f.seek(here+vl)
+ return group, element, b'Deferred loading of pixel data'
+ else:
+ if vl == 0xFFFFFFFF:
+ value = self._read_undefined_length_value()
+ else:
+ value = f.read(vl)
+ return group, element, value
+
+ def _read_undefined_length_value(self, read_size=128):
+ """ Copied (in compacted form) from PyDicom
+ Copyright Darcy Mason.
+ """
+ fp = self._file
+ #data_start = fp.tell()
+ search_rewind = 3
+ bytes_to_find = struct.pack(self._unpackPrefix+'HH',
+ SequenceDelimiterTag[0],
+ SequenceDelimiterTag[1])
+
+ found = False
+ value_chunks = []
+ while not found:
+ chunk_start = fp.tell()
+ bytes_read = fp.read(read_size)
+ if len(bytes_read) < read_size:
+ # try again,
+ # if still don't get required amount, this is last block
+ new_bytes = fp.read(read_size - len(bytes_read))
+ bytes_read += new_bytes
+ if len(bytes_read) < read_size:
+ raise EOFError("End of file reached before sequence "
+ "delimiter found.")
+ index = bytes_read.find(bytes_to_find)
+ if index != -1:
+ found = True
+ value_chunks.append(bytes_read[:index])
+ fp.seek(chunk_start + index + 4) # rewind to end of delimiter
+ length = fp.read(4)
+ if length != b"\0\0\0\0":
+ print("Expected 4 zero bytes after undefined length "
+ "delimiter")
+ else:
+ fp.seek(fp.tell() - search_rewind) # rewind a bit
+ # accumulate the bytes read (not including the rewind)
+ value_chunks.append(bytes_read[:-search_rewind])
+
+ # if get here then have found the byte string
+ return b"".join(value_chunks)
+
+ def _read_header(self):
+ f = self._file
+ TransferSyntaxUID = None
+
+ # Read all elements, store transferSyntax when we encounter it
+ try:
+ while True:
+ fp_save = f.tell()
+ # Get element
+ group, element, value = self._readDataElement()
+ if group == 0x02:
+ if group == 0x02 and element == 0x10:
+ TransferSyntaxUID = value.decode('ascii').strip('\x00')
+ else:
+ # No more group 2: rewind and break
+ # (don't trust group length)
+ f.seek(fp_save)
+ break
+ except (EOFError, struct.error): # pragma: no cover
+ raise RuntimeError('End of file reached while still in header.')
+
+ # Handle transfer syntax
+ self._info['TransferSyntaxUID'] = TransferSyntaxUID
+ #
+ if TransferSyntaxUID is None:
+ # Assume ExplicitVRLittleEndian
+ is_implicit_VR, is_little_endian = False, True
+ elif TransferSyntaxUID == '1.2.840.10008.1.2.1':
+ # ExplicitVRLittleEndian
+ is_implicit_VR, is_little_endian = False, True
+ elif TransferSyntaxUID == '1.2.840.10008.1.2.2':
+ # ExplicitVRBigEndian
+ is_implicit_VR, is_little_endian = False, False
+ elif TransferSyntaxUID == '1.2.840.10008.1.2':
+ # implicit VR little endian
+ is_implicit_VR, is_little_endian = True, True
+ elif TransferSyntaxUID == '1.2.840.10008.1.2.1.99':
+ # DeflatedExplicitVRLittleEndian:
+ is_implicit_VR, is_little_endian = False, True
+ self._inflate()
+ elif TransferSyntaxUID == '1.2.840.10008.1.2.4.70':
+ is_implicit_VR, is_little_endian = False, True
+ else:
+ raise RuntimeError('The simple dicom reader can only read files '
+ 'with uncompressed image data '
+ '(not %r)' % TransferSyntaxUID)
+
+ # From hereon, use implicit/explicit big/little endian
+ self.is_implicit_VR = is_implicit_VR
+ self.is_little_endian = is_little_endian
+ self._unpackPrefix = '><'[is_little_endian]
+
+ def _read_data_elements(self):
+ info = self._info
+ try:
+ while True:
+ # Get element
+ group, element, value = self._readDataElement()
+ # Is it a group we are interested in?
+ if group in GROUPS:
+ key = (group, element)
+ name, vr = MINIDICT.get(key, (None, None))
+ # Is it an element we are interested in?
+ if name:
+ # Store value
+ converter = self._converters.get(vr, lambda x: x)
+ info[name] = converter(value)
+ except (EOFError, struct.error):
+ pass # end of file ...
+
+ def get_numpy_array(self):
+ """ Get numpy arra for this DICOM file, with the correct shape,
+ and pixel values scaled appropriately.
+ """
+ # Is there pixel data at all?
+ if 'PixelData' not in self:
+ raise TypeError("No pixel data found in this dataset.")
+
+ # Load it now if it was not already loaded
+ if self._pixel_data_loc and len(self.PixelData) < 100:
+ # Reopen file?
+ close_file = False
+ if self._file is None:
+ close_file = True
+ self._file = open(self._filename, 'rb')
+ # Read data
+ self._file.seek(self._pixel_data_loc[0])
+ if self._pixel_data_loc[1] == 0xFFFFFFFF:
+ value = self._read_undefined_length_value()
+ else:
+ value = self._file.read(self._pixel_data_loc[1])
+ # Close file
+ if close_file:
+ self._file.close()
+ self._file = None
+ # Overwrite
+ self._info['PixelData'] = value
+
+ # Get data
+ data = self._pixel_data_numpy()
+ data = self._apply_slope_and_offset(data)
+
+ # Remove data again to preserve memory
+ # Note that the data for the original file is loaded twice ...
+ self._info['PixelData'] = (b'Data converted to numpy array, ' +
+ b'raw data removed to preserve memory')
+ return data
+
+ def _get_shape_and_sampling(self):
+ """ Get shape and sampling without actuall using the pixel data.
+ In this way, the user can get an idea what's inside without having
+ to load it.
+ """
+ # Get shape (in the same way that pydicom does)
+ if 'NumberOfFrames' in self and self.NumberOfFrames > 1:
+ if self.SamplesPerPixel > 1:
+ shape = (self.SamplesPerPixel, self.NumberOfFrames,
+ self.Rows, self.Columns)
+ else:
+ shape = self.NumberOfFrames, self.Rows, self.Columns
+ elif 'SamplesPerPixel' in self:
+ if self.SamplesPerPixel > 1:
+ if self.BitsAllocated == 8:
+ shape = self.SamplesPerPixel, self.Rows, self.Columns
+ else:
+ raise NotImplementedError("DICOM plugin only handles "
+ "SamplesPerPixel > 1 if Bits "
+ "Allocated = 8")
+ else:
+ shape = self.Rows, self.Columns
+ else:
+ raise RuntimeError('DICOM file has no SamplesPerPixel '
+ '(perhaps this is a report?)')
+
+ # Try getting sampling between pixels
+ if 'PixelSpacing' in self:
+ sampling = float(self.PixelSpacing[0]), float(self.PixelSpacing[1])
+ else:
+ sampling = 1.0, 1.0
+ if 'SliceSpacing' in self:
+ sampling = (abs(self.SliceSpacing),) + sampling
+
+ # Ensure that sampling has as many elements as shape
+ sampling = (1.0,)*(len(shape)-len(sampling)) + sampling[-len(shape):]
+
+ # Set shape and sampling
+ self._info['shape'] = shape
+ self._info['sampling'] = sampling
+
+ def _pixel_data_numpy(self):
+ """Return a NumPy array of the pixel data.
+ """
+ # Taken from pydicom
+ # Copyright (c) 2008-2012 Darcy Mason
+
+ if 'PixelData' not in self:
+ raise TypeError("No pixel data found in this dataset.")
+
+ # determine the type used for the array
+ need_byteswap = (self.is_little_endian != sys_is_little_endian)
+
+ # Make NumPy format code, e.g. "uint16", "int32" etc
+ # from two pieces of info:
+ # self.PixelRepresentation -- 0 for unsigned, 1 for signed;
+ # self.BitsAllocated -- 8, 16, or 32
+ format_str = '%sint%d' % (('u', '')[self.PixelRepresentation],
+ self.BitsAllocated)
+ try:
+ numpy_format = np.dtype(format_str)
+ except TypeError: # pragma: no cover
+ raise TypeError("Data type not understood by NumPy: format='%s', "
+ " PixelRepresentation=%d, BitsAllocated=%d" %
+ (numpy_format, self.PixelRepresentation,
+ self.BitsAllocated))
+
+ # Have correct Numpy format, so create the NumPy array
+ arr = np.fromstring(self.PixelData, numpy_format)
+
+ # XXX byte swap - may later handle this in read_file!!?
+ if need_byteswap:
+ arr.byteswap(True) # True means swap in-place, don't make new copy
+
+ # Note the following reshape operations return a new *view* onto arr,
+ # but don't copy the data
+ arr = arr.reshape(*self._info['shape'])
+ return arr
+
+ def _apply_slope_and_offset(self, data):
+ """
+ If RescaleSlope and RescaleIntercept are present in the data,
+ apply them. The data type of the data is changed if necessary.
+ """
+ # Obtain slope and offset
+ slope, offset = 1, 0
+ needFloats, needApplySlopeOffset = False, False
+ if 'RescaleSlope' in self:
+ needApplySlopeOffset = True
+ slope = self.RescaleSlope
+ if 'RescaleIntercept' in self:
+ needApplySlopeOffset = True
+ offset = self.RescaleIntercept
+ if int(slope) != slope or int(offset) != offset:
+ needFloats = True
+ if not needFloats:
+ slope, offset = int(slope), int(offset)
+
+ # Apply slope and offset
+ if needApplySlopeOffset:
+ # Maybe we need to change the datatype?
+ if data.dtype in [np.float32, np.float64]:
+ pass
+ elif needFloats:
+ data = data.astype(np.float32)
+ else:
+ # Determine required range
+ minReq, maxReq = data.min(), data.max()
+ minReq = min([minReq, minReq * slope + offset,
+ maxReq * slope + offset])
+ maxReq = max([maxReq, minReq * slope + offset,
+ maxReq * slope + offset])
+
+ # Determine required datatype from that
+ dtype = None
+ if minReq < 0:
+ # Signed integer type
+ maxReq = max([-minReq, maxReq])
+ if maxReq < 2**7:
+ dtype = np.int8
+ elif maxReq < 2**15:
+ dtype = np.int16
+ elif maxReq < 2**31:
+ dtype = np.int32
+ else:
+ dtype = np.float32
+ else:
+ # Unsigned integer type
+ if maxReq < 2**8:
+ dtype = np.int8
+ elif maxReq < 2**16:
+ dtype = np.int16
+ elif maxReq < 2**32:
+ dtype = np.int32
+ else:
+ dtype = np.float32
+ # Change datatype
+ if dtype != data.dtype:
+ data = data.astype(dtype)
+
+ # Apply slope and offset
+ data *= slope
+ data += offset
+
+ # Done
+ return data
+
+ def _inflate(self):
+ # Taken from pydicom
+ # Copyright (c) 2008-2012 Darcy Mason
+ import zlib
+ from io import BytesIO
+ # See PS3.6-2008 A.5 (p 71) -- when written, the entire dataset
+ # following the file metadata was prepared the normal way,
+ # then "deflate" compression applied.
+ # All that is needed here is to decompress and then
+ # use as normal in a file-like object
+ zipped = self._file.read()
+ # -MAX_WBITS part is from comp.lang.python answer:
+ # groups.google.com/group/comp.lang.python/msg/e95b3b38a71e6799
+ unzipped = zlib.decompress(zipped, -zlib.MAX_WBITS)
+ self._file = BytesIO(unzipped) # a file-like object
+
+
+class DicomSeries(object):
+ """ DicomSeries
+ This class represents a serie of dicom files (SimpleDicomReader
+ objects) that belong together. If these are multiple files, they
+ represent the slices of a volume (like for CT or MRI).
+ """
+
+ def __init__(self, suid, progressIndicator):
+ # Init dataset list and the callback
+ self._entries = []
+
+ # Init props
+ self._suid = suid
+ self._info = {}
+ self._progressIndicator = progressIndicator
+
+ def __len__(self):
+ return len(self._entries)
+
+ def __iter__(self):
+ return iter(self._entries)
+
+ def __getitem__(self, index):
+ return self._entries[index]
+
+ @property
+ def suid(self):
+ return self._suid
+
+ @property
+ def shape(self):
+ """ The shape of the data (nz, ny, nx). """
+ return self._info['shape']
+
+ @property
+ def sampling(self):
+ """ The sampling (voxel distances) of the data (dz, dy, dx). """
+ return self._info['sampling']
+
+ @property
+ def info(self):
+ """ A dictionary containing the information as present in the
+ first dicomfile of this serie. None if there are no entries. """
+ return self._info
+
+ @property
+ def description(self):
+ """ A description of the dicom series. Used fields are
+ PatientName, shape of the data, SeriesDescription, and
+ ImageComments.
+ """
+ info = self.info
+
+ # If no info available, return simple description
+ if not info: # pragma: no cover
+ return "DicomSeries containing %i images" % len(self)
+
+ fields = []
+ # Give patient name
+ if 'PatientName' in info:
+ fields.append(""+info['PatientName'])
+ # Also add dimensions
+ if self.shape:
+ tmp = [str(d) for d in self.shape]
+ fields.append('x'.join(tmp))
+ # Try adding more fields
+ if 'SeriesDescription' in info:
+ fields.append("'"+info['SeriesDescription']+"'")
+ if 'ImageComments' in info:
+ fields.append("'"+info['ImageComments']+"'")
+
+ # Combine
+ return ' '.join(fields)
+
+ def __repr__(self):
+ adr = hex(id(self)).upper()
+ return "<DicomSeries with %i images at %s>" % (len(self), adr)
+
+ def get_numpy_array(self):
+ """ Get (load) the data that this DicomSeries represents, and return
+ it as a numpy array. If this serie contains multiple images, the
+ resulting array is 3D, otherwise it's 2D.
+ """
+
+ # It's easy if no file or if just a single file
+ if len(self) == 0:
+ raise ValueError('Serie does not contain any files.')
+ elif len(self) == 1:
+ return self[0].get_numpy_array()
+
+ # Check info
+ if self.info is None:
+ raise RuntimeError("Cannot return volume if series not finished.")
+
+ # Init data (using what the dicom packaged produces as a reference)
+ slice = self[0].get_numpy_array()
+ vol = np.zeros(self.shape, dtype=slice.dtype)
+ vol[0] = slice
+
+ # Fill volume
+ self._progressIndicator.start('loading data', '', len(self))
+ for z in range(1, len(self)):
+ vol[z] = self[z].get_numpy_array()
+ self._progressIndicator.set_progress(z+1)
+ self._progressIndicator.finish()
+
+ # Done
+ import gc
+ gc.collect()
+ return vol
+
+ def _append(self, dcm):
+ self._entries.append(dcm)
+
+ def _sort(self):
+ self._entries.sort(key=lambda k: k.InstanceNumber)
+
+ def _finish(self):
+ """
+ Evaluate the series of dicom files. Together they should make up
+ a volumetric dataset. This means the files should meet certain
+ conditions. Also some additional information has to be calculated,
+ such as the distance between the slices. This method sets the
+ attributes for "shape", "sampling" and "info".
+
+ This method checks:
+ * that there are no missing files
+ * that the dimensions of all images match
+ * that the pixel spacing of all images match
+ """
+
+ # The datasets list should be sorted by instance number
+ L = self._entries
+ if len(L) == 0:
+ return
+ elif len(L) == 1:
+ self._info = L[0].info
+ return
+
+ # Get previous
+ ds1 = L[0]
+ # Init measures to calculate average of
+ distance_sum = 0.0
+ # Init measures to check (these are in 2D)
+ dimensions = ds1.Rows, ds1.Columns
+ #sampling = float(ds1.PixelSpacing[0]), float(ds1.PixelSpacing[1])
+ sampling = ds1.info['sampling'][:2] # row, column
+
+ for index in range(len(L)):
+ # The first round ds1 and ds2 will be the same, for the
+ # distance calculation this does not matter
+ # Get current
+ ds2 = L[index]
+ # Get positions
+ pos1 = float(ds1.ImagePositionPatient[2])
+ pos2 = float(ds2.ImagePositionPatient[2])
+ # Update distance_sum to calculate distance later
+ distance_sum += abs(pos1 - pos2)
+ # Test measures
+ dimensions2 = ds2.Rows, ds2.Columns
+ #sampling2 = float(ds2.PixelSpacing[0]), float(ds2.PixelSpacing[1])
+ sampling2 = ds2.info['sampling'][:2] # row, column
+ if dimensions != dimensions2:
+ # We cannot produce a volume if the dimensions match
+ raise ValueError('Dimensions of slices does not match.')
+ if sampling != sampling2:
+ # We can still produce a volume, but we should notify the user
+ self._progressIndicator.write('Warn: sampling does not match.')
+ # Store previous
+ ds1 = ds2
+
+ # Finish calculating average distance
+ # (Note that there are len(L)-1 distances)
+ distance_mean = distance_sum / (len(L)-1)
+
+ # Set info dict
+ self._info = L[0].info.copy()
+
+ # Store information that is specific for the serie
+ self._info['shape'] = (len(L),) + ds2.info['shape']
+ self._info['sampling'] = (distance_mean,) + ds2.info['sampling']
+
+
+def list_files(files, path):
+ """List all files in the directory, recursively. """
+ for item in os.listdir(path):
+ item = os.path.join(path, item)
+ if os.path.isdir(item):
+ list_files(files, item)
+ elif os.path.isfile(item):
+ files.append(item)
+
+
+def process_directory(request, progressIndicator, readPixelData=False):
+ """
+ Reads dicom files and returns a list of DicomSeries objects, which
+ contain information about the data, and can be used to load the
+ image or volume data.
+
+ if readPixelData is True, the pixel data of all series is read. By
+ default the loading of pixeldata is deferred until it is requested
+ using the DicomSeries.get_pixel_array() method. In general, both
+ methods should be equally fast.
+ """
+ # Get directory to examine
+ if os.path.isdir(request.filename):
+ path = request.filename
+ elif os.path.isfile(request.filename):
+ path = os.path.dirname(request.filename)
+ else: # pragma: no cover - tested earlier
+ raise ValueError('Dicom plugin needs a valid filename to examine '
+ 'the directory')
+
+ # Check files
+ files = []
+ list_files(files, path) # Find files recursively
+
+ # Gather file data and put in DicomSeries
+ series = {}
+ count = 0
+ progressIndicator.start('examining files', 'files', len(files))
+ for filename in files:
+ # Show progress (note that we always start with a 0.0)
+ count += 1
+ progressIndicator.set_progress(count)
+ # Skip DICOMDIR files
+ if filename.count("DICOMDIR"): # pragma: no cover
+ continue
+ # Try loading dicom ...
+ try:
+ dcm = SimpleDicomReader(filename)
+ except NotADicomFile:
+ continue # skip non-dicom file
+ except Exception as why: # pragma: no cover
+ progressIndicator.write(str(why))
+ continue
+ # Get SUID and register the file with an existing or new series object
+ try:
+ suid = dcm.SeriesInstanceUID
+ except AttributeError: # pragma: no cover
+ continue # some other kind of dicom file
+ if suid not in series:
+ series[suid] = DicomSeries(suid, progressIndicator)
+ series[suid]._append(dcm)
+
+ # Finish progress
+ #progressIndicator.finish('Found %i series.' % len(series))
+
+ # Make a list and sort, so that the order is deterministic
+ series = list(series.values())
+ series.sort(key=lambda x: x.suid)
+
+ # Split series if necessary
+ for serie in reversed([serie for serie in series]):
+ splitSerieIfRequired(serie, series, progressIndicator)
+
+ # Finish all series
+ #progressIndicator.start('analyse series', '', len(series))
+ series_ = []
+ for i in range(len(series)):
+ try:
+ series[i]._finish()
+ series_.append(series[i])
+ except Exception as err: # pragma: no cover
+ progressIndicator.write(str(err))
+ pass # Skip serie (probably report-like file without pixels)
+ #progressIndicator.set_progress(i+1)
+ progressIndicator.finish('Found %i correct series.' % len(series_))
+
+ # Done
+ return series_
+
+
+def splitSerieIfRequired(serie, series, progressIndicator):
+ """
+ Split the serie in multiple series if this is required. The choice
+ is based on examing the image position relative to the previous
+ image. If it differs too much, it is assumed that there is a new
+ dataset. This can happen for example in unspitted gated CT data.
+ """
+
+ # Sort the original list and get local name
+ serie._sort()
+ L = serie._entries
+ # Init previous slice
+ ds1 = L[0]
+ # Check whether we can do this
+ if "ImagePositionPatient" not in ds1:
+ return
+ # Initialize a list of new lists
+ L2 = [[ds1]]
+ # Init slice distance estimate
+ distance = 0
+
+ for index in range(1, len(L)):
+ # Get current slice
+ ds2 = L[index]
+ # Get positions
+ pos1 = float(ds1.ImagePositionPatient[2])
+ pos2 = float(ds2.ImagePositionPatient[2])
+ # Get distances
+ newDist = abs(pos1 - pos2)
+ #deltaDist = abs(firstPos-pos2)
+ # If the distance deviates more than 2x from what we've seen,
+ # we can agree it's a new dataset.
+ if distance and newDist > 2.1*distance:
+ L2.append([])
+ distance = 0
+ else:
+ # Test missing file
+ if distance and newDist > 1.5*distance:
+ progressIndicator.write('Warning: missing file after %r' %
+ ds1._filename)
+ distance = newDist
+ # Add to last list
+ L2[-1].append(ds2)
+ # Store previous
+ ds1 = ds2
+
+ # Split if we should
+ if len(L2) > 1:
+ # At what position are we now?
+ i = series.index(serie)
+ # Create new series
+ series2insert = []
+ for L in L2:
+ newSerie = DicomSeries(serie.suid, progressIndicator)
+ newSerie._entries = L
+ series2insert.append(newSerie)
+ # Insert series and remove self
+ for newSerie in reversed(series2insert):
+ series.insert(i, newSerie)
+ series.remove(serie)
diff --git a/imageio/plugins/example.py b/imageio/plugins/example.py
new file mode 100644
index 0000000..2756001
--- /dev/null
+++ b/imageio/plugins/example.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Example plugin. You can use this as a template for your own plugin.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format
+
+
+class DummyFormat(Format):
+ """ The dummy format is an example format that does nothing.
+ It will never indicate that it can read or save a file. When
+ explicitly asked to read, it will simply read the bytes. When
+ explicitly asked to save, it will raise an error.
+
+ This documentation is shown when the user does ``help('thisformat')``.
+
+ Parameters for reading
+ ----------------------
+ Specify arguments in numpy doc style here.
+
+ Parameters for saving
+ ---------------------
+ Specify arguments in numpy doc style here.
+
+ """
+
+ def _can_read(self, request):
+ # This method is called when the format manager is searching
+ # for a format to read a certain image. Return True if this format
+ # can do it.
+ #
+ # The format manager is aware of the extensions and the modes
+ # that each format can handle. However, the ability to read a
+ # format could be more subtle. Also, the format would ideally
+ # check the request.firstbytes and look for a header of some
+ # kind. Further, the extension might not always be known.
+ #
+ # The request object has:
+ # request.filename: a representation of the source (only for reporting)
+ # request.firstbytes: the first 256 bytes of the file.
+ # request.mode[0]: read or write mode
+ # request.mode[1]: what kind of data the user expects: one of 'iIvV?'
+
+ if request.mode[1] in (self.modes + '?'):
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ def _can_save(self, request):
+ # This method is called when the format manager is searching
+ # for a format to save a certain image. Return True if the
+ # format can do it.
+ #
+ # In most cases, the code does suffice.
+
+ if request.mode[1] in (self.modes + '?'):
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ # -- reader
+
+ class Reader(Format.Reader):
+
+ def _open(self, some_option=False):
+ # Specify kwargs here. Optionally, the user-specified kwargs
+ # can also be accessed via the request.kwargs object.
+ #
+ # The request object provides two ways to get access to the
+ # data. Use just one:
+ # - Use request.get_file() for a file object (preferred)
+ # - Use request.get_local_filename() for a file on the system
+ self._fp = self.request.get_file()
+
+ def _close(self):
+ # Close the reader.
+ # Note that the request object will close self._fp
+ pass
+
+ def _get_length(self):
+ # Return the number of images. Can be np.inf
+ return 1
+
+ def _get_data(self, index):
+ # Return the data and meta data for the given index
+ if index != 0:
+ raise IndexError('Dummy format only supports singleton images')
+ # Read all bytes
+ data = self._fp.read()
+ # Put in a numpy array
+ im = np.frombuffer(data, 'uint8')
+ im.shape = len(im), 1
+ # Return array and dummy meta data
+ return im, {}
+
+ def _get_meta_data(self, index):
+ # Get the meta data for the given index. If index is None, it
+ # should return the global meta data.
+ return {} # This format does not support meta data
+
+ # -- writer
+
+ class Writer(Format.Writer):
+
+ def _open(self, flags=0):
+ # Specify kwargs here. Optionally, the user-specified kwargs
+ # can also be accessed via the request.kwargs object.
+ #
+ # The request object provides two ways to write the data.
+ # Use just one:
+ # - Use request.get_file() for a file object (preferred)
+ # - Use request.get_local_filename() for a file on the system
+ self._fp = self.request.get_file()
+
+ def _close(self):
+ # Close the reader.
+ # Note that the request object will close self._fp
+ pass
+
+ def _append_data(self, im, meta):
+ # Process the given data and meta data.
+ raise RuntimeError('The dummy format cannot save image data.')
+
+ def set_meta_data(self, meta):
+ # Process the given meta data (global for all images)
+ # It is not mandatory to support this.
+ raise RuntimeError('The dummy format cannot save meta data.')
+
+
+# Register. You register an *instance* of a Format class. Here specify:
+format = DummyFormat('dummy', # short name
+ 'An example format that does nothing.', # one line descr.
+ '', # list of extensions as a space separated string
+ '' # modes, characters in iIvV
+ )
+formats.add_format(format)
diff --git a/imageio/plugins/ffmpeg.py b/imageio/plugins/ffmpeg.py
new file mode 100644
index 0000000..0d1f5f3
--- /dev/null
+++ b/imageio/plugins/ffmpeg.py
@@ -0,0 +1,659 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Plugin that uses ffmpeg to read and write series of images to
+a wide range of video formats.
+
+Code inspired/based on code from moviepy: https://github.com/Zulko/moviepy/
+by Zulko
+
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import sys
+import os
+import stat
+import re
+import time
+import threading
+import struct
+import subprocess as sp
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format, get_remote_file, string_types, read_n_bytes
+
+
+def get_exe():
+ """ Get ffmpeg exe
+ """
+ NBYTES = struct.calcsize('P') * 8
+ if sys.platform.startswith('linux'):
+ fname = 'ffmpeg.linux%i' % NBYTES
+ elif sys.platform.startswith('win'):
+ fname = 'ffmpeg.win32.exe'
+ elif sys.platform.startswith('darwin'):
+ fname = 'ffmpeg.osx.snowleopardandabove'
+ else: # pragma: no cover
+ fname = 'ffmpeg' # hope for the best
+ #
+ FFMPEG_EXE = 'ffmpeg'
+ if fname:
+ FFMPEG_EXE = get_remote_file('ffmpeg/' + fname)
+ os.chmod(FFMPEG_EXE, os.stat(FFMPEG_EXE).st_mode | stat.S_IEXEC) # exe
+ return FFMPEG_EXE
+
+# Get camera format
+if sys.platform.startswith('win'):
+ CAM_FORMAT = 'dshow' # dshow or vfwcap
+elif sys.platform.startswith('linux'):
+ CAM_FORMAT = 'video4linux2'
+elif sys.platform.startswith('darwin'):
+ CAM_FORMAT = '??'
+else: # pragma: no cover
+ CAM_FORMAT = 'unknown-cam-format'
+
+
+class FfmpegFormat(Format):
+ """ The ffmpeg format provides reading and writing for a wide range
+ of movie formats such as .avi, .mpeg, .mp4, etc. And also to read
+ streams from webcams and USB cameras.
+
+ To read from camera streams, supply "<video0>" as the filename,
+ where the "0" can be replaced with any index of cameras known to
+ the system.
+
+ Note that for reading regular video files, the avbin plugin is more
+ efficient.
+
+ Parameters for reading
+ ----------------------
+ loop : bool
+ If True, the video will rewind as soon as a frame is requested
+ beyond the last frame. Otherwise, IndexError is raised. Default False.
+ size : str | tuple
+ The frame size (i.e. resolution) to read the images, e.g.
+ (100, 100) or "640x480". For camera streams, this allows setting
+ the capture resolution. For normal video data, ffmpeg will
+ rescale the data.
+ pixelformat : str
+ The pixel format for the camera to use (e.g. "yuyv422" or
+ "gray"). The camera needs to support the format in order for
+ this to take effect. Note that the images produced by this
+ reader are always rgb8.
+ print_info : bool
+ Print information about the video file as reported by ffmpeg.
+
+ Parameters for saving
+ ---------------------
+ fps : scalar
+ The number of frames per second. Default 10.
+ codec : str
+ the video codec to use. Default 'libx264', which represents the
+ widely available mpeg4.
+ bitrate : int
+ A measure for quality. Default 400000
+ """
+
+ def _can_read(self, request):
+ if request.mode[1] not in 'I?':
+ return False
+
+ # Read from video stream?
+ # Note that we could write the _video flag here, but a user might
+ # select this format explicitly (and this code is not run)
+ if request.filename in ['<video%i>' % i for i in range(10)]:
+ return True
+
+ # Read from file that we know?
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ def _can_save(self, request):
+ if request.mode[1] in (self.modes + '?'):
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ # --
+
+ class Reader(Format.Reader):
+
+ def _get_cam_inputname(self, index):
+ if sys.platform.startswith('linux'):
+ return '/dev/' + self.request._video[1:-1]
+
+ elif sys.platform.startswith('win'):
+ # Ask ffmpeg for list of dshow device names
+ cmd = [self._exe, '-list_devices', 'true',
+ '-f', CAM_FORMAT, '-i', 'dummy']
+ proc = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE,
+ stderr=sp.PIPE)
+ proc.stdout.readline()
+ proc.terminate()
+ infos = proc.stderr.read().decode('utf-8')
+ # Parse the result
+ device_names = []
+ in_video_devices = False
+ for line in infos.splitlines():
+ if line.startswith('[dshow'):
+ line = line.split(']', 1)[1].strip()
+ if line.startswith('"'):
+ if in_video_devices:
+ device_names.append(line[1:-1])
+ elif 'video devices' in line:
+ in_video_devices = True
+ else:
+ in_video_devices = False
+ # Return device name at index
+ try:
+ name = device_names[index]
+ except IndexError:
+ raise IndexError('No ffdshow camera at index %i.' % index)
+ return 'video=%s' % name
+ else: # pragma: no cover
+ return '??'
+
+ def _open(self, loop=False, size=None, pixelformat=None,
+ print_info=False):
+ # Get exe
+ self._exe = get_exe()
+ # Process input args
+ self._arg_loop = bool(loop)
+ if size is None:
+ self._arg_size = None
+ elif isinstance(size, tuple):
+ self._arg_size = "%ix%i" % size
+ elif isinstance(size, string_types) and 'x' in size:
+ self._arg_size = size
+ else:
+ raise ValueError('FFMPEG size must be tuple of "NxM"')
+ if pixelformat is None:
+ pass
+ elif not isinstance(pixelformat, string_types):
+ raise ValueError('FFMPEG pixelformat must be str')
+ self._arg_pixelformat = pixelformat
+ # Write "_video"_arg
+ self.request._video = None
+ if self.request.filename in ['<video%i>' % i for i in range(10)]:
+ self.request._video = self.request.filename
+ # Get local filename
+ if self.request._video:
+ index = int(self.request._video[-2])
+ self._filename = self._get_cam_inputname(index)
+ else:
+ self._filename = self.request.get_local_filename()
+ # Determine pixel format and depth
+ self._pix_fmt = 'rgb24'
+ self._depth = 4 if self._pix_fmt == "rgba" else 3
+ # Initialize parameters
+ self._proc = None
+ self._pos = -1
+ self._meta = {'plugin': 'ffmpeg',
+ 'nframes': float('inf'), 'nframes': float('inf')}
+ self._lastread = None
+ # Start ffmpeg subprocess and get meta information
+ self._initialize()
+ self._load_infos()
+
+ # For cameras, create thread that keeps reading the images
+ self._frame_catcher = None
+ if self.request._video:
+ w, h = self._meta['size']
+ framesize = self._depth * w * h
+ self._frame_catcher = FrameCatcher(self._proc.stdout,
+ framesize)
+
+ def _close(self):
+ self._terminate(0.05) # Short timeout
+ self._proc = None
+
+ def _get_length(self):
+ return self._meta['nframes']
+
+ def _get_data(self, index):
+ """ Reads a frame at index. Note for coders: getting an
+ arbitrary frame in the video with ffmpeg can be painfully
+ slow if some decoding has to be done. This function tries
+ to avoid fectching arbitrary frames whenever possible, by
+ moving between adjacent frames. """
+ # Modulo index (for looping)
+ if self._meta['nframes'] and self._meta['nframes'] < float('inf'):
+ if self._arg_loop:
+ index = index % self._meta['nframes']
+
+ if index == self._pos:
+ return self._lastread, {}
+ elif index < 0:
+ raise IndexError('Frame index must be > 0')
+ elif index >= self._meta['nframes']:
+ raise IndexError('Reached end of video')
+ else:
+ if (index < self._pos) or (index > self._pos+100):
+ self._reinitialize(index)
+ else:
+ self._skip_frames(index-self._pos-1)
+ result = self._read_frame()
+ self._pos = index
+ return result, {}
+
+ def _get_meta_data(self, index):
+ return self._meta
+
+ def _initialize(self):
+ """ Opens the file, creates the pipe. """
+ # Create input args
+ if self.request._video:
+ iargs = ['-f', CAM_FORMAT]
+ if self._arg_pixelformat:
+ iargs.extend(['-pix_fmt', self._arg_pixelformat])
+ if self._arg_size:
+ iargs.extend(['-s', self._arg_size])
+ else:
+ iargs = []
+ # Output args, for writing to pipe
+ oargs = ['-f', 'image2pipe',
+ '-pix_fmt', self._pix_fmt,
+ '-vcodec', 'rawvideo']
+ oargs.extend(['-s', self._arg_size] if self._arg_size else [])
+ # Create process
+ cmd = [self._exe] + iargs + ['-i', self._filename] + oargs + ['-']
+ self._proc = sp.Popen(cmd, stdin=sp.PIPE,
+ stdout=sp.PIPE, stderr=sp.PIPE)
+ # Create thread that keeps reading from stderr
+ self._stderr_catcher = StreamCatcher(self._proc.stderr)
+
+ def _reinitialize(self, index=0):
+ """ Restarts the reading, starts at an arbitrary location
+ (!! SLOW !!) """
+ if self.request._video:
+ raise RuntimeError('No random access when streaming from cam.')
+ self._close()
+ if index == 0:
+ self._initialize()
+ else:
+ starttime = index / self._meta['fps']
+ offset = min(1, starttime)
+ # Create input args -> start time
+ iargs = ['-ss', "%.03f" % (starttime-offset)]
+ # Output args, for writing to pipe
+ oargs = ['-f', 'image2pipe',
+ '-pix_fmt', self._pix_fmt,
+ '-vcodec', 'rawvideo']
+ oargs.extend(['-s', self._arg_size] if self._arg_size else [])
+ # Create process
+ cmd = [self._exe]
+ cmd += iargs + ['-i', self._filename] + oargs + ['-']
+ self._proc = sp.Popen(cmd, stdin=sp.PIPE,
+ stdout=sp.PIPE, stderr=sp.PIPE)
+ # Create thread that keeps reading from stderr
+ self._stderr_catcher = StreamCatcher(self._proc.stderr)
+
+ def _terminate(self, timeout=1.0):
+ """ Terminate the sub process.
+ """
+ # Check
+ if self._proc is None: # pragma: no cover
+ return # no process
+ if self._proc.poll() is not None:
+ return # process already dead
+ # Terminate process
+ self._proc.terminate()
+ # Wait for it to close (but do not get stuck)
+ etime = time.time() + timeout
+ while time.time() < etime:
+ time.sleep(0.01)
+ if self._proc.poll() is not None:
+ break
+
+# def _close_streams(self):
+# for std in (self._proc.stdin,
+# self._proc.stdout,
+# self._proc.stderr):
+# try:
+# std.close()
+# except Exception: # pragma: no cover
+# pass
+
+ def _load_infos(self):
+ """ reads the FFMPEG info on the file and sets size fps
+ duration and nframes. """
+
+ # Wait for the catcher to get the meta information
+ etime = time.time() + 4.0
+ while (not self._stderr_catcher.header) and time.time() < etime:
+ time.sleep(0.01)
+
+ # Check whether we have the information
+ infos = self._stderr_catcher.header
+ if not infos:
+ self._terminate()
+ if self.request._video:
+ raise IndexError('No video4linux camera at %s.' %
+ self.request._video)
+ else:
+ err2 = self._stderr_catcher.get_text(0.2)
+ fmt = 'Could not load meta information\n=== stderr ===\n%s'
+ raise IOError(fmt % err2)
+
+ if self.request.kwargs.get('print_info', False):
+ # print the whole info text returned by FFMPEG
+ print(infos)
+ print('-'*80)
+ lines = infos.splitlines()
+ if "No such file or directory" in lines[-1]:
+ if self.request._video:
+ raise IOError("Could not open steam %s." % self._filename)
+ else: # pragma: no cover - this is checked by Request
+ raise IOError("%s not found! Wrong path?" % self._filename)
+
+ # Get version
+ ver = lines[0].split('version', 1)[-1].split('Copyright')[0]
+ self._meta['ffmpeg_version'] = ver.strip() + ' ' + lines[1].strip()
+
+ # get the output line that speaks about video
+ videolines = [l for l in lines if ' Video: ' in l]
+ line = videolines[0]
+
+ # get the frame rate
+ match = re.search("( [0-9]*.| )[0-9]* (tbr|fps)", line)
+ fps = float(line[match.start():match.end()].split(' ')[1])
+ self._meta['fps'] = fps
+
+ # get the size of the original stream, of the form 460x320 (w x h)
+ match = re.search(" [0-9]*x[0-9]*(,| )", line)
+ parts = line[match.start():match.end()-1].split('x')
+ self._meta['source_size'] = tuple(map(int, parts))
+
+ # get the size of what we receive, of the form 460x320 (w x h)
+ line = videolines[-1] # Pipe output
+ match = re.search(" [0-9]*x[0-9]*(,| )", line)
+ parts = line[match.start():match.end()-1].split('x')
+ self._meta['size'] = tuple(map(int, parts))
+
+ # Check the two sizes
+ if self._meta['source_size'] != self._meta['size']:
+ print('Warning: the frame size for reading %s is different '
+ 'from the source frame size %s.' %
+ (self._meta['size'], self._meta['source_size'], ))
+
+ # get duration (in seconds)
+ line = [l for l in lines if 'Duration: ' in l][0]
+ match = re.search(" [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]",
+ line)
+ if match is not None:
+ hms = map(float, line[match.start()+1:match.end()].split(':'))
+ self._meta['duration'] = duration = cvsecs(*hms)
+ self._meta['nframes'] = int(duration*fps)
+
+ def _read_frame_data(self):
+ # Init and check
+ w, h = self._meta['size']
+ framesize = self._depth * w * h
+ assert self._proc is not None
+
+ try:
+ # Read framesize bytes
+ if self._frame_catcher: # pragma: no cover - camera thing
+ s = self._frame_catcher.get_frame()
+ else:
+ s = read_n_bytes(self._proc.stdout, framesize)
+ # Check
+ assert len(s) == framesize
+ except Exception as err:
+ self._terminate()
+ err1 = str(err)
+ err2 = self._stderr_catcher.get_text(0.4)
+ fmt = 'Could not read frame:\n%s\n=== stderr ===\n%s'
+ raise RuntimeError(fmt % (err1, err2))
+ return s
+
+ def _skip_frames(self, n=1):
+ """ Reads and throws away n frames """
+ w, h = self._meta['size']
+ for i in range(n):
+ self._read_frame_data()
+ self._pos += n
+
+ def _read_frame(self):
+ # Read and convert to numpy array
+ w, h = self._meta['size']
+ #t0 = time.time()
+ s = self._read_frame_data()
+ result = np.fromstring(s, dtype='uint8')
+ result = result.reshape((h, w, self._depth))
+ #t1 = time.time()
+ #print('etime', t1-t0)
+
+ # Store and return
+ self._lastread = result
+ return result
+
+ # --
+
+ class Writer(Format.Writer):
+
+ def _open(self, fps=10, codec='libx264', bitrate=400000):
+ self._exe = get_exe()
+ # Get local filename
+ self._filename = self.request.get_local_filename()
+ # Determine pixel format and depth
+ self._pix_fmt = None
+ # Initialize parameters
+ self._proc = None
+ self._size = None
+
+ def _close(self):
+ # Close subprocess
+ if self._proc is not None:
+ self._proc.stdin.close()
+ self._proc.stderr.close()
+ self._proc.wait()
+ self._proc = None
+
+ def _append_data(self, im, meta):
+
+ # Get props of image
+ size = im.shape[:2]
+ depth = 1 if im.ndim == 2 else im.shape[2]
+
+ # Ensure that image is in uint8
+ if im.dtype != np.uint8:
+ im = im.astype(np.uint8) # pypy: no support copy=False
+
+ # Set size and initialize if not initialized yet
+ if self._size is None:
+ map = {1: 'gray', 2: 'gray8a', 3: 'rgb24', 4: 'rgba'}
+ self._pix_fmt = map.get(depth, None)
+ if self._pix_fmt is None:
+ raise ValueError('Image must have 1, 2, 3 or 4 channels')
+ self._size = size
+ self._depth = depth
+ self._initialize()
+
+ # Check size of image
+ if size != self._size:
+ raise ValueError('All images in a movie should have same size')
+ if depth != self._depth:
+ raise ValueError('All images in a movie should have same '
+ 'number of channels')
+
+ assert self._proc is not None # Check status
+
+ # Write
+ self._proc.stdin.write(im.tostring())
+
+ def set_meta_data(self, meta):
+ raise RuntimeError('The ffmpeg format does not support setting '
+ 'meta data.')
+
+ def _initialize(self):
+ """ Creates the pipe to ffmpeg. Open the file to write to. """
+
+ # Get parameters
+ # Note that H264 is a widespread and very good codec, but if we
+ # do not specify a bitrate, we easily get crap results.
+ sizestr = "%dx%d" % (self._size[1], self._size[0])
+ fps = self.request.kwargs.get('fps', 10)
+ codec = self.request.kwargs.get('codec', 'libx264')
+ bitrate = self.request.kwargs.get('bitrate', 400000)
+
+ # Get command
+ cmd = [self._exe, '-y',
+ "-f", 'rawvideo',
+ "-vcodec", "rawvideo",
+ '-s', sizestr,
+ '-pix_fmt', self._pix_fmt,
+ '-r', "%.02f" % fps,
+ '-i', '-', '-an',
+ '-vcodec', codec]
+ cmd += ['-b', str(bitrate)] if (bitrate is not None) else []
+ cmd += ['-r', "%d" % fps, self._filename]
+
+ # Launch process
+ self._proc = sp.Popen(cmd, stdin=sp.PIPE,
+ stdout=sp.PIPE, stderr=sp.PIPE)
+
+
+def cvsecs(*args):
+ """ converts a time to second. Either cvsecs(min, secs) or
+ cvsecs(hours, mins, secs).
+ """
+ if len(args) == 1:
+ return args[0]
+ elif len(args) == 2:
+ return 60*args[0] + args[1]
+ elif len(args) == 3:
+ return 3600*args[0] + 60*args[1] + args[2]
+
+
+def limit_lines(lines, N=32):
+ """ When number of lines > 2*N, reduce to N.
+ """
+ if len(lines) > 2*N:
+ lines = [b'... showing only last few lines ...'] + lines[-N:]
+ return lines
+
+
+class FrameCatcher(threading.Thread):
+ """ Thread to keep reading the frame data from stdout. This is
+ useful when streaming from a webcam. Otherwise, if the user code
+ does not grab frames fast enough, the buffer will fill up, leading
+ to lag, and ffmpeg can also stall (experienced on Linux). The
+ get_frame() method always returns the last available image.
+ """
+
+ def __init__(self, file, framesize):
+ self._file = file
+ self._framesize = framesize
+ self._frame = None
+ self._bytes_read = 0
+ #self._lock = threading.RLock()
+ threading.Thread.__init__(self)
+ self.setDaemon(True) # do not let this thread hold up Python shutdown
+ self.start()
+
+ def get_frame(self):
+ while self._frame is None: # pragma: no cover - an init thing
+ time.sleep(0.001)
+ return self._frame
+
+ def _read(self, n):
+ try:
+ return self._file.read(n)
+ except ValueError:
+ return b''
+
+ def run(self):
+ framesize = self._framesize
+
+ while True:
+ time.sleep(0.001)
+ s = self._read(framesize)
+ while len(s) < framesize:
+ need = framesize - len(s)
+ part = self._read(need)
+ if not part:
+ break
+ s += part
+ self._bytes_read += len(part)
+ # Stop?
+ if not s:
+ return
+ # Store frame
+ self._frame = s
+
+
+class StreamCatcher(threading.Thread):
+ """ Thread to keep reading from stderr so that the buffer does not
+ fill up and stalls the ffmpeg process. On stderr a message is send
+ on every few frames with some meta information. We only keep the
+ last ones.
+ """
+
+ def __init__(self, file):
+ self._file = file
+ self._header = ''
+ self._lines = []
+ self._remainder = b''
+ threading.Thread.__init__(self)
+ self.setDaemon(True) # do not let this thread hold up Python shutdown
+ self.start()
+
+ @property
+ def header(self):
+ """ Get header text. Empty string if the header is not yet parsed.
+ """
+ return self._header
+
+ def get_text(self, timeout=0):
+ """ Get the whole text printed to stderr so far. To preserve
+ memory, only the last 50 to 100 frames are kept.
+
+ If a timeout is givem, wait for this thread to finish. When
+ something goes wrong, we stop ffmpeg and want a full report of
+ stderr, but this thread might need a tiny bit more time.
+ """
+
+ # Wait?
+ if timeout > 0:
+ etime = time.time() + timeout
+ while self.isAlive() and time.time() < etime: # pragma: no cover
+ time.sleep(0.025)
+ # Return str
+ lines = b'\n'.join(self._lines)
+ return self._header + '\n' + lines.decode('utf-8', 'ignore')
+
+ def run(self):
+ while True:
+ time.sleep(0.001)
+ # Read one line. Detect when closed, and exit
+ try:
+ line = self._file.read(20)
+ except ValueError: # pragma: no cover
+ break
+ if not line:
+ break
+ # Process to divide in lines
+ line = line.replace(b'\r', b'\n').replace(b'\n\n', b'\n')
+ lines = line.split(b'\n')
+ lines[0] = self._remainder + lines[0]
+ self._remainder = lines.pop(-1)
+ # Process each line
+ for line in lines:
+ self._lines.append(line)
+ if line.startswith(b'Stream mapping'):
+ header = b'\n'.join(self._lines)
+ self._header += header.decode('utf-8', 'ignore')
+ self._lines = []
+ self._lines = limit_lines(self._lines)
+
+
+# Register. You register an *instance* of a Format class.
+format = FfmpegFormat('ffmpeg', 'Many video formats and cameras (via ffmpeg)',
+ '.mov .avi .mpg .mpeg .mp4', 'I')
+formats.add_format(format)
diff --git a/imageio/plugins/freeimage.py b/imageio/plugins/freeimage.py
new file mode 100644
index 0000000..d3b26d6
--- /dev/null
+++ b/imageio/plugins/freeimage.py
@@ -0,0 +1,380 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Plugin that wraps the freeimage lib. The wrapper for Freeimage is
+part of the core of imageio, but it's functionality is exposed via
+the plugin system (therefore this plugin is very thin).
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format
+from ._freeimage import fi, IO_FLAGS
+
+
+# todo: support files with only meta data
+# todo: multi-page files
+
+
+class FreeimageFormat(Format):
+ """ This is the default format used for FreeImage. Each Freeimage
+ format has the 'flags' keyword argument. See the Freeimage
+ documentation for more information.
+
+ Parameters for reading
+ ----------------------
+ flags : int
+ A freeimage-specific option. In most cases we provide explicit
+ parameters for influencing image reading.
+
+ Parameters for saving
+ ----------------------
+ flags : int
+ A freeimage-specific option. In most cases we provide explicit
+ parameters for influencing image saving.
+ """
+
+ _modes = 'i'
+
+ @property
+ def fif(self):
+ return self._fif # Set when format is created
+
+ def _can_read(self, request):
+ modes = self._modes + '?'
+ if fi and request.mode[1] in modes:
+ if not hasattr(request, '_fif'):
+ try:
+ request._fif = fi.getFIF(request.filename, 'r',
+ request.firstbytes)
+ except Exception: # pragma: no cover
+ request._fif = -1
+ if request._fif == self.fif:
+ return True
+
+ def _can_save(self, request):
+ modes = self._modes + '?'
+ if fi and request.mode[1] in modes:
+ if not hasattr(request, '_fif'):
+ try:
+ request._fif = fi.getFIF(request.filename, 'w')
+ except Exception: # pragma: no cover
+ request._fif = -1
+ if request._fif is self.fif:
+ return True
+
+ # --
+
+ class Reader(Format.Reader):
+
+ def _get_length(self):
+ return 1
+
+ def _open(self, flags=0):
+ self._bm = fi.create_bitmap(self.request.filename,
+ self.format.fif, flags)
+ self._bm.load_from_filename(self.request.get_local_filename())
+
+ def _close(self):
+ self._bm.close()
+
+ def _get_data(self, index):
+ if index != 0:
+ raise IndexError('This format only supports singleton images.')
+ return self._bm.get_image_data(), self._bm.get_meta_data()
+
+ def _get_meta_data(self, index):
+ if not (index is None or index == 0):
+ raise IndexError()
+ return self._bm.get_meta_data()
+
+ # --
+
+ class Writer(Format.Writer):
+
+ def _open(self, flags=0):
+ self._flags = flags # Store flags for later use
+ self._bm = None
+ self._is_set = False # To prevent appending more than one image
+ self._meta = {}
+
+ def _close(self):
+ # Set global meta data
+ self._bm.set_meta_data(self._meta)
+ # Write and close
+ self._bm.save_to_filename(self.request.get_local_filename())
+ self._bm.close()
+
+ def _append_data(self, im, meta):
+ # Check if set
+ if not self._is_set:
+ self._is_set = True
+ else:
+ raise RuntimeError('Singleton image; '
+ 'can only append image data once.')
+ # Pop unit dimension for grayscale images
+ if im.ndim == 3 and im.shape[-1] == 1:
+ im = im[:, :, 0]
+ # Lazy instantaion of the bitmap, we need image data
+ if self._bm is None:
+ self._bm = fi.create_bitmap(self.request.filename,
+ self.format.fif, self._flags)
+ self._bm.allocate(im)
+ # Set data
+ self._bm.set_image_data(im)
+ # There is no distinction between global and per-image meta data
+ # for singleton images
+ self._meta = meta
+
+ def _set_meta_data(self, meta):
+ self._meta = meta
+
+
+## Special plugins
+
+# todo: there is also FIF_LOAD_NOPIXELS,
+# but perhaps that should be used with get_meta_data.
+
+class FreeimageBmpFormat(FreeimageFormat):
+ """ A BMP format based on the Freeimage library.
+
+ This format supports grayscale, RGB and RGBA images.
+
+ Parameters for saving
+ ---------------------
+ compression : bool
+ Whether to compress the bitmap using RLE when saving. Default False.
+ It seems this does not always work, but who cares, you should use
+ PNG anyway.
+
+ """
+
+ class Writer(FreeimageFormat.Writer):
+ def _open(self, flags=0, compression=False):
+ # Build flags from kwargs
+ flags = int(flags)
+ if compression:
+ flags |= IO_FLAGS.BMP_SAVE_RLE
+ else:
+ flags |= IO_FLAGS.BMP_DEFAULT
+ # Act as usual, but with modified flags
+ return FreeimageFormat.Writer._open(self, flags)
+
+ def _append_data(self, im, meta):
+ if im.dtype in (np.float32, np.float64):
+ im = (im * 255).astype(np.uint8)
+ return FreeimageFormat.Writer._append_data(self, im, meta)
+
+
+class FreeimagePngFormat(FreeimageFormat):
+ """ A PNG format based on the Freeimage library.
+
+ This format supports grayscale, RGB and RGBA images.
+
+ Parameters for reading
+ ----------------------
+ ignoregamma : bool
+ Avoid gamma correction. Default False.
+
+ Parameters for saving
+ ---------------------
+ compression : {0, 1, 6, 9}
+ The compression factor. Higher factors result in more
+ compression at the cost of speed. Note that PNG compression is
+ always lossless. Default 9.
+ quantize : int
+ If specified, turn the given RGB or RGBA image in a paletted image
+ for more efficient storage. The value should be between 2 and 256.
+ If the value of 0 the image is not quantized.
+ interlaced : bool
+ Save using Adam7 interlacing. Default False.
+ """
+
+ class Reader(FreeimageFormat.Reader):
+ def _open(self, flags=0, ignoregamma=False):
+ # Build flags from kwargs
+ flags = int(flags)
+ if ignoregamma:
+ flags |= IO_FLAGS.PNG_IGNOREGAMMA
+ # Enter as usual, with modified flags
+ return FreeimageFormat.Reader._open(self, flags)
+
+ # --
+
+ class Writer(FreeimageFormat.Writer):
+ def _open(self, flags=0, compression=9, quantize=0, interlaced=False):
+ compression_map = {0: IO_FLAGS.PNG_Z_NO_COMPRESSION,
+ 1: IO_FLAGS.PNG_Z_BEST_SPEED,
+ 6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION,
+ 9: IO_FLAGS.PNG_Z_BEST_COMPRESSION, }
+ # Build flags from kwargs
+ flags = int(flags)
+ if interlaced:
+ flags |= IO_FLAGS.PNG_INTERLACED
+ try:
+ flags |= compression_map[compression]
+ except KeyError:
+ raise ValueError('Png compression must be 0, 1, 6, or 9.')
+ # Act as usual, but with modified flags
+ return FreeimageFormat.Writer._open(self, flags)
+
+ def _append_data(self, im, meta):
+ if im.dtype in (np.float32, np.float64):
+ im = (im * 255).astype(np.uint8)
+ FreeimageFormat.Writer._append_data(self, im, meta)
+ # Quantize?
+ q = int(self.request.kwargs.get('quantize', False))
+ if not q:
+ pass
+ elif not (im.ndim == 3 and im.shape[-1] == 3):
+ raise ValueError('Can only quantize RGB images')
+ elif q < 2 or q > 256:
+ raise ValueError('PNG quantize param must be 2..256')
+ else:
+ bm = self._bm.quantize(0, q)
+ self._bm.close()
+ self._bm = bm
+
+
+class FreeimageJpegFormat(FreeimageFormat):
+ """ A JPEG format based on the Freeimage library.
+
+ This format supports grayscale and RGB images.
+
+ Parameters for reading
+ ----------------------
+ exifrotate : bool
+ Automatically rotate the image according to the exif flag.
+ Default True. If 2 is given, do the rotation in Python instead
+ of freeimage.
+ quickread : bool
+ Read the image more quickly, at the expense of quality.
+ Default False.
+
+ Parameters for saving
+ ---------------------
+ quality : scalar
+ The compression factor of the saved image (1..100), higher
+ numbers result in higher quality but larger file size. Default 75.
+ progressive : bool
+ Save as a progressive JPEG file (e.g. for images on the web).
+ Default False.
+ optimize : bool
+ On saving, compute optimal Huffman coding tables (can reduce a
+ few percent of file size). Default False.
+ baseline : bool
+ Save basic JPEG, without metadata or any markers. Default False.
+
+ """
+
+ class Reader(FreeimageFormat.Reader):
+ def _open(self, flags=0, exifrotate=True, quickread=False):
+ # Build flags from kwargs
+ flags = int(flags)
+ if exifrotate and exifrotate != 2:
+ flags |= IO_FLAGS.JPEG_EXIFROTATE
+ if not quickread:
+ flags |= IO_FLAGS.JPEG_ACCURATE
+ # Enter as usual, with modified flags
+ return FreeimageFormat.Reader._open(self, flags)
+
+ def _get_data(self, index):
+ im, meta = FreeimageFormat.Reader._get_data(self, index)
+ im = self._rotate(im, meta)
+ return im, meta
+
+ def _rotate(self, im, meta):
+ """ Use Orientation information from EXIF meta data to
+ orient the image correctly. Freeimage is also supposed to
+ support that, and I am pretty sure it once did, but now it
+ does not, so let's just do it in Python.
+ Edit: and now it works again, just leave in place as a fallback.
+ """
+ if self.request.kwargs.get('exifrotate', None) == 2:
+ try:
+ ori = meta['EXIF_MAIN']['Orientation']
+ except KeyError: # pragma: no cover
+ pass # Orientation not available
+ else: # pragma: no cover - we cannot touch all cases
+ # www.impulseadventure.com/photo/exif-orientation.html
+ if ori in [1, 2]:
+ pass
+ if ori in [3, 4]:
+ im = np.rot90(im, 2)
+ if ori in [5, 6]:
+ im = np.rot90(im, 3)
+ if ori in [7, 8]:
+ im = np.rot90(im)
+ if ori in [2, 4, 5, 7]: # Flipped cases (rare)
+ im = np.fliplr(im)
+ return im
+
+ # --
+
+ class Writer(FreeimageFormat.Writer):
+ def _open(self, flags=0, quality=75, progressive=False, optimize=False,
+ baseline=False):
+ # Test quality
+ quality = int(quality)
+ if quality < 1 or quality > 100:
+ raise ValueError('JPEG quality should be between 1 and 100.')
+ # Build flags from kwargs
+ flags = int(flags)
+ flags |= quality
+ if progressive:
+ flags |= IO_FLAGS.JPEG_PROGRESSIVE
+ if optimize:
+ flags |= IO_FLAGS.JPEG_OPTIMIZE
+ if baseline:
+ flags |= IO_FLAGS.JPEG_BASELINE
+ # Act as usual, but with modified flags
+ return FreeimageFormat.Writer._open(self, flags)
+
+ def _append_data(self, im, meta):
+ if im.ndim == 3 and im.shape[-1] == 4:
+ raise IOError('JPEG does not support alpha channel.')
+ if im.dtype in (np.float32, np.float64):
+ im = (im * 255).astype(np.uint8)
+ return FreeimageFormat.Writer._append_data(self, im, meta)
+
+
+## Create the formats
+
+SPECIAL_CLASSES = {'jpeg': FreeimageJpegFormat,
+ 'png': FreeimagePngFormat,
+ 'bmp': FreeimageBmpFormat,
+ 'gif': None, # defined in freeimagemulti
+ 'ico': None, # defined in freeimagemulti
+ 'mng': None, # defined in freeimagemulti
+ }
+
+
+def create_freeimage_formats():
+
+ # Freeimage available?
+ if fi is None: # pragma: no cover
+ return
+
+ # Init
+ lib = fi._lib
+
+ # Create formats
+ for i in range(lib.FreeImage_GetFIFCount()):
+ if lib.FreeImage_IsPluginEnabled(i):
+ # Get info
+ name = lib.FreeImage_GetFormatFromFIF(i).decode('ascii')
+ des = lib.FreeImage_GetFIFDescription(i).decode('ascii')
+ ext = lib.FreeImage_GetFIFExtensionList(i).decode('ascii')
+ # Get class for format
+ FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat)
+ if FormatClass:
+ # Create Format and add
+ format = FormatClass(name, des, ext, FormatClass._modes)
+ format._fif = i
+ formats.add_format(format)
+
+create_freeimage_formats()
diff --git a/imageio/plugins/freeimagemulti.py b/imageio/plugins/freeimagemulti.py
new file mode 100644
index 0000000..9446513
--- /dev/null
+++ b/imageio/plugins/freeimagemulti.py
@@ -0,0 +1,303 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Plugin for multi-image freeimafe formats, like animated GIF and ico.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format
+from ._freeimage import fi, IO_FLAGS
+from .freeimage import FreeimageFormat
+
+
+class FreeimageMulti(FreeimageFormat):
+ """ Base class for freeimage formats that support multiple images.
+ """
+
+ _modes = 'iI'
+ _fif = -1
+
+ class Reader(Format.Reader):
+ def _open(self, flags=0):
+ flags = int(flags)
+ # Create bitmap
+ self._bm = fi.create_multipage_bitmap(self.request.filename,
+ self.format.fif, flags)
+ self._bm.load_from_filename(self.request.get_local_filename())
+
+ def _close(self):
+ self._bm.close()
+
+ def _get_length(self):
+ return len(self._bm)
+
+ def _get_data(self, index):
+ sub = self._bm.get_page(index)
+ try:
+ return sub.get_image_data(), sub.get_meta_data()
+ finally:
+ sub.close()
+
+ def _get_meta_data(self, index):
+ index = index or 0
+ if index < 0 or index >= len(self._bm):
+ raise IndexError()
+ sub = self._bm.get_page(index)
+ try:
+ return sub.get_meta_data()
+ finally:
+ sub.close()
+
+ # --
+
+ class Writer(FreeimageFormat.Writer):
+
+ def _open(self, flags=0):
+ # Set flags
+ self._flags = flags = int(flags)
+ # Instantiate multi-page bitmap
+ self._bm = fi.create_multipage_bitmap(self.request.filename,
+ self.format.fif, flags)
+ self._bm.save_to_filename(self.request.get_local_filename())
+
+ def _close(self):
+ # Close bitmap
+ self._bm.close()
+
+ def _append_data(self, im, meta):
+ # Prepare data
+ if im.ndim == 3 and im.shape[-1] == 1:
+ im = im[:, :, 0]
+ if im.dtype in (np.float32, np.float64):
+ im = (im * 255).astype(np.uint8)
+ # Create sub bitmap
+ sub1 = fi.create_bitmap(self._bm._filename, self.format.fif)
+ # Let subclass add data to bitmap, optionally return new
+ sub2 = self._append_bitmap(im, meta, sub1)
+ # Add
+ self._bm.append_bitmap(sub2)
+ sub2.close()
+ if sub1 is not sub2:
+ sub1.close()
+
+ def _append_bitmap(self, im, meta, bitmap):
+ # Set data
+ bitmap.allocate(im)
+ bitmap.set_image_data(im)
+ bitmap.set_meta_data(meta)
+ # Return that same bitmap
+ return bitmap
+
+ def _set_meta_data(self, meta):
+ pass # ignore global meta data
+
+
+class MngFormat(FreeimageMulti):
+ """ An Mng format based on the Freeimage library.
+
+ Read only. Seems broken.
+ """
+
+ _fif = 6
+
+ def _can_save(self, request): # pragma: no cover
+ return False
+
+
+class IcoFormat(FreeimageMulti):
+ """ An ICO format based on the Freeimage library.
+
+ This format supports grayscale, RGB and RGBA images.
+
+ Parameters for reading
+ ----------------------
+ makealpha : bool
+ Convert to 32-bit and create an alpha channel from the AND-
+ mask when loading. Default False. Note that this returns wrong
+ results if the image was already RGBA.
+
+ """
+
+ _fif = 1
+
+ class Reader(FreeimageMulti.Reader):
+ def _open(self, flags=0, makealpha=False):
+ # Build flags from kwargs
+ flags = int(flags)
+ if makealpha:
+ flags |= IO_FLAGS.ICO_MAKEALPHA
+ return FreeimageMulti.Reader._open(self, flags)
+
+
+class GifFormat(FreeimageMulti):
+ """ A format for reading and writing static and animated GIF, based
+ on the Freeimage library.
+
+ Images read with this format are always RGBA. Currently,
+ the alpha channel is ignored when saving RGB images with this
+ format.
+
+ Parameters for reading
+ ----------------------
+ playback : bool
+ 'Play' the GIF to generate each frame (as 32bpp) instead of
+ returning raw frame data when loading. Default True.
+
+ Parameters for saving
+ ---------------------
+ loop : int
+ The number of iterations. Default 0 (meaning loop indefinitely)
+ duration : {float, list}
+ The duration (in seconds) of each frame. Either specify one value
+ that is used for all frames, or one value for each frame.
+ Default 0.1
+ palettesize : int
+ The number of colors to quantize the image to. Is rounded to
+ the nearest power of two. Default 256.
+ quantizer : {'wu', 'nq'}
+ The quantization algorithm:
+ * wu - Wu, Xiaolin, Efficient Statistical Computations for
+ Optimal Color Quantization
+ * nq (neuqant) - Dekker A. H., Kohonen neural networks for
+ optimal color quantization
+ subrectangles : bool
+ If True, will try and optimize the GIG by storing only the
+ rectangular parts of each frame that change with respect to the
+ previous. Unfortunately, this option seems currently broken
+ because FreeImage does not handle DisposalMethod correctly.
+ Default False.
+ """
+
+ _fif = 25
+
+ class Reader(FreeimageMulti.Reader):
+
+ def _open(self, flags=0, playback=True):
+ # Build flags from kwargs
+ flags = int(flags)
+ if playback:
+ flags |= IO_FLAGS.GIF_PLAYBACK
+ FreeimageMulti.Reader._open(self, flags)
+
+ def _get_data(self, index):
+ im, meta = FreeimageMulti.Reader._get_data(self, index)
+ # im = im[:, :, :3] # Drop alpha channel
+ return im, meta
+
+ # -- writer
+
+ class Writer(FreeimageMulti.Writer):
+
+ # todo: subrectangles
+ # todo: global palette
+
+ def _open(self, flags=0, loop=0, duration=0.1, palettesize=256,
+ quantizer='Wu', subrectangles=False):
+ # Check palettesize
+ if palettesize < 2 or palettesize > 256:
+ raise ValueError('PNG quantize param must be 2..256')
+ if palettesize not in [2, 4, 8, 16, 32, 64, 128, 256]:
+ palettesize = 2 ** int(np.log2(128) + 0.999)
+ print('Warning: palettesize (%r) modified to a factor of '
+ 'two between 2-256.' % palettesize)
+ self._palettesize = palettesize
+ # Check quantizer
+ self._quantizer = {'wu': 0, 'nq': 1}.get(quantizer.lower(), None)
+ if self._quantizer is None:
+ raise ValueError('Invalid quantizer, must be "wu" or "nq".')
+ # Check frametime
+ if isinstance(duration, list):
+ self._frametime = [int(1000 * d) for d in duration]
+ elif isinstance(duration, (float, int)):
+ self._frametime = [int(1000 * duration)]
+ else:
+ raise ValueError('Invalid value for duration: %r' % duration)
+ # Check subrectangles
+ self._subrectangles = bool(subrectangles)
+ self._prev_im = None
+ # Init
+ FreeimageMulti.Writer._open(self, flags)
+ # Set global meta data
+ self._meta = {}
+ self._meta['ANIMATION'] = {
+ # 'GlobalPalette': np.array([0]).astype(np.uint8),
+ 'Loop': np.array([loop]).astype(np.uint32),
+ #'LogicalWidth': np.array([x]).astype(np.uint16),
+ #'LogicalHeight': np.array([x]).astype(np.uint16),
+ }
+
+ def _append_bitmap(self, im, meta, bitmap):
+ # Prepare meta data
+ meta = meta.copy()
+ meta_a = meta['ANIMATION'] = {}
+ # Set frame time
+ index = len(self._bm)
+ if index < len(self._frametime):
+ ft = self._frametime[index]
+ else:
+ ft = self._frametime[-1]
+ meta_a['FrameTime'] = np.array([ft]).astype(np.uint32)
+ # If this is the first frame, assign it our "global" meta data
+ if len(self._bm) == 0:
+ meta.update(self._meta)
+
+ # Check array
+ if im.ndim == 3 and im.shape[-1] == 4:
+ im = im[:, :, :3]
+ # Process subrectangles
+ im_uncropped = im
+ if self._subrectangles and self._prev_im is not None:
+ im, xy = self._get_sub_rectangles(self._prev_im, im)
+ meta_a['DisposalMethod'] = np.array([1]).astype(np.uint8)
+ meta_a['FrameLeft'] = np.array([xy[0]]).astype(np.uint16)
+ meta_a['FrameTop'] = np.array([xy[1]]).astype(np.uint16)
+ self._prev_im = im_uncropped
+ # Set image data
+ sub2 = sub1 = bitmap
+ sub1.allocate(im)
+ sub1.set_image_data(im)
+ # Quantize it if its RGB
+ if im.ndim == 3 and im.shape[-1] == 3:
+ sub2 = sub1.quantize(self._quantizer, self._palettesize)
+ # If single image, omit animation data
+ if self.request.mode[1] == 'i':
+ del meta['ANIMATION']
+ # Set meta data and return
+ sub2.set_meta_data(meta)
+ return sub2
+
+ def _get_sub_rectangles(self, prev, im):
+ """
+ Calculate the minimal rectangles that need updating each frame.
+ Returns a two-element tuple containing the cropped images and a
+ list of x-y positions.
+ """
+ # Get difference, sum over colors
+ diff = np.abs(im - prev)
+ if diff.ndim == 3:
+ diff = diff.sum(2)
+ # Get begin and end for both dimensions
+ X = np.argwhere(diff.sum(0))
+ Y = np.argwhere(diff.sum(1))
+ # Get rect coordinates
+ if X.size and Y.size:
+ x0, x1 = X[0], X[-1]+1
+ y0, y1 = Y[0], Y[-1]+1
+ else: # No change ... make it minimal
+ x0, x1 = 0, 2
+ y0, y1 = 0, 2
+ # Cut out and return
+ return im[y0:y1, x0:x1], (int(x0), int(y0))
+
+
+# formats.add_format(MngFormat('MNG', 'Multiple network graphics',
+# '.mng', 'iI'))
+formats.add_format(IcoFormat('ICO', 'Windows icon',
+ '.ico', 'iI'))
+formats.add_format(GifFormat('GIF', 'Static and animated gif',
+ '.gif', 'iI'))
diff --git a/imageio/plugins/npz.py b/imageio/plugins/npz.py
new file mode 100644
index 0000000..da52629
--- /dev/null
+++ b/imageio/plugins/npz.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" Storage of image data in npz format. Not a great format, but at least
+it supports volumetric data. And its less than 100 lines.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format
+
+
+class NpzFormat(Format):
+ """ NPZ is a file format by numpy that provides storage of array
+ data using gzip compression. This imageio plugin supports data of any
+ shape, and also supports multiple images per file.
+
+ However, the npz format does not provide streaming; all data is
+ read/saved at once. Further, there is no support for meta data.
+
+ Beware that the numpy npz format has a bug on a certain combination
+ of Python 2.7 and numpy, which can cause the resulting files to
+ become unreadable on Python 3. Also, this format is not available
+ on Pypy.
+
+ Parameters for reading
+ ----------------------
+ None
+
+ Parameters for saving
+ ---------------------
+ None
+ """
+
+ def _can_read(self, request):
+ if request.filename.lower().endswith('.npz'):
+ return True # We support any kind of image data
+ else:
+ return False
+
+ def _can_save(self, request):
+ if request.filename.lower().endswith('.npz'):
+ return True # We support any kind of image data
+ else:
+ return False
+
+ # -- reader
+
+ class Reader(Format.Reader):
+
+ def _open(self):
+ # Load npz file, which provides another file like object
+ self._npz = np.load(self.request.get_file())
+ assert isinstance(self._npz, np.lib.npyio.NpzFile)
+ # Get list of names, ordered by name, but smarter
+ sorter = lambda x: x.split('_')[-1]
+ self._names = sorted(self._npz.files, key=sorter)
+
+ def _close(self):
+ self._npz.close()
+
+ def _get_length(self):
+ return len(self._names)
+
+ def _get_data(self, index):
+ # Get data
+ if index < 0 or index >= len(self._names):
+ raise IndexError('Index out of range while reading from nzp')
+ im = self._npz[self._names[index]]
+ # Return array and empty meta data
+ return im, {}
+
+ def _get_meta_data(self, index):
+ # Get the meta data for the given index
+ raise RuntimeError('The npz format does not support meta data.')
+
+ # -- writer
+
+ class Writer(Format.Writer):
+
+ def _open(self):
+ # Npz is not such a great format. We cannot stream to the file.
+ # So we remember all images and write them to file at the end.
+ self._images = []
+
+ def _close(self):
+ # Write everything
+ np.savez_compressed(self.request.get_file(), *self._images)
+
+ def _append_data(self, im, meta):
+ self._images.append(im) # discart meta data
+
+ def set_meta_data(self, meta):
+ raise RuntimeError('The npz format does not support meta data.')
+
+
+# Register
+format = NpzFormat('npz', "Numpy's compressed array format", 'npz', 'iIvV')
+formats.add_format(format)
diff --git a/imageio/plugins/swf.py b/imageio/plugins/swf.py
new file mode 100644
index 0000000..9f31f5e
--- /dev/null
+++ b/imageio/plugins/swf.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# imageio is distributed under the terms of the (new) BSD License.
+
+""" SWF plugin. Most of the actual work is done in _swf.py.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import os
+import zlib
+from io import BytesIO
+
+import numpy as np
+
+from imageio import formats
+from imageio.core import Format, read_n_bytes
+
+from . import _swf
+
+
+class SWFFormat(Format):
+ """ Shockwave flash (SWF) is a media format designed for rich and
+ interactive animations. This plugin makes use of this format to
+ store a series of images in a lossless format with good compression
+ (zlib). The resulting images can be shown as an animation using
+ a flash player (such as the browser).
+
+ SWF stores images in RGBA format. RGB or grayscale images are
+ automatically converted. SWF does not support meta data.
+
+ Parameters for reading
+ ----------------------
+ loop : bool
+ If True, the video will rewind as soon as a frame is requested
+ beyond the last frame. Otherwise, IndexError is raised. Default False.
+
+ Parameters for saving
+ ---------------------
+ fps : int
+ The speed to play the animation. Default 12.
+ loop : bool
+ If True, add a tag to the end of the file to play again from
+ the first frame. Most flash players will then play the movie
+ in a loop. Note that the imageio SWF Reader does not check this
+ tag. Default True.
+ html : bool
+ If the output is a file on the file system, write an html file
+ (in HTML5) that shows the animation. Default False.
+ compress : bool
+ Whether to compress the swf file. Default False. You probably don't
+ want to use this. This does not decrease the file size since
+ the images are already compressed. It will result in slower
+ read and write time. The only purpose of this feature is to
+ create compressed SWF files, so that we can test the
+ functionality to read them.
+ """
+
+ def _can_read(self, request):
+ if request.mode[1] in (self.modes + '?'):
+ tmp = request.firstbytes[0:3].decode('ascii', 'ignore')
+ if tmp in ('FWS', 'CWS'):
+ return True
+
+ def _can_save(self, request):
+ if request.mode[1] in (self.modes + '?'):
+ for ext in self.extensions:
+ if request.filename.endswith('.' + ext):
+ return True
+
+ # -- reader
+
+ class Reader(Format.Reader):
+
+ def _open(self, loop=False):
+
+ self._arg_loop = bool(loop)
+
+ self._fp = self.request.get_file()
+
+ # Check file ...
+ tmp = self.request.firstbytes[0:3].decode('ascii', 'ignore')
+ if tmp == 'FWS':
+ pass # OK
+ elif tmp == 'CWS':
+ # Compressed, we need to decompress
+ bb = self._fp.read()
+ bb = bb[:8] + zlib.decompress(bb[8:])
+ # Wrap up in a file object
+ self._fp = BytesIO(bb)
+ else:
+ raise IOError('This does not look like a valid SWF file')
+
+ # Skip first bytes. This also tests support got seeking ...
+ try:
+ self._fp.seek(8)
+ self._streaming_mode = False
+ except Exception:
+ self._streaming_mode = True
+ self._fp_read(8)
+
+ # Skip header
+ # Note that the number of frames is there, which we could
+ # potentially use, but the number of frames does not necessarily
+ # correspond to the number of images.
+ nbits = _swf.bits2int(self._fp_read(1), 5)
+ nbits = 5 + nbits * 4
+ Lrect = nbits / 8.0
+ if Lrect % 1:
+ Lrect += 1
+ Lrect = int(Lrect)
+ self._fp_read(Lrect + 3)
+
+ # Now the rest is basically tags ...
+ self._imlocs = [] # tuple (loc, sze, T, L1)
+ if not self._streaming_mode:
+ # Collect locations of frame, while skipping through the data
+ # This does not read any of the tag *data*.
+ try:
+ while True:
+ isimage, sze, T, L1 = self._read_one_tag()
+ loc = self._fp.tell()
+ if isimage:
+ # Still need to check if the format is right
+ format = ord(self._fp_read(3)[2:])
+ if format == 5: # RGB or RGBA lossless
+ self._imlocs.append((loc, sze, T, L1))
+ self._fp.seek(loc + sze) # Skip over tag
+ except IndexError:
+ pass # done reading
+
+ def _fp_read(self, n):
+ return read_n_bytes(self._fp, n)
+
+ def _close(self):
+ pass
+
+ def _get_length(self):
+ if self._streaming_mode:
+ return np.inf
+ else:
+ return len(self._imlocs)
+
+ def _get_data(self, index):
+ # Check index
+ if index < 0:
+ raise IndexError('Index in swf file must be > 0')
+ if not self._streaming_mode:
+ if self._arg_loop and self._imlocs:
+ index = index % len(self._imlocs)
+ if index >= len(self._imlocs):
+ raise IndexError('Index out of bounds')
+
+ if self._streaming_mode:
+ # Walk over tags until we find an image
+ while True:
+ isimage, sze, T, L1 = self._read_one_tag()
+ bb = self._fp_read(sze) # always read data
+ if isimage:
+ im = _swf.read_pixels(bb, 0, T, L1) # can be None
+ if im is not None:
+ return im, {}
+
+ else:
+ # Go to corresponding location, read data, and convert to image
+ loc, sze, T, L1 = self._imlocs[index]
+ self._fp.seek(loc)
+ bb = self._fp_read(sze)
+ # Read_pixels should return ndarry, since we checked format
+ im = _swf.read_pixels(bb, 0, T, L1)
+ return im, {}
+
+ def _read_one_tag(self):
+ """
+ Return (True, loc, size, T, L1) if an image that we can read.
+ Return (False, loc, size, T, L1) if any other tag.
+ """
+
+ # Get head
+ head = self._fp_read(6)
+ if not head: # pragma: no cover
+ raise IndexError('Reached end of swf movie')
+
+ # Determine type and length
+ T, L1, L2 = _swf.get_type_and_len(head)
+ if not L2: # pragma: no cover
+ raise RuntimeError('Invalid tag length, could not proceed')
+
+ # Read data
+ isimage = False
+ sze = L2 - 6
+ #bb = self._fp_read(L2 - 6)
+
+ # Parse tag
+ if T == 0:
+ raise IndexError('Reached end of swf movie')
+ elif T in [20, 36]:
+ isimage = True
+ #im = _swf.read_pixels(bb, 0, T, L1) # can be None
+ elif T in [6, 21, 35, 90]: # pragma: no cover
+ print('Ignoring JPEG image: cannot read JPEG.')
+ else:
+ pass # Not an image tag
+
+ # Done. Return image. Can be None
+ #return im
+ return isimage, sze, T, L1
+
+ def _get_meta_data(self, index):
+ return {} # This format does not support meta data
+
+ # -- writer
+
+ class Writer(Format.Writer):
+
+ def _open(self, fps=12, loop=True, html=False, compress=False):
+ self._arg_fps = int(fps)
+ self._arg_loop = bool(loop)
+ self._arg_html = bool(html)
+ self._arg_compress = bool(compress)
+
+ self._fp = self.request.get_file()
+ self._framecounter = 0
+ self._framesize = (100, 100)
+
+ # For compress, we use an in-memory file object
+ if self._arg_compress:
+ self._fp_real = self._fp
+ self._fp = BytesIO()
+
+ def _close(self):
+ self._complete()
+ # Get size of (uncompressed) file
+ sze = self._fp.tell()
+ # set nframes, this is in the potentially compressed region
+ self._fp.seek(self._location_to_save_nframes)
+ self._fp.write(_swf.int2uint16(self._framecounter))
+ # Compress body?
+ if self._arg_compress:
+ bb = self._fp.getvalue()
+ self._fp = self._fp_real
+ self._fp.write(bb[:8])
+ self._fp.write(zlib.compress(bb[8:]))
+ sze = self._fp.tell() # renew sze value
+ # set size
+ self._fp.seek(4)
+ self._fp.write(_swf.int2uint32(sze))
+ self._fp = None # Disable
+
+ # Write html?
+ if self._arg_html and os.path.isfile(self.request.filename):
+ dirname, fname = os.path.split(self.request.filename)
+ filename = os.path.join(dirname, fname[:-4] + '.html')
+ w, h = self._framesize
+ html = HTML % (fname, w, h, fname)
+ with open(filename, 'wb') as f:
+ f.write(html.encode('utf-8'))
+
+ def _write_header(self, framesize, fps):
+ self._framesize = framesize
+ # Called as soon as we know framesize; when we get first frame
+ bb = b''
+ bb += 'FC'[self._arg_compress].encode('ascii')
+ bb += 'WS'.encode('ascii') # signature bytes
+ bb += _swf.int2uint8(8) # version
+ bb += '0000'.encode('ascii') # FileLength (leave open for now)
+ bb += _swf.Tag().make_rect_record(0, framesize[0], 0,
+ framesize[1]).tobytes()
+ bb += _swf.int2uint8(0) + _swf.int2uint8(fps) # FrameRate
+ self._location_to_save_nframes = len(bb)
+ bb += '00'.encode('ascii') # nframes (leave open for now)
+ self._fp.write(bb)
+
+ # Write some initial tags
+ taglist = _swf.FileAttributesTag(), _swf.SetBackgroundTag(0, 0, 0)
+ for tag in taglist:
+ self._fp.write(tag.get_tag())
+
+ def _complete(self):
+ # What if no images were saved?
+ if not self._framecounter:
+ self._write_header((10, 10), self._arg_fps)
+ # Write stop tag if we do not loop
+ if not self._arg_loop:
+ self._fp.write(_swf.DoActionTag('stop').get_tag())
+ # finish with end tag
+ self._fp.write('\x00\x00'.encode('ascii'))
+
+ def _append_data(self, im, meta):
+ # Correct shape and type
+ if im.ndim == 3 and im.shape[-1] == 1:
+ im = im[:, :, 0]
+ if im.dtype in (np.float32, np.float64):
+ im = (im * 255).astype(np.uint8)
+ # Get frame size
+ wh = im.shape[1], im.shape[0]
+ # Write header on first frame
+ isfirstframe = False
+ if self._framecounter == 0:
+ isfirstframe = True
+ self._write_header(wh, self._arg_fps)
+ # Create tags
+ bm = _swf.BitmapTag(im)
+ sh = _swf.ShapeTag(bm.id, (0, 0), wh)
+ po = _swf.PlaceObjectTag(1, sh.id, move=(not isfirstframe))
+ sf = _swf.ShowFrameTag()
+ # Write tags
+ for tag in [bm, sh, po, sf]:
+ self._fp.write(tag.get_tag())
+ self._framecounter += 1
+
+ def set_meta_data(self, meta):
+ pass
+
+
+HTML = """
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Show Flash animation %s</title>
+</head>
+<body>
+ <embed width="%i" height="%i" src="%s">
+</html>
+"""
+
+# Register. You register an *instance* of a Format class. Here specify:
+format = SWFFormat('swf', # shot name
+ 'Shockwave flash', # one line descr.
+ '.swf', # list of extensions as a space separated string
+ 'I' # modes, characters in iIvV
+ )
+formats.add_format(format)
diff --git a/imageio/testing.py b/imageio/testing.py
new file mode 100644
index 0000000..41ac91a
--- /dev/null
+++ b/imageio/testing.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2014, imageio contributors
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+""" Functionality used for testing. This code itself is not covered in tests.
+"""
+
+from __future__ import absolute_import, print_function, division
+
+import os
+import sys
+import inspect
+import shutil
+import atexit
+
+import pytest
+from _pytest import runner
+
+# Get root dir
+THIS_DIR = os.path.abspath(os.path.dirname(__file__))
+ROOT_DIR = THIS_DIR
+for i in range(9):
+ ROOT_DIR = os.path.dirname(ROOT_DIR)
+ if os.path.isfile(os.path.join(ROOT_DIR, '.gitignore')):
+ break
+
+
+## Functions to use in tests
+
+def run_tests_if_main(show_coverage=False):
+ """ Run tests in a given file if it is run as a script
+
+ Coverage is reported for running this single test. Set show_coverage to
+ launch the report in the web browser.
+ """
+ local_vars = inspect.currentframe().f_back.f_locals
+ if not local_vars.get('__name__', '') == '__main__':
+ return
+ # we are in a "__main__"
+ os.chdir(ROOT_DIR)
+ fname = str(local_vars['__file__'])
+ _clear_imageio()
+ _enable_faulthandler()
+ pytest.main('-v -x --color=yes --cov imageio '
+ '--cov-config .coveragerc --cov-report html %s' % repr(fname))
+ if show_coverage:
+ import webbrowser
+ fname = os.path.join(ROOT_DIR, 'htmlcov', 'index.html')
+ webbrowser.open_new_tab(fname)
+
+
+_the_test_dir = None
+
+
+def get_test_dir():
+ global _the_test_dir
+ if _the_test_dir is None:
+ # Define dir
+ from imageio.core import appdata_dir
+ _the_test_dir = os.path.join(appdata_dir('imageio'), 'testdir')
+ # Clear and create it now
+ clean_test_dir(True)
+ os.makedirs(_the_test_dir)
+ # And later
+ atexit.register(clean_test_dir)
+ return _the_test_dir
+
+
+def clean_test_dir(strict=False):
+ if os.path.isdir(_the_test_dir):
+ try:
+ shutil.rmtree(_the_test_dir)
+ except Exception:
+ if strict:
+ raise
+
+
+## Functions to use from make
+
+def test_unit(cov_report='term'):
+ """ Run all unit tests. Returns exit code.
+ """
+ orig_dir = os.getcwd()
+ os.chdir(ROOT_DIR)
+ try:
+ _clear_imageio()
+ _enable_faulthandler()
+ return pytest.main('-v --cov imageio --cov-config .coveragerc '
+ '--cov-report %s tests' % cov_report)
+ finally:
+ os.chdir(orig_dir)
+ import imageio
+ print('Tests were performed on', str(imageio))
+
+
+def test_style():
+ """ Test style using flake8
+ """
+ # Test if flake is there
+ try:
+ from flake8.main import main # noqa
+ except ImportError:
+ print('Skipping flake8 test, flake8 not installed')
+ return
+
+ # Reporting
+ print('Running flake8 on %s' % ROOT_DIR)
+ sys.stdout = FileForTesting(sys.stdout)
+
+ # Init
+ ignores = ['E226', 'E241', 'E265', 'W291', 'W293']
+ fail = False
+ count = 0
+
+ # Iterate over files
+ for dir, dirnames, filenames in os.walk(ROOT_DIR):
+ dir = os.path.relpath(dir, ROOT_DIR)
+ # Skip this dir?
+ exclude_dirs = set(['.git', 'docs', 'build', 'dist', '__pycache__'])
+ if exclude_dirs.intersection(dir.split(os.path.sep)):
+ continue
+ # Check all files ...
+ for fname in filenames:
+ if fname.endswith('.py'):
+ # Get test options for this file
+ filename = os.path.join(ROOT_DIR, dir, fname)
+ skip, extra_ignores = _get_style_test_options(filename)
+ if skip:
+ continue
+ # Test
+ count += 1
+ thisfail = _test_style(filename, ignores + extra_ignores)
+ if thisfail:
+ fail = True
+ print('----')
+ sys.stdout.flush()
+
+ # Report result
+ sys.stdout.revert()
+ if not count:
+ raise RuntimeError(' Arg! flake8 did not check any files')
+ elif fail:
+ raise RuntimeError(' Arg! flake8 failed (checked %i files)' % count)
+ else:
+ print(' Hooray! flake8 passed (checked %i files)' % count)
+
+
+## Requirements
+
+def _enable_faulthandler():
+ """ Enable faulthandler (if we can), so that we get tracebacks
+ on segfaults.
+ """
+ try:
+ import faulthandler
+ faulthandler.enable()
+ print('Faulthandler enabled')
+ except Exception:
+ print('Could not enable faulthandler')
+
+
+def _clear_imageio():
+ # Remove ourselves from sys.modules to force an import
+ for key in list(sys.modules.keys()):
+ if key.startswith('imageio'):
+ del sys.modules[key]
+
+
+class FileForTesting(object):
+ """ Alternative to stdout that makes path relative to ROOT_DIR
+ """
+ def __init__(self, original):
+ self._original = original
+
+ def write(self, msg):
+ if msg.startswith(ROOT_DIR):
+ msg = os.path.relpath(msg, ROOT_DIR)
+ self._original.write(msg)
+ self._original.flush()
+
+ def flush(self):
+ self._original.flush()
+
+ def revert(self):
+ sys.stdout = self._original
+
+
+def _get_style_test_options(filename):
+ """ Returns (skip, ignores) for the specifies source file.
+ """
+ skip = False
+ ignores = []
+ text = open(filename, 'rb').read().decode('utf-8')
+ # Iterate over lines
+ for i, line in enumerate(text.splitlines()):
+ if i > 20:
+ break
+ if line.startswith('# styletest:'):
+ if 'skip' in line:
+ skip = True
+ elif 'ignore' in line:
+ words = line.replace(',', ' ').split(' ')
+ words = [w.strip() for w in words if w.strip()]
+ words = [w for w in words if
+ (w[1:].isnumeric() and w[0] in 'EWFCN')]
+ ignores.extend(words)
+ return skip, ignores
+
+
+def _test_style(filename, ignore):
+ """ Test style for a certain file.
+ """
+ if isinstance(ignore, (list, tuple)):
+ ignore = ','.join(ignore)
+
+ orig_dir = os.getcwd()
+ orig_argv = sys.argv
+
+ os.chdir(ROOT_DIR)
+ sys.argv[1:] = [filename]
+ sys.argv.append('--ignore=' + ignore)
+ try:
+ from flake8.main import main
+ main()
+ except SystemExit as ex:
+ if ex.code in (None, 0):
+ return False
+ else:
+ return True
+ finally:
+ os.chdir(orig_dir)
+ sys.argv[:] = orig_argv
+
+
+## Patching
+
+def pytest_runtest_call(item):
+ """ Variant of pytest_runtest_call() that stores traceback info for
+ postmortem debugging.
+ """
+ try:
+ runner.pytest_runtest_call_orig(item)
+ except Exception:
+ type, value, tb = sys.exc_info()
+ tb = tb.tb_next # Skip *this* frame
+ sys.last_type = type
+ sys.last_value = value
+ sys.last_traceback = tb
+ del tb # Get rid of it in this namespace
+ raise
+
+# Monkey-patch pytest
+if not runner.pytest_runtest_call.__module__.startswith('imageio'):
+ runner.pytest_runtest_call_orig = runner.pytest_runtest_call
+ runner.pytest_runtest_call = pytest_runtest_call
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..7a3d0c7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014, imageio contributors
+#
+# imageio is distributed under the terms of the (new) BSD License.
+# The full license can be found in 'license.txt'.
+
+# styletest: skip
+
+"""
+
+Before release:
+
+ * Run test suite on pypy (with numpy)
+ * Run test suite on Windows 32
+ * Run test suite on Windows 64
+ * Run test suite on OS X
+ * Write release notes
+ * Check if docs are still good
+
+Release:
+
+ * Increase __version__
+ * git tag the release
+ * Upload to Pypi:
+ * python setup.py register
+ * python setup.py sdist upload
+ * Update, build and upload conda package
+
+Add "-r testpypi" to use the test repo.
+
+After release:
+
+ * Set __version__ to dev
+ * Announce
+
+"""
+
+import os
+import sys
+from distutils.core import setup
+
+name = 'imageio'
+description = 'Library for reading and writing a wide range of image formats.'
+
+
+# Get version and docstring
+__version__ = None
+__doc__ = ''
+docStatus = 0 # Not started, in progress, done
+initFile = os.path.join(os.path.dirname(__file__), 'imageio', '__init__.py')
+for line in open(initFile).readlines():
+ if (line.startswith('__version__')):
+ exec(line.strip())
+ elif line.startswith('"""'):
+ if docStatus == 0:
+ docStatus = 1
+ line = line.lstrip('"')
+ elif docStatus == 1:
+ docStatus = 2
+ if docStatus == 1:
+ __doc__ += line
+
+# Template for long description. __doc__ gets inserted here
+long_description = """
+.. image:: https://travis-ci.org/imageio/imageio.svg?branch=master
+ :target: https://travis-ci.org/imageio/imageio'
+
+.. image:: https://coveralls.io/repos/imageio/imageio/badge.png?branch=master
+ :target: https://coveralls.io/r/imageio/imageio?branch=master
+
+__doc__
+
+Release notes: http://imageio.readthedocs.org/en/latest/releasenotes.html
+
+Example:
+
+.. code-block:: python:
+
+ >>> import imageio
+ >>> im = imageio.imread('astronaut.png')
+ >>> im.shape # im is a numpy array
+ (512, 512, 3)
+ >>> imageio.imsave('astronaut-gray.jpg', im[:, :, 0])
+
+See the `user API <http://imageio.readthedocs.org/en/latest/userapi.html>`_
+or `examples <http://imageio.readthedocs.org/en/latest/examples.html>`_
+for more information.
+"""
+
+setup(
+ name = name,
+ version = __version__,
+ author = 'imageio contributors',
+ author_email = 'almar.klein at gmail.com',
+ license = '(new) BSD',
+
+ url = 'http://imageio.github.io/',
+ download_url = 'http://pypi.python.org/pypi/imageio',
+ keywords = "image imread imsave io animation volume FreeImage ffmpeg",
+ description = description,
+ long_description = long_description.replace('__doc__', __doc__),
+
+ platforms = 'any',
+ provides = ['imageio'],
+ requires = ['numpy'],
+
+ packages = ['imageio', 'imageio.core', 'imageio.plugins'],
+ package_dir = {'imageio': 'imageio'},
+
+ classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Science/Research',
+ 'Intended Audience :: Education',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: POSIX',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ ],
+ )
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/imageio.git
More information about the debian-science-commits
mailing list