Skip to content

v3.0.0

Latest

Choose a tag to compare

@wkjarosz wkjarosz released this 04 Sep 19:29
1145011

What's changed

This is the first HDRView release in the 3.0 series! It includes a huge amount of work across 55 pull requests and over 350 commits.

The major version bump is for four things HDRView simply could not do before:

  • HDR output on all desktop platforms, Windows and Wayland Linux now join macOS in supporting true HDR output on compatible displays!
  • it can edit images again, with a real undo history — the destructive editing HDRView had before the 2.0 rewrite, rebuilt on top of the modern channel/layer image model;
  • you can draw on an image — arrows, boxes, circles, scribbles and text — to mark up what you are looking at;
  • it can act as a display server for supported renderers to display in-progress renderings, over the same protocol tev and PBRT speak.

On top of that: sessions, multi-selection, JPEG 2000, HDR gain maps in various formats, a much faster AVIF decoder, a rebuilt statistics/histogram panel — and a lot of correctness work. Seven systematic bug hunts, plus AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer and libFuzzer jobs in CI, turned up several crashes, memory-safety bugs, and a long list of images that were being decoded, displayed or exported silently wrong. HDRView now has a steadily growing arsenal of over 340 automated tests, alongside ~100 GUI regression tests.

Highlights

  • Image editing with undo — crop, resize, flips and rotations, blurs, unsharp mask, median, exposure/gamma and brightness/contrast curves you can drag, channel mixer, hue/saturation, color-space conversion, seamless (Poisson) paste, mip pyramid generation, environment-map remapping and irradiance convolution — all undoable, with a History panel (#208)
  • Annotations — rectangles, ellipses, lines, arrows, freehand scribbles and text drawn over an image, in image coordinates, saved with the session and never baked into the pixels (#223, #226)
  • Live images from your renderer — a display server speaking tev's IPC protocol, verified end to end against pbrt-v4 (#207)
  • True HDR display output on macOS, Windows and Linux/Wayland, with the display's real headroom read from each platform and drawn in the histogram (#178, #192)
  • Sessions (.hsess) — save and restore your whole working state, and export a session bundle that zips the images along with it (#176)
  • Multi-selection over (image, channel group) pairs, with edits fanning out over the selection (#209)
  • HDR gain maps from iPhone and Android photos — Apple, ISO 21496-1, Adobe XMP and JPEG XL flavors (#190)
  • JPEG 2000 and HTJ2K read and write, including bare codestreams (#222)
  • AVIF decoding up to 6× faster via dav1d (#206)
  • A unified Pixel statistics window — pixel inspector, channel statistics and histogram in one panel, with a rebuilt histogram (#172, #181, #186)
  • Native Windows ARM64 builds (#170)

Image editing, with an undo system

Editing is back. The Edit menu currently contains 34 commands grouped by type. You can run edit commands on the current image, or on multiple selected images at once. Each one lands as a single step in a History panel that shows the whole stack, where you are in it, and what each step costs in memory.

  • Clipboard — cut, copy, paste, and gradient-domain/Poisson seamless paste
  • Size and orientation — flips, quarter turns, crop, image size, canvas size, and generating a mip pyramid as one image per level.
  • Tone — invert, clamp, zap gremlins (NaNs/infinities), exposure/gamma, brightness/contrast, fill.
  • Color — convert color space (which retags the image's metadata to match), channel mixer, hue/saturation, flatten over a background. CIE L*a*b* was added along the way, so brightness/contrast can work on lightness alone, or on the two opponent chromaticity axes alone.
  • Channels — ungroup a group into single channels, regroup them, delete a group outright.
  • Neighborhood filters — Gaussian, fast Gaussian and box blur, unsharp mask, median, shift with wrapping, bump-to-normal-map.
  • Environment maps — remap between six parameterizations with EWA filtering over a mip pyramid, and convolve one to irradiance. Both the 3×4 cube cross and OpenEXR's 1×6 column are handled.

Long filters run off the main thread behind a progress bar you can cancel.

Annotations

There is now a floating tools palette with the existing Pan, selection, and pixel inspector tools. And now also with an annotation tool. Pressing A brings it up and pressing it again cycles the type of annotation. F10 brings up the Annotations panel.

  • Drag on empty space to draw, drag a shape to move it, drag a handle to resize it. Esc puts back whatever a drag changed; Delete removes the annotation in hand.
  • Text is placed with a click and typed inline, with a choice of face, size and a 3×3 anchor grid; dragging a corner of its box scales the font.
  • Scribbles can be drawn as straight segments or smoothed into a curve.
  • The panel lists every annotation with visibility, lock, per-shape icon, rename and delete, and you can drag rows to reorder.
  • Stroke width and font size can be in image pixels (so it zooms with the image) or screen pixels (so it stays put), picked from a menu inside the field. (#226).

Annotations are view state, not image data: they never enter the undo history, never mark an image modified, and are never rasterized into the pixels. That is what lets them work on an image a renderer is still writing into. They are per image, survive a reload, and are saved into sessions.

Live images from a renderer

HDRView now listens for images over the protocol tev uses, so the clients that already exist for it work unchanged — a renderer can create an image, stream tiles into it as it converges, and lay vector graphics over it. It is off until you turn it on, from the Watched Folders panel, the command palette, or --listen (#207).

  • Verified end to end against pbrt-v4 (pbrt --display-server 127.0.0.1:14158): the render arrives tile by tile, and HDRView's channel statistics match the EXR pbrt writes.
  • The panel shows an indeterminate activity bar with the live rate and running totals.
  • Vector overlays (paths, shapes, text, in the image's own coordinates) are drawn through Dear ImGui's draw lists. The few commands with no equivalent are reported (but these seem to be uncommon in the wild).

HDR display output, everywhere

The EDR/HDR display support HDRView has long had on macOS now works on Windows and Linux/Wayland too (#178). Frames are composited in extended sRGB and converted once, at the end, into what the display actually wants — scRGB linear, a power curve, or PQ, over a wider gamut where available. The display's color space is re-queried every frame, so moving the window between monitors or toggling the OS HDR mode takes effect immediately.

This is thanks in large part to @wjakob and @Tom94, who worked out how to actually get HDR to the display in NanoGUI and tev; HDRView now depends on Tom94's patched GLFW fork.

The histogram now shows what your display can do with the image (#192): the span up to display value 1 is labeled SDR, the span from there to the display's current headroom is labeled HDR and grows and shrinks as the monitor's brightness changes. The About dialog reports the headroom too.

Sessions, selection and workflow

  • Sessions (#176): File > Save session… / Load session… store the loaded image list, current and reference selection, blend mode, view and display settings, annotations, and your channel selection (by channel name, so it survives a reload) to a .hsess file — with drag-and-drop, CLI, and Finder/Explorer "Open with" support. An exported session bundle zips the images with it, so it can be handed to someone else or opened in the web build. Use this to prepare image comparison sets before your next research meeting!
  • Multi-selection (#209) is back, extended from images to (image, channel group) pairs: Ctrl/Cmd-click toggles, Ctrl/Cmd+Shift-click selects a range, and Shift-click still sets the reference. Edits run once per selected image, each as its own undo step. This also allows you to ungroup an RGBA group, deselect the alpha, and group the rest back up.
  • Status-bar log badge (#171) surfaces the highest-severity message since you last looked at the Log window; click it to jump straight there.
  • Flipbook playback is now paced at the rate you asked for (#210). The frame period was rounded to whole milliseconds, which collapsed every broadcast rate onto its neighbor — 23.976 and 24 fps were bit-identical, and both actually ran at 24.39. Playback also no longer caps at the render rate or accumulates timing error, and the rate field shows three decimals.
  • Mouse wheel zoom and trackpad pinch (#203, #198). A wheel notch used to zoom by about 1.7%, which is why the wheel felt dead over the image and fine over the panels; a notch is now a quarter stop, with trackpad travel detected separately and scaled to match. Pinch-zoom on a touchscreen or trackpad continues to work.

Statistics, histogram and inspection

  • The Pixel Inspector, Channel Statistics and Histogram are merged into one dockable Pixel statistics window (F6), with a resizable histogram, a legend, and per-side clip warnings (#172, #181).
  • The histogram was rebuilt (#186). 8-bit images used to draw as a row of detached spikes; bins are now laid out in the space the x axis is drawn in, so they are equally wide on screen and their height is a plain count, and the bin count comes from the file's actual bit depth, per channel.
  • Overlapping channels composite correctly — an additive histogram fill, so R+G+B reads as white instead of a muddy alpha blend (#168).
  • Negative excursions now get the room they actually occupy rather than a mirror of the positive reach, so a hundredth of a stop of codec ringing below zero no longer costs half the plot (#192).
  • Straight (unpremultiplied) alpha is handled correctly (#185): isolating a channel no longer shows it with alpha baked in, and pixel values, statistics and histograms report what the file holds rather than HDRView's internal premultiplied representation.
  • Three ways the pixel readouts disagreed with the shader were fixed, so the numbers you read now match the pixels you see (#194).
  • Isolating the Y channel used the wrong luminance weights (#194).
  • Statistics over a rectangular selection that does not start at the origin were wrong (#159, in v2.8.3) — and a selection that misses the image entirely used to crash (#189).

New and improved format support

  • JPEG 2000 (#222): imageio/j2k.cpp reads and writes both the boxed JP2 file format and bare codestreams, at any precision up to 31 bits, via OpenJPEG. High-throughput HTJ2K (.jph, .jhc) needs no second library — OpenJPEG has decoded it since 2.5.0 — and files from OpenJPH, OpenHTJ2K, Grok and OpenJPEG itself all read. Also fixed a build issue where JPEG 2000 inside a HEIF container (.hej2) was not enabled on Windows.
  • HDR gain maps (#190): HDRView previously supported Android UltraHDR JPEGs and now also supports HDR iPhone photos which store a gainmap. Apple's vendor format (in JPEG, HEIC and AVIF), ISO 21496-1, Adobe's hdrgm: XMP, and JPEG XL's jhgm box are now all read. The map is appended as its own gainmap.* channel group whether or not it is applied, so it stays inspectable, and Image loading options has a target-headroom control (0 stops shows the SDR rendition as stored).
  • AVIF decodes up to 6× faster (#206) by asking libheif for its dav1d plugin, which it then prefers on its own. Measured on eight 24 MP 10-bit HDR photos: 875 ms against libaom's 5295 ms for the AV1 decode, and opening one 24 MP photo in the app goes 1.57 s → 0.73 s. Output is bit-identical — across 407 corpus files, every decoded pixel agrees. libaom stays in the build as the AVIF encoder. The HEIF sample-normalizing loop was parallelized in the same PR, and the Build info panel now names the AV1 decoder libheif actually chose. Linux and macOS builds get dav1d; Windows and the web build stay on libaom for now.
  • libheif's codec plugins were never registered at all on distributions that ship them as separate modules — HDRView never called heif_init() (#193). And the HEIF encoder used to be whichever one libheif happened to list first, regardless of the image.
  • The web build gains AVIF and JPEG 2000 (#203), by building libaom and OpenJPEG itself rather than looking for installed ones that Emscripten never has.
  • Formats HDRView could not write correctly, now fixed (#193): JPEG XL "Lossless" always failed, JPEG XL and HEIF could not write grayscale at all, save_exr_image() with its default argument wrote an empty channel list, and QOI and WebP previously refused grayscale and now expand to RGB.

Color correctness

A month of chasing images that looked almost right. Every fix here has a test that fails without
it.

  • Premultiplied alpha is now read in the space the file wrote it in (#211). Multiplying by alpha
    and applying a transfer function do not commute, and HDRView read and wrote associated-alpha TIFF
    the opposite way round from everyone else — so semi-transparent areas were off by up to 3× at low
    coverage. Since neither TIFF 6.0, MIAF nor JPEG XL says which convention applies, this was settled
    empirically: one known image written by Photoshop, OpenImageIO, ImageMagick, libvips, GraphicsMagick
    and Blender, all six unanimous. HDRView now matches what files actually contain, and an override
    exposes the other convention for files that contradict their own format.
  • Two ICC bugs affecting any P3, Adobe RGB or ProPhoto image (#190): the unadapted white point is
    now taken from the profile's own chad tag rather than derived through lcms, which on Apple's
    Display P3 answered a white of (0.278, 0.296) — no illuminant at all — and threw off every primary
    derived against it; and linearization now runs at relative rather than absolute colorimetric, which
    was scaling every channel by the D65/D50 ratio (a flat R ×0.88, G ×1.01, B ×1.33 on a Display P3
    JPEG, enough to push an ordinary 8-bit file above 1.0).
  • CMYK images across every format (#222). A CMYK profile made linearize_pixels() abandon the
    transform entirely and fall through to treating ink as sRGB, so every CMYK JPEG HDRView has ever
    opened was affected; the ink was then scaled the wrong way round for JPEG 2000 and TIFF; and a CMYK
    TIFF never reached its profile at all. Measured against an RGB reference, mean absolute error on one
    photograph saved seven ways: CMYK JP2 0.27 → 0.010, CMYK TIFF 0.199 → 0.011, CMYK JPEG (profile
    dropped) → 0.012.
  • PNG exports now record their transfer function where readers can actually see it (#188). Saving
    a PNG signalled the curve exclusively through the cICP chunk, which expresses any curve exactly
    but which most readers ignore and libpng only learned to write in 1.6.46 — so a linear or PQ export
    was read correctly by HDRView and quietly misinterpreted as sRGB by everything else. Worse on older
    libpng (including the 1.6.43 Ubuntu 24.04 ships), where the pixels were encoded with a curve and
    the file recorded no curve at all. The writer now also emits gAMA and the sRGB chunk whenever they
    can express the curve, and the save dialog says up front which curves can only be recorded in cICP.
  • PQ, HLG and DCI-P3 turned negative values into NaN (#187). Out-of-gamut colors are carried as
    negative components, so saving wide-gamut content through those curves corrupted exactly the
    channels that were out of gamut. All three are now mirrored around the origin, following OpenColorIO
    and colour-science.
  • Every QOI file was displayed far too bright (#191).
  • HDR images signalling PQ or HLG through an ICC profile lost their entire HDR range, and a file's
    video range was ignored when only its ICC profile stated it (#191).
  • DXT2 and DXT4 images were premultiplied by alpha twice and rendered too dark (#191).
  • An 8-bit TIFF carrying a TransferFunction tag decoded to black, a luminance-chroma EXR group
    exported as raw chroma, a wide-gamut image lost its gamut when saved as TIFF, and a monochrome
    HEIF's alpha plane was linearized as though it were color (#193).
  • A camera raw frame's masked border was uninitialized memory (#191) — LibRaw fills only the
    active area.
  • An EXIF orientation moved the pixels but not the image's data and display windows (#191), and
    flipping an image shifted everything HDRView knows about it by one pixel (#194).

Crashes, hangs and memory safety

CI gained AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer jobs plus a libFuzzer
target over the image loaders (#187), and seven hand-driven bug hunts (#187, #189, #191, #193, #194,
#196, #197) went after the paths a fuzzer cannot reach. Two of these were invisible to an ordinary
build: the tests passed while memory was being corrupted, which is how they shipped in a beta.

  • Opening a 10- or 12-bit image corrupted memory — histogram bin counts overflowed their storage
    for any source with 9 to 15 bits per sample, reachable from ordinary 10- and 12-bit HEIF/AVIF, 12-
    and 14-bit camera raw, and 10- and 12-bit TIFF and JPEG XL (#187).
  • Closing an image while its statistics were still computing freed channels the background task
    was reading, in two independent ways (#187).
  • A rectangular selection that misses the image crashed HDRView (#189).
  • A profile lcms refuses to open segfaulted on the spot, which a JPEG XL carrying a truncated ICC
    profile could reach (#224).
  • Saving a gray+alpha image corrupted its alpha, and saving one as JPEG terminated HDRView
    outright (#189, #193). save_heif_image() faulted for any caller outside the GUI (#193).
  • An EXR attribute with a zero denominator killed the process, and an ordinary XMP packet could
    make a PNG load zero images (#197).
  • A session passed on the command line dereferenced a null singleton, and an unresolved session
    entry walked off the end of the image list (#191).
  • An image named on the command line could fail to load while the same file opened from the GUI
    loaded fine — the maximum texture size was queried without a GL context, which quietly left it at
    zero, so every image was measured against a limit of zero (#201).
  • A malformed file could reach the raw decoder and spend minutes being demosaiced as an
    imaginary multi-megapixel image; files declaring implausible dimensions are now rejected before a
    loader allocates for them (#187).
  • A watched folder stopped being watched if you closed an image while the "Add watched folder…"
    dialog was open; a reload could duplicate an image or bring back one you had closed; and a folder
    entered Open Recent before anything in it had been read (#196).
  • Out-of-bounds reads in name shortening, the line helpers in common.cpp, zip entry extraction, PSD
    image resources, and an Apple maker-note bounds check that wrapped at 32 bits (#189, #191, #196,
    #197).
  • Saving an image containing NaN to an integer format, and displaying an Inf in the pixel inspector,
    were undefined behavior — neither std::clamp nor ImClamp filters a NaN (#187).
  • Indices, divisors and zoom values read from session and settings files went unvalidated, and
    --gamma 0 rendered every image black (#189).
  • Zoom in/out skipped a stop (#194), and image-list crashes, slow truncation and a scroll-to-selection
    stall were fixed (#173).
  • The image texture was sampled inside a bounds check, so texels at an image's edge could come from
    the wrong mip level (#210).
  • A Windows crash on long file paths, via a missing longPathAware manifest (#174).

Web version

  • Copy and paste work (#198). HDRView was installing its own clipboard handling while GLFW's Emscripten port didn't support it. It was flaky, and now the GLFW port has proper support.
  • Pinch-zoom works on phones and tablets (#198, #203) — GLFW has no gesture API on any platform
    and the Emscripten port tracks only one finger, so the two-finger separation is now read from the
    browser's own touch events.
  • AVIF and JPEG 2000 are available in the browser (#203). The wasm grows 10.1 MB → 13.2 MB, most
    of it the AV1 encoder.
  • The web app is now built on Ubuntu rather than macOS in CI (#200).

Packaging and platforms

  • Native Windows ARM64 builds for non-emulated performance on Windows-on-ARM devices (#170).
  • The Linux AppImage shipped uncompiled shader sources and would not start (#177) — thanks
    @Delivator for reporting it. Packaging gained better diagnostics and a CI check that the
    packaged artifact actually runs (#184).
  • Compiler warnings from HDRView's own code swept across macOS, Linux, Windows and Emscripten (#205).
  • All five icon sets build again — four of the five were broken (#208).

Under the hood

  • Switched from the hello_imgui fork to upstream hello_imgui v1.92.900 (#180), and modal dialogs
    were refactored onto shared ImGui helpers (#179). Enter/Esc now finish any
    modal dialog.
  • HDRView now has an extensive test suite with over 340 automated tests, plus ~100 Dear ImGui Test Engine-based GUI
    regression tests (#175) that drive a real HDRView window in CI.
  • The tests now use the data our own dependencies already ship (#224): nine camera JPEGs from
    libexif with the values libexif reads from them, 31 TIFFs libtiff's own regressions were written
    against, one image per block format from bcdec, 44 ICC profiles and six codestreams from libjxl,
    and the three deliberately broken profiles lcms keeps. One of them found the lcms crash above.
  • The README was rewritten (#202), and its screenshots are now generated by the GUI test
    suite rather than taken by hand so they are less likely to stay stale (#202, #227).

Notable behavior changes

  • Associated-alpha TIFFs read and write differently. HDRView now premultiplies in the encoded
    space, matching what every other application writes; an override exposes the previous behavior for
    files that contradict their own format's convention (#211).
  • Pixel values, statistics and histograms now report what the file holds, not HDRView's internal
    premultiplied representation (#185).
  • Your histogram axis-scale choices now persist between launches (#186).

Known limitations

  • ISO tmap gain maps in HEIC/AVIF still read as their base rendition: libheif has no API for
    that metadata yet (strukturag/libheif#1503),
    and forking it would make HEIC gain maps a CPM-only feature.
  • TIFF, WebP and the stb PNG writer still cannot record a non-sRGB transfer function.
  • In the web build: no HDR display path, and filters run inline with nothing to cancel (no pthreads).
  • dav1d is only wired up on Linux and macOS; Windows and the web build decode AVIF with libaom.

Notes

Precompiled binaries for macOS (Apple Silicon, Intel), Linux (AppImage, x86-64 and arm64), and
Windows (x64 and ARM64) are available below, and the web version is at
wkjarosz.github.io/hdrview.

Full Changelog: v2.8.3...v3.0.0