Ultra HDR and ISO 21496-1 gain-map JPEG boundaries
Modern smartphones (Android 14+ Ultra HDR and recent iPhone adaptive HDR JPEGs) widely produce ISO 21496-1 (or Google Ultra HDR) gain-map JPEGs. These .jpg files render as backward-compatible 8-bit SDR images on legacy displays, while boosting highlight luminance by several stops on HDR-capable browsers and phones.
Why do these HDR photos suddenly look flat when passed through a standard image resizing service? This page explains the container structure and documents the exact support boundaries across the four interfaces.
1. Inside a gain-map JPEG and why re-encoding drops HDR
Section titled “1. Inside a gain-map JPEG and why re-encoding drops HDR”An ISO 21496-1 / Ultra HDR JPEG is not a single 10-bit image; it bundles three components inside one JPEG stream:
- Base SDR image: a standard 8-bit sRGB or Display P3 JPEG decoded by every legacy viewer.
- MPF (Multi-Picture Format) secondary gain-map image: a second, smaller single-channel or 3-channel JPEG stored after the primary image’s EOI marker (typically at
1/4resolution), encoding the logarithmic luminance boost per pixel. - XMP / ISO 21496-1 metadata: parameters describing minimum/maximum headroom (
hdrCapacityMin/hdrCapacityMax), gamma, and offsets.
When a traditional pipeline runs Load -> Resize -> ExportJpeg:
- The JPEG loader decodes only the primary SDR image and ignores the trailing MPF secondary stream;
- Even if EXIF/XMP metadata is preserved, the secondary gain-map bitmap is either dropped or out of sync with the resized primary image, so the exported file falls back to flat SDR.
2. Cross-binding gain-map support matrix (as of October 2026)
Section titled “2. Cross-binding gain-map support matrix (as of October 2026)”Native libvips support for the "gainmap" metadata image and libuhdr landed in the libvips 8.18 series, creating a version boundary across bindings:
| Interface & verified version | Underlying libvips | Read gain map | Preserve or attach gain map on export | Status and boundary notes |
|---|---|---|---|---|
sharp 0.35.5 |
Bundled libvips 8.18.7 |
metadata.gainMap (returns secondary buffer & parsed info) |
.keepGainMap() / .withGainMap(options) |
Experimental: use keepGainMap() when re-encoding without geometry changes, or transform the gain map alongside the base image with withGainMap() |
pyvips 3.2.0 |
Depends on system libvips |
image.get_gainmap() (requires libvips >= 8.18) |
Update "gainmap" via set_type |
Returns None when linked against libvips 8.17.3 |
govips v2.19.0 |
Tested with libvips 8.17.3 |
No public API | No public API | Re-encoding drops the MPF gain-map stream and outputs SDR |
vips CLI 8.17.3 |
libvips 8.17.3 |
Not supported (requires 8.18+) |
Not supported (requires 8.18+) |
8.17.3 predates native gain-map metadata handling |
3. Code examples: inspecting and preserving gain maps
Section titled “3. Code examples: inspecting and preserving gain maps”// govips v2.19.0 does not wrap ISO 21496-1 gain-map metadata APIs.// Note: re-encoding an Ultra HDR JPEG via ExportJpeg / ExportWebp outputs only the SDR base image.// Keep the embedded ICC profile (typically Display P3) so wide-gamut colours are not lost alongside the gain map:params := vips.NewJpegExportParams()params.Quality = 85params.StripMetadata = false // Retain Display P3 ICC profileimport sharp from 'sharp';
// 1. Inspect whether the input JPEG carries an ISO 21496-1 / Ultra HDR gain map (experimental in sharp 0.35.5)const meta = await sharp('ultra-hdr-input.jpg').metadata();
if (meta.gainMap) { // Re-encode the base JPEG without geometry changes while keeping the gain map intact await sharp('ultra-hdr-input.jpg') .keepGainMap() .jpeg({ quality: 85 }) .toFile('ultra-hdr-reencoded.jpg');}import pyvips
image = pyvips.Image.new_from_file("ultra-hdr-input.jpg")
# pyvips 3.2.0 exposes get_gainmap() (requires libvips >= 8.18 built with libuhdr)gainmap = image.get_gainmap()if gainmap is not None: print(f"Detected gain map: {gainmap.width}x{gainmap.height}, bands={gainmap.bands}")else: print("No gain map found (or linked libvips is earlier than 8.18)")# On libvips 8.17.3, vipsheader does not expose a gainmap field;# on libvips 8.18+ built with libuhdr, vipsheader -a lists gainmap metadata.vipsheader -a ultra-hdr-input.jpg