IOFormat  IOF_v0.33.0_release
IOFormat Plugin System

IOFormat uses a system of plugin for all its reading and writing features. Actually, IOFormat alone does nothing, it mainly acts as a manager that redirects the requests to the fitted plugin. There are two families of plugins:

  • extractor plugins to decode formats
  • writer plugins to encode image views and output it.

Plugins are encapsulated in shared libraries that will be loaded at the initialization of IOFormat. Each shared library can declare up to one extractor plugin and up to one writer plugin. By default, they are searched in the folder "ioplugins", relative to the executable path. It can be manually set by using the IOFORMAT_PLUGINS_FOLDER environnment variable.

See Plugins chapter for a list of plugins natively available in IOFormat package.

Plugins loading

Extractor Plugins

Each extractor plugin declares the formats that it supports, and an extension mapping to these formats. For example, the plugin "Jpeg" tells that it supports the "JPEG" format and maps the "jpeg" and "jpg" extension to it. Some plugins supports multiple format, so this can become more complicated.

At the initialization of IOFormat, all the extractor plugins are loaded in lexicographic order. Yet, some plugins, tagged as fallbacks by IOFormat, will always be loaded last. These "fallback" plugins are tagged as such mainly because they offer to handle many format, but may not have sufficient features or performances. The loading order is important because it will etablish the sequence in wich plugins are called for opening a view using a specific format.

The default plugin order can be overriden via a YAML file, using the IOFORMAT_CONFIG_FILE environment variable. This file should describe a list for each format that should be customized. Formats not present in the configuration will be initialized as usual.

In this example, we use the config file to use ImageAccess first for "PNG" files, overriding its fallback behavior. For Tiff files, we force the use of Bioformat plugin. As ImageAccess suppports "TIFF" too, it will be used if Bioformat fails. All other formats will be initialized as in the default behaviour.

PNG:
- ImageAccess
- Png
- Bioformat
TIFF:
- Bioformat

The plugin order initialization, and the resulting format mapping can easily be viewed in the logs, at the LOG_INFO level. Here is an example of such a log:

<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Initialization of plugin Dicom v1.0
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Initialization of plugin Mrc v1.0
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Initialization of plugin Png v1.0
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Initialization of plugin Tiff v1.0
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Initialization of plugin ImageAccess v1.0
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format BMP plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format DICOM plugins order: Dicom -> ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format ECW plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format GATAN plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format JP2 plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format JP3D plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format JPG plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format MRC plugins order: Mrc -> ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format PGX plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format PNG plugins order: Png -> ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format PPM plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format SUN plugins order: ImageAccess
<IOFormat><Core>[12:27:36 +01:00] [info] [ioformat::ExtractorPluginSelector::initPlugins]: Format TIFF plugins order: Tiff -> ImageAccess

Writer Plugins

Writers plugins behave a lot like extractors ones, but they are not affected by the configuration file described before. The is currently no way to change the way each writer plugin is associated with a format.

Create a Custom Plugin

Custom plugins can be written in C++ only.

If you want to create your own Extractor or Writer plugin, you can by following the API contained in IOPluginAPI project:

IOPlugin APIs are versioned and each version of IOFormat is only compatible with one version of IOPLugin. All plugins developed with a different IOPlugin version of the one used by IOFormat won't be loaded.

See configuration to know the IOPLugin version compatible with this version of IOFormat.

Firstly you must add the IOPlugin Conan package to your plugin dependencies, so you would have the includes and logging libaries. You must link against this package using the classic Conan way. If you want your plugin to be used in IOFormat, it must be declared as a SHARED library in CMake.

add_library(${PROJECT_NAME} SHARED ${PLUGIN_SOURCES})
target_link_libraries(${PROJECT_NAME} CONAN_PKG::IOPlugin)

An IOFormat plugin must implement the matching plugin interface: ExtractorPlugin or WriterPlugin, depending on the type of plugin you want to make. When the plugin class is done, you must use one of the plugins macros in order to make the plugin usable by IOFormat's plugin system. These macros depend on the plugin type:

  • IOPLUGIN_DECLARE_EXTRACTOR_PLUGIN(PluginClass) for Extractor plugins.
  • IOPLUGIN_DECLARE_WRITER_PLUGIN(PluginClass) for Writer plugins.

The plugin class should be done in a source file (*.cpp or *.cxx) instead of a header file, because we only want to export the instance.

