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

> Learn how OpenImageIO's ImageInput and ImageOutput API provides format-agnostic image I/O by leveraging runtime plugin selection for seamless reading and writing of diverse image files.

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

---

**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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/libOpenImageIO/imageinput.h) and [`src/libOpenImageIO/imageoutput.h`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/src/jpeg.imageio/jpeginput.cpp) or [`src/png.imageio/pngoutput.cpp`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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`](https://github.com/academysoftwarefoundation/openimageio/blob/main/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.

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

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