gainmap-js
A Javascript (TypeScript) Encoder/Decoder Implementation of Adobe's Gain Map Technology for storing HDR Images using an SDR Image + a "Gain map"
:warning: This library is primarily intended for encoding and decoding gain map images for the three.js 3D Library
It can be used for general encode/decode of gain maps but it depends on the three.js library which, in itself, is quite heavy if you only use it to encode/decode gain maps.
Live Demo
https://monogrid.github.io/gainmap-js/
Compares loading:
- a
JPEG
file with embedded gain map data - a
webp
sdr file + a webp
gain map + metadata JSON - a comparable size
.hdr
file for comparison
Use it to convert .hdr
and .exr
files into gain maps. It's free and the whole process happens in your browser.
Installing
$ npm install @monogrid/gainmap-js three
What is a Gain map?
See here for a detailed explanation, here are some relevant parts:
A gain map is a single file with a second pseudo-image embedded in it to create an optimized result for a specific monitor. It can be used to generate the HDR version (which looks dramatically better where supported), the SDR version (without tone mapping to ensures great quality), or anything in between (to better support less capable HDR displays).
Gain maps are not a new type of file, but rather a technology which can be embedded into a variety of image formats. There are reference specs already for the JPG, AVIF, JXL, and HEIF file formats. JPG is especially notable as it could not properly support HDR without gain maps and it offers a very useful bridge to the future (i.e. highly compatible with today’s software).
A gain map includes:
- A base (default) image. This can be an SDR or an HDR image (JPG gain maps are always encoded with SDR as the base). If the browser or viewing software does not understand gain maps, it will just the treat file as if it were just the base image.
- The gain map. This is a secondary “image” embedded in the file. It is not a real image, but rather contains data to convert each pixel from the base image into the other (SDR or HDR) version of the image.
- Gain map metadata. This tells the browser how the gain map is encoded as well as critical information to optimize rendering on any display.
Please note that Google is adopting the gain map technology in Android 14 but its naming of the technology refers to it as Ultra HDR Image Format and a JPEG file with embedded gain map is apparently called JPEGR in their terminology, we call it HDRJPEG
for the moment.
API
Refer to the WIKI for detailed documentation about the API.
Examples
Decoding
The main use case of this library is to decode a JPEG file that contains gain map data
and use it instead of a traditional .exr
or .hdr
image.
Using a single JPEG with embedded Gain map Metadata
This approach lets you load a single file with an embedded Gain Map.
The advantage is to have a single file to load.
The disadvantages are:
- No WEBP compression
- The JPEG cannot be manipulated in Photoshop, GIMP, or any other software that does not support the gain map technology (no photo editing software supports it at the time of writing 06-11-2023).
- Photo sharing websites and/or services (i.e. sharing with Slack) will likely strip the Gain map metadata and the HDR information will be lost, leaving you with only the SDR Representation.
import { HDRJPGLoader } from '@monogrid/gainmap-js'
import {
EquirectangularReflectionMapping,
LinearFilter,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer
} from 'three'
const renderer = new WebGLRenderer()
const loader = new HDRJPGLoader(renderer)
const result = await loader.loadAsync('gainmap.jpeg')
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
scene.background = result.renderTarget.texture
scene.background.mapping = EquirectangularReflectionMapping
result.dispose()
Using separate files
Using separate files will get rid of the limitations of using a single JPEG file but it will force to use three separate files
- An SDR Representation file
- A Gainmap file
- A JSON containing the gainmap metadata used for decoding
import { GainMapLoader } from '@monogrid/gainmap-js'
import {
EquirectangularReflectionMapping,
LinearFilter,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer
} from 'three'
const renderer = new WebGLRenderer()
const loader = new GainMapLoader(renderer)
const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
const scene = new Scene()
const mesh = new Mesh(
new PlaneGeometry(),
new MeshBasicMaterial({ map: result.renderTarget.texture })
)
scene.add(mesh)
renderer.render(scene, new PerspectiveCamera())
scene.background = result.renderTarget.texture
scene.background.mapping = EquirectangularReflectionMapping
result.dispose()
Encoding
Encoding a Gain map starting from an EXR file.
This is generally not useful in a three.js
site but this library exposes methods
that allow to encode an .exr
or hdr
file into a jpeg
with an embedded gain map.
import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js'
import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
const loader = new EXRLoader()
const image = await loader.loadAsync('image.exr')
const textureMax = await findTextureMinMax(image)
const encodingResult = encode({
image,
maxContentBoost: Math.max.apply(this, textureMax)
})
const sdrImageData = new ImageData(
encodingResult.sdr.toArray(),
encodingResult.sdr.width,
encodingResult.sdr.height
)
const gainMapImageData = new ImageData(
encodingResult.gainMap.toArray(),
encodingResult.gainMap.width,
encodingResult.gainMap.height
)
const mimeType = 'image/jpeg'
const quality = 0.9
const [sdr, gainMap] = await Promise.all([
compress({
source: sdrImageData,
mimeType,
quality,
flipY: true
}),
compress({
source: gainMapImageData,
mimeType,
quality,
flipY: true
})
])
const metadata = encodingResult.getMetadata()
const jpeg = await encodeJPEGMetadata({
...encodingResult,
...metadata,
sdr,
gainMap
})
Libultrahdr in Vite
If you import @monogrid/gainmap-js/libultrahdr
You will need to exclude it from Vite optimizations.
module.exports = defineConfig({
...
optimizeDeps: {
exclude: ['@monogrid/gainmap-js/libultrahdr']
},
...
})
Hi, thanks for reporting this.
I'm afraid this is a documentation issue on my side, I'm going to update the README after resolving this
Here's a rough draft, let me know if it works for you (I already pushed the rollup.config.decodeonly.mjs
)
Building with full encoding support (libultrahdr-wasm)
Clone the repository with git submodules recursively:
$ git clone --recurse-submodules git@github.com:MONOGRID/gainmap-js.git
Proceed to build the libultrahdr-wasm module following the documentation found here, here's a quick summary
$ cd gainmap-js/libultrahdr-wasm/
Create a meson "cross compile config" named em.txt and place the following content inside:
[binaries]
c = 'emcc'
cpp = 'em++'
ar = 'emar'
nm = 'emnm'
[host_machine]
system = 'emscripten'
cpu_family = 'wasm32'
cpu = 'wasm32'
endian = 'little'
Then execute
$ meson setup build --cross-file=em.txt
$ meson compile -C build
After compiling the WASM, head back to the main repository
$ cd ..
$ npm i
$ npm run build
Building with no encoding support (requires no wasm)
:warning: Building the library with decode only capabilities will not allow to run playwright e2e tests with npm run test
this method should only be used by people who would like to customize the "decoding" part of the library but are unable to build the WASM module for some reason (emscripten can be tricky sometimes, I've been there)
Clone the repository normally:
$ git clone git@github.com:MONOGRID/gainmap-js.git
$ cd gainmap-js
$ npm i
build with
$ npm run build --config rollup.config.decodeonly.mjs
References
License