Example for an extractor plugin:

using namespace iolink;
class TestPlugin : public ExtractorPlugin
{
// override the interface's methods
};
IOPLUGIN_DECLARE_EXTRACTOR_PLUGIN(TestPlugin)

TestPlugin, in this example, is the entry point for the extractor plugin. It's up to this class methods openDataAccess and openFile to instanciate and return the appropriate view according to the format of the opened file.

Have a look on the code example provided with IOPLuginAPI.

e.g.: calling openDataAccess onto PNG plugin will require the instanciation of an object derived from ImageView, since PNG is only able to manage 2D Images. On the other hand, a TIFF plugin could possibly be either an object derived from ImageView or from MultiImageView, depending of the file content (a Tiff can contain either a volume, thus an ImageView, or a list of independant data, thus a MultiImageView)

Implementation guidelines

There are some guidelines common to the two types of plugins:

  • Never override the api method, it is already returning the API version of the IOplugin version you are using to create your plugin.
  • Logging methods have a default implementation, so if you do not override them, initLogger will do nothing, and logLevel will always return LogLevel::OFF.

Extractors

Factories

There are two different View factories in the ExtractorPlugin interface that you can implement in your custom plugin. First, openFile that take a path to the source file, and openDataAccess that take a DataAccess from which to decode from. The openDataAccess variant also take a format argument that enable to select the format to use to decode the accessor's data. This format is automatically selected using the path's extension in the openFile variant.

When one of these variants is not supported by your custom plugin, just make the method throw an Error describing that the feature is not supported.

Preliminary checks and operations

For both cases, a serie of checks and operations should be done immediately:

  • check the given format string parameter to adapt your extraction method to the current situation (i.e. when a plugin handles many different formats)
  • check the given path or dataAccess are valid (data must exist and be available for reading and most of the time for seeking in the case of streams)
  • check data content by starting to extract some basic information which will help to determine the type of View that your plugin should create and return:
    • type of the data (UINT8, UINT16, UINT32, ...)
    • number of frames (if supported by the format)
    • number of dimensions of the frame(s)
    • shape of the frame(s)
  • extract the information to fill the properties immediately (you should better extract these information as soon as possible, and not on the fly (when properties() method is called), for threadsafety reasons.
  • extract the tree of metadata (to do at opening, to avoid an extraction on the fly which could slow down the user application).

If any of these checks/operations fails, an exception will be raised.

Below table should help you to determine the kind of (existing) View to return:

View type case where this type of view should be created
ImageView data extracted by your plugin can be stored into a N-dimension array (e.g a 2D Image, a volume, a sequence of image...)
MultiImageView data extracted by your plugin corresponds to a set of images which may or may not have relation with each other (shape and type can be different)
LodImageView data extracted by your plugin corresponds to one multi-resolution image (many different resolutions of the same N-Dimension image)
ComposedView data extracted by your plugins are of miscellaneous types, which can be stored in a hierarchical order or not. (e.g. a zip file)

Up to you to return the view type corresponding the better to extracted data and which matches your needs.

Writers

Factories

There are two Writer factories in the WriterPlugin interface, all taking the source View as the first argument. The openFile one uses a path to specify the writer's output, and will infer the format to use from the file's extension. The openDataAccess variant use a DataAccess as output with its format specified with a format parameter.

When one of these variant is not supported by your custom plugin, just make the method throw an Error describing that the feature is not supported.

Input adaptation

Ideally, your custom writer should support all kind of View as entry, as long as it fits the underlying format. For example a plugin encoding to an image format could take some ImageView as input. In the image case, the underlying usually support one particular data layout, but a good plugin should accept different layouts, as long as they are easily adaptable, without damaging the content and without guessing the user intentions. For example, when a plugin uses a format that stores color data in an interleaved order, if the user gives a planar ImageView (one with a CHANNEL dimension) as input, it should adapt it, instead of throwing an error, saying that the view is unsupported. Another example is MultiImageView instances, that can be stacked to feed a volume or sequence format, if it is possible.

As long as there is one possibility to adapt the data to match the writer inputs, the plugin should try. But at the moment when there is more than one possibility to adapt the data, the plugin shall raise an exception to ask the user to make a decision.

Performances

In order to provide better features and performances, writers created by your custom plugin should not write all the output in one call. An approach using chunked write calls should be preferred. It will avoid hangs-on and blocking operations when using non-filesystem accessors.