HDR2gainmap
January 21, 2026 · View on GitHub
‼️‼️‼️ The development of this CLI tool is abandoned. Please consider using the GUI version available here ‼️‼️‼️
A tiny command‑line tool that turns HDR stills into 10‑bit HEIC with an Apple‑compatible gain map.
- Use your own SDR base (PNG in Display P3) to control SDR appearance, or
- Let the tool tone‑map the SDR base from the HDR image.
- Derives Maker Apple metadata keys
33and48from measured headroom (Apple’s piecewise mapping, clamped to 3 stops / 8×). See Apple’s docs: https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos. - Computes headroom via peak‑max (default) with a softening curve, or via a robust percentile (99.9th by default when enabled).
- Can emit a clip mask HEIC and/or a masked SDR overlay HEIC to visualize clipped areas (only when the SDR is generated by the tool).
- Parallel processing with a progress bar, per‑file structured logging, configurable log colors, and an end‑of‑run summary (written / skipped / failed).
- Optional on‑disk log file;
--debugadds extra diagnostics. - Selectable HEIF encoder strategy (
--heif_strategy auto|heif|heif10) with auto‑fallback plus post‑export gain‑map verification (enabled by default).
Features
- ✅ Apple Photos–friendly HEIC (10‑bit Display P3) with embedded gain map (works across iCloud).
- ✅ Two workflows: HDR+SDR pair or HDR‑only with tone‑mapped SDR.
- ✅ Peak‑max workflow by default (
--peak_max) with configurable--tonemap_ratio(default 0.2). - ✅ Optional percentile‑based headroom (
--peak_percentile [value], default 99.9 if enabled). - ✅ Dry‑run mode (
--tonemap_dryrun) prints headroom & clip stats without writing HEICs. - ✅ Optional clip mask (
--emit_clip_mask) and masked overlay (--emit_masked_image) for debugging.- Both are written as HEIC 10‑bit Display P3 using the same compression quality as the final output.
- Ignored if an SDR companion image already exists (user‑provided SDR takes precedence).
- ✅ Parallel processing (
--parallel) with configurable concurrency (--max_concurrent). - ✅ Progress bar (default), or verbose console logging (
--verbose/--debug), plus optional log file (--write_log [path]). - ✅ Colorized logs by default (auto‑disabled if not a TTY); force monochrome with
--no_color. - ✅ Gain‑map verification after export (by default). Opt out with
--do_not_verify. - ✅ HEIF encoder strategy control via
--heif_strategy auto|heif|heif10(final HEIC only). - ✅ Strict validation (color space, orientation, size) and an end‑of‑run summary.
- ✅ Batch processing of all
*.pngin./input_HDR.
Requirements
- macOS 15+ (uses
CIToneMapHeadroomand HDR gain‑map write options). - Xcode Command Line Tools (for
swiftc). - Inputs are PNG files:
- HDR: Display P3 PQ (tagged).
- SDR (optional): Display P3 (tagged).
Folder Layout & Input Specs
./input_HDR/ # HDR PNGs (required)
./input_SDR/ # optional SDR PNGs (same basename as HDR)
./output_HDR_with_gainmap/ # output HEICs (final results)
./output_clipped_mask/ # clip masks (HEIC, created only if requested)
./output_clipped_overlay/ # SDR overlays (HEIC, created only if requested)
HDR input (required)
- Format: PNG
- Color space: Display P3 PQ (tagged)
- Naming:
name.png→ output becomesname.heic - Tip: If your HDR PNGs are untagged, re‑export with an explicit Display P3 PQ profile.
SDR input (optional, per image)
- Format: PNG
- Color space: Display P3 (tagged)
- Constraints: same basename, same dimensions, same orientation as the HDR
- If present, it is used as the SDR base. If absent, the tool tone‑maps the SDR base from HDR.
Note: If an SDR is present, the options
--emit_clip_maskand--emit_masked_imageare ignored (the tool only visualizes clipping for SDRs it generates itself).
How It Works (high‑level)
- Scan
./input_HDR/for*.png. - For each HDR:
- If
./input_SDR/<same-name>.pngexists → use it (after checks). - Otherwise → tone‑map the HDR via
CIToneMapHeadroomto create the SDR base.
- If
- Measure the peak in linear P3 and compute linear headroom:
- Peak‑max (default): take absolute max and apply a softening curve with
tonemap_ratio(default 0.2), or - Percentile: normalized linear‑luminance histogram (up to 2048 bins) and a high percentile (default 99.9th when enabled).
- Peak‑max (default): take absolute max and apply a softening curve with
- Convert headroom → Maker Apple keys (
33,48) using Apple’s piecewise mapping
(clamped to 3 stops → 8× headroom; validated ex‑post).
Reference: Apple, Applying Apple HDR effect to your photos, section on encoding “stops” into33/48:
https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos - Build a temporary in‑memory HEIC from (SDR base + HDR) so Core Image generates a gain map.
- Extract the gain map as an auxiliary image.
- Write the final HEIC (10‑bit Display P3) with:
- SDR base,
- explicit gain map,
- Maker Apple metadata.
- Verify that the written HEIC contains a gain map (unless
--do_not_verifyis set). - Show an end‑of‑run summary with counts and reasons for any skipped/failed items.
Build & Run
Compile
xcrun --sdk macosx swiftc -O -o hdr2gainmap BatchHDR2HEIC.swift
Prepare folders
You can pre‑create all output folders, but it isn’t required.
output_clipped_mask and output_clipped_overlay are created only if the related flags are used.
mkdir -p input_HDR input_SDR output_HDR_with_gainmap
Run (basic)
./hdr2gainmap
Outputs to:
./output_HDR_with_gainmap/
# (the two debug folders are created on demand when their flags are used)
Command‑line Options (snake_case)
Usage:
hdr2gainmap [--suffix <text>] [--peak_percentile [value]] [--peak_max]
[--tonemap_ratio <0..1>]
[--heic_compression_quality <0..1>]
[--heif_strategy <auto|heif|heif10>]
[--tonemap_dryrun] [--emit_clip_mask]
[--emit_masked_image [color]]
[--parallel] [--max_concurrent <n>]
[--verbose] [--write_log [path]] [--no_color] [--debug]
[--do_not_verify]
Options:
--suffix <text> Suffix appended to output filename (e.g. "_sdrtm")
--peak_percentile [value] Use percentile‑based peak (default 99.9 if value omitted)
--peak_max Use absolute max + blend (tonemap_ratio applied) [DEFAULT]
--tonemap_ratio <0..1> Blend curve for peak_max (default 0.2)
--heic_compression_quality <q> HEIC quality for final/mask/overlay in [0,1] (default 0.97)
--heif_strategy <s> HEIF encoder strategy for the FINAL HEIC only:
auto (default) | heif | heif10
--tonemap_dryrun Only compute headroom + clipped fraction; no HEIC output
--emit_clip_mask Write a black/white **HEIC** mask of clipped pixels
(ignored if an SDR file already exists)
--emit_masked_image [color] Write an **HEIC** SDR image with clipped pixels painted solid
(ignored if an SDR file already exists)
Color can be a name (red, magenta, violet) or #RRGGBB (default: magenta)
--parallel Enable parallel processing of files
--max_concurrent <n> Max concurrent file processing (default: CPU count)
--verbose Print detailed log messages to stderr (disables progress bar)
--write_log [path] Write log messages to file (default: ./hdr2gainmap.log)
Progress bar remains active unless --verbose is also used
--no_color Disable ANSI colors in console output
--debug Print debug messages (implies --verbose)
--do_not_verify Skip post‑export gain‑map verification
--help Show this message
Troubleshooting
"Cannot read HDR" error
- Ensure PNG is tagged with Display P3 PQ profile
- Re-export from your editor with explicit colorspace
"No gain map extracted" error
- Try
--heif_strategy heif10or--heif_strategy heif - Update macOS to latest version
- Check with Adobe Gain Map Demo App
Progress bar doesn't update
- Disable with
--verboseto see detailed logs - Check file permissions on output directories
Logging behavior
- Default: Progress bar only, minimal console noise, per‑file logs go to the optional log file if
--write_logis used. - --verbose: Detailed console output instead of the progress bar.
- --write_log: Progress bar and a log file on disk (unless
--verboseis set). - --debug: Adds histogram/clipping internals and other diagnostics; implies
--verbose. - --no_color: Prints monochrome logs even on a TTY.
Examples
-
HDR‑only, peak‑max (default), HEIC quality 0.95, debug:
./hdr2gainmap --peak_max --heic_compression_quality 0.95 --debug -
HDR‑only, robust percentile (99.9th), clip mask + overlay, verbose:
./hdr2gainmap --peak_percentile --emit_clip_mask --emit_masked_image red --verbose -
Explicit percentile 99.0th, suffix appended:
./hdr2gainmap --peak_percentile 99.0 --suffix _p99 -
Peak‑max with a custom softening curve (0.3), dry‑run only:
./hdr2gainmap --peak_max --tonemap_ratio 0.3 --tonemap_dryrun -
Parallel processing with 6 workers, logging to file while keeping progress bar:
./hdr2gainmap --parallel --max_concurrent 6 --write_log ./hdr2gainmap.log -
Force the HEIF10 encoder for the final HEIC and skip verification:
./hdr2gainmap --heif_strategy heif10 --do_not_verify
Output
- HEIC, 10‑bit (
RGB10) Display P3 base image. - Embedded gain map (explicit, platform‑native; not forced to RGB payload).
- Maker Apple metadata keys
33and48, consistent with the measured (clamped) headroom. - Optional debug HEICs (only when SDR is generated by the tool):
output_clipped_mask/<name>_clipmask.heic(white = clipped; black = not clipped),output_clipped_overlay/<name>_clippedOverlay.heic(SDR with clipped pixels painted a solid color).
- End‑of‑run summary that lists counts of written / skipped / failed files and names (with reasons) for any skipped/failed items.
Verify the Gain Map (Important)
The tool verifies by default that each written HEIC contains a gain map.
If a potential issue is detected (e.g., encoder differences on some macOS configurations), a warning is emitted or the write may fail depending on strategy.
You can skip verification with --do_not_verify. For manual inspection you can also use Adobe’s Gain Map Demo App for macOS:
https://www.adobe.com/go/gainmap_demoapp_mac
HEIF encoder strategy
Some configurations (hardware or macOS versions) differ in how they emit auxiliary images (gain maps). The flag:
--heif_strategy auto|heif|heif10 (final HEIC only)
controls which encoder is tried:
auto(default): heuristically chooses an order (e.g. try HEIF10 first on Apple Silicon) and falls back to the other if verification fails.heif: forcewriteHEIFRepresentation(...).heif10: forcewriteHEIF10Representation(...).
The mask/overlay debug HEICs never carry a gain map and are always written with the standard HEIF writer.
Notes & Limitations
- Maximum metadata headroom in the Maker Apple mapping is 3 stops (8×).
If the measured headroom exceeds 8×, the tool clamps metadata only (the gain map can still encode broader dynamics). - Color tags are enforced:
- HDR must be Display P3 PQ (tagged).
- SDR (if provided) must be Display P3 (tagged).
- Files with mismatched size/orientation are rejected.
- Percentile headroom uses a normalized linear‑luminance histogram (up to 2048 bins) in extended linear P3.
- The peak‑max method applies a softening curve with
tonemap_ratio(default 0.2).
Compatibility
- Tested on both Intel- and Silicon‑based Mac running macOS 15.x -- both work fine with
--heif_strategy [auto | heif], do not work with--heif_strategy heif10. - On some systems (possibly macOS 26? More tests needed), switching using
--heif_strategy heif10may be required.
Credits
Huge thanks to chemharuka for prior art and inspiration:
https://github.com/chemharuka/toGainMapHDR
This project adapts and extends those ideas with:
- automatic Maker Apple metadata derivation (forward + inverse mapping),
- two peak measurement strategies (peak‑max and percentile),
- strict validation & ex‑post metadata checks,
- masks/overlays for debugging,
- batch processing, parallelism, progress/summary logging,
- HEIF encoder strategy & verification,
- and an Apple‑friendly export path.
License
MIT