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

> Learn to implement a custom ImageInput plugin for new image formats in OpenImageIO. Subclass ImageInput, implement key methods, and export your plugin for seamless integration.

- Repository: [Academy Software Foundation/openimageio](https://github.com/academysoftwarefoundation/openimageio)
- Tags: how-to-guide
- Published: 2026-02-23

---

**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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/myformatinput.cpp)** – Contains the `ImageInput` subclass implementation and plugin registration macros.
- **[`myformat_pvt.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/myformat_pvt.h)** *(optional)* – Hides third-party library dependencies behind a private namespace, similar to [`png_pvt.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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:

```cpp
// 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:

```cpp
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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/myformat.imageio/myformat_pvt.h):

```cpp
#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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/png_pvt.h) and [`png_pvt.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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:

```cpp
#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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/png.imageio/pnginput.cpp) for initialization, error handling, and resource cleanup.
- **Isolate third-party dependencies** in a private header like [`myformat_pvt.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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.