# Creating imageio plugins

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 `Format` objects, and corresponding `Reader` and `Writer` classes. Each Format object represents an implementation to read/write 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 write `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 `Request` class.
> - Implement `_can_write(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.

If the plugin requires a binary download from the imageio-binaries repository, implement the `download` method (see e.g. the ffmpeg plugin). Make sure that the download directory base name matches the plugin name. Otherwise, the download and removal command line scripts (see __main__.py) might not work.

## Example / template plugin

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150

    # -*- coding: utf-8 -*-
    # 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 .. import formats
    from ..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 write a file. When
        explicitly asked to read, it will simply read the bytes. When
        explicitly asked to write, 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. It will first ask all formats
            # that *seem* to be able to read it whether they can. If none
            # can, it will ask the remaining formats if they can: the
            # extension might be missing, and this allows formats to provide
            # functionality for certain extensions, while giving preference
            # to other plugins.
            #
            # If a format says it can, it should live up to it. The format
            # would ideally check the request.firstbytes and look for a
            # header of some kind.
            #
            # 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 + "?"):
                if request.extension in self.extensions:
                    return True

        def _can_write(self, request):
            # This method is called when the format manager is searching
            # for a format to write a certain image. It will first ask all
            # formats that *seem* to be able to write it whether they can.
            # If none can, it will ask the remaining formats if they can.
            #
            # Return True if the format can do it.

            # In most cases, this code does suffice:
            if request.mode[1] in (self.modes + "?"):
                if request.extension in self.extensions:
                    return True

        # -- reader

        class Reader(Format.Reader):
            def _open(self, some_option=False, length=1):
                # 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()
                self._length = length  # passed as an arg in this case for testing
                self._data = None

            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 self._length

            def _get_data(self, index):
                # Return the data and meta data for the given index
                if index >= self._length:
                    raise IndexError("Image index %i > %i" % (index, self._length))
                # Read all bytes
                if self._data is None:
                    self._data = self._fp.read()
                # Put in a numpy array
                im = np.frombuffer(self._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 write 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 write 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.
        ".foobar .nonexistentext",  # list of extensions
        "iI",  # modes, characters in iIvV
    )
    formats.add_format(format)