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 33 and 48 from 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; --debug adds 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 *.png in ./input_HDR.

Requirements

  • macOS 15+ (uses CIToneMapHeadroom and 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 becomes name.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_mask and --emit_masked_image are ignored (the tool only visualizes clipping for SDRs it generates itself).


How It Works (high‑level)

  1. Scan ./input_HDR/ for *.png.
  2. For each HDR:
    • If ./input_SDR/<same-name>.png exists → use it (after checks).
    • Otherwise → tone‑map the HDR via CIToneMapHeadroom to create the SDR base.
  3. 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).
  4. Convert headroom → Maker Apple keys (33, 48) using Apple’s piecewise mapping
    (clamped to 3 stops headroom; validated ex‑post).
    Reference: Apple, Applying Apple HDR effect to your photos, section on encoding “stops” into 33/48:
    https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
  5. Build a temporary in‑memory HEIC from (SDR base + HDR) so Core Image generates a gain map.
  6. Extract the gain map as an auxiliary image.
  7. Write the final HEIC (10‑bit Display P3) with:
    • SDR base,
    • explicit gain map,
    • Maker Apple metadata.
  8. Verify that the written HEIC contains a gain map (unless --do_not_verify is set).
  9. 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 heif10 or --heif_strategy heif
  • Update macOS to latest version
  • Check with Adobe Gain Map Demo App

Progress bar doesn't update

  • Disable with --verbose to 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_log is used.
  • --verbose: Detailed console output instead of the progress bar.
  • --write_log: Progress bar and a log file on disk (unless --verbose is 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 33 and 48, 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: force writeHEIFRepresentation(...).
  • heif10: force writeHEIF10Representation(...).

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 heif10 may 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