Skip to content

Conversation

@akre54
Copy link
Contributor

@akre54 akre54 commented Jan 15, 2026

Checklist

  • pull from nom once an test in browser after first publish
  • let robot update all docs in the end
  • let robot add tags and keywords etc to package
  • add typescript
  • add tests
  • add us as contributors and remove author field
  • Currently exrs-wasm - you may want to change to @exrs/wasm or similar for npm namespace
  • discuss case sensitivity
  • discuss sample precision per channel
  • discuss truly deferred decompression (currently the whole file is decompressed)
  • discuss data window and layer offset/size

out of scope:

  • refactoring the implementation details
  • renaming folders that don't affect public API
  • convert repo to rust workspaces

This PR adds exrs-wasm, an npm package that provides JavaScript/TypeScript bindings for writing EXR files from the browser. This enables web-based rendering tools to export multi-layer EXR files with AOVs directly from WebGL/WebGPU contexts.

Related issues:

Motivation

The exrs crate already compiles to WASM (as noted by the "Wasm Ready" badge in the README), but there was no JavaScript API to actually use it from the browser. This PR bridges that gap by providing ergonomic JS bindings.

Use cases:

  • Web-based 3D renderers exporting production-quality EXR files
  • Browser-based compositing tools
  • WebGL/WebGPU applications that need HDR image export with multiple AOV passes

Changes

New files

exrs-wasm/ - New WASM bindings crate:

  • Cargo.toml - Crate config using exr as a path dependency with default-features = false
  • src/lib.rs - Full implementation (~600 lines)

.github/workflows/wasm-publish.yml - CI workflow to publish to npm on wasm-v* tags

Modified files

.gitignore - Added exrs-wasm/target/ and exrs-wasm/pkg/ build artifacts

API

import init, { ExrImage, ExrSequenceWriter, CompressionMethod, SamplePrecision } from 'exrs-wasm';

await init();

// Single frame with multiple AOV layers
const exr = new ExrImage(1920, 1080, CompressionMethod.Rle);
exr.addRgbaLayer('beauty', beautyPixels);           // Float32Array, w*h*4
exr.addRgbLayer('normals', normalPixels);           // Float32Array, w*h*3
exr.addDepthLayer('depth', depthPixels);            // Float32Array, w*h
exr.addSingleChannelLayer('matte', 'A', matteData); // Custom channel name

const bytes = exr.toBytes();  // Uint8Array - complete EXR file
exr.free();

// Animation sequences (convenience API)
const writer = new ExrSequenceWriter(1920, 1080, CompressionMethod.Piz);
const frame1 = writer.writeFrame(beauty, depth, normals);

Features

  • Layer types: RGBA (4ch), RGB (3ch), single-channel with custom names
  • Compression: None, RLE, ZIP, ZIP16, PIZ, PXR24
  • Sample precision: F16 (half float), F32 (full float)
  • Validation: Input array length checked against dimensions
  • Error handling: Clear error messages via console_error_panic_hook

For maintainers

This PR is designed to be reviewed and potentially modified before merging. Some decisions for maintainers to consider:

  1. Package name: Currently exrs-wasm - you may want to change to @exrs/wasm or similar for npm namespace
  2. Version alignment: Currently 0.1.0 - you may want to sync with main crate version
  3. npm publishing: The workflow uses secrets.NPM_TOKEN - you'll need to set this up
  4. Repository field: Points to johannesvollmer/exrs - correct for your repo
  5. wasm-bindgen version: Pinned to 0.2.100 for compatibility - may need updating

Test plan

  • Native tests pass (cargo test in exrs-wasm/)
  • WASM build succeeds (wasm-pack build --target web)
  • Browser integration test (load WASM, create EXR, verify magic bytes)
  • Open generated EXR in Nuke/RV/other viewer

🤖 Generated with Claude Code

akre54 and others added 2 commits January 14, 2026 22:38
Introduces exrs-wasm package with JavaScript-friendly APIs for creating
multi-layer EXR files with AOVs (beauty, depth, normals) from WebGL/WebGPU
render buffers. Includes GitHub Actions workflow for npm publishing.

Co-Authored-By: Claude <[email protected]>
Interactive demo page showcasing:
- ExrImage API for multi-layer EXR creation
- All layer types: RGBA, RGB, depth, single-channel
- Compression method and precision controls
- ExrSequenceWriter for animation frames
- Canvas previews and file downloads

Co-Authored-By: Claude <[email protected]>
Copy link
Owner

