Merge pull request #2472 from pearcetm/canvas-fallback

In webgl drawer, fall back to canvas drawer for tiled images with tainted data
This commit is contained in:
Ian Gilman 2024-02-27 09:33:06 -08:00 committed by GitHub
commit 01e70ab7d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 352 additions and 175 deletions

View File

@ -139,6 +139,7 @@ class CanvasDrawer extends OpenSeadragon.DrawerBase{
this.canvas.height = 1;
this.sketchCanvas = null;
this.sketchContext = null;
this.container.removeChild(this.canvas);
}
/**

View File

@ -280,6 +280,37 @@ OpenSeadragon.DrawerBase = class DrawerBase{
});
}
/**
* Called by implementations to fire the drawer-error event
* @private
*/
_raiseDrawerErrorEvent(tiledImage, errorMessage){
if(!this.viewer) {
return;
}
/**
* Raised when a tiled image is drawn to the canvas. Used internally for testing.
* The update-viewport event is preferred if you want to know when a frame has been drawn.
*
* @event drawer-error
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
* @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn.
* @property {OpenSeadragon.DrawerBase} drawer - The drawer that raised the error.
* @property {String} error - A message describing the error.
* @property {?Object} userData - Arbitrary subscriber-defined object.
* @private
*/
this.viewer.raiseEvent( 'drawer-error', {
tiledImage: tiledImage,
drawer: this,
error: errorMessage,
});
}
};
}( OpenSeadragon ));

View File

