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).
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:
image: a data item is Image object as we know it from DOM,
context2d: a CanvasRenderingContext2D object,
rasterBlob: a Blob object containing raster image data,
imageBitmap: an ImageBitmap object with workers used for image decoding.
There are methods OpenSeadragon.converter.learn() and OpenSeadragon.converter.learnDestroy()
you can use to add custom types to the system.
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);
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;
});
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):
1, 1 because
if there are n pixels, the cost is 1 * n1
2, 1.
0 for instant computations, or use Math.log,
Math.sqrt for more accurate estimations depending on your knowledge.
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);
Here, we download a plaintext, and render it on each tile repeatedly with different background.
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
},
}
});
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.