Add CCITT fax and JPEG-in-TIFF decoding via the Rust/WASM path#8
Merged
Conversation
The bundled tiff crate (0.10.3) already links the fax and zune-jpeg decoders, but several TIFF compressions never reached the screen: - CCITT Group 3 / T.4 (compression 3) and Modified Huffman (2) are not supported by the tiff crate at all. - CCITT Group 4 / T.6 (compression 4) is decoded by the tiff crate, but it returns packed 1-bit data that the wrapper dropped (the 1-bit arm of get_data_as_f32 produced an empty buffer). - JPEG-in-TIFF (compression 7) decoded fine but the stale prebuilt wasm predated the jpeg feature. Changes: - Route CCITT compressions 2/3/4 through the hayro-ccitt crate, which understands the TIFF encoding options (FillOrder, T4Options 1D/2D and byte-alignment). Output is expanded to 8-bit grayscale and mapped through PhotometricInterpretation so a fax image renders identically to the same image stored uncompressed. - Expand uncompressed/LZW/PackBits/Deflate 1-bit bilevel images to 8-bit instead of returning an empty buffer. - Report YCbCr color type as 3 channels (the tiff crate hands JPEG data back as interleaved RGB). - Disable the optional wasm-opt post-pass so the build does not require downloading binaryen, and make the build:wasm path-rewrite step cross-platform (the macOS-only `sed -i ''` failed on Linux/CI). - Add small CCITT/JPEG/bilevel TIFF fixtures and a WASM decoder regression test (npm run test:wasm) that checks the CCITT output is bit-identical to the uncompressed reference. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
Two more compression/photometric cases that previously fell back to geotiff.js are now handled by the Rust decoder: - Palette / RGBPalette images. The tiff crate refuses RGBPalette at colortype() and read_image(), so we detect PhotometricInterpretation 3, rewrite the tag to BlackIsZero in a copy of the bytes (reusing the crate's compression/predictor/strip handling to decode the raw indices), then expand the indices through the ColorMap tag into interleaved 8-bit RGB. Output matches Pillow's palette->RGB conversion exactly. - ZSTD (compression 50000) via the tiff crate's `zstd` feature. This pulls in the C zstd library, which grows the wasm binary from ~407 KB to ~708 KB. Extends the WASM decoder test with palette and ZSTD cases. Existing formats (LZW, Deflate, uint16, float32) re-verified against the rebuilt binary; full suite green. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
24-bit grayscale is a post-decode reinterpretation (combine R/G/B into a single value), applied in renderTiff/ImageRenderer and the stats block, both of which run on the decoded rasters regardless of which decoder produced them. The two `!use24BitMode` guards that forced these images onto the geotiff.js path were a conservative legacy gate, not a decode requirement, so removing them lets 24-bit depth images use the faster off-thread WASM decoder like any other RGB TIFF. Note: best confirmed by opening a real 24-bit depth TIFF in the extension, since the webview render path can't be exercised headlessly. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
The Playwright workflow failed before any test ran: - Both jobs start with `npm ci`, which requires a committed lockfile, but package-lock.json was gitignored. Commit it (and stop ignoring it) so `npm ci` has something to install from. - The test-cursor job ran `npm run compile`, which rebuilds the WASM via wasm-pack/cargo — tooling the workflow never installs. Switch it to `npm run compile:quick`, which reuses the committed wasm binary and needs no Rust toolchain (matching the other job). https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
The "Run Extension" launch ran `npm: compile`, which chains `build:wasm` (wasm-pack/cargo). On a machine without the Rust toolchain that step fails, and because the script is `&&`-chained, esbuild never runs and out/extension.js is never produced — so the extension host fails to activate with "Cannot find module .../out/extension.js". The wasm binary is committed, so F5 only needs esbuild. Point the preLaunchTask at a new `npm: compile:quick` task, which builds with esbuild alone (reusing media/wasm/tiff-wasm.wasm) in ~1-2s and needs no Rust toolchain. Run `npm run build:wasm` manually when changing the decoder. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
Loading a binary PBM (P4) failed with "Insufficient data for binary PBM". The header reader (readToken) reads until the next whitespace, but raster bytes are arbitrary and frequently not whitespace, so when the single whitespace separating the header from the data was absent (or a data byte simply wasn't whitespace) it swallowed the raster into the numeric height field, leaving the read offset at EOF. - Parse width/height/maxval with a digit-bounded reader that stops at the first non-digit, so it can never consume raster bytes. - Skip the single header-terminating whitespace in the P4 branch (the P5/P6 branch already did this); this also fixes a latent off-by-one where valid binary PBM files read the separator as the first data byte. Adds test/ppm-decode-test.js exercising the real _parsePpm across the ASCII/binary PBM/PGM/PPM sample files, with the binary PBM checkerboard as a regression guard. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
These format processors previously had no automated coverage (the same gap that let the binary PBM bug through). Add small fixtures and a smoke-test that drives the real parser entry points the decode worker uses: - NumPy .npy (float32, uint16, uint8 RGB) and .npz via NpyProcessor._parseNpy / _parseNpz - PFM grayscale (Pf) and color (PF) via PfmProcessor._parsePfm - PNG 16-bit grayscale and 8-bit RGB via UPNG.decode (the worker's PNG path) Wired in as `npm run test:formats`. EXR/HDR are left out for now because they can't be generated without OpenEXR/imageio here; RAW is out of scope. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
The Playwright "test" job failed on two stale assertions that never matched the current package.json (they were only reachable once CI's npm ci was fixed): - displayName is "Scientific Image Visualizer", not the old "Float and TIFF Visualizer". - filterByMask was removed as a command, so drop it from the required command list. Also remove the test-cursor job: it ran `playwright test cursor-test.spec.ts`, which matches nothing (playwright.config.ts pins testMatch to extension-core.spec.ts) and whose tests are all skipped. Cursor is no longer targeted. https://claude.ai/code/session_01KfaJ5sxn4ZXBHEebgcQK3e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds decoding for TIFF compressions that previously failed to display, all routed through the Rust/WASM decoder rather than geotiff.js. The bundled
tiffcrate (0.10.3) already links thefaxandzune-jpegdecoders, but several compressions never reached the screen.CCITT T.3 compression was added as suggested in #2 by @130040167
Additionally add some tests for these test formats
tiffdecoded it, but the committed.wasmwas staleYCbCr → 3 channels(the crate returns interleaved RGB)tiffdecodes it, but returns packed 1‑bit data the wrapper dropped (empty buffer)hayro-ccitt, expanded to 8‑bittifferrorsFax3 unsupported(compression 2 is the originally reported broken file)hayro-ccitt, honoringFillOrder/T4Options(1D/2D, byte-align)hayro-ccittwas chosen over thefaxcrate because thefaxcrate requires a leading EOL marker that TIFF Group‑3 streams omit (it failed on real libtiff G3 data).hayro-ccitt'sDecodeSettingsmaps 1:1 to TIFF tags.Why these weren't working
tiffcrate at all.tiffbut yields packed 1‑bit data; the wrapper'sget_data_as_f32had no 1‑bit arm and returned an empty buffer..wasmpredated thejpegfeature.Implementation notes
PhotometricInterpretation, so a fax image renders identically to the same image stored uncompressed.wasm-optpost-pass so the build no longer needs to download binaryen.sed -i ''inbuild:wasmwith a portable Node one-liner (it was silently failing on Linux/CI).Testing
npm run test:wasmloads the compiled WASM and asserts CCITT G3/G4 decode bit-identically to the uncompressed reference (0 differing pixels), the bilevel expansion, and 3‑channel JPEG output.test-samples/(a.gitignoreexception keeps them tracked despite the global*.tifignore).npm test) green;npm run lintclean.Not covered
tiff.