How to Implement a Custom ImageInput Plugin for a New Image Format in OpenImageIO

To implement a custom ImageInput plugin for OpenImageIO, subclass the abstract ImageInput base class, implement virtual methods including valid_file(), open(), close(), and read_native_scanline(), and expose the plugin using the OIIO_PLUGIN_EXPORTS_BEGIN/END macros with a factory function.

OpenImageIO (OIIO) provides a plugin architecture that allows developers to add support for new image formats without modifying the core library. Each format is handled by a concrete subclass of ImageInput defined in src/include/OpenImageIO/imageio.h that implements format-specific reading logic. This guide walks through creating a custom ImageInput plugin based on the patterns used in the academysoftwarefoundation/openimageIO repository, specifically referencing the PNG implementation as a reference model.

Understand the ImageInput API

The ImageInput base class in src/include/OpenImageIO/imageio.h defines the interface that all image reader plugins must implement. According to the source code, the most critical virtual methods for a minimal implementation include:

  • virtual bool valid_file(Filesystem::IOProxy* ioproxy) const – Performs a lightweight check to verify the file matches your format's signature without fully opening it.
  • virtual bool open(const std::string& name, ImageSpec& spec) – Initializes the decoder, validates the file, and populates the ImageSpec object with image dimensions, channels, and metadata.
  • virtual bool close() – Releases all allocated resources and resets internal state.
  • virtual bool read_native_scanline(int subimage, int miplevel, int y, int z, void* data) – Reads raw pixel data for a single scanline; alternative methods like read_native_tile() exist for tiled formats.

