Compare commits

...

16 Commits

Author SHA1 Message Date
Ian Gilman
0335f3f4ab Changelog for #2148 2022-06-28 14:15:35 -07:00
Ian Gilman
d1f46148a7 Merge branch 'master' of github.com:openseadragon/openseadragon 2022-06-28 14:13:21 -07:00
Ian Gilman
1d327d673e
Merge pull request #2148 from Aiosa/master
Delegate tile fetching and caching to the TileSource
2022-06-28 14:12:44 -07:00
Aiosa
2fbaf71448
Merge branch 'master' into master 2022-06-28 14:14:05 +02:00
Aiosa
0aadc23fcb Merge branch 'openseadragon-master' 2022-06-28 14:11:30 +02:00
Aiosa
bb1755613e Merge branch 'master' of https://github.com/openseadragon/openseadragon into openseadragon-master
# Conflicts:
#	src/tiledimage.js
2022-06-28 14:11:02 +02:00
Ian Gilman
71e9d5775c Changelog for #2173 2022-06-20 13:59:58 -07:00
Jirka
7b759558b1 Add tile reference to imagejob options. 2022-06-07 14:53:18 +02:00
Aiosa
1d99a2d6be
Merge branch 'openseadragon:master' into master 2022-06-07 14:48:33 +02:00
Aiosa
6e80e2d184
Merge branch 'openseadragon:master' into master 2022-05-06 16:52:24 +02:00
Jirka
8a2c998cb9 Finish on image job now accepts request argument. Further comments cleanup. Deprecation message for image property in tile loaded event. Removal of downloadTileFinish(). More robust aborting that cleans up an image properties when aborted (not done until now). 2022-05-04 15:16:13 +02:00
Aiosa
150e750ece
Merge branch 'openseadragon:master' into master 2022-05-04 13:40:06 +02:00
Jirka
d82fd35323 Fix comments on #2148: part 2. Better commens on the new TileSource API. Deprecation if 'tile-loaded' image event parameter. Unwrap ImageJob and add userData property. 2022-04-29 23:45:01 +02:00
Jirka
45a7a4aaf3 Fix comments on #2148: hasTransparency as a property. Move '.image' tile property to be a getter instead. Rename 'ImageJob' data property from 'image' to 'data'. Repair comments related to #2148. 2022-04-29 18:41:43 +02:00
Aiosa
92ad2c5552
Merge branch 'openseadragon:master' into master 2022-04-29 17:40:14 +02:00
Jirka
4f79f29238 Move cache creation, image downloading process and transparency deduction to the TileSource instance to allow custom data fetching, caching, processing. 2022-04-16 21:19:54 +02:00
10 changed files with 374 additions and 170 deletions

View File

