Skip to main content

The image-viewer element

This is a live <image-viewer> showing an OpenEXR file. Drag the slider on the left to change the exposure (double-click it to reset), and use the gear for tone mapping, size and image details. The demo uses the viewer built with this documentation, which may include changes not yet published to npm.

<image-viewer> is a web component: it works in plain HTML and in any framework, with no build step. It reads every format HDRify reads, detected from the file's content rather than its name:

  • OpenEXR (.exr), all compressions HDRify supports
  • Radiance HDR (.hdr)
  • JPEG gain maps: Ultra HDR (JPEG-R) and Adobe gain map JPEGs
  • Any of these compressed with gzip, such as .hdr.gz

It decodes and tone maps on the CPU and draws an sRGB preview on a 2D canvas, so it needs no WebGL.

Add it to a page​

Load the self-contained bundle from jsDelivr, then add the element:

<script type="module" src="https://cdn.jsdelivr.net/npm/hdrify-react/dist/image-viewer.js"></script>
<image-viewer src="/images/memorial.exr" exposure-control style="height: 600px"></image-viewer>

This unversioned URL follows the latest published release, subject to CDN and browser caching. Add an exact version, such as [email protected], to pin one. To self-host, copy hdrify-react/dist/image-viewer.js into your site's public files.

With a bundler, install the package and import the element instead. This uses your copies of react and react-dom and needs no CSS loader:

npm install hdrify-react hdrify react react-dom
import 'hdrify-react/element';

The element is 480 pixels tall by default; set its height with CSS. Files fetched from another origin must allow it with CORS.

Attributes​

<image-viewer
src="/images/memorial.exr"
tone-mapping="agx"
exposure="1.5"
fit="contain"
settings-panel="closed"
exposure-control
></image-viewer>
AttributeValuesDefault
srcURL of an EXR, HDR or gain map JPEG, optionally gzipped
tone-mappingneutral, agx, aces or reinhardneutral
exposureExposure in stops (EV). 0 leaves the image unchanged; each +1 doubles the brightness0
fitcontain scales the image to fit; actual shows one image pixel per CSS pixel and scrollscontain
settings-panelclosed (gear shown), open, or hidden (no gear or panel)closed
exposure-controlPresent (and not "false") to show the vertical exposure slider over the left edge, −10 to +10 EVabsent

When the user changes a setting with the slider or the options panel, the element updates the matching attribute and fires settingschange. Opening or closing the panel updates settings-panel.

Load an image from JavaScript​

To show a file the user picked, give the element its bytes instead of a URL:

const viewer = document.querySelector('image-viewer');
viewer.addEventListener('load', (event) => console.log(event.detail.image.width));
viewer.addEventListener('error', (event) => console.error(event.detail.message));
viewer.data = await file.arrayBuffer();

Attach listeners before setting a source.

PropertyWhat it takes
srcURL to fetch (also an attribute)
dataEncoded file bytes, an ArrayBuffer or Uint8Array; takes precedence over src
imageAn already decoded HdrifyImage, such as one from readExr; takes precedence over data and src
settings{ toneMapping, exposure, fit }. Reading returns a copy; assigning a partial object changes only those settings
Eventevent.detail
progressLoading progress; see Loading progress
load{ image }, the decoded HdrifyImage, once it is drawn
error{ message, error } for fetch and decode failures; the message is also shown
settingschange{ settings } after the user changes a setting

Events bubble and cross the shadow root. Changing the source cancels an unfinished request.

Loading progress​

While an image loads, a thin bar across the top of the viewer fills as it downloads, decodes and draws, then fades out. The element sets aria-busy="true" until the image is drawn. The bar follows <material-viewer>, so the same customizations work:

image-viewer {
--progress-bar-height: 3px;
--progress-bar-color: #f2b134;
}
image-viewer::part(default-progress-bar) {
border-radius: 0 2px 2px 0;
}

To replace the bar, add your own element with slot="progress-bar", and drive it from progress events. An empty slotted element hides the bar. Each progress event's detail has:

FieldMeaning
totalProgress0 to 1 across the whole load: the download is the first 80%, decoding and drawing the rest. Never decreases
stagedownload, decode or render. render is reported once, with totalProgress 1, when the image is drawn
loadedBytes downloaded so far (0 for data and image sources)
totalFile size in bytes, or null when the server sends no usable Content-Length (for example, compressed)
progressDownload fraction from 0 to 1, or null while total is unknown
viewer.addEventListener('progress', (event) => {
console.log(`${event.detail.stage}: ${Math.round(event.detail.totalProgress * 100)}%`);
});

Decoding runs on the main thread, so the page pauses briefly while a large file decodes. The bar is drawn before decoding starts.

Wait for the image and save it​

whenReady() resolves once the current image has loaded and been drawn. toBlob() waits for the same thing, then encodes the tone-mapped image with the current tone mapping and exposure:

const viewer = document.querySelector('image-viewer');
viewer.src = '/images/memorial.exr';
await viewer.whenReady();
const png = await viewer.toBlob();
const thumbnail = await viewer.toBlob({ type: 'image/webp', quality: 0.85, width: 256 });
toBlob optionWhat it does
typeimage/png (default), image/jpeg or image/webp
quality0–1 for JPEG and WebP
width, heightOutput size in pixels, up to 16384. Without them, the image's own size; with one, the aspect ratio is kept
  • The image contains only the picture: no exposure slider, options panel or progress bar. It is an opaque, 8-bit sRGB rendering of the image at its own resolution (unless resized), independent of the viewer's size and fit. To keep the full dynamic range, use HDRify's writers on the decoded image (event.detail.image from load).
  • If a browser can't encode the requested type, it returns a PNG. Check blob.type.
  • If the source changes before it finishes loading, whenReady() waits for the new source. Changes made right before the call, such as setting src or data, are included.
  • Both reject on the same failures as the error event, and if the element is removed from the page or was never added. toBlob() also rejects when there is no image.
  • An element without a source is ready immediately.

Match your site's colours​

The controls take their colours from --hdrify-viewer-* custom properties, set on the element or an ancestor. Without them the viewer uses a dark theme on a black background.

image-viewer {
--hdrify-viewer-background: #101c20;
--hdrify-viewer-foreground: #f4f1e8;
--hdrify-viewer-popover: rgb(16 28 32 / 90%);
--hdrify-viewer-border: #3a4a4f;
--hdrify-viewer-input: #1b2a2f;
--hdrify-viewer-primary: #f2b134;
}

The other tokens are --hdrify-viewer-muted-foreground, --hdrify-viewer-ring (focus outline), --hdrify-viewer-destructive (errors), --hdrify-viewer-radius, --hdrify-viewer-font-family and --hdrify-viewer-font-size. The VS Code extension maps them to the editor's theme colours.

In React​

The element is built on the ImageViewer React component from hdrify-react. In a React application, use the component directly; it takes the same options as props:

import { ImageViewer } from 'hdrify-react';

<ImageViewer
src="/images/memorial.exr"
exposureControl
toneMapping="agx"
style={{ height: 600 }}
onLoad={(image) => console.log(image.width, image.height)}
onError={(error) => console.error(error)}
onProgress={(progress) => console.log(progress.totalProgress)}
/>;

With React 19, pass a ref to get the same whenReady() and toBlob():

import { ImageViewer, type ImageViewerHandle } from 'hdrify-react';
import { useRef } from 'react';

export function Preview() {
const viewer = useRef<ImageViewerHandle>(null);
const save = async () => {
const blob = await viewer.current!.toBlob({ type: 'image/jpeg', quality: 0.9 });
window.open(URL.createObjectURL(blob));
};
return (
<>
<ImageViewer ref={viewer} src="/images/memorial.exr" />
<button onClick={save}>Save preview</button>
</>
);
}

Settings props (toneMapping, exposure, fit) set the initial values and override the user's choice whenever they change. Use onSettingsChange to follow the user's changes. See React for the lower-level HdrifyCanvas, and the React API reference for every prop.

To decode a file yourself with the same format detection, use decodeHdrifyImage(bytes) from hdrify-react.