TIFF Scientific Viewer
Preview scientific TIFF images inside VS Code — including the 32-bit float,
16-bit and signed CT/microscopy images written by tifffile, which VS Code's
built-in image preview and the general-purpose TIFF extensions cannot open.
The contrast model is a direct port of ImageJ's: the pixel data is never
modified, and a separate display range (min, max) is mapped to the screen.
That is the difference between a black rectangle and a readable CT slice.

The same slice: full data range on the left, Auto on the right.
Why
A CT slice normalised to [-1, 1] has most of its structure packed into a
narrow band. Rendered naively across the full data range it looks almost black.
ImageJ's Auto finds the band; so does this.
For sample.tif in this repo, the data spans [-1.0000, 0.9805] but
Auto picks [-1.0000, -0.1490] — everything above -0.149 is bone and
saturates, and the soft tissue that occupies most of the image gets the full
256 levels.
Features
- Formats: 8/16/32/64-bit integer (signed and unsigned), float16/32/64,
RGB and RGBA, palette; classic TIFF and BigTIFF, both byte orders.
- Compression: none, LZW, Deflate/ZIP, PackBits, with horizontal (2) and
floating-point (3) predictors. Strips and tiles, chunky and planar.
- Contrast, ported from ImageJ:
Auto — ContrastAdjuster.autoAdjust, including the pixelCount/10 rule
that stops a constant background from swallowing the stretch, and the
stateful threshold that tightens on each press.
Enhance — ContrastEnhancer.stretchHistogram, saturating 0.35% by default.
Reset — the full data range.
- Exact numeric min/max entry, for comparing two images on one scale.
- Min/Max/Brightness/Contrast sliders and a draggable histogram.
- LUTs: Grays, Inverted Grays, Fire, Ice, Spectrum, 3-3-2 RGB, Red/Green/Blue.
- Stacks: multi-page files get a slice slider and arrow-key navigation.
The display range is held across slices by default.
- Readout: live
x, y, value of the raw value under the cursor — the actual
float, not the 8-bit screen value.
- Zoom on ImageJ's ladder, cursor-anchored, nearest-neighbour always.
- NaN / Inf are excluded from statistics and drawn in red.
Remote work
Built for the "processing runs on a remote box" workflow:
extensionKind is workspace, so the extension runs on the remote host
where the files are. Nothing large crosses the SSH link except one slice.
- Pages are decoded lazily and cached with a pixel budget, so a multi-gigabyte
stack does not have to fit in memory.
- Pixel data reaches the webview as base64 rather than a transferred
ArrayBuffer, because structured clone of binary is not dependable across
every VS Code transport (Remote-SSH, vscode.dev).
tifSciviewer.maxDecodedMegabytes (default 512) refuses an oversized page
with a clear message rather than exhausting the login node.
Install
npm install
npm run build
npm run package # produces tif-sciviewer.vsix
code --install-extension tif-sciviewer.vsix
Under Remote-SSH, install it into the remote host from the Extensions view
("Install in SSH: hostname"), or run code --install-extension in the remote
terminal.
Then open any .tif/.tiff file. To get back to the raw bytes, use
Open With… → Hex Editor.
Keyboard
| Key |
Action |
A |
Auto contrast (press again to tighten) |
E |
Enhance contrast |
R |
Reset to full range |
F |
Fit to window |
1 |
100% zoom |
+ / - |
Zoom in / out |
← → ↑ ↓ |
Previous / next slice |
Drag to pan, wheel to zoom at the cursor, double-click to fit.
Settings
| Setting |
Default |
Meaning |
tifSciviewer.autoContrastOnOpen |
true |
Apply Auto when an image opens |
tifSciviewer.defaultLut |
Grays |
LUT for newly opened images |
tifSciviewer.recomputeRangePerSlice |
false |
Re-run Auto on every slice |
tifSciviewer.saturatedPercent |
0.35 |
Saturation used by Enhance |
tifSciviewer.maxDecodedMegabytes |
512 |
Per-page decode ceiling |
Measured on a 2021 laptop; see docs/ITERATIONS.md for the profiling.
|
|
| Open a 120-slice, 120 MB stack |
1.9 ms (headers only) |
| Scrub that stack |
3.8 ms per slice, ~5 MB heap |
| Decode a 2048² float32 page |
~5 ms warm |
| Re-map 2048² on a slider drag |
~20 ms per frame |
Pages are decoded on demand, so the file never has to fit in memory.
Development
npm run typecheck
npm run build
npm test # 197 tests
Fixtures are generated by tifffile itself, so the decoder is checked against
the library that wrote the files:
python3 -m venv .venv && .venv/bin/pip install numpy tifffile imagecodecs
.venv/bin/python test/make_fixtures.py
.venv/bin/python test/make_contrast_truth.py
test/make_contrast_truth.py contains an independent Python transcription of
the ImageJ Java, so the contrast tests are a genuine cross-check of the
TypeScript port rather than a restatement of it.
To eyeball the pipeline without launching VS Code:
node tools/render.mjs sample.tif out.png --mode auto --lut Fire
docs/IMAGEJ_ANALYSIS.md holds the source-level
analysis this is built from, docs/REQUIREMENTS.md the
requirement IDs the tests trace to, docs/ITERATIONS.md
the build log and known gaps, and docs/MANUAL_TEST.md
the checklist for things the automated suite cannot reach.
Licence
MIT — see LICENSE.