Fork me on GitHub

OpenSeadragon 6.1.0

Flexible Pyramids and Rendering Controls

Many rendering systems like OpenSeadragon usually force powers-of-two pyramids, sometimes even with square tiles. Each level is two times bigger/smaller than the previous/next, and tiles are 256 or 521-pixel sized squares. While this is simple to reason about, it poses a significant limitation. Real-world data often does not follow this assumption: servers may expose arbitrary scales, scientific formats may use custom level layouts or remove certain levels to optimize storage, and synthetic sources may want a single very large tile at the top of the pyramid.

OpenSeadragon respects whatever getLevelScale, getTileWidth and getTileHeight return on a per-level basis. This page covers how to take advantage of that, and documents the rendering knobs (immediateRender, minPixelRatio, and the new discardLevelsBelowDownsampleRatio) that decide which levels OpenSeadragon actually fetches and how it transitions between them.

If you are new to writing custom tile sources, please first read the custom tile source and advanced custom tile source chapters. The page below assumes you are already familiar with the TileSource class.

Level Convention

OpenSeadragon treats level 0 as the smallest resolution and maxLevel as the highest native resolution. This is the opposite of the convention used by some scientific imaging libraries (OpenSlide, libvips, etc.) where index 0 is the native resolution. When you port a pyramid from those systems, remember to flip the level index — or override getLevelScale so that scale 1.0 lands on the highest level and smaller scales on the lower levels.

Custom Per-Level Scales and Tile Sizes

For an irregular pyramid you typically override three methods on your TileSource:

The standard properties (width, height, minLevel, maxLevel) describe the native resolution and the inclusive level range; everything else is computed from the methods above.

Example: irregular pyramid with custom scales and tile sizes

The viewer above is fed by a synthetic TileSource that renders each tile with a coloured gradient and prints the level, coordinates, scale and tile size onto it. The pyramid uses non-power-of-two scales and a different tile size on every level. Zoom in and out to see how OpenSeadragon picks levels.

class FlexiblePyramidSource extends OpenSeadragon.TileSource {
    constructor(options) {
        const minLevel = options.minLevel != null ? options.minLevel : 0;
        const maxLevel = options.maxLevel != null
            ? options.maxLevel
            : (options.levelScales ? options.levelScales.length - 1 : 0);

        super(Object.assign({}, options, { minLevel, maxLevel }));

        this.width  = options.width;
        this.height = options.height;
        this.minLevel = minLevel;
        this.maxLevel = maxLevel;
        this.levelScales    = options.levelScales    || [];
        this.levelTileSizes = options.levelTileSizes || [];
    }

    // Arbitrary per-level scale relative to the full-resolution image.
    getLevelScale(level) {
        if (this.levelScales[level] != null) {
            return this.levelScales[level];
        }
        return 1 / Math.pow(2, this.maxLevel - level);
    }

    // Tile size can vary level by level.
    getTileWidth(level) {
        return this.levelTileSizes[level] != null
            ? this.levelTileSizes[level]
            : 256;
    }
    getTileHeight(level) {
        return this.getTileWidth(level);
    }

    // The URL doubles as a per-tile cache key for our synthetic data.
    getTileUrl(level, x, y) {
        return `flexible-pyramid::${level}/${x}-${y}`;
    }

    // Render the tile directly as a context2d — no network involved.
    downloadTileStart(context) {
        const { level, x, y } = context.tile;
        const tileSize = this.getTileWidth(level);
        const size = this.getTileBounds(level, x, y, true);

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width  = Math.max(1, Math.floor(size.width));
        canvas.height = Math.max(1, Math.floor(size.height));

        const hue = (level * 47) % 360;
        const grd = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        grd.addColorStop(0, `hsl(${hue}, 70%, 80%)`);
        grd.addColorStop(1, `hsl(${(hue + 60) % 360}, 70%, 55%)`);
        ctx.fillStyle = grd;
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        ctx.strokeStyle = 'rgba(0,0,0,0.5)';
        ctx.lineWidth = 1;
        ctx.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);

