Modern Go bindings for the OpenSlide C library — the standard C library for reading whole slide images (WSI) used in digital pathology.
The existing Go bindings for OpenSlide all target OpenSlide 3.4.x, expose raw C pointers, lack thread safety, and are no longer maintained. go-openslide is built specifically for OpenSlide 4.0 and fills these gaps:
| Feature | go-openslide | Existing libraries |
|---|---|---|
| OpenSlide version | 4.0.0+ | 3.4.x |
| Thread safe | ✓ | ✗ |
| ICC color profiles | ✓ | ✗ |
| Tile cache management | ✓ | ✗ |
| Deep Zoom tile generation | ✓ | ✗ |
| Idiomatic Go API | ✓ | Partial |
| Actively maintained | ✓ | ✗ |
- Complete OpenSlide 4.0 API coverage — every C function wrapped with idiomatic Go types and error handling
- Thread safe —
sync.RWMutexprotects the C handle; safe for concurrent tile serving from multiple goroutines - ICC color profile support — read embedded ICC profiles for color-accurate pathology rendering
- Shared tile cache — attach a single
Cacheto multiple slides to share a fixed memory budget across all open handles - Deep Zoom tile generation —
DeepZoomGeneratorproduces DZI manifests and 256×256 tiles consumable by web viewers like OpenSeadragon - Property helpers — typed accessors for common metadata (
GetMPP,GetObjectivePower,GetBounds,GetVendor) - Zero unnecessary allocations — 3 allocs per
ReadRegioncall; hot path is allocation-minimal - pkg-config integration — no manual
CGO_CFLAGSorCGO_LDFLAGSneeded
OpenSlide supports the following formats out of the box:
| Format | Extensions |
|---|---|
| Aperio | .svs, .tif |
| DICOM | .dcm |
| Hamamatsu | .ndpi, .vms, .vmu |
| Leica | .scn |
| MIRAX | .mrxs |
| Philips | .tiff |
| Sakura | .svslide |
| Trestle | .tif |
| Ventana | .bif, .tif |
| Generic tiled TIFF | .tif |
| Platform | Status | Notes |
|---|---|---|
| macOS Apple Silicon (ARM64) | ✓ Supported | Homebrew installs to /opt/homebrew |
| macOS x86_64 | ✓ Supported | Homebrew installs to /usr/local |
| Linux x86_64 / ARM64 | ✓ Supported | Use system package manager |
| Windows | ✗ Not recommended | Use WSL2 or Docker instead |
Windows: CGO on Windows requires a GCC toolchain (MinGW-w64) and manual wiring of OpenSlide's pre-built DLLs. This is error-prone and not actively tested. Use WSL2 (treated as Linux) or the Docker option below.
- Go 1.25+
- A C compiler (
gccorclang) - OpenSlide 4.0+ and
pkg-config
brew install pkg-config openslideHomebrew on Apple Silicon installs to /opt/homebrew. If go build fails with a missing header, add this to your ~/.zshrc:
export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig"
source ~/.zshrcVerify it works:
pkg-config --modversion openslide # should print 4.0.0Same brew install commands. Homebrew installs to /usr/local on Intel:
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig"# Debian / Ubuntu
sudo apt install pkg-config libopenslide-dev
# Fedora / RHEL
sudo dnf install pkgconfig openslide-develFROM golang:1.25
RUN apt-get update && apt-get install -y \
pkg-config \
libopenslide-dev \
&& rm -rf /var/lib/apt/lists/*go get github.com/mrmushfiq/go-openslidepackage main
import (
"fmt"
"image/png"
"log"
"os"
openslide "github.com/mrmushfiq/go-openslide"
)
func main() {
// Open a slide
slide, err := openslide.Open("tumor_001.svs")
if err != nil {
log.Fatal(err)
}
defer slide.Close()
// Slide dimensions
w, h, _ := slide.Level0Dimensions()
fmt.Printf("Slide: %d × %d pixels\n", w, h)
// Zoom levels
count, _ := slide.LevelCount()
fmt.Printf("Levels: %d\n", count)
for i := int32(0); i < count; i++ {
lw, lh, _ := slide.LevelDimensions(i)
ds, _ := slide.LevelDownsample(i)
fmt.Printf(" level %d: %d × %d (downsample %.1f×)\n", i, lw, lh, ds)
}
// Metadata
props, _ := slide.Properties()
fmt.Printf("Vendor: %s\n", props.GetVendor())
if mppX, mppY, ok := props.GetMPP(); ok {
fmt.Printf("MPP: x=%.4f y=%.4f µm/px\n", mppX, mppY)
}
if power, ok := props.GetObjectivePower(); ok {
fmt.Printf("Objective power: %.0f×\n", power)
}
// Read a 512×512 region from level 0 and save as PNG
region, _ := slide.ReadRegion(0, 0, 0, 512, 512)
f, _ := os.Create("region.png")
defer f.Close()
png.Encode(f, region)
fmt.Println("Saved region.png")
// ICC color profile
icc, _ := slide.ICCProfile()
if icc != nil {
fmt.Printf("ICC profile: %d bytes\n", len(icc))
}
}DeepZoomGenerator exposes a slide as a standard DZI pyramid for web viewers like OpenSeadragon:
// Create a Deep Zoom generator
// tileSize=254, overlap=1 is the standard DZI configuration
dz, err := openslide.NewDeepZoomGenerator(slide, 254, 1, true)
if err != nil {
log.Fatal(err)
}
// Serve the DZI manifest (requested first by the viewer)
dzi, _ := dz.GetDZI("jpeg")
fmt.Println(dzi)
// <?xml version="1.0" encoding="UTF-8"?>
// <Image xmlns="http://schemas.microsoft.com/deepzoom/2008"
// Format="jpeg" Overlap="1" TileSize="254">
// <Size Width="46000" Height="32914"/>
// </Image>
// Pyramid info
fmt.Printf("DZ levels: %d\n", dz.LevelCount())
fmt.Printf("Total tiles: %d\n", dz.TileCount())
// Get and encode a tile (level, col, row)
tile, _ := dz.GetTile(dz.LevelCount()-1, 0, 0)
data, _ := dz.EncodeTile(tile, "jpeg")
// serve data bytes via HTTPA minimal HTTP tile server:
http.HandleFunc("/slide.dzi", func(w http.ResponseWriter, r *http.Request) {
dzi, _ := dz.GetDZI("jpeg")
w.Header().Set("Content-Type", "application/xml")
fmt.Fprint(w, dzi)
})
http.HandleFunc("/slide_files/", func(w http.ResponseWriter, r *http.Request) {
// Parse /slide_files/{level}/{col}_{row}.jpeg
var level, col, row int
fmt.Sscanf(r.URL.Path, "/slide_files/%d/%d_%d.jpeg", &level, &col, &row)
tile, err := dz.GetTile(level, col, row)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data, _ := dz.EncodeTile(tile, "jpeg")
w.Header().Set("Content-Type", "image/jpeg")
w.Write(data)
})
log.Fatal(http.ListenAndServe(":8080", nil))For a tile server opening many slides simultaneously, share a single cache across all slide handles to control total memory usage:
// Create a 512MB shared cache — do this once at startup
cache, err := openslide.NewCache(512 * 1024 * 1024)
if err != nil {
log.Fatal(err)
}
defer cache.Release()
// Attach to every slide you open
slide1, _ := openslide.Open("slide1.svs")
slide1.SetCache(cache)
slide2, _ := openslide.Open("slide2.svs")
slide2.SetCache(cache)
// Both slides now share the 512MB budget.
// Frequently accessed tiles stay hot across all handles.Cache sizing guidelines:
| Server RAM | Recommended cache |
|---|---|
| 8 GB | 1–2 GB |
| 32 GB | 8–16 GB |
| 128 GB | 32–64 GB |
All Slide methods are safe for concurrent use. The internal sync.RWMutex allows multiple goroutines to call ReadRegion simultaneously while Close is exclusive:
// Safe — multiple goroutines can read in parallel
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(col int) {
defer wg.Done()
tile, _ := dz.GetTile(dz.LevelCount()-1, col, 0)
_ = tile
}(i)
}
wg.Wait()Always defer Close:
slide, err := openslide.Open("slide.svs")
if err != nil {
return err
}
defer slide.Close()Use BestLevelForDownsample when zooming:
// Don't always read from level 0 — pick the right level for your zoom
downsample := 16.0
level, _ := slide.BestLevelForDownsample(downsample)
region, _ := slide.ReadRegion(x, y, level, w, h)Check vendor before opening:
vendor, _ := openslide.DetectVendor("unknown.tif")
if vendor == "" {
return fmt.Errorf("unrecognised slide format")
}Use property constants instead of raw strings:
// Preferred
props[openslide.PropertyVendor]
// Avoid
props["openslide.vendor"]Tests require a real whole slide image not included in this repo. Download the CMU-1 sample:
mkdir -p testdata
curl -L -o testdata/CMU-1.tiff \
https://openslide.cs.cmu.edu/download/openslide-testdata/Generic-TIFF/CMU-1.tiffThe file is ~195 MB and is listed in .gitignore.
# Verify OpenSlide is visible to pkg-config
make check-openslide
# Build
make build
# Run tests with race detector
make test
# Run benchmarks
make bench
# Lint
make lintOpenSlide 4.0.0 changed its pkg-config Cflags entry to point directly into the openslide/ subdirectory. Headers are now included as:
#include <openslide.h>Older bindings used #include <openslide/openslide.h> — that no longer works with OpenSlide 4.x. This library uses the correct 4.x form throughout.
LGPL-2.1. See LICENSE.