Copyright © 2025 Iris Developers; MIT Software License
The Iris Codec Community module is part of the Iris Digital Pathology project, enabling:
- Reading and writing Iris whole slide image (WSI) files (.iris)
- Decoding Iris Codec-compressed tile image data
This repository provides extremely fast slide access through a simple API. Available as:
Tip
Iris files can directly replace deep zoom images (DZI) in your OpenSeaDragon-based image stacks. Use our Iris RESTful Server and OpenSeaDragon IrisTileSource for seamless integration.
Note
For scanner manufacturers: The Iris File Extension (IFE) repository provides the specification for custom encoder/decoder development.
Warning
This tool is in Beta: There may be minor bugs present (some major) with edge conditions. If you notice them, please open an issue and we will address them as soon as we are able.
The following example WSI files are publically available from AWS S3:
- cervix_2x_jpeg.iris (jpeg encoded at 2x downsampling)
- cervix_4x_jpeg.iris (jpeg encoded at 4x downsampling)
The Iris Codec encoder converts WSI files from various vendor formats into optimized Iris format, supporting DICOM (via libdicom) and OpenSlide formats with flexible compression and metadata options.
Key Features:
- Native DICOM Support: Byte-stream preservation for lossless quality
- Multi-format Support: SVS, NDPI, VSI, MRXS, and other OpenSlide formats
- Modern Compression: JPEG (default) or AVIF
- Automatic Pyramid Derivation: Generate 2x or 4x, or use source pyramid format.
- Privacy Controls: Metadata stripping and anonymization options
- High Performance: Multi-threaded with real-time progress tracking
Requirements:
- C++ 20 Standard Library
- libjpeg-turbo, libavif
- OpenSlide and libdicom (for encoder only)
Basic Build:
git clone --depth 1 https://github.com/IrisDigitalPathology/Iris-Codec.git
cmake -B build -D IRIS_BUILD_SHARED=ON -D IRIS_BUILD_ENCODER=ON ./Iris-Codec
cmake --build build --config Release -j$(nproc)
cmake --install build
The build system provides several configurable options to customize your build:
Option | Default | Description |
---|---|---|
IRIS_BUILD_SHARED |
ON |
Build shared library (.so/.dylib/.dll) |
IRIS_BUILD_STATIC |
ON |
Build static library (.a/.lib) |
IRIS_BUILD_ENCODER |
ON |
Build the encoder executable for converting WSI files |
IRIS_BUILD_PYTHON |
OFF |
Build Python bindings |
IRIS_BUILD_DEPENDENCIES |
OFF |
Build all dependencies from source and statically link |
IRIS_USE_OPENSLIDE |
ON |
Enable OpenSlide support (required for most WSI formats) |
# Conda (recommended)
conda install -c conda-forge iris-codec
# PyPI
pip install iris-codec openslide-bin
Option 1: Iris RESTful Server (recommended for server deployments)
- Extremely high performance (multi-threaded with dual stack)
- Server-side serialization with virtual memory mapping
- Compatible with OpenSeaDragon IrisTileSource
Option 2: WebAssembly Module (for client-side/bucket storage)
<script type="module">
import createModule from 'https://cdn.jsdelivr.net/npm/iris-codec@latest/iris-codec.js';
const irisCodec = await createModule();
</script>
Important
RESTful server has substantially better performance. WASM is useful for bucket storage (S3, GCS) where custom servers can't be deployed. There is a substantial performance cost to client side deserialization relative to IrisRESTful. This WASM API is a unique feature of Iris but is not our preferred method of HTTP streaming.
Command Line:
# Basic encoding
./IrisCodecEncoder -s input.svs -o ./output/ -d use_source
# Advanced options
./IrisCodecEncoder -s input.dcm -o ./output/ -e AVIF -d 2x -c 8
# Research Example (strip metadata)
./IrisCodecEncoder -s patient.dcm -o ./research/ -sm -d 4x -c 4
Available Arguments:
-h, --help
: Print help text-s, --source
: File path to the source WSI file (must be compatible with OpenSlide)-o, --outdir
: Output directory path (encoder names file as XXX.iris based on source filename)-d, --derive
: Generate lower resolution layers - Options:2x
,4x
, oruse-source
(default)-sm, --strip_metadata
: Strip patient identifiers from encoded metadata-e, --encoding
: Compression format -JPEG
(default) orAVIF
-c, --concurrency
: Number of threads to use (defaults to all CPU cores)
Python:
from Iris import Encoder
result = Encoder.encode_slide_file('input.svs', './output/')
C++:
#include "IrisCodecCore.hpp"
auto encoder = IrisCodec::create_encoder({.srcFilePath = "input.svs"});
IrisCodec::dispatch_encoder(encoder);
C++:
#include <Iris/IrisCodecCore.hpp>
auto slide = open_slide({.filePath = "slide.iris"});
SlideInfo info;
get_slide_info(slide, info);
// Read tiles
Iris::Buffer tile = read_slide_tile(SlideTileReadInfo {
.slide = slide,
.layer = 0,
.tile = 0
});
Python:
from Iris import Codec
slide = Codec.open_slide('slide.iris')
result, info = slide.get_info()
# Read tiles
tile_data = slide.read_slide_tile(layer=0, tile=0)
JavaScript:
import createModule from 'https://cdn.jsdelivr.net/npm/iris-codec@latest/iris-codec.js';
(async () => {
const irisCodec = await createModule();
const validationResult = await irisCodec.validateFileStructure('https://example.com/slide.iris');
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
const slide = await irisCodec.openIrisSlide('https://example.com/slide.iris');
const info = slide.getSlideInfo();
})();
For complete examples and advanced usage, see the full documentation and API reference.
// Import the Iris Codec header
// This import includes the types header automatically
#include <filesystem>
#include <Iris/IrisCodecCore.hpp>
You may choose to perform your own file system validations and recovery routines. Iris will, however catch all of these (and the main API methods are declared noexcept
).
if (!std::filesystem::exists(file_path)) {
printf(file_path.string() + " file does not exist\n");
return EXIT_FAILURE;
}
if (!is_iris_codec_file(file_path.string())) {
printf(file_path.string() + " is not a valid Iris slide file\n");
return EXIT_FAILURE;
}
IrisResult result = validate_slide(SlideOpenInfo{
.filePath = file_path.string()
// Default values for any undefined parameters
});
if (result != IRIS_SUCCESS) {
printf(result.message);
return EXIT_FAILURE;
}
Should a runtime error occur, it will be reported in the form of an IrisResult
message, as seen in the IrisResult validate_slide(const SlideOpenInfo&) noexcept;
call.
Successful loading of a slide file will return a valid IrisCodec::Slide
object; failure will return a nullptr
.
auto slide = open_slide(SlideOpenInfo{
.filePath = file_path.string(),
.context = nullptr,
.writeAccess = false
});
if (!slide) return EXIT_FAILURE;
Once opened, the slide IrisCodec::SlideInfo
structure can be loaded using the Result get_slide_info(const Slide&, SlideInfo&) noexcept
call and used as an initialized structure containing all the information needed to navigate the slide file and read elements.
SlideInfo info;
IrisResult result = get_slide_info(slide, info);
if (result != IRIS_SUCCESS) {
printf(result.message);
return EXIT_FAILURE;
}
The SlideTileReadInfo
struct provides a simple mechanism for reading slide image data. The info.extent
struct is extremely simple to navigate. Please refer to the struct Extent
type in the IrisTypes.hpp core header file for more information about slide extents.
struct SlideTileReadInfo read_info{
.slide = slide,
.layer = 0,
.optionalDestination = NULL, /*wrapper can go here*/
.desiredFormat = Iris::FORMAT_R8G8B8A8,
};
for (auto& layer : info.extent.layers) {
for (int y_index = 0; y_index < layer.yTiles; ++y_index) {
for (int x_index = 0; x_index < layer.xTiles; ++x_index) {
// Read the tile slide tile
Iris::Buffer rgba = read_slide_tile(read_info);
// Do something with the tile pixel values in rgba
// Do not worry about clean up;
// The Iris::Buffer will deallocate it.
}
}
read_info.layer++;
}
Note
Iris::Buffer is a reference counted buffer wrapper that can be safely copied will internally manage the memory lifetime. If externally managing memory you can wrap an existing buffer in a weak Iris::Buffer for API compatability but without lifetime management.
Decompressed slide data can be optionally read into preallocated memory. If the optional destination buffer is insufficiently sized, Iris will instead allocate a new buffer and return that new buffer with the pixel data.
Note
If writing into externally managed memory, Iris::Buffer
should weakly reference the underlying memory using Wrap_weak_buffer_from_data()
as strongly referenced Iris::Buffer
objects deallocate underlying memory when they pass out of scope.
// In this example we have some preallocated buffer we want
// to write our slide pixel data into. A GPU buffer is a great
// example and the GPU API manages that memory:
char* GPU_DST;
// We will write in R8G8B8A8 format for simplicity
Iris::Format format = Iris::FORMAT_R8G8B8A8;
size_t tile_bytes = 256*256*4;
Iris::Buffer wrapper = Wrap_weak_buffer_from_data(GPU_DST, tile_bytes);
// Read the data
struct SlideTileReadInfo read_info{
.slide = slide,
.optionalDestination = wrapper,
.desiredFormat = format,
};
Buffer result = read_slide_tile(read_info);
// If there was insufficient space in the provided
// destination buffer, a new buffer will be allocated.
if (wrapper != result) {
printf("Insufficient sized buffer, new buffer was allocated");
}
Iris can decompress into different pixel byte orderings and exchange data ownership via the Iris::Buffer
strength. The codec uses Google Highway SIMD instructions for high-performance format conversions, including:
- Channel Reordering: RGB ↔ BGR byte swapping using vectorized operations
- Alpha Channel Operations: Adding/removing alpha channels with SIMD acceleration
- Format Conversion: Converting between R8G8B8, B8G8R8, R8G8B8A8, and B8G8R8A8 formats
- Tile Downsampling: 2x and 4x averaging downsampling with vectorized arithmetic
The SIMD implementations automatically adapt to your CPU's capabilities (SSE2, AVX2, AVX-512, ARM NEON) for optimal performance. Format conversions are performed in-place when possible to minimize memory allocation.
For more information about the Iris Buffer, which was designed primarily as a networking buffer, please see IrisBuffer.hpp and IrisSIMD.hpp in the core headers.
Import the Python API and Iris Codec Module.
# Import the Iris Codec Module
from Iris import Codec
slide_path = 'path/to/slide_file.iris'
Perform a deep validation of the slide file structure. This will navigate the internal offset-chain and check for violations of the IFE standard.
result = Codec.validate_slide_path(slide_path)
if (result.success() == False):
raise Exception(f'Invalid slide file path: {result.message()}')
print(f"Slide file '{slide_path}' successfully passed validation")
Open a slide file. The following conditional will always return True if the slide has already passed validation but you may skip validation and it will return with a null slide object (but without providing the Result debug info).
slide = Codec.open_slide(slide_path)
if (not slide):
raise Exception('Failed to open slide file')
Get the slide abstraction, read off the slide dimensions, and then print it to the console.
# Get the slide abstraction
result, info = slide.get_info()
if (result.success() == False):
raise Exception(f'Failed to read slide information: {result.message()}')
# Print the slide extent to the console
extent = info.extent
print(f"Slide file {extent.width} px by {extent.height}px with an encoding of {info.encoding}. The layer extents are as follows:")
print(f'There are {len(extent.layers)} layers comprising the following dimensions:')
for i, layer in enumerate(extent.layers):
print(f' Layer {i}: {layer.x_tiles} x-tiles, {layer.y_tiles} y-tiles, {layer.scale:0.0f}x scale')
Generate a quick low-power view of the slide using Pillow images.
from PIL import Image
layer_index = 0 # Lowest power layer is layer zero (0)
scale = int(extent.layers[layer_index].scale)
composite = Image.new('RGBA', (extent.width * scale, extent.height * scale))
layer_extent = extent.layers[layer_index]
for y in range(layer_extent.y_tiles):
for x in range(layer_extent.x_tiles):
tile_index = y*layer_extent.x_tiles+x
composite.paste(Image.fromarray(slide.read_slide_tile(layer_index, tile_index)), (256*x, 256*y))
composite.show()
Caution
Despite Iris' native fast read speed, higher resolution layers may take substantial time and memory for Pillow to create a full image as it does not create tiled images. I do not recommend doing this above layer 0 or 1 as it may be onerous for PIL.Image
Investigate the metadata attribute array and view a thumbnail image:
result, info = slide.get_info()
if (result.success() == False):
raise Exception(f'Failed to read slide information: {result.message()}')
print("Slide metadata attributes")
for attribute in info.metadata.attributes:
print(f"{attribute}: {info.metadata.attributes[attribute]}")
from PIL import Image
if ('thumbnail' in info.metadata.associated_images):
image = Image.fromarray(slide.read_associated_image('thumbnail'))
image.show()
The Python module also includes encoding capabilities for converting existing WSI files to the Iris format.
Basic Encoding:
from Iris import Encoder
# Simple encoding with default settings
result = Encoder.encode_slide_file(
source='path/to/input.svs',
outdir='./output/'
)
if not result.success():
raise Exception(f'Encoding failed: {result.message()}')
print('Encoding completed successfully!')
Advanced Encoding Options:
from Iris import Encoder, Codec, iris_core
# Encode with custom parameters
result = Encoder.encode_slide_file(
source='path/to/input.svs',
outdir='./output/',
desired_encoding=Codec.Encoding.TILE_ENCODING_AVIF, # Use AVIF compression
desired_byte_format=iris_core.Format.FORMAT_R8G8B8A8, # RGBA format
strip_metadata=True, # Remove patient identifiers
derivation=Encoder.EncoderDerivation.layer_4x, # 4x pyramid layers
concurrency=8 # Use 8 threads
)
DICOM Encoding with Progress Monitoring:
from Iris import Encoder
import time
# The encode_slide_file function includes built-in progress monitoring
# It will automatically display a progress bar during encoding
result = Encoder.encode_slide_file(
source='sample.dcm', # DICOM input with byte-stream preservation
outdir='./encoded/',
desired_encoding=Encoder.Codec.Encoding.TILE_ENCODING_JPEG
)
# The function handles progress display automatically:
# [████████████████████████████████████████] 100.0% ETA: 00:00
# Iris Encoder completed successfully
# Slide written to ./encoded/sample.iris
Available Encoding Parameters:
source
: Path to input WSI file (required)outdir
: Output directory (defaults to source directory)desired_encoding
: Compression format (JPEG or AVIF, default: JPEG)desired_byte_format
: Pixel format (default: R8G8B8)strip_metadata
: Remove patient identifiers (default: False)anonymize
: Anonymize metadata (default: False)derivation
: Layer derivation strategy (default: layer_2x)concurrency
: Number of threads (default: CPU count)codec_context
: Optional codec context for advanced usage
Iris RESTful has a simple API (and supports DICOMweb WADO-RS), outlined here, and explained in greater detail within the Iris RESTful API Explained Section. This will return the slide metadata in JSON format and slide tile image data.
Iris RESTful
GET <URL>/slides/<slide-name>/metadata
GET <URL>/slides/<slide-name>/layers/<layer>/tiles/<tile>
Supported WADO-RS
GET <URL>/studies/<study>/series/<UID>/metadata
GET <URL>/studies/<study>/series/<UID>/instances/<layer>/metadata
GET <URL>/studies/<study>/series/<UID>/instances/<layer>/frames/<tile>
Load the Iris-Codec NPM WebAssembly module via jsDelivr (or download the latest javascript release and include your local copy):
<!DOCTYPE html>
<script type="module">
import createModule
from 'https://cdn.jsdelivr.net/npm/iris-codec@latest/iris-codec.js';
// Compile the WASM module and stall execution until ready
const irisCodec = await createModule();
console.log("Iris-Codec has been loaded");
// ...Your code goes here...
</script>
Once loaded, you can access image data in a manner similar to the C++ and Python Iris-Codec API. Importantly, the metadata will be returned in IrisCodec::Abstraction C++ types (file metadata abstractions) exposed using Emscripten bindings. Refer to the included TypeScript file for those definitions. Image tile data will be returned as an image (MIME) source and can be used directly as an image data source.
Perform a deep validation of the slide file structure. This will navigate the internal offset-chain and check for violations of the IFE standard. This can be omitted if you are confident of the source.
(async () => {
const irisCodec = await createModule();
console.log("Iris-Codec has been loaded");
const url = "https://irisdigitalpathology.s3.us-east-2.amazonaws.com/example-slides/cervix_2x_jpeg.iris";
try {
const validationResult = await irisCodec.validateFileStructure(url);
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
console.log(`Slide file at ${url} successfully passed validation`);
} catch (error) {
console.log(`Slide file at ${url} failed validation: ${error.message}`);
}
})();
Open a slide file. The following conditional will succeed without throwing an exception if the slide has already passed validation but you may skip validation to reduce server requests.
(async () => {
const irisCodec = await createModule();
console.log("Iris-Codec has been loaded");
const url = "https://irisdigitalpathology.s3.us-east-2.amazonaws.com/example-slides/cervix_2x_jpeg.iris";
try {
const validationResult = await irisCodec.validateFileStructure(url);
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
const slide = await irisCodec.openIrisSlide(url);
if (!slide) {
throw new Error("Failed to open slide");
}
// ...Do something with slide
slide.delete();
} catch (error) {
console.error(error);
}
})();
Get the slide abstraction, read off the slide dimensions, and then print it to the console.
(async () => {
const irisCodec = await createModule();
const url = "https://irisdigitalpathology.s3.us-east-2.amazonaws.com/example-slides/cervix_4x_jpeg.iris";
try {
const validationResult = await irisCodec.validateFileStructure(url);
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
const slide = await irisCodec.openIrisSlide(url);
if (!slide) {
throw new Error("Failed to open slide");
}
// Let's get the slide dimensions and print them to the console.
const info = slide.getSlideInfo();
const extent = info.extent;
console.log(`Slide file ${extent.width} px by ${extent.height}px at lowest resolution layer. The layer extents are as follows:`);
console.log(`There are ${extent.layers.size()} layers comprising the following dimensions:`)
for (var i = 0; i < extent.layers.size(); i++) {
const layer = extent.layers.get(i);
console.log(` Layer ${i}: ${layer.xTiles} x-tiles, ${layer.yTiles} y-tiles, ${layer.scale}x scale`);
}
slide.delete();
} catch (error) {
console.error(error);
}
})();
Generate a quick view of one of the images (tileImage
) somewhere earlier in the HTML page.
<img id="tileImage" width="128" height="128" alt="Loading..." style="border: 1px solid black;"/>
<!-- ... Somewhere Earlier -->
<script type="module">
import createModule from 'https://cdn.jsdelivr.net/npm/iris-codec@latest/iris-codec.js';
(async () => {
try {
const irisCodec = await createModule();
const url = "https://example.com/slide.iris";
const validationResult = await irisCodec.validateFileStructure(url);
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
const slide = await irisCodec.openIrisSlide(url);
if (!slide) {
throw new Error("Failed to open slide");
}
const layer = 0;
const tile = 0;
const tileBlob = await slide.getSlideTile(layer, tile);
// Now pass the image off to the 'tileImage' element.
const objectUrl = URL.createObjectURL(tileBlob);
const imgElement = document.getElementById("tileImage");
imgElement.src = objectUrl;
// Clean up after the image is loaded
imgElement.onload = () => {
URL.revokeObjectURL(objectUrl);
slide.delete();
};
} catch (error) {
console.error(error);
}
})();
</script>
Bringing it all together, the following full HTML page source will show a low power view of the image using a tile grid view similar to the above view that Pillow.Images produces in the Python API example.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Iris-Codec WASM: Tile Grid</title>
<style>
/* The grid container: auto-flows row by row */
#tileGrid {
display: grid;
justify-content: start; /* left-align if container is wide */
align-content: start; /* top-align if container is tall */
}
#tileGrid img {
width: 128px;
height: 128px;
object-fit: cover; /* crop or letterbox if needed */
background: #f0f0f0; /* placeholder color */
}
</style>
</head>
<body>
<h1>Iris-Codec WASM: Tile Grid</h1>
<div id="loadTimeText">Load time: —</div>
<div id="tileGrid"></div>
<script type="module">
import createModule from 'https://cdn.jsdelivr.net/npm/iris-codec@latest/iris-codec.js';
// ————— Main Entry Point —————
(async () => {
const irisCodec = await createModule();
const url = "https://irisdigitalpathology.s3.us-east-2.amazonaws.com/example-slides/cervix_4x_jpeg.iris";
try {
// Validate file structure
const validationResult = await irisCodec.validateFileStructure(url);
if (validationResult.flag !== irisCodec.ResultFlag.IRIS_SUCCESS) {
throw new Error(`Validation failed: ${validationResult.message}`);
}
const slide = await irisCodec.openIrisSlide(url);
if (!slide) {
throw new Error("Failed to open slide");
}
// 1) Read layer info
const layer = 1;
const info = slide.getSlideInfo();
const extent = info.extent.layers.get(layer);
const { xTiles, yTiles } = extent;
const nTiles = xTiles * yTiles;
// 2) Configure the grid container
const gridEl = document.getElementById("tileGrid");
gridEl.style.gridTemplateColumns = `repeat(${xTiles}, 128px)`;
gridEl.style.gridTemplateRows = `repeat(${yTiles}, 128px)`;
// 3) Fetch all tiles in parallel (or chunked if you prefer)
const startAll = performance.now();
const objectUrls = await Promise.all(
Array.from({ length: nTiles }, async (_, idx) => {
const blob = await slide.getSlideTile(layer, idx);
return URL.createObjectURL(blob);
})
);
const endAll = performance.now();
document.getElementById("loadTimeText").textContent =
`All tiles fetched in ${Math.round(endAll - startAll)} ms`;
// 4) Insert <img> elements in tile order
objectUrls.forEach(u => {
const img = new Image(128, 128);
img.src = u;
gridEl.appendChild(img);
});
// 5) Cleanup on unload
window.addEventListener("beforeunload", () => {
// Revoke all blob URLs
objectUrls.forEach(u => URL.revokeObjectURL(u));
// Free the slide
slide.delete();
});
} catch (err) {
console.error(err);
alert("Error: " + err.message);
}
})();
</script>
</body>
</html>