Fork me on GitHub

OpenSeadragon 6.0.0

Data Type Concept in OpenSeadragon

Data types define how OpenSeadragon knows how to handle interaction between modular parts of its architecture. There are multiple TileSources that can insert into the system any data such as zip archives, strings, XML nodes... On the other side stands Drawers that require the data in formats they can work with - CanvasDrawer with Canvas or Image objects, WebGLDrawer with GPU textures. To add insult to the injury, there can be multiple plugins that interact in the system, and convert the data to yet another format.

For all this to work, OpenSeadragon must provide each component with data type it needs, and reason about the cheapest conversion cost between formats. It must also support asynchronous processing, because not every conversion can be done directly (see image.onload).

Supported Types and Naming Conventions

The type syntax should follow JavaScript naming style, lowerCamelCase. Types should be as tightly descriptive, as possible. Example:

If you provide a type string to the system with your image URI, you have basically broken the whole logic. OpenSeadragon does not know that your string is actually URI, and does not know what to do with your data to convert it, for example, to an Image.

Seems simple? Well, URI is too generic - it can be any resource specification, not just an internet resource location. But if we use URL instead, we are still not guaranteed that OpenSeadragon will handle our data correctly - it can point to a text, a binary, a HTML page...

Therefore, always think of a name for your type that encapsulates all its behavior to avoid collisions with other types (WebGL canvas vs Context2D canvas). OpenSeadragon supports the following types out of the box:

There are methods OpenSeadragon.converter.learn() and OpenSeadragon.converter.learnDestroy() you can use to add custom types to the system.

Conversion of Data

Adding support for a custom data type in the system is not complicated. You can use both synchronous and asynchronous logic. In order for your data to be fully compatible with the system, a type should define three converters: a converter to some other existing type, copy-converter (will be explained later), and optionally also a converter from an existing type to our own type (going back, not necessarily from the same type).

If you are familiar with mathematical graphs, you can imagine converters like directional edges between nodes (types), with edge costs (conversion costs).

See the following pseudocode on how to work with converters:

OpenSeadragon.converter.learn("typeA", "typeB", (tile, data) => {
    //data comes as type A, provide type B
    return data.asTypeB();
}, 1, 1);

// or asynchronous:
OpenSeadragon.converter.learn("typeA", "typeB", async (tile, data) => {
    const data = await data.asTypeBAsync();
    return data;  // yes, we could just return data.asAsync(..), but we want
                  // to explicitly show await usage
}, 1, 1);

// or promise version:
OpenSeadragon.converter.learn("typeA", "typeB", (tile, data) => {
    //data comes as type A, provide type B
    return new Promise(resolve => {
        data.convertToTypeBWithCallBack(() => resolve());
    });
}, 1, 1);

Destructors

If you work with a data objects that should be freed, you should provide a destructor for the type as well. OpenSeadragon will call it for you once the data is not needed, ensuring low memory footprint on the session. The following code is taken directly from OpenSeadragon, where canvas objects are freed to avoid issues with Apple devices. This feature is also especially useful with WebGL textures.

OpenSeadragon.converter.learnDestroy("context2d", ctx => {
    ctx.canvas.width = 0;
    ctx.canvas.height = 0;
});

Optimizing Data Transfer

For performant code, provide converters for multiple built-in data types, and set accurate conversion cost estimation.

So far we avoided the mysterious 1, 1 parameters at the end of the converter. These are conversion cost evaluators. OpenSeadragon will use these to find the cheapest conversion path should there be available multiple options.

The arguments estimate conversion cost using power argument (first) and multiplier argument (second):

Providing 'Copy Constructors'

For data to be use-able in the system, a copy logic need to be defined, too.

Copy constructors are needed once you start adjusting OpenSeadragon behavior, for example with plugins. With vanilla rendering, they are typically not necessary. A copy constructor either must return a full object copy, or return immutable reference. If you fail to keep this rule, you can experience artifacts on the screen.

// copy constructor is just conversion to itself
OpenSeadragon.converter.learn("same", "same", (tile, data) => {
    return data.clone();
}, 1, 1);

Example Custom TileSource

Here, we download a plaintext, and render it on each tile repeatedly with different background.

Example: adding support for custom type

If you need to work with a data type not on the list, you must specify a conversion to at least one of the built-in types. These conversions are directional, meaning if you provide a conversion from MyAwesomeType to image only, you can download the data as myAwesomeType, but you cannot for example request OpenSeadragon to give you myAwesomeType object within Tile data invalidation routine.

function getRandomColor() {
    const letters = '0123456789ABCDEF';
    let color = '#';
    for (let i = 0; i < 6; i++) {
        color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
}

OpenSeadragon.converter.learn("myAwesomeType", "myAwesomeType", (tile, text) => {
    return text;
}, 0, 1);

// We don't provide converter context2d -> myAwesomeType, since we won't be needing it
OpenSeadragon.converter.learn("myAwesomeType", "context2d", (tile, text) => {
    const canvas = document.createElement('canvas');
    canvas.width = tile.sourceBounds.width;
    canvas.height = tile.sourceBounds.height;
    const context = canvas.getContext('2d');

    const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height);
    gradient.addColorStop(0, getRandomColor());
    gradient.addColorStop(1, getRandomColor());

    context.fillStyle = gradient;
    context.fillRect(0, 0, canvas.width, canvas.height);

    context.font = 'bold 30px Arial';
    context.fillStyle = '#FFFFFF';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillText(text, canvas.width / 2, canvas.height / 2);

    return context;
}, 1, Math.sqrt(2)); // sometimes it is hard to estimate the cost, let just say
// the cost is sure greater than n, at least by factor of sqrt(2) - text render

OpenSeadragon({
    id:            "example-custom-tilesource-advanced",
    prefixUrl:     "/openseadragon/images/",
    navigatorSizeRatio: 0.25,
    wrapHorizontal:     true,
    loadTilesWithAjax:  true, // no effect
    tileSources:   {
        height: 1024,
        width:  1024,
        tileSize: 256,
        minLevel: 9,
        getTileUrl: function(level, x, y) {
            return `${level}/${x}-${y}`;
        },
        downloadTileStart: function(imageJob) {
            imageJob.finish("すごい!", null, "myAwesomeType");
        },
        downloadTileAbort: function(context) {
            //pass
        },
    }
});
    

Removing Async Support

OpenSeadragon viewer options also have supportsAsync: false option that can remove asynchronous execution. OpenSeadragon methods then ensure any API call executes synchronously. However, if you need to disable asynchronous behavior, you have to make sure there is no asynchronous function used anywhere. That means any raiseEventAwaiting handles must not be async functions, and all data type conversion logic must be synchronous too. OpenSeadragon on its own does not add asynchronous logic; the only exception is conversion to image type. This one specific scenario is watched for and if the system tries to convert data to image, it will notify you in the console.