Skip to content

Ultra HDR and ISO 21496-1 gain-map JPEG boundaries

Copy page

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:

  1. Base SDR image: a standard 8-bit sRGB or Display P3 JPEG decoded by every legacy viewer.
  2. 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/4 resolution), encoding the logarithmic luminance boost per pixel.
  3. 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 = 85
params.StripMetadata = false // Retain Display P3 ICC profile