HoloHDR is a mobile-first browser tool for tuning ordinary photos into brighter Ultra HDR exports with gain-map controls.
The app is designed around phone use: load an image from the camera roll, preview the adjusted result, compare against the original by holding the image, tune with compact bottom controls, save custom presets, and export the version you like.
- Local image loading from the browser, including iPhone Safari.
- Fixed mobile workspace with a scrollable bottom tool rail.
- HDR and SDR preview modes.
- Manual controls for exposure, contrast, shadows, highlights, color/vibrance, headroom, HDR brightness, threshold, softness, power, and gain-map gamma.
- Wand menu with browser-side automatic tuning passes.
- Saved presets stored locally in the browser.
- JPEG, gain-mask, and Ultra HDR exports directly in the browser.
- No account, upload service, analytics, cookies, or tracking scripts.
Clone submodules before rebuilding the vendored Ultra HDR encoder:
git submodule update --init --recursiveAny static file server works:
python3 -m http.server 8000Then open:
http://127.0.0.1:8000/
For development, the included server adds cache-busting redirects and no-store cache headers:
python3 app_server.pyHoloHDR is plain static HTML, CSS, JavaScript, WebAssembly, and image assets. A production host only needs to serve the repository files over HTTPS with the correct MIME type for WebAssembly:
application/wasm .wasm
Ultra HDR JPEG export is handled in the browser with WebAssembly from open-ultrahdr-wasm, built from upstream google/libultrahdr.
To build a clean deployable static directory:
make siteThat writes the production files to .build/site/ and excludes local-only source/build folders such as third_party/, .git/, and comparison scratch images. To publish with rsync:
make publish DEPLOY_TARGET=user@example.com:/absolute/site/path/The vendored files in vendor/open-ultrahdr/ are an optimized browser build based on open-ultrahdr-wasm 0.2.0 and upstream google/libultrahdr.
The important build goals were:
- Keep Ultra HDR encoding fully client-side.
- Stay compatible with mobile Safari.
- Avoid requiring cross-origin isolation headers.
- Keep preview/export work off the main thread where possible.
- Expose libultrahdr options HoloHDR needs for previews, including realtime mode and multi-channel gain maps.
The build is intentionally single-threaded. Emscripten pthreads can improve throughput, but browsers require SharedArrayBuffer and cross-origin isolation for pthread-backed WASM. HoloHDR avoids that requirement so the app can run as a simple static site on iPhone Safari and other mobile browsers.
The optimized build uses the same shape as the upstream wrapper, with these key Emscripten choices:
emcmake cmake -S . -B build-wasm \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -flto -msimd128"
emmake cmake --build build-wasm --config ReleaseFor an equivalent manual emcc/em++ setup, the relevant flags are:
-O3
-flto
-msimd128
-s MODULARIZE=1
-s EXPORT_ES6=1
-s ENVIRONMENT=web,worker
-s ALLOW_MEMORY_GROWTH=1
-s FILESYSTEM=0
Do not enable pthreads unless the deployment also sends the required cross-origin isolation headers and you are willing to drop compatibility with browsers that do not expose SharedArrayBuffer in that context. The current build uses SIMD but no pthreads, which is the safer compatibility/performance tradeoff for HoloHDR.
The reproducible rebuild path is:
make wasmThis target initializes the third_party/lib-open-ultrahdr submodule, copies it into .build/lib-open-ultrahdr/, applies vendor/open-ultrahdr/patches/0001-preserve-sdr-intent.patch, runs the upstream Emscripten build, and replaces:
vendor/open-ultrahdr/open_ultrahdr.js
vendor/open-ultrahdr/open_ultrahdr.wasm
The HoloHDR patch is important: it passes the browser-created JPEG to libultrahdr as the SDR intent (UHDR_SDR_IMG) and marks the SDR/HDR buffers as BT.709/sRGB-compatible. Without that patch, neutral Ultra HDR output can shift darker and more saturated because libultrahdr treats the input JPEG as a base image and interprets the HDR buffer through the wrong color path.
Then verify:
make verifyUltra HDRlocally builds an adjusted SDR JPEG plus a linear HDR RGB buffer, then writes an Ultra HDR / ISO 21496 gain-map JPEG in WebAssembly.JPEGsaves the adjusted SDR fallback image directly in the browser.Gainsaves a grayscale gain-mask preview PNG directly in the browser.
The hdr-comparison/ directory contains the test image, old server outputs, old WASM outputs, patched WASM outputs, and a static comparison page. It is kept in the repository as a visual regression fixture for checking that neutral Ultra HDR output preserves the SDR base image.
The full investigation write-up is in docs/hdr-encoder-investigation.md.
The browser stores the last loaded image and editor state in IndexedDB, so refreshing restores the previous session. Saved presets are stored in localStorage on the same browser and device.
The hosted app does not upload images, presets, exports, or slider settings. It does not use cookies, analytics, advertising scripts, or tracking pixels. Browser storage is used only on the local device for session restore and saved presets.
- Ultra HDR WebAssembly wrapper: https://www.npmjs.com/package/open-ultrahdr-wasm
- Upstream Ultra HDR encoder: https://github.com/google/libultrahdr
The vendored Ultra HDR code follows the upstream Apache-2.0 or MIT licensing noted by that project.