@ -5,6 +5,8 @@ OPENSEADRAGON CHANGELOG
* Improved the constraints that keep the image in the viewer, specifically when zoomed out a lot (#2160 @joedf) * Improved the constraints that keep the image in the viewer, specifically when zoomed out a lot (#2160 @joedf)
* You can now provide an element for the navigator (as an alternative to an ID) (#1303 @cameronbaney, #2166 #2175 @joedf) * You can now provide an element for the navigator (as an alternative to an ID) (#1303 @cameronbaney, #2166 #2175 @joedf)
* Now supporting IIIF "id" and "identifier" in addition to "@id" (#2173 @ahankinson)
* We now delegate tile fetching and caching to the TileSource, to allow for custom tile formats (#2148 @Aiosa)
3.1.0: 3.1.0:

View File

@ -347,15 +347,16 @@ $.Drawer.prototype = {
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency * position and size of tiles supporting alpha channel in non-transparency
* context. * context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/ */
drawTile: function(tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize) { drawTile: function( tile, drawingHandler, useSketch, scale, translate, shouldRoundPositionAndSize, source) {
$.console.assert(tile, '[Drawer.drawTile] tile is required'); $.console.assert(tile, '[Drawer.drawTile] tile is required');
$.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); $.console.assert(drawingHandler, '[Drawer.drawTile] drawingHandler is required');
if (this.useCanvas) { if (this.useCanvas) {
var context = this._getContext(useSketch); var context = this._getContext(useSketch);
scale = scale || 1; scale = scale || 1;
tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize); tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source);
} else { } else {
tile.drawHTML( this.canvas ); tile.drawHTML( this.canvas );
} }

View File

@ -35,21 +35,23 @@
(function($){ (function($){
/** /**
* @private
* @class ImageJob * @class ImageJob
* @classdesc Handles downloading of a single image. * @classdesc Handles downloading of a single image.
* @param {Object} options - Options for this ImageJob. * @param {Object} options - Options for this ImageJob.
* @param {String} [options.src] - URL of image to download. * @param {String} [options.src] - URL of image to download.
* @param {Tile} [options.tile] - Tile that belongs the data to.
* @param {TileSource} [options.source] - Image loading strategy
* @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX.
* @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX. * @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX.
* @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
* @param {String} [options.crossOriginPolicy] - CORS policy to use for downloads * @param {String} [options.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [options.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form, * @param {String} [options.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData) or null * see TileSource::getPostData) or null
* @param {Function} [options.callback] - Called once image has been downloaded. * @param {Function} [options.callback] - Called once image has been downloaded.
* @param {Function} [options.abort] - Called when this image job is aborted. * @param {Function} [options.abort] - Called when this image job is aborted.
* @param {Number} [options.timeout] - The max number of milliseconds that this image job may take to complete. * @param {Number} [options.timeout] - The max number of milliseconds that this image job may take to complete.
*/ */
function ImageJob (options) { $.ImageJob = function(options) {
$.extend(true, this, { $.extend(true, this, {
timeout: $.DEFAULT_SETTINGS.timeout, timeout: $.DEFAULT_SETTINGS.timeout,
@ -57,107 +59,61 @@ function ImageJob (options) {
}, options); }, options);
/** /**
* Image object which will contain downloaded image. * Data object which will contain downloaded image data.
* @member {Image} image * @member {Image|*} image data object, by default an Image object (depends on TileSource)
* @memberof OpenSeadragon.ImageJob# * @memberof OpenSeadragon.ImageJob#
*/ */
this.image = null; this.data = null;
}
ImageJob.prototype = { /**
errorMsg: null, * User workspace to populate with helper variables
* @member {*} userData to append custom data and avoid namespace collision
* @memberof OpenSeadragon.ImageJob#
*/
this.userData = {};
/**
* Error message holder
* @member {string} error message
* @memberof OpenSeadragon.ImageJob#
* @private
*/
this.errorMsg = null;
};
$.ImageJob.prototype = {
/** /**
* Starts the image job. * Starts the image job.
* @method * @method
*/ */
start: function(){ start: function() {
var self = this; var self = this;
var selfAbort = this.abort; var selfAbort = this.abort;
this.image = new Image(); this.jobId = window.setTimeout(function () {
self.finish(null, null, "Image load exceeded timeout (" + self.timeout + " ms)");
this.image.onload = function(){
self.finish(true);
};
this.image.onabort = this.image.onerror = function() {
self.errorMsg = "Image load aborted";
self.finish(false);
};
this.jobId = window.setTimeout(function(){
self.errorMsg = "Image load exceeded timeout (" + self.timeout + " ms)";
self.finish(false);
}, this.timeout); }, this.timeout);
// Load the tile with an AJAX request if the loadWithAjax option is
// set. Otherwise load the image by setting the source proprety of the image object.
if (this.loadWithAjax) {
this.request = $.makeAjaxRequest({
url: this.src,
withCredentials: this.ajaxWithCredentials,
headers: this.ajaxHeaders,
responseType: "arraybuffer",
postData: this.postData,
success: function(request) {
var blb;
// Make the raw data into a blob.
// BlobBuilder fallback adapted from
// http://stackoverflow.com/questions/15293694/blob-constructor-browser-compatibility
try {
blb = new window.Blob([request.response]);
} catch (e) {
var BlobBuilder = (
window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder
);
if (e.name === 'TypeError' && BlobBuilder) {
var bb = new BlobBuilder();
bb.append(request.response);
blb = bb.getBlob();
}
}
// If the blob is empty for some reason consider the image load a failure.
if (blb.size === 0) {
self.errorMsg = "Empty image response.";
self.finish(false);
}
// Create a URL for the blob data and make it the source of the image object.
// This will still trigger Image.onload to indicate a successful tile load.
var url = (window.URL || window.webkitURL).createObjectURL(blb);
self.image.src = url;
},
error: function(request) {
self.errorMsg = "Image load aborted - XHR error: Ajax returned " + request.status;
self.finish(false);
}
});
// Provide a function to properly abort the request.
this.abort = function() { this.abort = function() {
self.request.abort(); self.source.downloadTileAbort(self);
// Call the existing abort function if available
if (typeof selfAbort === "function") { if (typeof selfAbort === "function") {
selfAbort(); selfAbort();
} }
}; };
} else {
if (this.crossOriginPolicy !== false) {
this.image.crossOrigin = this.crossOriginPolicy;
}
this.image.src = this.src; this.source.downloadTileStart(this);
}
}, },
finish: function(successful) { /**
this.image.onload = this.image.onerror = this.image.onabort = null; * Finish this job.
if (!successful) { * @param {*} data data that has been downloaded
this.image = null; * @param {XMLHttpRequest} request reference to the request if used
} * @param {string} errorMessage description upon failure
*/
finish: function(data, request, errorMessage ) {
this.data = data;
this.request = request;
this.errorMsg = errorMessage;
if (this.jobId) { if (this.jobId) {
window.clearTimeout(this.jobId); window.clearTimeout(this.jobId);
@ -165,7 +121,6 @@ ImageJob.prototype = {
this.callback(this); this.callback(this);
} }
}; };
/** /**
@ -196,23 +151,38 @@ $.ImageLoader.prototype = {
* @method * @method
* @param {Object} options - Options for this job. * @param {Object} options - Options for this job.
* @param {String} [options.src] - URL of image to download. * @param {String} [options.src] - URL of image to download.
* @param {Tile} [options.tile] - Tile that belongs the data to. The tile instance
* is not internally used and serves for custom TileSources implementations.
* @param {TileSource} [options.source] - Image loading strategy
* @param {String} [options.loadWithAjax] - Whether to load this image with AJAX. * @param {String} [options.loadWithAjax] - Whether to load this image with AJAX.
* @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX. * @param {String} [options.ajaxHeaders] - Headers to add to the image request if using AJAX.
* @param {String|Boolean} [options.crossOriginPolicy] - CORS policy to use for downloads * @param {String|Boolean} [options.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [options.postData] - POST parameters (usually but not necessarily in k=v&k2=v2... form, * @param {String} [options.postData] - POST parameters (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData) or null * see TileSource::getPostData) or null
* @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX * @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX
* requests. * requests.
* @param {Function} [options.callback] - Called once image has been downloaded. * @param {Function} [options.callback] - Called once image has been downloaded.
* @param {Function} [options.abort] - Called when this image job is aborted. * @param {Function} [options.abort] - Called when this image job is aborted.
*/ */
addJob: function(options) { addJob: function(options) {
if (!options.source) {
$.console.error('ImageLoader.prototype.addJob() requires [options.source]. ' +
'TileSource since new API defines how images are fetched. Creating a dummy TileSource.');
var implementation = $.TileSource.prototype;
options.source = {
downloadTileStart: implementation.downloadTileStart,
downloadTileAbort: implementation.downloadTileAbort
};
}
var _this = this, var _this = this,
complete = function(job) { complete = function(job) {
completeJob(_this, job, options.callback); completeJob(_this, job, options.callback);
}, },
jobOptions = { jobOptions = {
src: options.src, src: options.src,
tile: options.tile || {},
source: options.source,
loadWithAjax: options.loadWithAjax, loadWithAjax: options.loadWithAjax,
ajaxHeaders: options.loadWithAjax ? options.ajaxHeaders : null, ajaxHeaders: options.loadWithAjax ? options.ajaxHeaders : null,
crossOriginPolicy: options.crossOriginPolicy, crossOriginPolicy: options.crossOriginPolicy,
@ -222,7 +192,7 @@ $.ImageLoader.prototype = {
abort: options.abort, abort: options.abort,
timeout: this.timeout timeout: this.timeout
}, },
newJob = new ImageJob(jobOptions); newJob = new $.ImageJob(jobOptions);
if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) { if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) {
newJob.start(); newJob.start();
@ -268,7 +238,7 @@ function completeJob(loader, job, callback) {
loader.jobsInProgress++; loader.jobsInProgress++;
} }
callback(job.image, job.errorMsg, job.request); callback(job.data, job.errorMsg, job.request);
} }
}(OpenSeadragon)); }(OpenSeadragon));

View File

@ -2327,7 +2327,7 @@ function OpenSeadragon( options ){
* @param {Object} options.headers - headers to add to the AJAX request * @param {Object} options.headers - headers to add to the AJAX request
* @param {String} options.responseType - the response type of the AJAX request * @param {String} options.responseType - the response type of the AJAX request
* @param {String} options.postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form, * @param {String} options.postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData), GET method used if null * see TileSource::getPostData), GET method used if null
* @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials * @param {Boolean} [options.withCredentials=false] - whether to set the XHR's withCredentials
* @throws {Error} * @throws {Error}
* @returns {XMLHttpRequest} * @returns {XMLHttpRequest}

View File

@ -53,7 +53,7 @@
* drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing * drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing
* with HTML the entire tile is always used. * with HTML the entire tile is always used.
* @param {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form, * @param {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData) or null * see TileSource::getPostData) or null
* @param {String} cacheKey key to act as a tile cache, must be unique for tiles with unique image data * @param {String} cacheKey key to act as a tile cache, must be unique for tiles with unique image data
*/ */
$.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) { $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) {
@ -104,7 +104,7 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* Post parameters for this tile. For example, it can be an URL-encoded string * Post parameters for this tile. For example, it can be an URL-encoded string
* in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used * in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used
* @member {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form, * @member {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData) or null * see TileSource::getPostData) or null
* @memberof OpenSeadragon.Tile# * @memberof OpenSeadragon.Tile#
*/ */
this.postData = postData; this.postData = postData;
@ -127,18 +127,18 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @memberof OpenSeadragon.Tile# * @memberof OpenSeadragon.Tile#
*/ */
this.ajaxHeaders = ajaxHeaders; this.ajaxHeaders = ajaxHeaders;
/**
* The unique cache key for this tile.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
if (cacheKey === undefined) { if (cacheKey === undefined) {
$.console.error("Tile constructor needs 'cacheKey' variable: creation tile cache" + $.console.error("Tile constructor needs 'cacheKey' variable: creation tile cache" +
" in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used."); " in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used.");
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData); cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
} }
/**
* The unique cache key for this tile.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
this.cacheKey = cacheKey; this.cacheKey = cacheKey;
/** /**
* Is this tile loaded? * Is this tile loaded?
* @member {Boolean} loaded * @member {Boolean} loaded
@ -164,12 +164,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
* @memberof OpenSeadragon.Tile# * @memberof OpenSeadragon.Tile#
*/ */
this.imgElement = null; this.imgElement = null;
/**
* The Image object for this tile.
* @member {Object} image
* @memberof OpenSeadragon.Tile#
*/
this.image = null;
/** /**
* The alias of this.element.style. * The alias of this.element.style.
@ -222,6 +216,13 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
*/ */
this.visibility = null; this.visibility = null;
/**
* The transparency indicator of this tile.
* @member {Boolean} hasTransparency true if tile contains transparency for correct rendering
* @memberof OpenSeadragon.Tile#
*/
this.hasTransparency = false;
/** /**
* Whether this tile is currently being drawn. * Whether this tile is currently being drawn.
* @member {Boolean} beingDrawn * @member {Boolean} beingDrawn
@ -266,6 +267,8 @@ $.Tile.prototype = {
// private // private
_hasTransparencyChannel: function() { _hasTransparencyChannel: function() {
console.warn("Tile.prototype._hasTransparencyChannel() has been " +
"deprecated and will be removed in the future. Use TileSource.prototype.hasTransparency() instead.");
return !!this.context2D || this.url.match('.png'); return !!this.context2D || this.url.match('.png');
}, },
@ -294,8 +297,13 @@ $.Tile.prototype = {
// content during animation of the container size. // content during animation of the container size.
if ( !this.element ) { if ( !this.element ) {
var image = this.getImage();
if (!image) {
return;
}
this.element = $.makeNeutralElement( "div" ); this.element = $.makeNeutralElement( "div" );
this.imgElement = this.cacheImageRecord.getImage().cloneNode(); this.imgElement = image.cloneNode();
this.imgElement.style.msInterpolationMode = "nearest-neighbor"; this.imgElement.style.msInterpolationMode = "nearest-neighbor";
this.imgElement.style.width = "100%"; this.imgElement.style.width = "100%";
this.imgElement.style.height = "100%"; this.imgElement.style.height = "100%";
@ -322,6 +330,35 @@ $.Tile.prototype = {
$.setElementOpacity( this.element, this.opacity ); $.setElementOpacity( this.element, this.opacity );
}, },
/**
* The Image object for this tile.
* @member {Object} image
* @memberof OpenSeadragon.Tile#
* @deprecated
* @return {Image}
*/
get image() {
$.console.error("[Tile.image] property has been deprecated. Use [Tile.prototype.getImage] instead.");
return this.getImage();
},
/**
* Get the Image object for this tile.
* @return {Image}
*/
getImage: function() {
return this.cacheImageRecord.getImage();
},
/**
* Get the CanvasRenderingContext2D instance for tile image data drawn
* onto Canvas if enabled and available
* @return {CanvasRenderingContext2D}
*/
getCanvasContext: function() {
return this.context2D || this.cacheImageRecord.getRenderedContext();
},
/** /**
* Renders the tile in a canvas-based context. * Renders the tile in a canvas-based context.
* @function * @function
@ -334,8 +371,9 @@ $.Tile.prototype = {
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round * @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency * position and size of tiles supporting alpha channel in non-transparency
* context. * context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/ */
drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize ) { drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
var position = this.position.times($.pixelDensityRatio), var position = this.position.times($.pixelDensityRatio),
size = this.size.times($.pixelDensityRatio), size = this.size.times($.pixelDensityRatio),
@ -348,7 +386,7 @@ $.Tile.prototype = {
return; return;
} }
rendered = this.context2D || this.cacheImageRecord.getRenderedContext(); rendered = this.getCanvasContext();
if ( !this.loaded || !rendered ){ if ( !this.loaded || !rendered ){
$.console.warn( $.console.warn(
@ -360,7 +398,6 @@ $.Tile.prototype = {
} }
context.save(); context.save();
context.globalAlpha = this.opacity; context.globalAlpha = this.opacity;
if (typeof scale === 'number' && scale !== 1) { if (typeof scale === 'number' && scale !== 1) {
@ -378,7 +415,7 @@ $.Tile.prototype = {
//ie its done fading or fading is turned off, and if we are drawing //ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way //an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle //to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 && this._hasTransparencyChannel()) { if (context.globalAlpha === 1 && this.hasTransparency) {
if (shouldRoundPositionAndSize) { if (shouldRoundPositionAndSize) {
// Round to the nearest whole pixel so we don't get seams from overlap. // Round to the nearest whole pixel so we don't get seams from overlap.
position.x = Math.round(position.x); position.x = Math.round(position.x);

View File

@ -46,43 +46,22 @@ var TileRecord = function( options ) {
// private class // private class
var ImageRecord = function(options) { var ImageRecord = function(options) {
$.console.assert( options, "[ImageRecord] options is required" ); $.console.assert( options, "[ImageRecord] options is required" );
$.console.assert( options.image, "[ImageRecord] options.image is required" ); $.console.assert( options.data, "[ImageRecord] options.data is required" );
this._image = options.image;
this._tiles = []; this._tiles = [];
options.create.apply(null, [this, options.data, options.ownerTile]);
this._destroyImplementation = options.destroy.bind(null, this);
this.getImage = options.getImage.bind(null, this);
this.getData = options.getData.bind(null, this);
this.getRenderedContext = options.getRenderedContext.bind(null, this);
}; };
ImageRecord.prototype = { ImageRecord.prototype = {
destroy: function() { destroy: function() {
this._image = null; this._destroyImplementation();
this._renderedContext = null;
this._tiles = null; this._tiles = null;
}, },
getImage: function() {
return this._image;
},
getRenderedContext: function() {
if (!this._renderedContext) {
var canvas = document.createElement( 'canvas' );
canvas.width = this._image.width;
canvas.height = this._image.height;
this._renderedContext = canvas.getContext('2d');
this._renderedContext.drawImage( this._image, 0, 0 );
//since we are caching the prerendered image on a canvas
//allow the image to not be held in memory
this._image = null;
}
return this._renderedContext;
},
setRenderedContext: function(renderedContext) {
$.console.error("ImageRecord.setRenderedContext is deprecated. " +
"The rendered context should be created by the ImageRecord " +
"itself when calling ImageRecord.getRenderedContext.");
this._renderedContext = renderedContext;
},
addTile: function(tile) { addTile: function(tile) {
$.console.assert(tile, '[ImageRecord.addTile] tile is required'); $.console.assert(tile, '[ImageRecord.addTile] tile is required');
this._tiles.push(tile); this._tiles.push(tile);
@ -158,9 +137,22 @@ $.TileCache.prototype = {
var imageRecord = this._imagesLoaded[options.tile.cacheKey]; var imageRecord = this._imagesLoaded[options.tile.cacheKey];
if (!imageRecord) { if (!imageRecord) {
$.console.assert( options.image, "[TileCache.cacheTile] options.image is required to create an ImageRecord" );
if (!options.data) {
$.console.error("[TileCache.cacheTile] options.image was renamed to options.data. '.image' attribute " +
"has been deprecated and will be removed in the future.");
options.data = options.image;
}
$.console.assert( options.data, "[TileCache.cacheTile] options.data is required to create an ImageRecord" );
imageRecord = this._imagesLoaded[options.tile.cacheKey] = new ImageRecord({ imageRecord = this._imagesLoaded[options.tile.cacheKey] = new ImageRecord({
image: options.image data: options.data,
ownerTile: options.tile,
create: options.tiledImage.source.createTileCache,
destroy: options.tiledImage.source.destroyTileCache,
getImage: options.tiledImage.source.getTileCacheDataAsImage,
getData: options.tiledImage.source.getTileCacheData,
getRenderedContext: options.tiledImage.source.getTileCacheDataAsContext2D,
}); });
this._imagesLoadedCount++; this._imagesLoadedCount++;

View File

@ -1427,8 +1427,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
} else { } else {
var imageRecord = this._tileCache.getImageRecord(tile.cacheKey); var imageRecord = this._tileCache.getImageRecord(tile.cacheKey);
if (imageRecord) { if (imageRecord) {
var image = imageRecord.getImage(); this._setTileLoaded(tile, imageRecord.getData());
this._setTileLoaded(tile, image);
} }
} }
} }
@ -1571,13 +1570,15 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
tile.loading = true; tile.loading = true;
this._imageLoader.addJob({ this._imageLoader.addJob({
src: tile.url, src: tile.url,
tile: tile,
source: this.source,
postData: tile.postData, postData: tile.postData,
loadWithAjax: tile.loadWithAjax, loadWithAjax: tile.loadWithAjax,
ajaxHeaders: tile.ajaxHeaders, ajaxHeaders: tile.ajaxHeaders,
crossOriginPolicy: this.crossOriginPolicy, crossOriginPolicy: this.crossOriginPolicy,
ajaxWithCredentials: this.ajaxWithCredentials, ajaxWithCredentials: this.ajaxWithCredentials,
callback: function( image, errorMsg, tileRequest ){ callback: function( data, errorMsg, tileRequest ){
_this._onTileLoad( tile, time, image, errorMsg, tileRequest ); _this._onTileLoad( tile, time, data, errorMsg, tileRequest );
}, },
abort: function() { abort: function() {
tile.loading = false; tile.loading = false;
@ -1591,12 +1592,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* Callback fired when a Tile's Image finished downloading. * Callback fired when a Tile's Image finished downloading.
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile
* @param {Number} time * @param {Number} time
* @param {Image} image * @param {*} data image data
* @param {String} errorMsg * @param {String} errorMsg
* @param {XMLHttpRequest} tileRequest * @param {XMLHttpRequest} tileRequest
*/ */
_onTileLoad: function( tile, time, image, errorMsg, tileRequest ) { _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
if ( !image ) { if ( !data ) {
$.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg );
/** /**
* Triggered when a tile fails to load. * Triggered when a tile fails to load.
@ -1632,7 +1633,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
finish = function() { finish = function() {
var ccc = _this.source; var ccc = _this.source;
var cutoff = ccc.getClosestLevel(); var cutoff = ccc.getClosestLevel();
_this._setTileLoaded(tile, image, cutoff, tileRequest); _this._setTileLoaded(tile, data, cutoff, tileRequest);
}; };
// Check if we're mid-update; this can happen on IE8 because image load events for // Check if we're mid-update; this can happen on IE8 because image load events for
@ -1649,11 +1650,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @private * @private
* @inner * @inner
* @param {OpenSeadragon.Tile} tile * @param {OpenSeadragon.Tile} tile
* @param {Image|undefined} image * @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
* @param {Number|undefined} cutoff * @param {Number|undefined} cutoff
* @param {XMLHttpRequest|undefined} tileRequest * @param {XMLHttpRequest|undefined} tileRequest
*/ */
_setTileLoaded: function(tile, image, cutoff, tileRequest) { _setTileLoaded: function(tile, data, cutoff, tileRequest) {
var increment = 0, var increment = 0,
_this = this; _this = this;
@ -1667,9 +1668,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
if (increment === 0) { if (increment === 0) {
tile.loading = false; tile.loading = false;
tile.loaded = true; tile.loaded = true;
tile.hasTransparency = _this.source.hasTransparency(
tile.context2D, tile.url, tile.ajaxHeaders, tile.postData
);
if (!tile.context2D) { if (!tile.context2D) {
_this._tileCache.cacheTile({ _this._tileCache.cacheTile({
image: image, data: data,
tile: tile, tile: tile,
cutoff: cutoff, cutoff: cutoff,
tiledImage: _this tiledImage: _this
@ -1686,7 +1690,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
* @event tile-loaded * @event tile-loaded
* @memberof OpenSeadragon.Viewer * @memberof OpenSeadragon.Viewer
* @type {object} * @type {object}
* @property {Image} image - The image of the tile. * @property {Image || *} image - The image (data) of the tile. Deprecated.
* @property {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile. * @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded. * @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable). * @property {XMLHttpRequest} tileRequest - The AJAX request that loaded this tile (if applicable).
@ -1699,7 +1704,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
tile: tile, tile: tile,
tiledImage: this, tiledImage: this,
tileRequest: tileRequest, tileRequest: tileRequest,
image: image, get image() {
$.console.error("[tile-loaded] event 'image' has been deprecated. Use 'data' property instead.");
return data;
},
data: data,
getCompletionCallback: getCompletionCallback getCompletionCallback: getCompletionCallback
}); });
// In case the completion callback is never called, we at least force it once. // In case the completion callback is never called, we at least force it once.
@ -1842,7 +1851,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
if (tile) { if (tile) {
useSketch = this.opacity < 1 || useSketch = this.opacity < 1 ||
(this.compositeOperation && this.compositeOperation !== 'source-over') || (this.compositeOperation && this.compositeOperation !== 'source-over') ||
(!this._isBottomItem() && tile._hasTransparencyChannel()); (!this._isBottomItem() &&
this.source.hasTransparency(tile.context2D, tile.url, tile.ajaxHeaders, tile.postData));
} }
var sketchScale; var sketchScale;
@ -1984,7 +1994,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
for (var i = lastDrawn.length - 1; i >= 0; i--) { for (var i = lastDrawn.length - 1; i >= 0; i--) {
tile = lastDrawn[ i ]; tile = lastDrawn[ i ];
this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale, sketchTranslate, shouldRoundPositionAndSize ); this._drawer.drawTile( tile, this._drawingHandler, useSketch, sketchScale,
sketchTranslate, shouldRoundPositionAndSize, this.source );
tile.beingDrawn = true; tile.beingDrawn = true;
if( this.viewer ){ if( this.viewer ){

View File

@ -553,7 +553,7 @@ $.TileSource.prototype = {
* @property {String} message * @property {String} message
* @property {String} source * @property {String} source
* @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form, * @property {String} postData - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSrouce::getPostData) or null * see TileSource::getPostData) or null
* @property {?Object} userData - Arbitrary subscriber-defined object. * @property {?Object} userData - Arbitrary subscriber-defined object.
*/ */
_this.raiseEvent( 'open-failed', { _this.raiseEvent( 'open-failed', {
@ -709,6 +709,194 @@ $.TileSource.prototype = {
y >= 0 && y >= 0 &&
x < numTiles.x && x < numTiles.x &&
y < numTiles.y; y < numTiles.y;
},
/**
* Decide whether tiles have transparency: this is crucial for correct images blending.
* @return {boolean} true if the image has transparency
*/
hasTransparency: function(context2D, url, ajaxHeaders, post) {
return !!context2D || url.match('.png');
},
/**
* Download tile data.
* Note that if you override this function, you should override also downloadTileAbort().
* @param {ImageJob} context job context that you have to call finish(...) on.
* @param {String} [context.src] - URL of image to download.
* @param {String} [context.loadWithAjax] - Whether to load this image with AJAX.
* @param {String} [context.ajaxHeaders] - Headers to add to the image request if using AJAX.
* @param {Boolean} [context.ajaxWithCredentials] - Whether to set withCredentials on AJAX requests.
* @param {String} [context.crossOriginPolicy] - CORS policy to use for downloads
* @param {String} [context.postData] - HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* @param {*} [context.userData] - Empty object to attach your own data and helper variables to.
* @param {Function} [context.finish] - Should be called unless abort() was executed, e.g. on all occasions,
* be it successful or unsuccessful request.
* Usage: context.finish(data, request, errMessage). Pass the downloaded data object or null upon failure.
* Add also reference to an ajax request if used. Provide error message in case of failure.
* @param {Function} [context.abort] - Called automatically when the job times out.
* Usage: context.abort().
* @param {Function} [context.callback] @private - Called automatically once image has been downloaded
* (triggered by finish).
* @param {Number} [context.timeout] @private - The max number of milliseconds that
* this image job may take to complete.
* @param {string} [context.errorMsg] @private - The final error message, default null (set by finish).
*/
downloadTileStart: function (context) {
var dataStore = context.userData,
image = new Image();
dataStore.image = image;
dataStore.request = null;
var finish = function(error) {
if (!image) {
context.finish(null, dataStore.request, "Image load failed: undefined Image instance.");
return;
}
image.onload = image.onerror = image.onabort = null;
context.finish(error ? null : image, dataStore.request, error);
};
image.onload = function () {
finish();
};
image.onabort = image.onerror = function() {
finish("Image load aborted.");
};
// Load the tile with an AJAX request if the loadWithAjax option is
// set. Otherwise load the image by setting the source proprety of the image object.
if (context.loadWithAjax) {
dataStore.request = $.makeAjaxRequest({
url: context.src,
withCredentials: context.ajaxWithCredentials,
headers: context.ajaxHeaders,
responseType: "arraybuffer",
postData: context.postData,
success: function(request) {
var blb;
// Make the raw data into a blob.
// BlobBuilder fallback adapted from
// http://stackoverflow.com/questions/15293694/blob-constructor-browser-compatibility
try {
blb = new window.Blob([request.response]);
} catch (e) {
var BlobBuilder = (
window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder
);
if (e.name === 'TypeError' && BlobBuilder) {
var bb = new BlobBuilder();
bb.append(request.response);
blb = bb.getBlob();
}
}
// If the blob is empty for some reason consider the image load a failure.
if (blb.size === 0) {
finish("Empty image response.");
} else {
// Create a URL for the blob data and make it the source of the image object.
// This will still trigger Image.onload to indicate a successful tile load.
image.src = (window.URL || window.webkitURL).createObjectURL(blb);
}
},
error: function(request) {
finish("Image load aborted - XHR error");
}
});
} else {
if (context.crossOriginPolicy !== false) {
image.crossOrigin = context.crossOriginPolicy;
}
image.src = context.src;
}
},
/**
* Provide means of aborting the execution.
* Note that if you override this function, you should override also downloadTileStart().
* @param {ImageJob} context job, the same object as with downloadTileStart(..)
* @param {*} [context.userData] - Empty object to attach (and mainly read) your own data.
*/
downloadTileAbort: function (context) {
if (context.userData.request) {
context.userData.request.abort();
}
var image = context.userData.image;
if (context.userData.image) {
image.onload = image.onerror = image.onabort = null;
}
},
/**
* Create cache object from the result of the download process. The
* cacheObject parameter should be used to attach the data to, there are no
* conventions on how it should be stored - all the logic is implemented within *TileCache() functions.
*
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @param {*} data image data, the data sent to ImageJob.prototype.finish(), by default an Image object
* @param {Tile} tile instance the cache was created with
*/
createTileCache: function(cacheObject, data, tile) {
cacheObject._data = data;
},
/**
* Cache object destructor, unset all properties you created to allow GC collection.
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
*/
destroyTileCache: function (cacheObject) {
cacheObject._data = null;
cacheObject._renderedContext = null;
},
/**
* Raw data getter
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @return {*} cache data
*/
getTileCacheData: function(cacheObject) {
return cacheObject._data;
},
/**
* Compatibility image element getter
* - plugins might need image representation of the data
* - div HTML rendering relies on image element presence
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @return {Image} cache data as an Image
*/
getTileCacheDataAsImage: function(cacheObject) {
return cacheObject._data; //the data itself by default is Image
},
/**
* Compatibility context 2D getter
* - most heavily used rendering method is a canvas-based approach,
* convert the data to a canvas and return it's 2D context
* Note that if you override any of *TileCache() functions, you should override all of them.
* @param {object} cacheObject context cache object
* @return {CanvasRenderingContext2D} context of the canvas representation of the cache data
*/
getTileCacheDataAsContext2D: function(cacheObject) {
if (!cacheObject._renderedContext) {
var canvas = document.createElement( 'canvas' );
canvas.width = cacheObject._data.width;
canvas.height = cacheObject._data.height;
cacheObject._renderedContext = canvas.getContext('2d');
cacheObject._renderedContext.drawImage( cacheObject._data, 0, 0 );
//since we are caching the prerendered image on a canvas
//allow the image to not be held in memory
cacheObject._data = null;
}
return cacheObject._renderedContext;
} }
}; };

View File

@ -305,7 +305,7 @@
// The Wikipedia logo has CORS enabled // The Wikipedia logo has CORS enabled
var corsImg = 'http://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png'; var corsImg = 'https://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png';
QUnit.test( 'CrossOriginPolicyMissing', function (assert) { QUnit.test( 'CrossOriginPolicyMissing', function (assert) {
var done = assert.async(); var done = assert.async();

View File

@ -19,10 +19,12 @@
raiseEvent: function() {} raiseEvent: function() {}
}; };
var fakeTiledImage0 = { var fakeTiledImage0 = {
viewer: fakeViewer viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
}; };
var fakeTiledImage1 = { var fakeTiledImage1 = {
viewer: fakeViewer viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
}; };
var fakeTile0 = { var fakeTile0 = {
@ -74,7 +76,8 @@
raiseEvent: function() {} raiseEvent: function() {}
}; };
var fakeTiledImage0 = { var fakeTiledImage0 = {
viewer: fakeViewer viewer: fakeViewer,
source: OpenSeadragon.TileSource.prototype
}; };
var fakeTile0 = { var fakeTile0 = {