        ctx.fillStyle = '#111';
        ctx.font = Math.max(12, tileSize / 14) + 'px sans-serif';
        let row = 1;
        const line = () => 8 + (tileSize / 12) * row++;
        ctx.fillText(`level ${level}`,                        8, line());
        ctx.fillText(`tile ${x},${y}`,                        8, line());
        ctx.fillText(`scale ${this.getLevelScale(level).toFixed(4)}`, 8, line());
        ctx.fillText(`tileSize ${tileSize}px`,                8, line());

        context.finish(ctx, null, 'context2d');
    }

    downloadTileAbort() { /* synthetic — nothing to abort */ }
}

OpenSeadragon({
    id: 'example-flexible-pyramid',
    prefixUrl: '/openseadragon/images/',
    showNavigator: true,
    blendTime: 0.2,
    minZoomImageRatio: 0.5,
    tileSources: new FlexiblePyramidSource({
        width:  8192,
        height: 8192,
        minLevel: 0,
        maxLevel: 6,
        // Non-power-of-two scales — fully respected by the level picker.
        levelScales:    [0.01, 0.04, 0.10, 0.25, 0.50, 0.80, 1.00],
        // Tile size differs per level on purpose.
        levelTileSizes: [128,  256,  256,  512,  512,  1024, 1024]
    })
});
    

A few subtleties worth knowing:

Rendering Controls

Several viewer (and per-TiledImage) options govern how OpenSeadragon decides which levels to fetch and how it transitions between them. They all become more visible on irregular pyramids, which is why this page groups them together.

immediateRender (default: false)

By default, OpenSeadragon walks up the pyramid from a low resolution (the biggest single tile that covers the whole image) to the closest level, producing the familiar blurry-to-sharp progression. Setting immediateRender: true skips that progression and renders the minPixelRatio matching level directly. This avoids transient lower-resolution frames at the cost of a visible "patchwork" effect as the tiles come in. This can be worthwhile on mobile devices to reduce data transfer.

minPixelRatio (default: 0.5)

Sets the lowest acceptable pixel ratio (rendered pixels per source pixel) for a level to be considered "good enough". The default 0.5 means OpenSeadragon will pre-fetch one level above the target so that zooming in reveals already-loaded sharper data.

discardLevelsBelowDownsampleRatio (default: 1)

Lets you skip pyramid levels whose downsample gap to the previously accepted level is below the given ratio. Concretely, with a power-of-two pyramid (each level twice as detailed as the next), setting discardLevelsBelowDownsampleRatio: 4 makes the viewer use every other level — i.e. levels spaced by a 4× downsample factor. For irregular pyramids this is useful when you want to hide near-duplicate levels: e.g. if a server provides scales 0.1, 0.12, 0.5, 1.0, a ratio of 2 will collapse the 0.1/0.12 pair down to a single chosen level.

Example: skipping near-duplicate levels with discardLevelsBelowDownsampleRatio

Use the buttons to rebuild the viewer with different discard ratios. The pyramid below has 8 power-of-two levels; each tile prints its level, so you can see at a glance which levels actually get fetched as you zoom. With ratio 1 (default) every level is eligible; with 4 the viewer uses every other level; with 16 only every fourth.

function makePyramid(numLevels, tileSize) {
    const scales = [];
    const tileSizes = [];
    const max = numLevels - 1;
    for (let l = 0; l <= max; l++) {
        scales.push(1 / Math.pow(2, max - l));
        tileSizes.push(tileSize);
    }
    return { scales, tileSizes };
}

function buildViewer(opts) {
    const { scales, tileSizes } = makePyramid(8, 256);
    return OpenSeadragon({
        id: 'example-discard-levels',
        prefixUrl: '/openseadragon/images/',
        showNavigator: false,
        blendTime: 0.1,
        immediateRender: !!opts.immediateRender,
        minPixelRatio: opts.minPixelRatio,
        discardLevelsBelowDownsampleRatio: opts.discardRatio,
        tileSources: new FlexiblePyramidSource({
            width: 4096, height: 4096,
            minLevel: 0, maxLevel: 7,
            levelScales: scales,
            levelTileSizes: tileSizes
        })
    });
}
    

Putting It Together

A practical recipe for a custom pyramid that performs well across devices:

For full API details, see the documentation pages for OpenSeadragon.TileSource, OpenSeadragon.TiledImage, and the global viewer options.