How OpenImageIO's ImageInput/ImageOutput API Enables Format-Agnostic Image I/O

OpenImageIO provides a unified C++ API for reading and writing any supported image format through abstract base classes ImageInput and ImageOutput, which automatically delegate to format-specific plugins selected at runtime based on file extensions or magic bytes.

The OpenImageIO (OIIO) library eliminates the need for format-specific code by abstracting dozens of image formats behind a single, consistent interface. Through the ImageInput and ImageOutput API, developers can process EXR, JPEG, PNG, TIFF, and other formats using identical function calls without linking against individual format libraries. This architecture centers on a dynamic plugin system implemented in src/libOpenImageIO/ while concrete format drivers reside in separate src/*.imageio/ directories.

Architecture of the Format-Agnostic API

The API rests on two abstract base classes defined in src/libOpenImageIO/imageinput.h and src/libOpenImageIO/imageoutput.h. These classes declare pure virtual methods for open(), close(), and read/write operations, while concrete implementations live in format-specific plugins such as src/jpeg.imageio/jpeginput.cpp or src/png.imageio/pngoutput.cpp.

Factory functions handle instantiation without exposing concrete types. When you call ImageInput::create("photo.exr") or ImageOutput::create("output.png"), the implementation in src/libOpenImageIO/imageioplugin.cpp (around line 611) iterates over registered plugins and invokes each candidate's valid_file() method. The first plugin confirming it can handle the file—either by extension or magic bytes—is instantiated and returned as a std::unique_ptr<ImageInput> or std::unique_ptr<ImageOutput>, completely hiding the underlying concrete class from the caller.

Reading Images with ImageInput

The ImageInput API provides format-agnostic reading through generic methods that handle both tiled and scanline-based images automatically.

// Create a format-agnostic reader
auto in = ImageInput::create("texture.exr");  // src/libOpenImageIO/imageioplugin.cpp#L611

// Open and retrieve specifications
ImageSpec spec;
if (!in->open("texture.exr", spec)) {
    std::cerr << "Error: " << in->geterror() << "\n";
    return;
}

// Allocate buffer based on spec
std::vector<unsigned char> pixels(spec.image_bytes());

// Read entire image (works for any format)
if (!in->read_image(0, 0, 0, spec.nchannels, 
                    TypeDesc::UNKNOWN, &pixels[0])) {
    std::cerr << "Read failed: " << in->geterror() << "\n";
}
in->close();

Internally, read_image() (implemented around line 910 in src/libOpenImageIO/imageinput.cpp) inspects spec.tile_width to determine whether to call read_tiles() or read_scanlines(). Before any data transfer, the library validates buffer sizes via check_span_size() to prevent overruns. If the caller specifies TypeDesc::UNKNOWN, OIIO preserves the file's native pixel type; otherwise, it automatically converts data using internal helper functions like convert_image().

Writing Images with ImageOutput

Writing follows a symmetric pattern through the ImageOutput base class, allowing code to output to any format without changing the write logic.

// Create writer based on filename extension
auto out = ImageOutput::create("render.exr");  // src/libOpenImageIO/imageioplugin.cpp#L611

// Configure output specification
ImageSpec spec(width, height, channels, TypeDesc::FLOAT);
spec.attribute("compression", "zip");

if (!out->open("render.exr", spec)) {
    std::cerr << "Cannot open: " << out->geterror() << "\n";
    return;
}

// Write entire image (format-agnostic)
if (!out->write_image(TypeDesc::FLOAT, pixelData)) {
    std::cerr << "Write failed: " << out->geterror() << "\n";
}
out->close();

The write_image() method (around line 1125 in src/libOpenImageIO/imageoutput.cpp) similarly delegates to write_scanlines() or write_tiles() based on the ImageSpec configuration. Default implementations loop over per-scanline or per-tile virtual methods, allowing plugins to override only the granularity they support. As with reading, check_span_size() validates the provided buffer before I/O operations commence.

Key Features That Maintain Format Agnosticism

Automatic Plugin Selection

The factory mechanism in imageioplugin.cpp maintains a registry of all compiled format plugins. When create() is invoked, the system locks the imageio_mutex and queries each plugin's valid_file() implementation, which typically inspects magic bytes or file headers. This allows OIIO to open files correctly even when the extension is missing or incorrect, all without user intervention.

Unified Tile and Scanline Access

Whether a format stores images as scanlines (JPEG) or tiles (OpenEXR), the API presents a uniform interface. The ImageSpec::tile_width field determines storage strategy; callers simply invoke read_image() or write_image() while OIIO handles the underlying access pattern. This abstraction eliminates conditional logic in application code for different file types.

Custom I/O Streams

The Filesystem::IOProxy class (defined in src/libutil/filesystem.h) enables reading from or writing to arbitrary sources such as memory buffers, network sockets, or compressed archives. When an IOProxy is supplied to open(), the base class stores it in Impl::m_io, allowing plugins to perform I/O through the proxy while the core API remains unchanged. This preserves format agnosticism even for non-file data sources.

Thread Safety and Error Handling

Each ImageInput and ImageOutput instance contains a per-object std::recursive_mutex (Impl::m_mutex) to protect internal state. Errors are stored in thread-local robin_map structures rather than global variables, ensuring that concurrent operations on different objects do not interfere. The errorfmt() function records messages that geterror() retrieves, maintaining clean error attribution per instance.

Summary

  • Abstract base classes (ImageInput/ImageOutput) define a uniform interface implemented by format-specific plugins in src/*.imageio/ directories.
  • Factory functions create() automatically instantiate the correct plugin by testing valid_file() against the target path.
  • Generic read/write methods (read_image(), write_image()) handle both tiled and scanline data transparently, delegating to appropriate internal routines based on ImageSpec.
  • Built-in safety checks like check_span_size() prevent buffer overruns, while TypeDesc::UNKNOWN allows native-format passthrough without conversion overhead.
  • Thread-local error handling and per-instance mutexes ensure safe concurrent usage across multiple images.

Frequently Asked Questions

How does OpenImageIO determine which plugin to use for a file?

When ImageInput::create() or ImageOutput::create() is called, the factory in src/libOpenImageIO/imageioplugin.cpp iterates through all registered plugins and invokes their valid_file() method. The first plugin returning true—indicating it recognizes the file by extension or magic bytes—is instantiated. This mechanism allows OIIO to handle format detection automatically without requiring the application to specify the file type explicitly.

Can I use OpenImageIO to read images from memory buffers instead of files?

Yes. By supplying a Filesystem::IOProxy instance to the open() method, you can redirect I/O to memory buffers, network streams, or custom sources. The plugin reads and writes through this proxy stored in Impl::m_io, maintaining the same ImageInput/ImageOutput API regardless of whether the underlying source is a disk file or memory block.

Is the ImageInput/ImageOutput API thread-safe?

Individual ImageInput and ImageOutput objects are thread-safe for concurrent use through per-instance recursive mutexes (Impl::m_mutex). However, sharing a single instance across threads requires external synchronization. Multiple threads can safely operate on separate instances simultaneously, with errors isolated via thread-local storage maps rather than global state.

What happens if I request a pixel type different from the file's native format?

When reading or writing, if you specify a TypeDesc different from spec.format, OIIO automatically converts pixel data using internal conversion routines like convert_image() and convert_pixel_values. If you pass TypeDesc::UNKNOWN, the API preserves the file's native type without conversion, maximizing performance and fidelity.

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 →