[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