A Swiss-army knife of utilities for whole-slide imaging (WSI) files used in digital pathology.
Warning
Pre-1.0 — expect breaking changes. wsitools is under active development
and not yet API/CLI stable. Anything may change between releases without a
deprecation period: CLI flags and subcommands, JSON output fields, and
output file-format details. Pin a specific version and review
CHANGELOG.md before upgrading.
See CHANGELOG.md for release notes.
Inspection
wsitools info— slide summary: format, levels (dimensions + tile size + compression), associated images, scanner metadata. Text or--json. Analog ofopenslide-show-properties.wsitools dump-ifds— format-aware per-IFD layout dump. Annotates each IFD with its classification (pyramid L0/L1/…/label/macro/thumbnail/ overview/probability/map) and reports wsitools private tags (65080– 65084). Slim tiffinfo analog. Use--rawfor a full per-tag dump (every TIFF tag with name + type + count + value + enum interpretation; composes with--json);--raw-fulldisables smart truncation of long arrays and binary blobs.wsitools region --x --y --w --h --level -o out.png— extract a rectangular pixel region as PNG (analog ofopenslide-write-png).wsitools hash— content hash.--mode file(default,sha256sum-equivalent) or--mode pixel(L0 RGB tiles in raster order, stable across re-encode).wsitools extract --type <t> -o <path>— save an associated image (label / macro / thumbnail / overview) as PNG (default) or JPEG. JPEG output is byte-pass-through when the source is already JPEG.
Associated-image editing
wsitools label remove <slide>— strip the label image (PHI) from an SVS or generic-TIFF file. The pyramid tile bytes are copied verbatim (no decode, no re-encode); only the tail IFD is rewritten. Output is a clean, compact file with no recoverable label PHI. Writes<stem>_relabeled<ext>next to the input by default; use-o/--outputfor an explicit path or--in-placeto atomically overwrite the original (temp + fsync + rename).wsitools label replace --image new.png <slide>— replace the label with a new image. Encoding defaults to LZW + Predictor 2 (lossless, barcode-safe); override with--compression {jpeg,lzw,deflate,none}.--resize fit|stretch|none(defaultfit),--bg RRGGBBfor letterbox fill (defaultF5F5E6),--forceto skip the aspect guard,--label-dims WxHto override target dimensions.wsitools macro|thumbnail|overview remove|replace …— same splice mechanics for the other associated-image types;replacedefaults to JPEG encoding.removeworks for every type on both formats.replaceis supported for all types on generic-TIFF, but on SVS only the label can be replaced today — opentile-go reads Aperio thumbnail/macro/overview as abbreviated JPEG (tables in theJPEGTablestag), so re-encoding those is a Slice-2 item; SVS thumbnail/macro/overviewreplaceerrors with a clear message.
Other formats (DICOM, NDPI, Philips, BIF, IFE, Leica) are not supported
for in-place editing — use convert --to {svs,tiff} --no-associated plus
label replace instead.
Conversion
wsitools convert --to cog-wsi— losslessly copy a WSI into the COG-WSI container (Cloud Optimized GeoTIFF + WSI extension tags). Tile bytes are copied verbatim; no decode, no re-encode. Seedocs/superpowers/specs/2026-05-20-cog-wsi-format.mdfor the format spec.wsitools convert --to {svs, tiff, ome-tiff}— tile-copy re-container (no--codecset) or re-encode (--codec {jpeg, jpegxl, avif, webp, htj2k}). Streaming pipeline; no L0 raster materialisation.wsitools convert --to dzi— DeepZoom pyramid output, OpenSeadragon- compatible (256×256 tiles, 1 px overlap, JPEG Q=85 by default). Single- pass pyramid-descent generator with parallel libjpeg-turbo encoders; ~150× faster than v0.16, faster than libvipsdzsaveon CMU-1.ndpi.wsitools convert --to szi— Smart Zoom Image: DZI pyramid wrapped in a store-method ZIP, plus an optionalscan-properties.xmlpopulated from source metadata.wsitools downsample— downsample a WSI by a power-of-2 factor (e.g. 40x → 20x), format-preserving: the output is the same container as the source (SVS→SVS, OME-TIFF→OME-TIFF, generic-TIFF→generic-TIFF, COG-WSI→COG-WSI). Regenerates the full pyramid from the new base, scales MPP ×N / magnification ÷N, and passes associated images through verbatim. Sources with no matching writer error with a pointer toconvert. To downsample into a different container, useconvert --to {svs,tiff,ome-tiff,cog-wsi} --factor N(dzi/szinot yet supported).
Source formats accepted: SVS, Philips-TIFF, OME-TIFF (tiled), BIF, IFE, generic-TIFF, NDPI, OME-OneFrame, Leica SCN (single-image), COG-WSI, and DICOM-WSI.
| Source format | info |
region |
dump-ifds |
extract¹ |
hash² |
convert (from)³ | convert (to)⁴ | downsample |
label/macro remove|replace⁷ |
|---|---|---|---|---|---|---|---|---|---|
| SVS | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Philips-TIFF | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | — | — |
| OME-TIFF | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | —⁸ |
| BIF | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | — | — |
| generic-TIFF | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| NDPI | ✓ | ✓ | ✓ | ✓ | ✓ | ✓* | — | — | — |
| OME-OneFrame | ✓ | ✓ | ✓ | ✓ | ✓ | ✓* | — | — | — |
| Leica SCN | ✓ | ✓ | ✓ | ✓ | ✓ | ✓* | — | — | — |
| COG-WSI | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | —⁸ |
| IFE | ✓ | ✓ | — | ✓ | ✓ | ✓ | — | — | — |
| DICOM-WSI | ✓ | ✓ | — | ✓ | ✓⁵ | ✓ | —⁶ | — | — |
¹ extract works when the slide carries that associated image (label/macro/thumbnail/overview); run info to list which.
² hash: --mode pixel works for every format; the default file-mode is a single-file SHA-256.
³ convert (from) — readable as a convert source. ✓* = striped source: opentile-go synthesizes a tile grid over the source strips, so convert decodes + re-encodes (reproducible JPEG tiles) rather than doing a bit-exact tile-copy. The lossless tile-copy fast path applies only to natively-tiled sources (plain ✓).
⁴ convert (to) — available as a convert output target. The full target set is cog-wsi, svs, tiff (→ generic-TIFF), ome-tiff, dzi, szi; DZI and SZI are output-only pyramid formats (not readable sources, so not listed as rows). All ✓ targets except dzi/szi also accept --factor N / --target-mag M to downsample during conversion (scales MPP ×N / magnification ÷N).
⁵ DICOM directory input → use --mode pixel (file-mode is undefined for a multi-file series; a multi-series directory errors — see below).
⁶ DICOM-WSI write is planned (writer scoped, not yet built).
⁷ label/macro remove|replace — applies equally to thumbnail and overview. Pyramid tile bytes are copied verbatim (no decode/re-encode); only the tail IFD is rewritten.
⁸ Planned (Slice 2): SubIFD-range-aware splice for OME-TIFF + OME-XML <Image> sync; SubIFD splice for COG-WSI.
downsample is format-preserving — it reduces a slide and emits the same
container it read (the ✓ rows: SVS, OME-TIFF, generic-TIFF, COG-WSI). Other
source formats error with a pointer to convert --to … --factor.
Striped sources produce reproducible but synthesized JPEG tiles in the output (bit-exact tile-copy applies only to natively-tiled sources).
DICOM-WSI input. A DICOM source may be either a single .dcm instance
or a directory containing a WSM series — pass the path to either. A named
.dcm always opens the series it belongs to (its siblings sharing the same
SeriesUID), even when the directory holds other slides. If a directory holds
more than one distinct WSM series, wsitools refuses with an error that lists
the candidate series; pass a specific .dcm of the slide you want to resolve
the ambiguity. (dump-ifds is TIFF-only and does not apply to
DICOM; use hash --mode pixel rather than the default file-mode for a DICOM
content hash.)
Diagnostics
wsitools doctor— report installed codec libraries, physical RAM, and the active soft memory limit (see Memory).wsitools version— print version + Go runtime info.
A global --max-memory flag caps the process's soft memory limit (default
75% of RAM); see Memory.
See docs/roadmap.md for the full list of planned
utilities (dump-tile, tagset, inventory, verify, diff,
tile-server, dicom-wsi, more codecs) and architectural items still
queued.
cgo dependencies (macOS via Homebrew):
brew install jpeg-turbo openjpeg jpeg-xl libavif webp openjphpkg-config resolves all of them at build time. Linux equivalents
(Debian/Ubuntu):
apt install libturbojpeg0-dev libopenjp2-7-dev libjxl-dev libavif-dev libwebp-dev
# OpenJPH (HTJ2K) typically requires source build on Linux as of 2026-05.Build a slim binary that skips selected codecs via build tags:
go build -tags 'noavif nowebp nohtj2k' ./cmd/wsitools # only JPEG-XL + JPEG
go build -tags 'nojxl noavif nowebp nohtj2k' ./cmd/wsitools # only JPEGgo install github.com/wsilabs/wsitools/cmd/wsitools@latest# Slide summary (analog of openslide-show-properties)
wsitools info slide.svs
# Same data as JSON for scripting
wsitools info --json slide.svs | jq .levels
# Format-aware per-IFD layout dump (slim)
wsitools dump-ifds slide.svs
# Full per-tag dump with names + enum interpretation (tiffinfo analog)
wsitools dump-ifds --raw slide.svs
# Same content as JSON
wsitools dump-ifds --raw --json slide.svs | jq .
# Extract a rectangular pixel region as PNG
wsitools region --x 10000 --y 8000 --w 1024 --h 1024 --level 0 -o tile.png slide.svs
# Save the slide's label as a standalone PNG
wsitools extract --type label -o label.png slide.svs
# Content hash for cache identity / dedup (default: SHA-256 of file bytes)
wsitools hash slide.svs
# Pixel-stable hash (decodes L0 tiles → SHA-256 of RGB raster)
wsitools hash --mode pixel slide.svs# Strip the label (PHI) — pyramid bytes untouched, no decode/re-encode
wsitools label remove slide.svs
# → writes slide_relabeled.svs next to the input
# Overwrite the original atomically (temp + fsync + rename)
wsitools label remove --in-place slide.svs
# Explicit output path
wsitools label remove -o deidentified.svs slide.svs
# Replace the label with a new image (LZW+predictor2 default — lossless, barcode-safe)
wsitools label replace --image new_label.png slide.svs
# Replace with explicit compression and letterbox background
wsitools label replace --image new_label.png --compression jpeg --bg F5F5E6 slide.svs
# Remove or replace the macro / thumbnail / overview the same way
wsitools macro remove slide.svs
wsitools macro replace --image new_macro.jpg slide.svs
wsitools thumbnail remove slide.svs
wsitools overview remove slide.svsSupported for SVS and generic-TIFF. OME-TIFF and COG-WSI support is
planned (Slice 2). Other formats (DICOM, NDPI, Philips, BIF, IFE, Leica)
are not writable — convert first with convert --to {svs,tiff}.
# Lossless tile-copy into a different container
wsitools convert --to cog-wsi -o slide.cog.tiff slide.svs
wsitools convert --to ome-tiff -o slide.ome.tiff slide.svs
# Re-encode to a different codec (still SVS-shaped)
wsitools convert --to svs --codec jpegxl -o slide-jxl.svs slide.svs
# Force BigTIFF (default `auto` promotes when predicted output > 2 GiB)
wsitools convert --to cog-wsi --bigtiff on -o slide.cog.tiff slide.svs
# Skip label/macro/thumbnail/overview
wsitools convert --to cog-wsi --no-associated -o slide.cog.tiff slide.svs
# DeepZoom output (OpenSeadragon-compatible)
wsitools convert --to dzi -o slide.dzi slide.svs
# Smart Zoom Image (DZI in a store-method ZIP)
wsitools convert --to szi -o slide.szi slide.svs
# Downsample a 40x SVS to 20x (factor 2 default)
wsitools downsample -o slide-20x.svs slide-40x.svs
# Or via target magnification
wsitools downsample --target-mag 10 -o slide-10x.svs slide-40x.svs# Check installed codec libs
wsitools doctor
# Suppress progress bar (useful in CI / scripts)
wsitools --quiet convert --to cog-wsi -o out.tiff in.svs
# Per-level timing summaries on stderr
wsitools --verbose convert --to dzi -o out.dzi in.svs
# Structured JSON logging (for log aggregators)
wsitools --log-format json convert --to cog-wsi -o out.tiff in.svs$ wsitools convert --to dzi -o CMU-1.dzi CMU-1.ndpi
encoding 100% 15847/15847 tiles 1132 tiles/s ETA 0s
wrote CMU-1.dzi (47.3 MB, 14s)
Conversion footprint scales with slide width, not a fixed ceiling.
convert --to dzi|szi streams top-to-bottom but holds full-width strip
buffers across every pyramid level plus the reader's per-tile decode
caches, so peak resident memory grows with the widest level:
- CMU-1.ndpi (L0 51200 × 38144): ~3.5 GB peak
- OS-2.ndpi (L0 126976 × 73728): ~5.4 GB peak
downsample (v0.1) additionally materialises the full L0 raster (a 40x
L0 ≈ 100K × 60K × 3 ≈ 18 GB); a streaming retrofit is queued — see
docs/roadmap.md.
To keep a runaway conversion from exhausting the machine, wsitools sets a
soft memory limit at 75% of physical RAM by default (via Go's
GOMEMLIMIT). Under pressure the garbage collector works harder —
trading some speed — instead of letting the process OOM the host. Tune or
disable it:
# Cap the soft limit at 4 GiB (slower, lower peak)
wsitools --max-memory 4GiB convert --to dzi -o out.dzi in.ndpi
# Disable the cap entirely
wsitools --max-memory off convert --to dzi -o out.dzi in.ndpi
# GOMEMLIMIT env is respected and takes precedence over the default
GOMEMLIMIT=8GiB wsitools convert --to dzi -o out.dzi in.ndpiPrecedence: --max-memory > GOMEMLIMIT > 75% default. wsitools doctor
reports the active limit and its source. The reader's own decode-cache
budget is separately tunable via OPENTILE_READ_MEMORY_BUDGET (default
1 GiB; opentile-go v0.30+).
make test # unit tests + integration, race-detector
make vetIntegration tests are fixture-gated by WSI_TOOLS_TESTDIR (default
./sample_files). CI downloads CMU-1-Small-Region.svs + CMU-1.ndpi from
wsilabs/wsi-fixtures on every
push and PR. For local work, soft-link to your fixture pool:
ln -s $HOME/GitHub/opentile-go/sample_files sample_filesApache 2.0. See LICENSE.