Skip to content

mrmushfiq/go-openslide

Repository files navigation

go-openslide

Modern Go bindings for the OpenSlide C library — the standard C library for reading whole slide images (WSI) used in digital pathology.

Go Reference OpenSlide 4.0+ Go 1.25+ License: LGPL-2.1


Why go-openslide?

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

Key features

  • Complete OpenSlide 4.0 API coverage — every C function wrapped with idiomatic Go types and error handling
  • Thread safesync.RWMutex protects 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 Cache to multiple slides to share a fixed memory budget across all open handles
  • Deep Zoom tile generationDeepZoomGenerator produces 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 ReadRegion call; hot path is allocation-minimal
  • pkg-config integration — no manual CGO_CFLAGS or CGO_LDFLAGS needed

Supported slide formats

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 support

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.


Prerequisites

  • Go 1.25+
  • A C compiler (gcc or clang)
  • OpenSlide 4.0+ and pkg-config

macOS (Apple Silicon)

brew install pkg-config openslide

Homebrew 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 ~/.zshrc

Verify it works:

pkg-config --modversion openslide   # should print 4.0.0

macOS (Intel)

Same brew install commands. Homebrew installs to /usr/local on Intel:

export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig"

Linux

# Debian / Ubuntu
sudo apt install pkg-config libopenslide-dev

# Fedora / RHEL
sudo dnf install pkgconfig openslide-devel

Docker

FROM golang:1.25

RUN apt-get update && apt-get install -y \
    pkg-config \
    libopenslide-dev \
    && rm -rf /var/lib/apt/lists/*

Installation

go get github.com/mrmushfiq/go-openslide

Quick start

package 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))
    }
}

Deep Zoom tile serving

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 HTTP

A 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))

Shared tile cache

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

Thread safety

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()

Best practices

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"]

Test data

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.tiff

The file is ~195 MB and is listed in .gitignore.


Build and test

# 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 lint

A note on the OpenSlide 4.x include path

OpenSlide 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.


License

LGPL-2.1. See LICENSE.

About

Modern Go bindings for the OpenSlide C library — the standard C library for reading whole slide images (WSI) used in digital pathology.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors