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.
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.
For an irregular pyramid you typically override three methods on
your TileSource:
getLevelScale(level) — return the scale of a level
relative to the native (max) resolution. The default returns
1 / 2^(maxLevel - level); override it for arbitrary
scales (e.g. 0.005, 0.02, 0.06, 0.15, 0.35, 0.7, 1.0).
getTileWidth(level) /
getTileHeight(level) — return the tile dimensions for
a given level. They can differ between levels and need not be square.
Make sure both methods return the value you want; the default
falls back to _tileWidth/_tileHeight
members initialized from tileSize or
tileWidth/tileHeight options.
getNumTiles(level) — only override if your tile grid
is not derivable from scale * imageDimensions / tileSize;
for most use cases the default does the right thing.
The standard properties (width, height,
minLevel, maxLevel) describe the native
resolution and the inclusive level range; everything else is computed
from the methods above.
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:
getLevelScale caches
its results by replacing itself with a closure over a precomputed
table. If you provide your own getLevelScale on the
prototype or instance, the default memoization simply does not run.
You do not need to call super — your override
completely replaces the base behaviour.
minPixelRatio
when deciding how far down the pyramid it may go, and may draw or
blend multiple levels until the viewport is fully covered. This works
for irregular pyramid shapes as well as power-of-two pyramids, and is
what makes the custom-scale demo above feel correct as you zoom.
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.
1.0 to fetch roughly at the screen pixel ratio
and reduce bandwidth.1.5+ for low-bandwidth or mobile use; you may
see slightly upsampled content during fast zoom but far fewer tile
requests.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.
OpenSeadragon({ /* ... */ discardLevelsBelowDownsampleRatio: 2 });
TileSource — just expose
discardLevelsBelowDownsampleRatio on the source
instance and OpenSeadragon will copy it onto the
TiledImage at construction. This lets a tile source
that knows it ships near-duplicate levels enforce a sensible
default for itself.
1 are invalid — they would mean "keep
levels that are more similar than the same one", which is
not meaningful — and will be replaced with the default after a
warning.
maxLevel is always preserved so the highest detail
remains reachable.
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
})
});
}
A practical recipe for a custom pyramid that performs well across devices:
getLevelScale, getTileWidth,
getTileHeight to describe the pyramid faithfully —
do not lie about scales just to make the picker happy.
discardLevelsBelowDownsampleRatio on the source so
consumers do not have to know the quirk.
immediateRender: true and raise
minPixelRatio to 1.0 (or a touch above)
— fewer requests, no intermediate-quality renders.
immediateRender: false,
minPixelRatio: 0.5) give the smoothest user experience.
For full API details, see the documentation pages for
OpenSeadragon.TileSource,
OpenSeadragon.TiledImage, and the
global viewer options.