| Crates.io | ultrahdr-core |
| lib.rs | ultrahdr-core |
| version | 0.1.1 |
| created_at | 2026-01-23 17:19:49.902952+00 |
| updated_at | 2026-01-24 02:48:20.282318+00 |
| description | Core gain map math and metadata for Ultra HDR - no codec dependencies |
| homepage | |
| repository | https://github.com/imazen/ultrahdr |
| max_upload_size | |
| id | 2065054 |
| size | 289,574 |
Pure Rust implementation of Ultra HDR (gain map HDR) encoding and decoding.
Ultra HDR is a backward-compatible HDR image format that embeds a gain map in a standard JPEG, allowing HDR-capable displays to reconstruct the full HDR image while remaining viewable as SDR on legacy displays.
| Crate | Description |
|---|---|
ultrahdr |
Full encoder/decoder with jpegli-rs JPEG codec |
ultrahdr-core |
Pure math and metadata - no codec dependency, WASM-compatible |
ultrahdr-core compiles to WebAssemblyuse ultrahdr::{Encoder, RawImage, PixelFormat, ColorGamut, ColorTransfer};
// Create HDR image (linear float RGB, BT.2020 gamut)
let hdr_image = RawImage {
width: 1920,
height: 1080,
format: PixelFormat::Rgba32F,
gamut: ColorGamut::Bt2100,
transfer: ColorTransfer::Linear,
data: hdr_pixels,
stride: 1920 * 16,
};
// Encode to Ultra HDR JPEG (SDR is auto-generated via tone mapping)
let ultrahdr_jpeg = Encoder::new()
.set_hdr_image(hdr_image)
.set_quality(90, 85) // base quality, gainmap quality
.set_gainmap_scale(4) // 1/4 resolution gain map
.set_target_display_peak(1000.0) // nits
.encode()?;
std::fs::write("output.jpg", &ultrahdr_jpeg)?;
use ultrahdr::{Decoder, HdrOutputFormat};
let data = std::fs::read("ultrahdr.jpg")?;
let decoder = Decoder::new(&data)?;
if decoder.is_ultrahdr() {
// Get HDR output (4x display boost)
let hdr = decoder.decode_hdr(4.0, HdrOutputFormat::LinearFloat)?;
// Or just get SDR
let sdr = decoder.decode_sdr()?;
// Inspect metadata
let metadata = decoder.metadata();
println!("HDR capacity: {:.1}x", metadata.hdr_capacity_max);
}
When editing HDR content, use AdaptiveTonemapper to learn the original tone curve and reproduce it:
use ultrahdr_core::color::{AdaptiveTonemapper, FitConfig};
// Learn tone curve from original HDR/SDR pair
let tonemapper = AdaptiveTonemapper::fit(&original_hdr, &original_sdr)?;
// Apply to edited HDR - preserves the original artistic intent
let new_sdr = tonemapper.apply(&edited_hdr)?;
Rgba32F - Linear float RGBARgba16F - Half-float RGBAP010 - 10-bit YUV (BT.2020)Rgba8 - 8-bit sRGB RGBARgb8 - 8-bit sRGB RGBLinearFloat - Linear RGB floatPq1010102 - PQ-encoded 10-bit packedSrgb8 - Clipped to SDR rangeBoth XMP and ISO 21496-1 metadata are supported for maximum compatibility:
For memory-constrained environments, ultrahdr-core provides streaming APIs that process images row-by-row:
use ultrahdr_core::gainmap::streaming::{RowDecoder, RowEncoder, DecodeInput, EncodeInput};
| Type | Direction | Memory | Use Case |
|---|---|---|---|
RowDecoder |
SDR+gainmap→HDR | Full gainmap in RAM | Gainmap fits in memory |
StreamDecoder |
SDR+gainmap→HDR | 16-row ring buffer | Parallel JPEG decode |
RowEncoder |
HDR+SDR→gainmap | Synchronized batches | Same-rate inputs |
StreamEncoder |
HDR+SDR→gainmap | Independent buffers | Parallel decode sources |
use ultrahdr_core::gainmap::streaming::{RowDecoder, DecodeInput};
use ultrahdr_core::{HdrOutputFormat, ColorGamut};
// Load gainmap fully, then stream SDR rows
let mut decoder = RowDecoder::new(
gainmap, metadata, width, height, 4.0, HdrOutputFormat::LinearFloat, ColorGamut::Bt709
)?;
// Process in 16-row batches (JPEG MCU alignment)
for batch_start in (0..height).step_by(16) {
let batch_height = 16.min(height - batch_start);
let sdr_batch = jpeg_decoder.next_rows(batch_height);
let hdr_batch = decoder.process_rows(&sdr_batch, batch_height)?;
write_output(&hdr_batch);
}
| API | Peak Memory |
|---|---|
| Full decode | ~166 MB |
| Streaming (16 rows) | ~2 MB |
For more control, use ultrahdr-core (math + metadata only) with jpegli-rs for JPEG operations:
use ultrahdr_core::{
gainmap::compute::{compute_gainmap, GainMapConfig},
metadata::xmp::generate_xmp,
RawImage, PixelFormat, ColorGamut, ColorTransfer, Unstoppable,
};
use jpegli::encoder::{EncoderConfig, PixelLayout, ChromaSubsampling, Unstoppable as JpegliStop};
// 1. Compute gain map from HDR + SDR
let config = GainMapConfig::default();
let (gainmap, metadata) = compute_gainmap(&hdr_image, &sdr_image, &config, Unstoppable)?;
// 2. Encode gain map to JPEG
let gainmap_jpeg = {
let cfg = EncoderConfig::grayscale(75.0);
let mut enc = cfg.encode_from_bytes(gainmap.width, gainmap.height, PixelLayout::Gray8Srgb)?;
enc.push_packed(&gainmap.data, JpegliStop)?;
enc.finish()?
};
// 3. Generate XMP metadata
let xmp = generate_xmp(&metadata, gainmap_jpeg.len());
// 4. Encode UltraHDR with embedded gain map
let ultrahdr = {
let cfg = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.xmp(xmp.as_bytes().to_vec())
.add_gainmap(gainmap_jpeg);
let mut enc = cfg.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)?;
enc.push_packed(&sdr_rgb, JpegliStop)?;
enc.finish()?
};
use ultrahdr_core::{
gainmap::apply::{apply_gainmap, HdrOutputFormat},
metadata::xmp::parse_xmp,
GainMap, RawImage, Unstoppable,
};
use jpegli::decoder::{Decoder, PreserveConfig};
// 1. Decode with metadata preservation
let decoded = Decoder::new()
.preserve(PreserveConfig::default())
.decode(&ultrahdr_jpeg)?;
let extras = decoded.extras().expect("extras");
// 2. Parse XMP metadata
let xmp_str = extras.xmp().expect("XMP");
let (metadata, _) = parse_xmp(xmp_str)?;
// 3. Decode gain map JPEG
let gainmap_jpeg = extras.gainmap().expect("gainmap");
let gainmap_decoded = Decoder::new().decode(gainmap_jpeg)?;
// 4. Build RawImage and GainMap structs
let sdr = RawImage::from_data(
decoded.width, decoded.height,
PixelFormat::Rgba8, ColorGamut::Bt709, ColorTransfer::Srgb,
rgba_pixels,
)?;
let gainmap = GainMap {
width: gainmap_decoded.width,
height: gainmap_decoded.height,
channels: 1,
data: gainmap_decoded.data,
};
// 5. Apply gain map to reconstruct HDR
let hdr = apply_gainmap(&sdr, &gainmap, &metadata, 4.0, HdrOutputFormat::LinearFloat, Unstoppable)?;
// Decode
let decoded = Decoder::new().preserve(PreserveConfig::default()).decode(&ultrahdr)?;
let extras = decoded.extras().unwrap();
// Edit SDR pixels...
let edited_sdr: Vec<u8> = /* your edits */;
// Re-encode preserving XMP + gainmap
let encoder_segments = extras.to_encoder_segments();
let cfg = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.with_segments(encoder_segments); // Preserves XMP + gainmap
let mut enc = cfg.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)?;
enc.push_packed(&edited_sdr, JpegliStop)?;
let re_encoded = enc.finish()?;
Long-running operations accept an impl Stop parameter from the enough crate for cooperative cancellation:
use ultrahdr_core::{Unstoppable, Stop};
use enough::AtomicStop;
// Simple usage - no cancellation
let (gainmap, metadata) = compute_gainmap(&hdr, &sdr, &config, Unstoppable)?;
// With cancellation support
let stop = AtomicStop::new();
let stop_clone = stop.clone();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(5));
stop_clone.stop();
});
let result = compute_gainmap(&hdr, &sdr, &config, &stop);
Apache-2.0
This library was developed with assistance from Claude (Anthropic). The implementation has been tested against reference Ultra HDR images and passes comprehensive unit tests. Not all code has been manually reviewed - please review critical paths before production use.