@ -126,7 +126,7 @@ class HTMLDrawer extends OpenSeadragon.DrawerBase{
* Destroy the drawer (unload current loaded tiles)
*/
destroy() {
this.canvas.innerHTML = "";
this.container.removeChild(this.canvas);
}
/**

View File

@ -166,6 +166,7 @@ $.TiledImage = function( options ) {
_lastDrawn: [], // array of tiles that were last fetched by the drawer
_isBlending: false, // Are any tiles still being blended?
_wasBlending: false, // Were any tiles blending before the last draw?
_isTainted: false, // Has a Tile been found with tainted data?
//configurable settings
springStiffness: $.DEFAULT_SETTINGS.springStiffness,
animationTime: $.DEFAULT_SETTINGS.animationTime,
@ -326,6 +327,25 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
return this._needsDraw;
},
/**
* Set the internal _isTainted flag for this TiledImage. Lazy loaded - not
* checked each time a Tile is loaded, but can be set if a consumer of the
* tiles (e.g. a Drawer) discovers a Tile to have tainted data so that further
* checks are not needed and alternative rendering strategies can be used.
* @private
*/
setTainted(isTainted){
this._isTainted = isTainted;
},
/**
* @private
* @returns {Boolean} whether the TiledImage has been marked as tainted
*/
isTainted(){
return this._isTainted;
},
/**
* Destroy the TiledImage (unload current loaded tiles).
*/

View File

@ -455,35 +455,13 @@ $.Viewer = function( options ) {
this.drawer = null;
for (let i = 0; i < drawerCandidates.length; i++) {
let drawerCandidate = drawerCandidates[i];
let Drawer = null;
//if inherits from a drawer base, use it
if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
Drawer = drawerCandidate;
drawerCandidate = 'custom';
} else if (typeof drawerCandidate === "string") {
Drawer = $.determineDrawer(drawerCandidate);
} else {
$.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
continue;
}
// if the drawer is supported, create it and break the loop
if (Drawer && Drawer.isSupported()) {
this.drawer = new Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor,
options: this.drawerOptions[drawerCandidate],
});
for (const drawerCandidate of drawerCandidates){
let success = this.requestDrawer(drawerCandidate, {mainDrawer: true, redrawImmediately: false});
if(success){
break;
}
}
if (!this.drawer){
$.console.error('No drawer could be created!');
throw('Error with creating the selected drawer(s)');
@ -950,6 +928,73 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype,
this.removeAllHandlers();
},
/**
* Request a drawer for this viewer, as a supported string or drawer constructor.
* @param {String | OpenSeadragon.DrawerBase} drawerCandidate The type of drawer to try to construct.
* @param { Object } options
* @param { Boolean } [options.mainDrawer] Whether to use this as the viewer's main drawer. Default = true.
* @param { Boolean } [options.redrawImmediately] Whether to immediately draw a new frame. Only used if options.mainDrawer = true. Default = true.
* @param { Object } [options.drawerOptions] Options for this drawer. Defaults to viewer.drawerOptions.
* for this viewer type. See {@link OpenSeadragon.Options}.
* @returns {Object | Boolean} The drawer that was created, or false if the requested drawer is not supported
*/
requestDrawer(drawerCandidate, options){
const defaultOpts = {
mainDrawer: true,
redrawImmediately: true,
drawerOptions: null
};
options = $.extend(true, defaultOpts, options);
const mainDrawer = options.mainDrawer;
const redrawImmediately = options.redrawImmediately;
const drawerOptions = options.drawerOptions;
const oldDrawer = this.drawer;
let Drawer = null;
//if the candidate inherits from a drawer base, use it
if (drawerCandidate && drawerCandidate.prototype instanceof $.DrawerBase) {
Drawer = drawerCandidate;
drawerCandidate = 'custom';
} else if (typeof drawerCandidate === "string") {
Drawer = $.determineDrawer(drawerCandidate);
}
if(!Drawer){
$.console.warn('Unsupported drawer! Drawer must be an existing string type, or a class that extends OpenSeadragon.DrawerBase.');
}
// if the drawer is supported, create it and return true
if (Drawer && Drawer.isSupported()) {
// first destroy the previous drawer
if(oldDrawer && mainDrawer){
oldDrawer.destroy();
}
// create the new drawer
const newDrawer = new Drawer({
viewer: this,
viewport: this.viewport,
element: this.canvas,
debugGridColor: this.debugGridColor,
options: drawerOptions || this.drawerOptions[drawerCandidate],
});
if(mainDrawer){
this.drawer = newDrawer;
if(redrawImmediately){
this.forceRedraw();
}
}
return newDrawer;
}
return false;
},
/**
* @function
* @returns {Boolean}

View File

@ -90,10 +90,13 @@
this._clippingCanvas = null;
this._clippingContext = null;
this._renderingCanvas = null;
this._backupCanvasDrawer = null;
// Add listeners for events that require modifying the scene or camera
this.viewer.addHandler("tile-ready", ev => this._tileReadyHandler(ev));
this.viewer.addHandler("image-unloaded", ev => this._imageUnloadedHandler(ev));
this._boundToTileReady = ev => this._tileReadyHandler(ev);
this._boundToImageUnloaded = ev => this._imageUnloadedHandler(ev);
this.viewer.addHandler("tile-ready", this._boundToTileReady);
this.viewer.addHandler("image-unloaded", this._boundToImageUnloaded);
// 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");
@ -154,9 +157,23 @@
ext.loseContext();
}
// unbind our event listeners from the viewer
this.viewer.removeHandler("tile-ready", this._boundToTileReady);
this.viewer.removeHandler("image-unloaded", this._boundToImageUnloaded);
// set our webgl context reference to null to enable garbage collection
this._gl = null;
if(this._backupCanvasDrawer){
this._backupCanvasDrawer.destroy();
this._backupCanvasDrawer = null;
}
this.container.removeChild(this.canvas);
if(this.viewer.drawer === this){
this.viewer.drawer = null;
}
// set our destroyed flag to true
this._destroyed = true;
}
@ -206,6 +223,21 @@
return canvas;
}
/**
* Get the backup renderer (CanvasDrawer) to use if data cannot be used by webgl
* Lazy loaded
* @private
* @returns {CanvasDrawer}
*/
_getBackupCanvasDrawer(){
if(!this._backupCanvasDrawer){
this._backupCanvasDrawer = this.viewer.requestDrawer('canvas', {mainDrawer: false});
this._backupCanvasDrawer.canvas.style.setProperty('visibility', 'hidden');
}
return this._backupCanvasDrawer;
}
/**
*
* @param {Array} tiledImages Array of TiledImage objects to draw
@ -237,6 +269,22 @@
//iterate over tiled images and draw each one using a two-pass rendering pipeline if needed
tiledImages.forEach( (tiledImage, tiledImageIndex) => {
if(tiledImage.isTainted()){
// first, draw any data left in the rendering buffer onto the output canvas
if(renderingBufferHasImageData){
this._outputContext.drawImage(this._renderingCanvas, 0, 0);
// clear the buffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.clear(gl.COLOR_BUFFER_BIT); // clear the back buffer
renderingBufferHasImageData = false;
}
// next, use the backup canvas drawer to draw this tainted image
const canvasDrawer = this._getBackupCanvasDrawer();
canvasDrawer.draw([tiledImage]);
this._outputContext.drawImage(canvasDrawer.canvas, 0, 0);
} else {
let tilesToDraw = tiledImage.getTilesToDraw();
if ( tiledImage.placeholderFillStyle && tiledImage._hasOpaqueTile === false ) {
@ -323,6 +371,15 @@
let tileContext = tile.getCanvasContext();
let textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
if(!textureInfo){
// tile was not processed in the tile-ready event (this can happen
// if this drawer was created after the tile was downloaded)
this._tileReadyHandler({tile: tile, tiledImage: tiledImage});
// retry getting textureInfo
textureInfo = tileContext ? this._TextureMap.get(tileContext.canvas) : null;
}
if(textureInfo){
this._getTileData(tile, tiledImage, textureInfo, overallMatrix, indexInDrawArray, texturePositionArray, textureDataArray, matrixArray, opacityArray);
} else {
@ -403,6 +460,9 @@
if(tiledImageIndex === 0){
this._raiseTiledImageDrawnEvent(tiledImage, tilesToDraw.map(info=>info.tile));
}
}
});
@ -805,8 +865,28 @@
_tileReadyHandler(event){
let tile = event.tile;
let tiledImage = event.tiledImage;
// If a tiledImage is already known to be tainted, don't try to upload any
// textures to webgl, because they won't be used even if it succeeds
if(tiledImage.isTainted()){
return;
}
let tileContext = tile.getCanvasContext();
let canvas = tileContext.canvas;
let canvas = tileContext && tileContext.canvas;
// if the tile doesn't provide a canvas, or is tainted by cross-origin
// data, marked the TiledImage as tainted so the canvas drawer can be
// used instead, and return immediately - tainted data cannot be uploaded to webgl
if(!canvas || $.isCanvasTainted(canvas)){
const wasTainted = tiledImage.isTainted();
if(!wasTainted){
tiledImage.setTainted(true);
$.console.warn('WebGL cannot be used to draw this TiledImage because it has tainted data. Does crossOriginPolicy need to be set?');
this._raiseDrawerErrorEvent(tiledImage, 'Tainted data cannot be used by the WebGLDrawer. Falling back to CanvasDrawer for this TiledImage.');
}
return;
}
let textureInfo = this._TextureMap.get(canvas);
// if this is a new image for us, create a texture