@johannesvollmer johannesvollmer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this pull requests! While I generally approve the idea to add js bindings, there are some big things I would want to discuss with you first.

The most important change would be to include reading of images in the first version, not as a later addition. This is important to make roundtrip tests and to avoid building an architecture which prevents adding the reading of images.

One example is compression being in the top level image structure in your PR, but it actually belongs into the layer. Or the ChannelType enum, which probably makes reading more complicated than it needs to be. This problem and other problems will surface when reading is added. That's why I propose we do deciding in the first version now.

Furthermore I would ask you to reduce the total amount of code needed. The html file contains an example for developers, yet I would think that the code should be easy to read and understand.

One last thing that I would prefer is to wrap the generated JavaScript in a clean handwritten API, which hides the details such as init() and free().

I know this is quite a lot of feedback. This is only because the exr library is already used in the public by some projects. Adding JavaScript bindings should be solid even in the first version if we do it in this repository.

You also have the choice to publish it independently of me, in your own account, then you can experiment more and I won't talk your ear off about architecture. I would think js bindings are a cool addition to the project, so adding it here would also be appreciated, if you want to address the feedback.

@akre54
Copy link
Contributor Author

akre54 commented Jan 17, 2026

Great! That's all straightforward feedback and I'm happy to implement. Appreciate you taking a look and appreciate the initial code. Thanks for the pointers on the compression.

I was thinking the html file was more about showing a quick demo to smoke test / help users understand more than it is about showcasing the API but I'm fine to remove.

I'll have limited access to my computer this weekend but I'll try to get to this when I have some downtime.

FWIW I created a js-native port at @akre54/exr-js which might be interesting to folks too. It has reading and writing. Let me know if there's anything from there that you'd like me to port here too. I see them as related but orthogonal projects.

@akre54
Copy link
Contributor Author

akre54 commented Jan 18, 2026

Ok updated based on your feedback. Mind taking another look?

I left the example html file in there but if you would rather I remove it I'm fine with that too.

The most important change would be to include reading of images in the first version, not as a later addition. This is important to make roundtrip tests and to avoid building an architecture which prevents adding the reading of images.

Done!

One last thing that I would prefer is to wrap the generated JavaScript in a clean handwritten API, which hides the details such as init() and free().

init is pretty standard for wasm-bindgen libraries and can't be avoided. free is now optional thanks to FinalizationRegistry. Open to another round if you have more feedback.

- Add EXR reading API (readExr, ExrReadResult)
- Add U32 sample precision support
- Move compression setting to per-layer level
- Add convenience writing functions (writeExrRgba, writeExrRgb, writeExrSingleChannel)
- Remove ExrSequenceWriter and addDepthLayer
- Add JavaScript integration tests via wasm-bindgen-test
- Update CI to run JavaScript tests
- Update example HTML for new simplified API

Co-Authored-By: Claude <[email protected]>
@johannesvollmer
Copy link
Owner

Awesome! I will have a look as soon as possible

@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 18, 2026

I left the example html file in there but if you would rather I remove it I'm fine with that too.

It's nice to have an example, but I would prefer to have it shorter. Or at least, another file with a short example.

I see you added the convenience wrappers, but I had hoped to have a Javascript layer which we can write by hand. That way, we could easily build a pass-by-value single function for encoding images. I have tried this approach and it is not so easy, so I understand the hesitation. Perhaps using the serde typescript wrapper will make the API nice enough that we don't need an additional javascript layer? The tests could be in a separate (otherwise unused) npm package subfolder which is never published and uses the generated npm package as dependency.

I want to propose that we work on an entirely different branch than master, such that we may collaborate and experiment on that branch with smaller PRs, instead of having one huge PR. What do you think?

@akre54
Copy link
Contributor Author

akre54 commented Jan 18, 2026 via email

@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 18, 2026

  • We should probably rename the structs. For example, it would make sense to have ExrDecoder and ExrEncoder instead of ExrReadResult for writing and another for reading. Currently it is not symmetric. Alternatively, it might also make sense to use the ExrImage as a return result from the reading.
  • When reading, we should probably stick to the SpecificChannels where useful, for performance reasons (it loads only the channels we asked for) and for simplicity.
  • We should make the LayerData struct contain the AllChannels<FlatSamples> data directly, then we can remove the enum ChannelType completely and save some boilerplate code.
  • We should probably use [f64] instead of [f32] because JavaScript is all f64 anyways, and this probably allows use to store U32 without loss of precision

@johannesvollmer
Copy link
Owner

