Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
third_party/tinyexr/** -whitespace
tests/fixtures/openexr-images/**/*.rst -whitespace
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ on:
workflow_dispatch:

jobs:
tinyexr-wasm:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install pinned Emscripten SDK
run: |
git init "$RUNNER_TEMP/emsdk"
git -C "$RUNNER_TEMP/emsdk" remote add origin https://github.com/emscripten-core/emsdk.git
git -C "$RUNNER_TEMP/emsdk" fetch --depth 1 origin e3a0604c3d130d6ab2c40e14a1861accd939a255
git -C "$RUNNER_TEMP/emsdk" checkout --detach FETCH_HEAD
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.7
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.7

- name: Rebuild pinned TinyEXR WASM
run: |
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
npm run build:tinyexr-wasm

- name: Verify generated artifacts are current
run: git diff --exit-code -- src/vendor/tinyexr_wasm.js src/vendor/tinyexr_wasm.wasm

verify:
runs-on: ubuntu-latest
steps:
Expand All @@ -14,7 +37,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: npm

- name: Setup Rust
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: npm

- name: Install dependencies
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Plenoview is a multichannel image viewer for computational imaging, rendering, a

## Features

- OpenEXR decode via a browser-safe `exrs` WASM adapter with full layer/channel extraction.
- OpenEXR decode via a browser-safe TinyEXR v3.2.0 WASM adapter with multipart, arbitrary-channel, cropped-window, subsampled-channel, scanline, and tiled image support.
- Local EXR load via `File > Open...` or drag/drop (drag-and-drop supports multiple files and recursive folder drops in one action).
- Recursive folder EXR load via `File > Open Folder...`; all `.exr` files under the selected folder are appended as sessions.
- `File > Export...` exports the full active display to PNG at display image size with configurable PNG compression and current channel/stokes, exposure/gamma, colormap, and alpha settings applied.
Expand Down Expand Up @@ -95,13 +95,13 @@ Plenoview is a multichannel image viewer for computational imaging, rendering, a

- Vite + Vanilla TypeScript
- WebGL2 renderer
- `exrs` (WASM OpenEXR decoder)
- TinyEXR v3.2.0 (vendored WASM OpenEXR decoder)
- Vitest (unit/integration-style tests)
- Playwright (workflow E2E)

## Requirements

- Node.js 20+
- Node.js 22+
- npm
- Modern browser with WebGL2

Expand Down Expand Up @@ -135,7 +135,7 @@ The extension reuses the Plenoview viewer UI and supports local EXR file/folder

Prerequisites:

- Node.js 20+ and npm 10+
- Node.js 22+ and npm 10+
- Rust stable (`rustc` and `cargo`)
- Tauri platform prerequisites for your OS