See the declaration here: [imageio.h – ImageInput definition](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/include/OpenImageIO/imageio.h#L43).

Create the Plugin Skeleton

Create a new directory following OIIO's naming convention: src/myformat.imageio/. Add these files:

  • myformatinput.cpp – Contains the ImageInput subclass implementation and plugin registration macros.
  • myformat_pvt.h (optional) – Hides third-party library dependencies behind a private namespace, similar to png_pvt.h in the PNG plugin.

Implement the Subclass

A minimal but functional subclass follows the pattern established in src/png.imageio/pnginput.cpp. The implementation requires a class declaration, constructor initialization, and the mandatory plugin export block.

Class Declaration and Constructor

Define your class with proper initialization and cleanup:

// src/myformat.imageio/myformatinput.cpp
#include "myformat_pvt.h"
OIIO_PLUGIN_NAMESPACE_BEGIN

class MyFormatInput final : public ImageInput {
public:
    MyFormatInput() { init(); }
    ~MyFormatInput() override { close(); }

    const char* format_name() const override { return "myformat"; }

    int supports(string_view feature) const override {
        return (feature == "ioproxy");
    }

    bool valid_file(Filesystem::IOProxy* ioproxy) const override;
    bool open(const std::string& name, ImageSpec& spec) override;
    bool close() override;
    bool read_native_scanline(int subimage, int miplevel, int y, int z,
                             void* data) override;

private:
    std::string m_filename;
    MyLibDecoder* m_decoder = nullptr;
    int m_subimage = -1;
    ImageSpec m_spec;
    
    void init() { m_subimage = -1; m_decoder = nullptr; }
};

The constructor calls a private init() method to zero member state, mirroring the pattern in PNGInput::init() found at line 61 of the PNG source.

Plugin Registration Macros

Expose the plugin to OIIO using the export macros after your class definition:

OIIO_PLUGIN_EXPORTS_BEGIN

OIIO_EXPORT ImageInput* myformat_input_imageio_create() {
    return new MyFormatInput;
}

OIIO_EXPORT int myformat_imageio_version = OIIO_PLUGIN_VERSION;

OIIO_EXPORT const char* myformat_imageio_library_version() {
    return MyLibVersion();
}

OIIO_EXPORT const char* myformat_input_extensions[] = { "myf", nullptr };

OIIO_PLUGIN_EXPORTS_END
OIIO_PLUGIN_NAMESPACE_END

The factory function myformat_input_imageio_create serves as the entry point OIIO looks for when loading plugins. This pattern appears in [pnginput.cpp at line 6](https://github.com/AcademySoftwareFoundation/OpenImageIO/blob/main/src/png.imageio/pnginput.cpp#L6).

Implementing valid_file()

Provide a fast header check to identify files without fully initializing the decoder:

bool MyFormatInput::valid_file(Filesystem::IOProxy* ioproxy) const {
    if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Mode::Read)
        return false;
    unsigned char hdr[8];
    size_t n = ioproxy->pread(hdr, sizeof(hdr), 0);
    return n == sizeof(hdr) && MyFormat_pvt::check_header(hdr);
}

Implementing open()

Handle file initialization and ImageSpec population:

bool MyFormatInput::open(const std::string& name, ImageSpec& spec) {
    m_filename = name;
    m_subimage = 0;
    
    // Acquire IOProxy (implements ioproxy support)
    if (!ioproxy_use_or_open(name))
        return false;
    
    // Verify file header
    unsigned char hdr[8];
    if (ioproxy()->pread(hdr, sizeof(hdr), 0) != sizeof(hdr))
        return false;
    if (!MyFormat_pvt::check_header(hdr))
        return false;
    
    // Initialize decoder
    std::string err;
    if (!MyFormat_pvt::create_decoder(m_decoder, m_filename, err)) {
        errorf("Could not create decoder: %s", err);
        return false;
    }
    
    // Query image properties and fill spec
    int width, height, channels;
    MyFormat_pvt::get_image_info(m_decoder, width, height, channels);
    m_spec = ImageSpec(width, height, channels, TypeDesc::UINT8);
    
    // Add any format-specific attributes
    m_spec.attribute("myformat:version", MyFormat_pvt::get_version(m_decoder));
    
    spec = m_spec;
    return true;
}

This follows the initialization pattern in PNGInput::open (lines 42-76), including error handling and IOProxy management.

Implementing close()

Release resources to prevent memory leaks:

bool MyFormatInput::close() {
    if (m_decoder) {
        MyFormat_pvt::destroy_decoder(m_decoder);
        m_decoder = nullptr;
    }
    init();
    return true;
}

This mirrors the cleanup logic in PNGInput::close at lines 39-44.

Implementing read_native_scanline()

Read raw pixel data for a specific scanline:

bool MyFormatInput::read_native_scanline(int subimage, int miplevel,
                                         int y, int /*z*/, void* data) {
    lock_guard lock(*this);
    if (!seek_subimage(subimage, miplevel))
        return false;
    
    y -= m_spec.y;  // Account for image origin offset
    if (y < 0 || y >= m_spec.height)
        return false;
    
    return MyFormat_pvt::read_scanline(m_decoder, y, data);
}

For interlaced formats, you may need to buffer the entire image first, as demonstrated in the PNG implementation around lines 12-23.

Private Helper Headers

If your format relies on an external decoding library, isolate those dependencies in a private header to keep your main plugin clean. Create src/myformat.imageio/myformat_pvt.h:

#pragma once
#include <string>

struct MyLibDecoder;  // Forward declaration

namespace MyFormat_pvt {
    bool check_header(const unsigned char* header);
    bool create_decoder(MyLibDecoder*& decoder, const std::string& filename, 
                       std::string& error);
    void get_image_info(MyLibDecoder* decoder, int& w, int& h, int& channels);
    bool read_scanline(MyLibDecoder* decoder, int y, void* dst);
    void destroy_decoder(MyLibDecoder* decoder);
    const char* get_version(MyLibDecoder* decoder);
    const char* MyLibVersion();
}

This approach mirrors png_pvt.h and png_pvt.cpp, which wrap libpng calls in the PNG_pvt namespace.

Build System Integration

Add your new source files to the OIIO CMake build system. The repository already contains patterns for each plugin directory. After compilation, the plugin installs to the plugins/ directory and becomes automatically discoverable by OIIO at runtime through the OIIO_PLUGIN_SEARCHPATH.

Using the New Plugin

Once compiled and installed, your plugin works transparently with OIIO's standard API:

#include <OpenImageIO/imageio.h>
#include <iostream>
#include <vector>

int main() {
    // Plugin loads automatically if available on OIIO_PLUGIN_SEARCHPATH
    auto in = OIIO::ImageInput::open("example.myf");
    if (!in) {
        std::cerr << "Failed to open: " << OIIO::geterror() << "\n";
        return 1;
    }
    
    const OIIO::ImageSpec &spec = in->spec();
    std::vector<unsigned char> pixels(spec.image_bytes());
    
    if (!in->read_image(0, 0, 0, spec.nchannels, 
                       OIIO::TypeDesc::UINT8, pixels.data())) {
        std::cerr << "Read failed: " << OIIO::geterror() << "\n";
        return 1;
    }
    
    in->close();
    return 0;
}

Summary

  • Subclass ImageInput and implement the virtual interface (valid_file, open, close, and read methods) to handle your format's specific requirements.
  • Use the export macros OIIO_PLUGIN_EXPORTS_BEGIN/END to expose the factory function myformat_input_imageio_create, version symbols, and file extensions array.
  • Follow the PNG pattern in src/png.imageio/pnginput.cpp for initialization, error handling, and resource cleanup.
  • Isolate third-party dependencies in a private header like myformat_pvt.h to maintain clean separation between OIIO's interface and external libraries.
  • Place your implementation in src/myformat.imageio/ and integrate it into the CMake build system for automatic plugin discovery.

Frequently Asked Questions

What is the minimum set of methods required for an ImageInput plugin?

You must implement format_name(), valid_file(), open(), close(), and at least one read method—typically read_native_scanline() for scanline-based formats or read_native_tile() for tiled formats. The supports() method is optional but recommended for advertising capabilities like IOProxy support.

How does OpenImageIO discover custom plugins at runtime?

OIIO searches directories listed in the OIIO_PLUGIN_SEARCHPATH environment variable (defaulting to the plugins/ subdirectory of the installation). It loads shared libraries and looks for exported factory functions matching the naming convention formatname_input_imageio_create, as defined in your OIIO_PLUGIN_EXPORTS block.

Can I implement tiled reading instead of scanline reading?

Yes. If your format is inherently tiled, override read_native_tile() instead of (or in addition to) read_native_scanline(). You may also override read_native_scanline() to provide convenience access by internally reading the containing tile and extracting the scanline, though this is less efficient than native scanline support.

How do I handle format-specific metadata in the ImageSpec?

During the open() method, populate the ImageSpec object passed by reference. Use spec.attribute() to add custom metadata with namespaced keys (e.g., "myformat:compression", "myformat:icc_profile"). These attributes become accessible to applications through the ImageSpec::extra_attribs vector or get_attribute() methods.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →