HDR images
HDR images hold light brighter than SDR white. Egami loads HDR pixels as linear light in floating point, unclamped, with 1.0 as reference white (203 cd/m²), the same scale the color management guide's tone mapping and HDR metadata use. Radiance (.hdr) and OpenEXR (.exr) files hold such pixels directly; see the Radiance and OpenEXR pages. Phone photos carry HDR as a gain map beside an SDR picture, which this guide covers. PQ and HLG images are covered with their formats: AVIF, HEIC and JPEG XL.
Gain maps
An Ultra HDR JPEG, as Android phones, Chrome and Photoshop write it, is an SDR picture any viewer shows, followed (through
MPF) by a gain map: how much brighter each part of the picture is in the HDR rendition. ISO 21496-1, Adobe's hdrgm XMP (Ultra
HDR 1.0) and Apple's HDRGainMap are three ways of writing the same idea down; GainMapFormat says which a file used, and
GainMapInfo holds its values, in log2 units as the standards give them. A load gives the SDR picture as stored, with the map in
RasterMetadata.GainMap as a GainMap (GainMapValues per channel); a damaged map is dropped with Jpeg.GainMapDropped.
ReaderOptions.Hdr asks for the HDR rendition instead: HdrDecode.Headroom is how much brighter than SDR white the display can
go (or the map's full capacity), and the load gives linear RgbaF16, within about 1% of libultrahdr's decode.
GainMap.Apply does the same for a raster in hand.
// As stored: the SDR base image, with the gain map in the metadata.
using Raster sdr = Raster.Load("ultrahdr.jpg");
GainMap? map = sdr.Metadata.GainMap; // null for a plain JPEG
// The HDR rendition for a display 4x as bright as SDR white: linear half floats, 1.0 = SDR white.
using Raster<RgbaF16> hdr = Raster.Load<RgbaF16>("ultrahdr.jpg", options: new ReaderOptions { Hdr = new HdrDecode { Headroom = 4 } });
// The same from a raster in hand, at the map's full capacity.
using Raster<RgbaF16> full = map!.Apply(sdr);
Saving as JPEG writes the map back, in both metadata forms, as Skia and libultrahdr write them. Crops, turns, flips and resizes
move the map with the picture; operations that change pixel values drop it (Operation.GainMapDropped), as do other formats
(Save.GainMapDropped) and JpegWriterOptions.KeepGainMap = false.
// Crops, turns, flips and resizes move the gain map with the picture; saving as JPEG writes it back.
using Raster photo = Raster.Load("ultrahdr.jpg");
using Raster cropped = photo.Derive(ops => ops.Crop(16, 16, 200, 150).Rotate(90));
cropped.Save("cropped.jpg"); // still Ultra HDR
// Operations that change pixel values drop it (Operation.GainMapDropped), and so do formats other than JPEG
// (Save.GainMapDropped); KeepGainMap = false leaves it out on purpose.
cropped.Save("sdr-only.jpg", new JpegWriterOptions { KeepGainMap = false });
To write an HDR raster as Ultra HDR, set JpegWriterOptions.GainMap: the raster is tone mapped to the SDR picture and
GainMap.Create makes the map that brings it back. GainMapOptions has libultrahdr's defaults (a full-size, color map, gamma 1,
quality 95) and each can be changed; Create also takes an SDR rendition you graded yourself.
// An HDR raster as Ultra HDR: the SDR base, tone mapped with BT.2390, and a gain map that brings the HDR raster back.
using Raster<RgbaF32> hdr = Raster.Load<RgbaF32>("render.exr");
hdr.Save("render.jpg", new JpegWriterOptions { Quality = 90, GainMap = new GainMapOptions() });
// Or choose the SDR rendition: the map goes in its metadata, and saving as JPEG writes both.
using Raster<Rgb8> sdr = hdr.Derive(ops => ops.ToneMap(ToneMapping.Reinhard)).As<Rgb8>();
sdr.Metadata.GainMap = GainMap.Create(hdr, sdr, new GainMapOptions { Scale = 4 });
sdr.Save("graded.jpg", new JpegWriterOptions { Quality = 90 });
Saving HDR to an SDR format
PNG, JPEG and the other 8 and 16-bit formats hold 0 to 1. Saving a floating-point raster to them clamps, as other libraries do;
when the raster holds light above reference white, the save also reports Save.HdrClipped, naming ToneMap, which rolls the
highlights off instead of cutting them.
Recipes
Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.
Show an Ultra HDR photo as HDR
// An Ultra HDR photo for an HDR display whose peak is displayHeadroom times SDR white: linear light, 1.0 = SDR white,
// here written as a half-float OpenEXR file; an SDR display shows the JPEG as it is.
using Raster<RgbaF16> photo = Raster.Load<RgbaF16>("ultrahdr.jpg", options: new ReaderOptions { Hdr = new HdrDecode { Headroom = displayHeadroom } });
photo.Save("photo-hdr.exr");
The gain map is applied for the display's headroom: 1 gives the SDR picture, and the map's own capacity (leave Headroom null)
the full HDR rendition.
Make an Ultra HDR JPEG from an OpenEXR render
// An HDR render as a JPEG every viewer opens, which HDR displays (Android, Chrome, Apple Photos) show brighter.
using Raster<RgbaF32> render = Raster.Load<RgbaF32>("render.exr");
render.Save("render-ultrahdr.jpg", new JpegWriterOptions { Quality = 90, GainMap = new GainMapOptions { Scale = 2 } });
The SDR picture is the render tone mapped with BT.2390; the gain map, half the size here, brings the HDR render back.