Yeah let's do it in this branch, I don't see any reason why not :) We can both open PRs against this branch to propose more complex ideas. Perhaps simple ideas we could do in this branch directly, but I have no strong opinion on that

- Rename ExrImage → ExrEncoder, ExrReadResult → ExrDecoder for symmetry
- Change f32 → f64 in JS API (JS operates in f64 natively)
- Add optimized readExrRgba/readExrRgb using SpecificChannels
- Add hand-written JS wrapper (js/index.js) hiding init()/free()
- Add TypeScript definitions (js/index.d.ts)
- Add README.md with documentation and examples

Co-Authored-By: Claude <[email protected]>
akre54 and others added 2 commits January 20, 2026 09:54
- Remove ChannelType enum, store AnyChannels<FlatSamples> directly
- Build channels at add_*_layer time instead of at encode time
- Add tests-js/ subfolder as separate npm package for JS integration tests
- Update native tests to use public API

Co-Authored-By: Claude <[email protected]>
akre54 and others added 3 commits January 21, 2026 01:47
- Make encode/decode functions synchronous (require init() first)
- getData() now auto-detects RGBA/RGB/single channel from layer contents
- Add getChannel(name) for explicit channel access
- Update TypeScript definitions, tests, and README

Co-Authored-By: Claude <[email protected]>
@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 21, 2026

I proposed changes in your fork at akre54#1. Mainly this PR aims to make the wrapper a proper npm package. This way, we can have a cleaner build and publishing process.

@akre54
Copy link
Contributor Author

akre54 commented Jan 21, 2026

Merged akre54#1!

@johannesvollmer
Copy link
Owner

We should probably use [f64] instead of [f32] because JavaScript is all f64 anyways, and this probably allows use to store U32 without loss of precision

M comment had a logic flaw. If you never had a number[], only FloatArrayBuffer, then it actually does save space at the cost of precision in JavaScript. So forcing f64 might be unnecessary memory usage. Instead we should probably revert to f32, or allow both variants side by side.

@johannesvollmer
Copy link
Owner

The workflow fails because I didn't test it locally, so this was kind of expected, I will try to fix it directly in this branch

johannesvollmer and others added 2 commits January 21, 2026 22:05
Since the API uses Float32Array (from WebGL) rather than JS number[],
using f64 wastes memory without benefit. Reverts the API to use f32
throughout for encoding/decoding pixel data.

Co-Authored-By: Claude <[email protected]>
@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 22, 2026

another big refactoring is over at your fork, akre54#2 (reduces code size by simplifying internal logic). i'd like to know what you think about this

@akre54
Copy link
Contributor Author

akre54 commented Jan 22, 2026

Thanks! Left a review

@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 23, 2026

once we merge the pr at your fork, I'd be happy with the API for V1, and we can try an initial release.

Do you have a use case at hand to test whether this will work for you?

…bgl-aov-export-jv2

generalize channel interleaving and deinterleaving
@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 26, 2026

Just opening the html file directly will not work:
image

It loads forever. The console contains errors about CORS.

Did this work for you before, or do we have to host it on a local server? In that case, we should include that hosting code in the example, such that it is ready to run with one command.

@johannesvollmer
Copy link
Owner

johannesvollmer commented Jan 26, 2026

I added an npm based example which seems to work

@johannesvollmer
Copy link
Owner

Okay, so there are some open points we can improve later as a follow-up:

  • possibly make the reader case-insensitive
  • allow encoder to specify compression type per channel
  • add truly deferred decompression (currently the whole file is decompressed even if only some parts are copied to js)
  • discuss data window and layer offset/size: at least offer automatic conversion from layer space to data window

I will create issues for those. I will now merge the PR and publish the NPM package. I would appreciate if you test the package when you have the time :)

@johannesvollmer johannesvollmer merged commit adee554 into johannesvollmer:master Jan 27, 2026
6 checks passed
@johannesvollmer
Copy link
Owner

Done 🎉 https://www.npmjs.com/package/exrs

@johannesvollmer
Copy link
Owner

Thanks for your time and your contributions! It was a pleasure to work with you.

@johannesvollmer
Copy link
Owner

For some reason you are not listed as collaborator on https://www.npmjs.com/package/exrs - but why...? you are recorded in the collaborators field in package.json

@akre54
Copy link
Contributor Author

akre54 commented Jan 27, 2026

Likewise! And no worries. Thanks for all the work you put in. I'm just glad this is now available

@johannesvollmer
Copy link
Owner

Thanks! I added a contributors section in the README :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make parallelization customizable (for wasm)

2 participants