Drawers now use new cache API to draw onto a canvas. The type conversion now requires also the tile argument so that conversion can rely on the tile metadata.

This commit is contained in:
Aiosa 2024-02-04 18:48:25 +01:00
parent 3fa13570ef
commit fcf20be8ea
12 changed files with 300 additions and 246 deletions

View File

@ -50,6 +50,8 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
constructor(options){ constructor(options){
super(options); super(options);
this.declareSupportedDataFormats("context2d");
/** /**
* The HTML element (canvas) that this drawer uses for drawing * The HTML element (canvas) that this drawer uses for drawing
* @member {Element} canvas * @member {Element} canvas
@ -255,26 +257,26 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
* *
*/ */
_drawTiles( tiledImage ) { _drawTiles( tiledImage ) {
var lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile); const lastDrawn = tiledImage.getTilesToDraw().map(info => info.tile);
if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) { if (tiledImage.opacity === 0 || (lastDrawn.length === 0 && !tiledImage.placeholderFillStyle)) {
return; return;
} }
var tile = lastDrawn[0]; let tile = lastDrawn[0];
var useSketch; let useSketch;
if (tile) { if (tile) {
useSketch = tiledImage.opacity < 1 || useSketch = tiledImage.opacity < 1 ||
(tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') || (tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over') ||
(!tiledImage._isBottomItem() && (!tiledImage._isBottomItem() &&
tiledImage.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData)); tiledImage.source.hasTransparency(null, tile.getUrl(), tile.ajaxHeaders, tile.postData));
} }
var sketchScale; let sketchScale;
var sketchTranslate; let sketchTranslate;
var zoom = this.viewport.getZoom(true); const zoom = this.viewport.getZoom(true);
var imageZoom = tiledImage.viewportToImageZoom(zoom); const imageZoom = tiledImage.viewportToImageZoom(zoom);
if (lastDrawn.length > 1 && if (lastDrawn.length > 1 &&
imageZoom > tiledImage.smoothTileEdgesMinZoom && imageZoom > tiledImage.smoothTileEdgesMinZoom &&
@ -284,13 +286,19 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
// So we have to composite them at ~100% and scale them up together. // So we have to composite them at ~100% and scale them up together.
// Note: Disabled on iOS devices per default as it causes a native crash // Note: Disabled on iOS devices per default as it causes a native crash
useSketch = true; useSketch = true;
sketchScale = tile.getScaleForEdgeSmoothing();
const context = tile.length && this.getCompatibleData(tile);
if (context) {
sketchScale = context.canvas.width / (tile.size.x * $.pixelDensityRatio);
} else {
sketchScale = 1;
}
sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale, sketchTranslate = tile.getTranslationForEdgeSmoothing(sketchScale,
this._getCanvasSize(false), this._getCanvasSize(false),
this._getCanvasSize(true)); this._getCanvasSize(true));
} }
var bounds; let bounds;
if (useSketch) { if (useSketch) {
if (!sketchScale) { if (!sketchScale) {
// Except when edge smoothing, we only clean the part of the // Except when edge smoothing, we only clean the part of the
@ -337,13 +345,13 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
} }
} }
var usedClip = false; let usedClip = false;
if ( tiledImage._clip ) { if ( tiledImage._clip ) {
this._saveContext(useSketch); this._saveContext(useSketch);
var box = tiledImage.imageToViewportRectangle(tiledImage._clip, true); let box = tiledImage.imageToViewportRectangle(tiledImage._clip, true);
box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); box = box.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipRect = this.viewportToDrawerRectangle(box); let clipRect = this.viewportToDrawerRectangle(box);
if (sketchScale) { if (sketchScale) {
clipRect = clipRect.times(sketchScale); clipRect = clipRect.times(sketchScale);
} }
@ -356,17 +364,17 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
} }
if (tiledImage._croppingPolygons) { if (tiledImage._croppingPolygons) {
var self = this; const self = this;
if(!usedClip){ if(!usedClip){
this._saveContext(useSketch); this._saveContext(useSketch);
} }
try { try {
var polygons = tiledImage._croppingPolygons.map(function (polygon) { const polygons = tiledImage._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) { return polygon.map(function (coord) {
var point = tiledImage const point = tiledImage
.imageToViewportCoordinates(coord.x, coord.y, true) .imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true)); .rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipPoint = self.viewportCoordToDrawerCoord(point); let clipPoint = self.viewportCoordToDrawerCoord(point);
if (sketchScale) { if (sketchScale) {
clipPoint = clipPoint.times(sketchScale); clipPoint = clipPoint.times(sketchScale);
} }
@ -384,7 +392,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
} }
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) { if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
var placeholderRect = this.viewportToDrawerRectangle(tiledImage.getBounds(true)); let placeholderRect = this.viewportToDrawerRectangle(tiledImage.getBounds(true));
if (sketchScale) { if (sketchScale) {
placeholderRect = placeholderRect.times(sketchScale); placeholderRect = placeholderRect.times(sketchScale);
} }
@ -392,7 +400,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
placeholderRect = placeholderRect.translate(sketchTranslate); placeholderRect = placeholderRect.translate(sketchTranslate);
} }
var fillStyle = null; let fillStyle;
if ( typeof tiledImage.placeholderFillStyle === "function" ) { if ( typeof tiledImage.placeholderFillStyle === "function" ) {
fillStyle = tiledImage.placeholderFillStyle(tiledImage, this.context); fillStyle = tiledImage.placeholderFillStyle(tiledImage, this.context);
} }
@ -403,19 +411,18 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._drawRectangle(placeholderRect, fillStyle, useSketch); this._drawRectangle(placeholderRect, fillStyle, useSketch);
} }
var subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency); const subPixelRoundingRule = determineSubPixelRoundingRule(tiledImage.subPixelRoundingForTransparency);
var shouldRoundPositionAndSize = false; let shouldRoundPositionAndSize = false;
if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) { if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ALWAYS) {
shouldRoundPositionAndSize = true; shouldRoundPositionAndSize = true;
} else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) { } else if (subPixelRoundingRule === $.SUBPIXEL_ROUNDING_OCCURRENCES.ONLY_AT_REST) {
var isAnimating = this.viewer && this.viewer.isAnimating(); shouldRoundPositionAndSize = !(this.viewer && this.viewer.isAnimating());
shouldRoundPositionAndSize = !isAnimating;
} }
// Iterate over the tiles to draw, and draw them // Iterate over the tiles to draw, and draw them
for (var i = 0; i < lastDrawn.length; i++) { for (let i = 0; i < lastDrawn.length; i++) {
tile = lastDrawn[ i ]; tile = lastDrawn[ i ];
this._drawTile( tile, tiledImage, useSketch, sketchScale, this._drawTile( tile, tiledImage, useSketch, sketchScale,
sketchTranslate, shouldRoundPositionAndSize, tiledImage.source ); sketchTranslate, shouldRoundPositionAndSize, tiledImage.source );
@ -499,9 +506,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._drawDebugInfo( tiledImage, lastDrawn ); this._drawDebugInfo( tiledImage, lastDrawn );
// Fire tiled-image-drawn event. // Fire tiled-image-drawn event.
this._raiseTiledImageDrawnEvent(tiledImage, lastDrawn); this._raiseTiledImageDrawnEvent(tiledImage, lastDrawn);
} }
/** /**
@ -559,52 +564,25 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
$.console.assert(tile, '[Drawer._drawTile] tile is required'); $.console.assert(tile, '[Drawer._drawTile] tile is required');
$.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required'); $.console.assert(tiledImage, '[Drawer._drawTile] drawingHandler is required');
var context = this._getContext(useSketch); if ( !tile.loaded ){
scale = scale || 1;
this._drawTileToCanvas(tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source);
}
/**
* Renders the tile in a canvas-based context.
* @private
* @function
* @param {OpenSeadragon.Tile} tile - the tile to draw to the canvas
* @param {Canvas} context
* @param {OpenSeadragon.TiledImage} tiledImage - Method for firing the drawing event.
* drawingHandler({context, tile, rendered})
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Number} [scale=1] - Apply a scale to position and size
* @param {OpenSeadragon.Point} [translate] - A translation vector
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency
* context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
_drawTileToCanvas( tile, context, tiledImage, scale, translate, shouldRoundPositionAndSize, source) {
var position = tile.position.times($.pixelDensityRatio),
size = tile.size.times($.pixelDensityRatio),
rendered;
if (!tile.context2D && !tile.cacheImageRecord) {
$.console.warn(
'[Drawer._drawTileToCanvas] attempting to draw tile %s when it\'s not cached',
tile.toString());
return;
}
rendered = tile.getCanvasContext();
if ( !tile.loaded || !rendered ){
$.console.warn( $.console.warn(
"Attempting to draw tile %s when it's not yet loaded.", "Attempting to draw tile %s when it's not yet loaded.",
tile.toString() tile.toString()
); );
return; return;
} }
const rendered = this.getCompatibleData(tile);
if (!rendered) {
return;
}
const context = this._getContext(useSketch);
scale = scale || 1;
let position = tile.position.times($.pixelDensityRatio),
size = tile.size.times($.pixelDensityRatio);
context.save(); context.save();
// context.globalAlpha = this.options.opacity; // this was deprecated previously and should not be applied as it is set per TiledImage // context.globalAlpha = this.options.opacity; // this was deprecated previously and should not be applied as it is set per TiledImage
@ -644,7 +622,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this._raiseTileDrawingEvent(tiledImage, context, tile, rendered); this._raiseTileDrawingEvent(tiledImage, context, tile, rendered);
var sourceWidth, sourceHeight; let sourceWidth, sourceHeight;
if (tile.sourceBounds) { if (tile.sourceBounds) {
sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width); sourceWidth = Math.min(tile.sourceBounds.width, rendered.canvas.width);
sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height); sourceHeight = Math.min(tile.sourceBounds.height, rendered.canvas.height);
@ -672,6 +650,8 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
context.restore(); context.restore();
} }
/** /**
* Get the context of the main or sketch canvas * Get the context of the main or sketch canvas
* @private * @private

View File

@ -148,13 +148,24 @@ class WeightedGraph {
} }
/** /**
* Node on the conversion path in OpenSeadragon.converter.getConversionPath(). * Edge.transform function on the conversion path in OpenSeadragon.converter.getConversionPath().
* It can be also conversion to undefined if used as destructor implementation. * It can be also conversion to undefined if used as destructor implementation.
* *
* @callback TypeConvertor * @callback TypeConvertor
* @memberof OpenSeadragon * @memberof OpenSeadragon
* @param {?} data data in the input format * @param {OpenSeadragon.Tile} tile reference tile that owns the data
* @return {?} data in the output format * @param {any} data data in the input format
* @returns {any} data in the output format
*/
/**
* Destructor called every time a data type is to be destroyed or converted to another type.
*
* @callback TypeDestructor
* @memberof OpenSeadragon
* @param {any} data data in the format the destructor is registered for
* @returns {any} can return any value that is carried over to the caller if desirable.
* Note: not used by the OSD cache system.
*/ */
/** /**
@ -184,13 +195,13 @@ $.DataTypeConvertor = class {
this.copyings = {}; this.copyings = {};
// Teaching OpenSeadragon built-in conversions: // Teaching OpenSeadragon built-in conversions:
const imageCreator = (url) => new $.Promise((resolve, reject) => { const imageCreator = (tile, url) => new $.Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onerror = img.onabort = reject; img.onerror = img.onabort = reject;
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.src = url; img.src = url;
}); });
const canvasContextCreator = (imageData) => { const canvasContextCreator = (tile, imageData) => {
const canvas = document.createElement( 'canvas' ); const canvas = document.createElement( 'canvas' );
canvas.width = imageData.width; canvas.width = imageData.width;
canvas.height = imageData.height; canvas.height = imageData.height;
@ -199,15 +210,25 @@ $.DataTypeConvertor = class {
return context; return context;
}; };
this.learn("context2d", "url", ctx => ctx.canvas.toDataURL(), 1, 2); this.learn("context2d", "url", (tile, ctx) => ctx.canvas.toDataURL(), 1, 2);
this.learn("image", "url", image => image.url); this.learn("image", "url", (tile, image) => image.url);
this.learn("image", "context2d", canvasContextCreator, 1, 1); this.learn("image", "context2d", canvasContextCreator, 1, 1);
this.learn("url", "image", imageCreator, 1, 1); this.learn("url", "image", imageCreator, 1, 1);
//Copies //Copies
this.learn("image", "image", image => imageCreator(image.src), 1, 1); this.learn("image", "image", (tile, image) => imageCreator(tile, image.src), 1, 1);
this.learn("url", "url", url => url, 0, 1); //strings are immutable, no need to copy this.learn("url", "url", (tile, url) => url, 0, 1); //strings are immutable, no need to copy
this.learn("context2d", "context2d", ctx => canvasContextCreator(ctx.canvas)); this.learn("context2d", "context2d", (tile, ctx) => canvasContextCreator(tile, ctx.canvas));
/**
* Free up canvas memory
* (iOS 12 or higher on 2GB RAM device has only 224MB canvas memory,
* and Safari keeps canvas until its height and width will be set to 0).
*/
this.learnDestroy("context2d", ctx => {
ctx.canvas.width = 0;
ctx.canvas.height = 0;
});
} }
/** /**
@ -263,9 +284,9 @@ $.DataTypeConvertor = class {
* Teach the system to convert data type 'from' -> 'to' * Teach the system to convert data type 'from' -> 'to'
* @param {string} from unique ID of the data item 'from' * @param {string} from unique ID of the data item 'from'
* @param {string} to unique ID of the data item 'to' * @param {string} to unique ID of the data item 'to'
* @param {OpenSeadragon.TypeConvertor} callback convertor that takes type 'from', and converts to type 'to'. * @param {OpenSeadragon.TypeConvertor} callback convertor that takes two arguments: a tile reference, and
* Callback can return function. This function returns the data in type 'to', * a data object of a type 'from'; and converts this data object to type 'to'. It can return also the value
* it can return also the value wrapped in a Promise (returned in resolve) or it can be async function. * wrapped in a Promise (returned in resolve) or it can be async function.
* @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7. * @param {Number} [costPower=0] positive cost class of the conversion, smaller or equal than 7.
* Should reflect the actual cost of the conversion: * Should reflect the actual cost of the conversion:
* - if nothing must be done and only reference is retrieved (or a constant operation done), * - if nothing must be done and only reference is retrieved (or a constant operation done),
@ -298,7 +319,7 @@ $.DataTypeConvertor = class {
* for example, textures loaded to GPU have to be also manually removed when not needed anymore. * for example, textures loaded to GPU have to be also manually removed when not needed anymore.
* Needs to be defined only when the created object has extra deletion process. * Needs to be defined only when the created object has extra deletion process.
* @param {string} type * @param {string} type
* @param {OpenSeadragon.TypeConvertor} callback destructor, receives the object created, * @param {OpenSeadragon.TypeDestructor} callback destructor, receives the object created,
* it is basically a type conversion to 'undefined' - thus the type. * it is basically a type conversion to 'undefined' - thus the type.
*/ */
learnDestroy(type, callback) { learnDestroy(type, callback) {
@ -312,12 +333,13 @@ $.DataTypeConvertor = class {
* Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion). * Note: conversion DOES NOT COPY data if [to] contains type 'from' (e.g., the cheapest conversion is no conversion).
* It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these * It automatically calls destructor on immediate types, but NOT on the x and the result. You should call these
* manually if these should be destroyed. * manually if these should be destroyed.
* @param {*} x data item to convert * @param {OpenSeadragon.Tile} tile
* @param {any} data data item to convert
* @param {string} from data item type * @param {string} from data item type
* @param {string} to desired type(s) * @param {string} to desired type(s)
* @return {OpenSeadragon.Promise<?>} promise resolution with type 'to' or undefined if the conversion failed * @return {OpenSeadragon.Promise<?>} promise resolution with type 'to' or undefined if the conversion failed
*/ */
convert(x, from, ...to) { convert(tile, data, from, ...to) {
const conversionPath = this.getConversionPath(from, to); const conversionPath = this.getConversionPath(from, to);
if (!conversionPath) { if (!conversionPath) {
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`); $.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
@ -331,7 +353,7 @@ $.DataTypeConvertor = class {
return $.Promise.resolve(x); return $.Promise.resolve(x);
} }
let edge = conversionPath[i]; let edge = conversionPath[i];
let y = edge.transform(x); let y = edge.transform(tile, x);
if (!y) { if (!y) {
$.console.warn(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting to %s)`, edge.target); $.console.warn(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting to %s)`, edge.target);
return $.Promise.resolve(); return $.Promise.resolve();
@ -344,19 +366,20 @@ $.DataTypeConvertor = class {
return result.then(res => step(res, i + 1)); return result.then(res => step(res, i + 1));
}; };
//destroy only mid-results, but not the original value //destroy only mid-results, but not the original value
return step(x, 0, false); return step(data, 0, false);
} }
/** /**
* Destroy the data item given. * Destroy the data item given.
* @param {OpenSeadragon.Tile} tile
* @param {any} data data item to convert
* @param {string} type data type * @param {string} type data type
* @param {?} data
* @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor * @return {OpenSeadragon.Promise<?>|undefined} promise resolution with data passed from constructor
*/ */
copy(data, type) { copy(tile, data, type) {
const copyTransform = this.copyings[type]; const copyTransform = this.copyings[type];
if (copyTransform) { if (copyTransform) {
const y = copyTransform(data); const y = copyTransform(tile, data);
return $.type(y) === "promise" ? y : $.Promise.resolve(y); return $.type(y) === "promise" ? y : $.Promise.resolve(y);
} }
$.console.warn(`[OpenSeadragon.convertor.copy] is not supported with type %s`, type); $.console.warn(`[OpenSeadragon.convertor.copy] is not supported with type %s`, type);
@ -399,7 +422,7 @@ $.DataTypeConvertor = class {
} }
if (Array.isArray(to)) { if (Array.isArray(to)) {
$.console.assert(typeof to === "string" || to.length > 0, "[getConversionPath] conversion 'to' type must be defined."); $.console.assert(to.length > 0, "[getConversionPath] conversion 'to' type must be defined.");
let bestCost = Infinity; let bestCost = Infinity;
//FIXME: pre-compute all paths in 'to' array? could be efficient for multiple //FIXME: pre-compute all paths in 'to' array? could be efficient for multiple

View File

@ -77,7 +77,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
this.container.style.textAlign = "left"; this.container.style.textAlign = "left";
this.container.appendChild( this.canvas ); this.container.appendChild( this.canvas );
this._checkForAPIOverrides(); this._checkInterfaceImplementation();
} }
// protect the canvas member with a getter // protect the canvas member with a getter
@ -98,6 +98,54 @@ OpenSeadragon.DrawerBase = class DrawerBase{
return undefined; return undefined;
} }
/**
* Define which data types are compatible for this drawer to work with.
* See default type list in OpenSeadragon.DataTypeConvertor
* @param formats
*/
declareSupportedDataFormats(...formats) {
this._formats = formats;
}
/**
* Retrieve data types
* @return {[string]}
*/
getSupportedDataFormats() {
if (!this._formats || this._formats.length < 1) {
$.console.error("A drawer must define its supported rendering data types using declareSupportedDataFormats!");
}
return this._formats;
}
/**
* Check a particular cache record is compatible.
* This function _MUST_ be called: if it returns a falsey
* value, the rendering _MUST NOT_ proceed. It should
* await next animation frames and check again for availability.
* @param {OpenSeadragon.Tile} tile
*/
getCompatibleData(tile) {
const cache = tile.getCache(tile.cacheKey);
if (!cache) {
return null;
}
const formats = this.getSupportedDataFormats();
if (!formats.includes(cache.type)) {
cache.transformTo(formats.length > 1 ? formats : formats[0]);
return false; // type is NOT compatible
}
// Cache in the process of loading, no-op
if (!cache.loaded) {
return false; // cache is NOT ready
}
// Ensured compatible
return cache.data;
}
/** /**
* @abstract * @abstract
* @returns {Boolean} Whether the drawer implementation is supported by the browser. Must be overridden by extending classes. * @returns {Boolean} Whether the drawer implementation is supported by the browser. Must be overridden by extending classes.
@ -146,8 +194,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
*/ */
minimumOverlapRequired() { minimumOverlapRequired() {
return false; return false;
} }
/** /**
* @abstract * @abstract
@ -182,7 +229,7 @@ OpenSeadragon.DrawerBase = class DrawerBase{
* @private * @private
* *
*/ */
_checkForAPIOverrides(){ _checkInterfaceImplementation(){
if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){ if(this._createDrawingElement === $.DrawerBase.prototype._createDrawingElement){
throw(new Error("[drawer]._createDrawingElement must be implemented by child class")); throw(new Error("[drawer]._createDrawingElement must be implemented by child class"));
} }

View File

@ -51,6 +51,8 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){ constructor(options){
super(options); super(options);
this.declareSupportedDataFormats("image");
/** /**
* The HTML element (div) that this drawer uses for drawing * The HTML element (div) that this drawer uses for drawing
* @member {Element} canvas * @member {Element} canvas
@ -210,7 +212,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
// content during animation of the container size. // content during animation of the container size.
if ( !tile.element ) { if ( !tile.element ) {
var image = tile.getImage(); const image = this.getCompatibleData(tile);
if (!image) { if (!image) {
return; return;
} }

View File

@ -510,7 +510,7 @@ $.Tile.prototype = {
} }
if (!type) { if (!type) {
if (this.tiledImage && !this.tiledImage.__typeWarningReported) { if (!this.tiledImage.__typeWarningReported) {
$.console.warn(this, "[Tile.setCache] called without type specification. " + $.console.warn(this, "[Tile.setCache] called without type specification. " +
"Automated deduction is potentially unsafe: prefer specification of data type explicitly."); "Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
this.tiledImage.__typeWarningReported = true; this.tiledImage.__typeWarningReported = true;
@ -520,10 +520,11 @@ $.Tile.prototype = {
const writesToRenderingCache = key === this.cacheKey; const writesToRenderingCache = key === this.cacheKey;
if (writesToRenderingCache && _safely) { if (writesToRenderingCache && _safely) {
//todo after-merge-aiosa decide dynamically // Need to get the supported type for rendering out of the active drawer.
const conversion = $.convertor.getConversionPath(type, "context2d"); const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(type, supportedTypes);
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" + $.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" +
"to render. Make sure OpenSeadragon.convertor was taught to convert type: " + type); "to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type);
} }
if (!this.__cutoff) { if (!this.__cutoff) {

View File

@ -136,8 +136,9 @@
* @returns {OpenSeadragon.Promise<?>} desired data type in promise, undefined if the cache was destroyed * @returns {OpenSeadragon.Promise<?>} desired data type in promise, undefined if the cache was destroyed
*/ */
getDataAs(type = this._type, copy = true) { getDataAs(type = this._type, copy = true) {
const referenceTile = this._tiles[0];
if (this.loaded && type === this._type) { if (this.loaded && type === this._type) {
return copy ? $.convertor.copy(this._data, type) : this._promise; return copy ? $.convertor.copy(referenceTile, this._data, type) : this._promise;
} }
return this._promise.then(data => { return this._promise.then(data => {
@ -146,10 +147,10 @@
return undefined; return undefined;
} }
if (type !== this._type) { if (type !== this._type) {
return $.convertor.convert(data, this._type, type); return $.convertor.convert(referenceTile, data, this._type, type);
} }
if (copy) { //convert does not copy data if same type, do explicitly if (copy) { //convert does not copy data if same type, do explicitly
return $.convertor.copy(data, type); return $.convertor.copy(referenceTile, data, type);
} }
return data; return data;
}); });
@ -158,11 +159,15 @@
/** /**
* Transform cache to desired type and get the data after conversion. * Transform cache to desired type and get the data after conversion.
* Does nothing if the type equals to the current type. Asynchronous. * Does nothing if the type equals to the current type. Asynchronous.
* @param {string} type * @param {string|[string]} type if array provided, the system will
* try to optimize for the best type to convert to.
* @return {OpenSeadragon.Promise<?>|*} * @return {OpenSeadragon.Promise<?>|*}
*/ */
transformTo(type = this._type) { transformTo(type = this._type) {
if (!this.loaded || type !== this._type) { if (!this.loaded ||
type !== this._type ||
(Array.isArray(type) && !type.includes(this._type))) {
if (!this.loaded) { if (!this.loaded) {
this._conversionJobQueue = this._conversionJobQueue || []; this._conversionJobQueue = this._conversionJobQueue || [];
let resolver = null; let resolver = null;
@ -173,7 +178,8 @@
if (this._destroyed) { if (this._destroyed) {
return; return;
} }
if (type !== this._type) { //must re-check types since we perform in a queue of conversion requests
if (type !== this._type || (Array.isArray(type) && !type.includes(this._type))) {
//ensures queue gets executed after finish //ensures queue gets executed after finish
this._convert(this._type, type); this._convert(this._type, type);
this._promise.then(data => resolver(data)); this._promise.then(data => resolver(data));
@ -351,10 +357,13 @@
/** /**
* Private conversion that makes sure the cache knows its data is ready * Private conversion that makes sure the cache knows its data is ready
* @param to array or a string - allowed types
* @param from string - type origin
* @private * @private
*/ */
_convert(from, to) { _convert(from, to) {
const convertor = $.convertor, const convertor = $.convertor,
referenceTile = this._tiles[0],
conversionPath = convertor.getConversionPath(from, to); conversionPath = convertor.getConversionPath(from, to);
if (!conversionPath) { if (!conversionPath) {
$.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`); $.console.error(`[OpenSeadragon.convertor.convert] Conversion conversion ${from} ---> ${to} cannot be done!`);
@ -372,7 +381,7 @@
return $.Promise.resolve(x); return $.Promise.resolve(x);
} }
let edge = conversionPath[i]; let edge = conversionPath[i];
return $.Promise.resolve(edge.transform(x)).then( return $.Promise.resolve(edge.transform(referenceTile, x)).then(
y => { y => {
if (!y) { if (!y) {
$.console.error(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting using %s)`, edge); $.console.error(`[OpenSeadragon.convertor.convert] data mid result falsey value (while converting using %s)`, edge);
@ -391,7 +400,8 @@
this.loaded = false; this.loaded = false;
this._data = undefined; this._data = undefined;
this._type = to; // Read target type from the conversion path: [edge.target] = Vertex, its value=type
this._type = conversionPath[stepCount - 1].target.value;
this._promise = convert(originalData, 0); this._promise = convert(originalData, 0);
} }
}; };
@ -657,6 +667,11 @@
this._tilesLoaded.splice( deleteAtIndex, 1 ); this._tilesLoaded.splice( deleteAtIndex, 1 );
} }
// Possible error: it can happen that unloaded tile gets to this stage. Should it even be allowed to happen?
if (!tile.loaded) {
return;
}
const tiledImage = tile.tiledImage; const tiledImage = tile.tiledImage;
tile.unload(); tile.unload();

View File

@ -2126,14 +2126,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
); );
//make sure cache data is ready for drawing, if not, request the desired format //make sure cache data is ready for drawing, if not, request the desired format
const cache = tile.getCache(tile.cacheKey), const cache = tile.getCache(tile.cacheKey),
// TODO: after-merge-aiosa dynamic type declaration from the drawer base class interface requiredTypes = _this.viewer.drawer.getSupportedDataFormats();
requiredType = _this._drawer.useCanvas ? "context2d" : "image";
if (!cache) { if (!cache) {
$.console.warn("Tile %s not cached at the end of tile-loaded event: tile will not be drawn - it has no data!", tile); $.console.warn("Tile %s not cached at the end of tile-loaded event: tile will not be drawn - it has no data!", tile);
resolver(tile); resolver(tile);
} else if (cache.type !== requiredType) { } else if (!requiredTypes.includes(cache.type)) {
//initiate conversion as soon as possible if incompatible with the drawer //initiate conversion as soon as possible if incompatible with the drawer
cache.transformTo(requiredType).then(_ => { cache.transformTo(requiredTypes).then(_ => {
tile.loading = false; tile.loading = false;
tile.loaded = true; tile.loaded = true;
resolver(tile); resolver(tile);

View File

@ -379,7 +379,9 @@ $.Viewer = function( options ) {
//update tiles //update tiles
item.opacity = 0; //prevent draw item.opacity = 0; //prevent draw
item.maxTilesPerFrame = 50; //todo based on image size and also number of images! item.maxTilesPerFrame = 50; //todo based on image size and also number of images!
item._updateViewport();
//TODO check if the method is used correctly
item._updateLevelsForViewport();
item._needsDraw = true; //we did not draw item._needsDraw = true; //we did not draw
item.opacity = origOpacity; item.opacity = origOpacity;
item.maxTilesPerFrame = origMaxTiles; item.maxTilesPerFrame = origMaxTiles;

View File

@ -40,23 +40,23 @@
/** /**
* @class OpenSeadragon.WebGLDrawer * @class OpenSeadragon.WebGLDrawer
* @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer * @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. The WebGLDrawer
* loads tile data as textures to the graphics card as soon as it is available (via the tile-ready event), * defines its own data type that ensures textures are correctly loaded to and deleted from the GPU memory.
* and unloads the data (via the image-unloaded event). The drawer utilizes a context-dependent two pass drawing pipeline. * The drawer utilizes a context-dependent two pass drawing pipeline. For the first pass, tile composition
* For the first pass, tile composition for a given TiledImage is always done using a canvas with a WebGL context. * for a given TiledImage is always done using a canvas with a WebGL context. This allows tiles to be stitched
* This allows tiles to be stitched together without seams or artifacts, without requiring a tile source with overlap. If overlap is present, * together without seams or artifacts, without requiring a tile source with overlap. If overlap is present,
* overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output canvas * overlapping pixels are discarded. The second pass copies all pixel data from the WebGL context onto an output
* with a Context2d context. This allows applications to have access to pixel data and other functionality provided by * canvas with a Context2d context. This allows applications to have access to pixel data and other functionality
* Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options, including compositeOperation, * provided by Context2d, regardless of whether the CanvasDrawer or the WebGLDrawer is used. Certain options,
* clip, croppingPolygons, and debugMode are implemented using Context2d operations; in these scenarios, each TiledImage is * including compositeOperation, clip, croppingPolygons, and debugMode are implemented using Context2d operations;
* drawn onto the output canvas immediately after the tile composition step (pass 1). Otherwise, for efficiency, all TiledImages * in these scenarios, each TiledImage is drawn onto the output canvas immediately after the tile composition step
* are copied over to the output canvas at once, after all tiles have been composited for all images. * (pass 1). Otherwise, for efficiency, all TiledImages are copied over to the output canvas at once, after all
* tiles have been composited for all images.
* @param {Object} options - Options for this Drawer. * @param {Object} options - Options for this Drawer.
* @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer.
* @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport.
* @param {Element} options.element - Parent element. * @param {Element} options.element - Parent element.
* @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details.
*/ */
OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{ OpenSeadragon.WebGLDrawer = class WebGLDrawer extends OpenSeadragon.DrawerBase{
constructor(options){ constructor(options){
super(options); super(options);
@ -76,24 +76,20 @@
// private members // private members
this._destroyed = false; this._destroyed = false;
this._TextureMap = new Map();
this._TileMap = new Map();
this._gl = null; this._gl = null;
this._firstPass = null; this._firstPass = null;
this._secondPass = null; this._secondPass = null;
this._glFrameBuffer = null; this._glFrameBuffer = null;
this._renderToTexture = null; this._renderToTexture = null;
this._glFramebufferToCanvasTransform = null;
this._outputCanvas = null; this._outputCanvas = null;
this._outputContext = null; this._outputContext = null;
this._clippingCanvas = null; this._clippingCanvas = null;
this._clippingContext = null; this._clippingContext = null;
this._renderingCanvas = null; this._renderingCanvas = null;
// Add listeners for events that require modifying the scene or camera // Unique type per drawer: uploads texture to unique webgl context.
this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev)); this._dataType = `${Date.now()}_TEX_2D`;
this.viewer.addHandler("image-unloaded", ev => this._imageUnloadedHandler(ev)); this._setupTextureHandlers(this._dataType);
// Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire // Reject listening for the tile-drawing and tile-drawn events, which this drawer does not fire
this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event"); this.viewer.rejectEventHandler("tile-drawn", "The WebGLDrawer does not raise the tile-drawn event");
@ -132,11 +128,6 @@
gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
let canvases = Array.from(this._TextureMap.keys());
canvases.forEach(canvas => {
this._cleanupImageData(canvas); // deletes texture, removes from _TextureMap
});
// Delete all our created resources // Delete all our created resources
gl.deleteBuffer(this._secondPass.bufferOutputPosition); gl.deleteBuffer(this._secondPass.bufferOutputPosition);
gl.deleteFramebuffer(this._glFrameBuffer); gl.deleteFramebuffer(this._glFrameBuffer);
@ -316,15 +307,21 @@
let tile = tilesToDraw[tileIndex].tile; let tile = tilesToDraw[tileIndex].tile;
let indexInDrawArray = tileIndex % maxTextures; let indexInDrawArray = tileIndex % maxTextures;
let numTilesToDraw = indexInDrawArray + 1; let numTilesToDraw = indexInDrawArray + 1;
let tileContext = tile.getCanvasContext();
let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null; if ( !tile.loaded ) {
if(textureInfo){ $.console.warn(
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray); "Attempting to draw tile %s when it's not yet loaded.",
} else { tile.toString()
// console.log('No tile info', tile); );
return;
} }
if( (numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){ const textureInfo = this.getCompatibleData(tile);
if (!textureInfo) {
return;
}
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
if ((numTilesToDraw === maxTextures) || (tileIndex === tilesToDraw.length - 1)){
// We've filled up the buffers: time to draw this set of tiles // We've filled up the buffers: time to draw this set of tiles
// bind each tile's texture to the appropriate gl.TEXTURE# // bind each tile's texture to the appropriate gl.TEXTURE#
@ -786,27 +783,12 @@
}); });
} }
// private _setupTextureHandlers(thisType) {
_makeQuadVertexBuffer(left, right, top, bottom){ const tex2DCompatibleLoader = (tile, data) => {
return new Float32Array([ let tiledImage = tile.tiledImage;
left, bottom, //todo verify we are calling conversion just right amount of time!
right, bottom, // e.g. no upload of cpu-existing texture
left, top,
left, top,
right, bottom,
right, top]);
}
// private
_tileReadyHandler(event){
let tile = event.tile;
let tiledImage = event.tiledImage;
let tileContext = tile.getCanvasContext();
let canvas = tileContext.canvas;
let textureInfo = this._TextureMap.get(canvas);
// if this is a new image for us, create a texture
if(!textureInfo){
let gl = this._gl; let gl = this._gl;
// create a gl Texture for this tile and bind the canvas with the image data // create a gl Texture for this tile and bind the canvas with the image data
@ -828,13 +810,6 @@
position = this._unitQuad; position = this._unitQuad;
} }
let textureInfo = {
texture: texture,
position: position,
};
// add it to our _TextureMap
this._TextureMap.set(canvas, textureInfo);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture); gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image. // Set the parameters so we can render any size image.
@ -843,11 +818,55 @@
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Upload the image into the texture. try{
this._uploadImageData(tileContext); // This depends on gl.TEXTURE_2D being bound to the texture
// associated with this canvas before calling this function
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
} catch (e){
$.console.error('Error uploading image data to WebGL', e);
}
} // TextureInfo stored in the cache
return {
texture: texture,
position: position,
cpuData: data,
};
};
const tex2DCompatibleDestructor = textureInfo => {
if (textureInfo) {
this._gl.deleteTexture(textureInfo.texture);
}
};
const dataRetrieval = (tile, data) => {
return data.cpuData;
};
// Differentiate type also based on type used to upload data: we can support bidirectional conversion.
const c2dTexType = thisType + ":context2d",
imageTexType = thisType + ":image";
this.declareSupportedDataFormats(imageTexType, c2dTexType);
// We should be OK uploading any of these types.
$.convertor.learn("context2d", c2dTexType, tex2DCompatibleLoader, 1, 2);
$.convertor.learn("image", imageTexType, tex2DCompatibleLoader, 1, 2);
$.convertor.learn(c2dTexType, "context2d", dataRetrieval, 1, 2);
$.convertor.learn(imageTexType, "image", dataRetrieval, 1, 2);
$.convertor.learnDestroy(c2dTexType, tex2DCompatibleDestructor);
$.convertor.learnDestroy(imageTexType, tex2DCompatibleDestructor);
}
// private
_makeQuadVertexBuffer(left, right, top, bottom){
return new Float32Array([
left, bottom,
right, bottom,
left, top,
left, top,
right, bottom,
right, top]);
} }
// private // private
@ -865,43 +884,6 @@
}; };
} }
// private
_uploadImageData(tileContext){
let gl = this._gl;
let canvas = tileContext.canvas;
try{
if(!canvas){
throw('Tile context does not have a canvas', tileContext);
}
// This depends on gl.TEXTURE_2D being bound to the texture
// associated with this canvas before calling this function
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
} catch (e){
$.console.error('Error uploading image data to WebGL', e);
}
}
// private
_imageUnloadedHandler(event){
let canvas = event.context2D.canvas;
this._cleanupImageData(canvas);
}
// private
_cleanupImageData(tileCanvas){
let textureInfo = this._TextureMap.get(tileCanvas);
//remove from the map
this._TextureMap.delete(tileCanvas);
//release the texture from the GPU
if(textureInfo){
this._gl.deleteTexture(textureInfo.texture);
}
}
// private // private
_setClip(rect){ _setClip(rect){
this._clippingContext.beginPath(); this._clippingContext.beginPath();
@ -1133,9 +1115,7 @@
return shaderProgram; return shaderProgram;
} }
}; };
}( OpenSeadragon )); }( OpenSeadragon ));

View File

@ -332,9 +332,10 @@
} ] } ]
} ); } );
viewer.addOnceHandler('tiled-image-drawn', function(event) { viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas), event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
"Canvas should be tainted."); assert.ok(OpenSeadragon.isCanvasTainted(context.canvas),
done(); "Canvas should be tainted.")
).then(done);
}); });
} ); } );
@ -352,9 +353,10 @@
} ] } ]
} ); } );
viewer.addOnceHandler('tiled-image-drawn', function(event) { viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(!OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas), event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
"Canvas should not be tainted."); assert.notOk(OpenSeadragon.isCanvasTainted(context.canvas),
done(); "Canvas should be tainted.")
).then(done);
}); });
} ); } );
@ -376,9 +378,10 @@
crossOriginPolicy : false crossOriginPolicy : false
} ); } );
viewer.addOnceHandler('tiled-image-drawn', function(event) { viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas), event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
"Canvas should be tainted."); assert.ok(OpenSeadragon.isCanvasTainted(context.canvas),
done(); "Canvas should be tainted.")
).then(done);
}); });
} ); } );
@ -400,9 +403,10 @@
} }
} ); } );
viewer.addOnceHandler('tiled-image-drawn', function(event) { viewer.addOnceHandler('tiled-image-drawn', function(event) {
assert.ok(!OpenSeadragon.isCanvasTainted(event.tiles[0].getCanvasContext().canvas), event.tiles[0].getCache().getDataAs("context2d", false).then(context =>
"Canvas should not be tainted."); assert.notOk(OpenSeadragon.isCanvasTainted(context.canvas),
done(); "Canvas should be tainted.")
).then(done);
}); });
} ); } );

View File

@ -33,46 +33,46 @@
// other tests will interfere // other tests will interfere
let typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0; let typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
//set all same costs to get easy testing, know which path will be taken //set all same costs to get easy testing, know which path will be taken
Convertor.learn(T_A, T_B, x => { Convertor.learn(T_A, T_B, (tile, x) => {
typeAtoB++; typeAtoB++;
return x+1; return x+1;
}); });
Convertor.learn(T_B, T_C, x => { Convertor.learn(T_B, T_C, (tile, x) => {
typeBtoC++; typeBtoC++;
return x+1; return x+1;
}); });
Convertor.learn(T_C, T_A, x => { Convertor.learn(T_C, T_A, (tile, x) => {
typeCtoA++; typeCtoA++;
return x+1; return x+1;
}); });
Convertor.learn(T_D, T_A, x => { Convertor.learn(T_D, T_A, (tile, x) => {
typeDtoA++; typeDtoA++;
return x+1; return x+1;
}); });
Convertor.learn(T_C, T_E, x => { Convertor.learn(T_C, T_E, (tile, x) => {
typeCtoE++; typeCtoE++;
return x+1; return x+1;
}); });
//'Copy constructors' //'Copy constructors'
let copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0; let copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
//also learn destructors //also learn destructors
Convertor.learn(T_A, T_A,x => { Convertor.learn(T_A, T_A,(tile, x) => {
copyA++; copyA++;
return x+1; return x+1;
}); });
Convertor.learn(T_B, T_B,x => { Convertor.learn(T_B, T_B,(tile, x) => {
copyB++; copyB++;
return x+1; return x+1;
}); });
Convertor.learn(T_C, T_C,x => { Convertor.learn(T_C, T_C,(tile, x) => {
copyC++; copyC++;
return x-1; return x-1;
}); });
Convertor.learn(T_D, T_D,x => { Convertor.learn(T_D, T_D,(tile, x) => {
copyD++; copyD++;
return x+1; return x+1;
}); });
Convertor.learn(T_E, T_E,x => { Convertor.learn(T_E, T_E,(tile, x) => {
copyE++; copyE++;
return x+1; return x+1;
}); });

View File

@ -31,23 +31,23 @@
let imageToCanvas = 0, srcToImage = 0, context2DtoImage = 0, canvasToContext2D = 0, imageToUrl = 0, let imageToCanvas = 0, srcToImage = 0, context2DtoImage = 0, canvasToContext2D = 0, imageToUrl = 0,
canvasToUrl = 0; canvasToUrl = 0;
//set all same costs to get easy testing, know which path will be taken //set all same costs to get easy testing, know which path will be taken
Convertor.learn("__TEST__canvas", "__TEST__url", canvas => { Convertor.learn("__TEST__canvas", "__TEST__url", (tile, canvas) => {
canvasToUrl++; canvasToUrl++;
return canvas.toDataURL(); return canvas.toDataURL();
}, 1, 1); }, 1, 1);
Convertor.learn("__TEST__image", "__TEST__url", image => { Convertor.learn("__TEST__image", "__TEST__url", (tile,image) => {
imageToUrl++; imageToUrl++;
return image.url; return image.url;
}, 1, 1); }, 1, 1);
Convertor.learn("__TEST__canvas", "__TEST__context2d", canvas => { Convertor.learn("__TEST__canvas", "__TEST__context2d", (tile,canvas) => {
canvasToContext2D++; canvasToContext2D++;
return canvas.getContext("2d"); return canvas.getContext("2d");
}, 1, 1); }, 1, 1);
Convertor.learn("__TEST__context2d", "__TEST__canvas", context2D => { Convertor.learn("__TEST__context2d", "__TEST__canvas", (tile,context2D) => {
context2DtoImage++; context2DtoImage++;
return context2D.canvas; return context2D.canvas;
}, 1, 1); }, 1, 1);
Convertor.learn("__TEST__image", "__TEST__canvas", image => { Convertor.learn("__TEST__image", "__TEST__canvas", (tile,image) => {
imageToCanvas++; imageToCanvas++;
const canvas = document.createElement( 'canvas' ); const canvas = document.createElement( 'canvas' );
canvas.width = image.width; canvas.width = image.width;
@ -56,7 +56,7 @@
context.drawImage( image, 0, 0 ); context.drawImage( image, 0, 0 );
return canvas; return canvas;
}, 1, 1); }, 1, 1);
Convertor.learn("__TEST__url", "__TEST__image", url => { Convertor.learn("__TEST__url", "__TEST__image", (tile, url) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
srcToImage++; srcToImage++;
const img = new Image(); const img = new Image();
@ -68,7 +68,8 @@
let canvasDestroy = 0, imageDestroy = 0, contex2DDestroy = 0, urlDestroy = 0; let canvasDestroy = 0, imageDestroy = 0, contex2DDestroy = 0, urlDestroy = 0;
//also learn destructors //also learn destructors
Convertor.learnDestroy("__TEST__canvas", () => { Convertor.learnDestroy("__TEST__canvas", canvas => {
canvas.width = canvas.height = 0;
canvasDestroy++; canvasDestroy++;
}); });
Convertor.learnDestroy("__TEST__image", () => { Convertor.learnDestroy("__TEST__image", () => {
@ -145,20 +146,20 @@
context.drawImage( image, 0, 0 ); context.drawImage( image, 0, 0 );
//copy URL //copy URL
const URL2 = await Convertor.copy(URL, "url"); const URL2 = await Convertor.copy(null, URL, "url");
//we cannot check if they are not the same object, strings are immutable (and we don't copy anyway :D ) //we cannot check if they are not the same object, strings are immutable (and we don't copy anyway :D )
test.equal(URL, URL2, "String copy is equal in data."); test.equal(URL, URL2, "String copy is equal in data.");
test.equal(typeof URL, typeof URL2, "Type of copies equals."); test.equal(typeof URL, typeof URL2, "Type of copies equals.");
test.equal(URL.length, URL2.length, "Data length is also equal."); test.equal(URL.length, URL2.length, "Data length is also equal.");
//copy context //copy context
const context2 = await Convertor.copy(context, "context2d"); const context2 = await Convertor.copy(null, context, "context2d");
test.notEqual(context, context2, "Copy is not the same as original canvas."); test.notEqual(context, context2, "Copy is not the same as original canvas.");
test.equal(typeof context, typeof context2, "Type of copies equals."); test.equal(typeof context, typeof context2, "Type of copies equals.");
test.equal(context.canvas.toDataURL(), context2.canvas.toDataURL(), "Data is equal."); test.equal(context.canvas.toDataURL(), context2.canvas.toDataURL(), "Data is equal.");
//copy image //copy image
const image2 = await Convertor.copy(image, "image"); const image2 = await Convertor.copy(null, image, "image");
test.notEqual(image, image2, "Copy is not the same as original image."); test.notEqual(image, image2, "Copy is not the same as original image.");
test.equal(typeof image, typeof image2, "Type of copies equals."); test.equal(typeof image, typeof image2, "Type of copies equals.");
test.equal(image.src, image2.src, "Data is equal."); test.equal(image.src, image2.src, "Data is equal.");
@ -173,7 +174,7 @@
const done = test.async(); const done = test.async();
//load image object: url -> image //load image object: url -> image
Convertor.convert("/test/data/A.png", "__TEST__url", "__TEST__image").then(i => { Convertor.convert(null, "/test/data/A.png", "__TEST__url", "__TEST__image").then(i => {
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion."); test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
test.equal(srcToImage, 1, "Conversion happened."); test.equal(srcToImage, 1, "Conversion happened.");
@ -182,14 +183,14 @@
test.equal(urlDestroy, 1, "Url destructor called."); test.equal(urlDestroy, 1, "Url destructor called.");
test.equal(imageDestroy, 0, "Image destructor not called."); test.equal(imageDestroy, 0, "Image destructor not called.");
return Convertor.convert(i, "__TEST__image", "__TEST__canvas"); return Convertor.convert(null, i, "__TEST__image", "__TEST__canvas");
}).then(c => { //path image -> canvas }).then(c => { //path image -> canvas
test.equal(OpenSeadragon.type(c), "canvas", "Got canvas object after conversion."); test.equal(OpenSeadragon.type(c), "canvas", "Got canvas object after conversion.");
test.equal(srcToImage, 1, "Conversion ulr->image did not happen."); test.equal(srcToImage, 1, "Conversion ulr->image did not happen.");
test.equal(imageToCanvas, 1, "Conversion image->canvas happened."); test.equal(imageToCanvas, 1, "Conversion image->canvas happened.");
test.equal(urlDestroy, 1, "Url destructor not called."); test.equal(urlDestroy, 1, "Url destructor not called.");
test.equal(imageDestroy, 0, "Image destructor not called unless we ask it."); test.equal(imageDestroy, 0, "Image destructor not called unless we ask it.");
return Convertor.convert(c, "__TEST__canvas", "__TEST__image"); return Convertor.convert(null, c, "__TEST__canvas", "__TEST__image");
}).then(i => { //path canvas, image: canvas -> url -> image }).then(i => { //path canvas, image: canvas -> url -> image
test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion."); test.equal(OpenSeadragon.type(i), "image", "Got image object after conversion.");
test.equal(srcToImage, 2, "Conversion ulr->image happened."); test.equal(srcToImage, 2, "Conversion ulr->image happened.");
@ -314,7 +315,7 @@
const done = test.async(); const done = test.async();
let conversionHappened = false; let conversionHappened = false;
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", value => { Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", (tile, value) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
conversionHappened = true; conversionHappened = true;
@ -358,7 +359,7 @@
const done = test.async(); const done = test.async();
let conversionHappened = false; let conversionHappened = false;
Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", value => { Convertor.learn("__TEST__url", "__TEST__longConversionProcessForTesting", (tile, value) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
conversionHappened = true; conversionHappened = true;