Expand Down Expand Up @@ -337,7 +337,7 @@ Controller methods:
}
```
- Texture sampling uses `NEAREST` for both `MIN_FILTER` and `MAG_FILTER`.
- EXR WASM is initialized through a local adapter module backed by a vendored wasm loader, avoiding app-level deep imports into `exrs` internals.
- EXR WASM is initialized through a local adapter backed by the pinned TinyEXR v3.2.0 source snapshot. `npm run build:tinyexr-wasm` reproduces the committed module with Emscripten 6.0.7, and CI rejects stale generated artifacts.
- EXR metadata is parsed directly from header bytes before pixel decode because the current WASM decoder only exposes dimensions, layers, channels, and pixel data. Metadata parse failures do not block image loading.
- Performance path for large images/channel sets:
- channel thumbnail DOM updates are throttled to selection/image changes only,
Expand Down
37 changes: 37 additions & 0 deletions e2e/channels-display.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,43 @@ test('carries exposure when opening and switching files @smoke', async ({ page }
await expect(exposureValue).toHaveValue('-2.5');
});

test('loads the TinyEXR worker module once and reuses it for a second file @smoke', async ({ page }) => {
const wasmResponses: Array<{ url: string; contentType: string | undefined }> = [];
const pageErrors: string[] = [];
page.on('response', (response) => {
if (new URL(response.url()).pathname.endsWith('.wasm')) {
wasmResponses.push({
url: response.url(),
contentType: response.headers()['content-type']
});
}
});
page.on('pageerror', (error) => pageErrors.push(error.message));

await gotoViewerApp(page);
const openedImages = page.locator('#opened-images-select');

await page.setInputFiles('#file-input', {
name: 'first.exr',
mimeType: 'image/exr',
buffer: buildScalarChannelExr()
});
await expect(openedImages.locator('option:checked')).toContainText('first.exr', { timeout: 30000 });

await page.setInputFiles('#file-input', {
name: 'second.exr',
mimeType: 'image/exr',
buffer: buildRgbAuxExr()
});
await expect(openedImages.locator('option:checked')).toContainText('second.exr', { timeout: 30000 });

expect(wasmResponses).toHaveLength(1);
expect(wasmResponses[0]?.url).toMatch(/\/tinyexr_wasm-[A-Za-z0-9_-]+\.wasm$/u);
expect(wasmResponses[0]?.contentType).toContain('application/wasm');
expect(wasmResponses.some((response) => /exrs/iu.test(response.url))).toBe(false);
expect(pageErrors).toEqual([]);
});

test('auto exposure updates in None mode and pauses while Colormap is active', async ({ page }) => {
await gotoViewerApp(page);

Expand Down
13 changes: 2 additions & 11 deletions e2e/helpers/exr-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { readFileSync } from 'node:fs';
import {
CompressionMethod,
ExrEncoder,
initSync,
SamplePrecision
} from '../../src/vendor/exrs_raw_wasm_bindgen.js';
} from '../../tests/helpers/exr-fixture-encoder';

interface ColormapManifest {
colormaps: Array<{
Expand All @@ -19,8 +18,6 @@ const colormapManifest = JSON.parse(

export const expectedColormapLabels = colormapManifest.colormaps.map((colormap) => colormap.label);

let exrEncoderInitialized = false;

export function buildScalarChannelExr(): Buffer {
ensureExrEncoderInitialized();

Expand Down Expand Up @@ -428,11 +425,5 @@ export function buildRgbStokesExr(): Buffer {
}

function ensureExrEncoderInitialized(): void {
if (exrEncoderInitialized) {
return;
}

const wasmBytes = readFileSync(new URL('../../src/vendor/exrs_raw_wasm_bindgen_bg.wasm', import.meta.url));
initSync({ module: wasmBytes });
exrEncoderInitialized = true;
// The independent TypeScript fixture writer has no runtime initialization.
}
34 changes: 1 addition & 33 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
"private": true,
"type": "module",
"engines": {
"node": ">=20 <26",
"node": ">=22 <26",
"npm": ">=10"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vite build && node scripts/verify-web-assets.mjs",
"build:tinyexr-wasm": "bash scripts/build-tinyexr-wasm.sh",
"build:desktop-web": "vite build --mode desktop && node scripts/stage-desktop-assets.mjs && node scripts/verify-desktop-assets.mjs && node scripts/build-windows-thumbnail-provider.mjs",
"build:vscode-web": "vite build --mode vscode && node scripts/stage-vscode-assets.mjs && node scripts/verify-vscode-assets.mjs",
"build:e2e": "VITE_E2E=true vite build",
"build:e2e": "VITE_E2E=true vite build && node scripts/verify-web-assets.mjs",
"capture:thumbnail": "node scripts/capture-thumbnail.mjs",
"capture:project-page": "npm run build:e2e && node scripts/capture-project-page-screenshots.mjs",
"lint": "eslint src tests e2e playwright.config.ts vite.config.ts vitest.config.ts --max-warnings=0",
Expand All @@ -36,7 +37,6 @@
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.7.1",
"exrs": "^1.0.3",
"fflate": "^0.8.2"
},
"devDependencies": {
Expand Down
29 changes: 29 additions & 0 deletions public/licenses/tinyexr/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD 3-Clause License

Copyright (c) 2014 - 2021, Syoyo Fujita and many contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 changes: 45 additions & 0 deletions public/licenses/tinyexr/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
TinyEXR
Copyright (c) 2014-2026 Syoyo Fujita and TinyEXR authors

This product includes software developed by third parties, as listed below.
The full text of each license is reproduced in the LICENSE file and/or in the
header of the corresponding source file.

--------------------------------------------------------------------------------
1. fpnge - https://github.com/veluca93/fpnge
--------------------------------------------------------------------------------
The fpnge-derived DEFLATE literal encoder in src/exr_fpnge.c (the constrained
Huffman-table construction and code-length encoding) and its PSHUFB per-byte
Huffman-table lookup kernel in src/exr_simd_x86.c are derived from fpnge and
have been modified (ported from C++ intrinsics to pure C11, reduced to a
generic literal-only DEFLATE encoder, and the bit-packing reimplemented).

Copyright 2021 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

--------------------------------------------------------------------------------
2. fpng - https://github.com/richgel999/fpng
--------------------------------------------------------------------------------
Portions of the DEFLATE encoder (Huffman table construction and bitstream
emission) in src/exr_deflate.c are derived from fpng, which is released into
the public domain under the Unlicense.

fpng - Copyright (C) 2021 Richard Geldreich, Jr.
This is free and unencumbered software released into the public domain.
For more information, see <http://unlicense.org/>.

fpng's low-level DEFLATE/Huffman routines are themselves derived from the
original 2011 Google Code release of miniz (public domain, Richard
Geldreich, Jr.) and the minimum-redundancy Huffman code-length function by
Alistair Moffat and Jyrki Katajainen (November 1996, public domain).
30 changes: 30 additions & 0 deletions public/licenses/tinyexr/zstd-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
BSD License

For Zstandard software

Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name Facebook, nor Meta, nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Loading