diff --git a/src/drawer.js b/src/drawer.js index a0dd58ef..2ce83584 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -347,15 +347,16 @@ $.Drawer.prototype = { * @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. */ - 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(drawingHandler, '[Drawer.drawTile] drawingHandler is required'); if (this.useCanvas) { var context = this._getContext(useSketch); scale = scale || 1; - tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize); + tile.drawCanvas(context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source); } else { tile.drawHTML( this.canvas ); } diff --git a/src/imageloader.js b/src/imageloader.js index db9fc366..91d33b50 100644 --- a/src/imageloader.js +++ b/src/imageloader.js @@ -35,21 +35,23 @@ (function($){ /** - * @private * @class ImageJob * @classdesc Handles downloading of a single image. * @param {Object} options - Options for this ImageJob. * @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.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.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.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. */ -function ImageJob (options) { +$.ImageJob = function(options) { $.extend(true, this, { timeout: $.DEFAULT_SETTINGS.timeout, @@ -57,107 +59,61 @@ function ImageJob (options) { }, options); /** - * Image object which will contain downloaded image. - * @member {Image} image + * Data object which will contain downloaded image data. + * @member {Image|*} image data object, by default an Image object (depends on TileSource) * @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. * @method */ - start: function(){ + start: function() { var self = this; var selfAbort = this.abort; - this.image = new Image(); - - 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.jobId = window.setTimeout(function () { + self.finish(null, null, "Image load exceeded timeout (" + self.timeout + " ms)"); }, 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() { - self.request.abort(); - - // Call the existing abort function if available - if (typeof selfAbort === "function") { - selfAbort(); - } - }; - } else { - if (this.crossOriginPolicy !== false) { - this.image.crossOrigin = this.crossOriginPolicy; + this.abort = function() { + self.source.downloadTileAbort(self); + if (typeof selfAbort === "function") { + selfAbort(); } + }; - this.image.src = this.src; - } + this.source.downloadTileStart(this); }, - finish: function(successful) { - this.image.onload = this.image.onerror = this.image.onabort = null; - if (!successful) { - this.image = null; - } + /** + * Finish this job. + * @param {*} data data that has been downloaded + * @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) { window.clearTimeout(this.jobId); @@ -165,7 +121,6 @@ ImageJob.prototype = { this.callback(this); } - }; /** @@ -196,23 +151,38 @@ $.ImageLoader.prototype = { * @method * @param {Object} options - Options for this job. * @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.ajaxHeaders] - Headers to add to the image request if using AJAX. * @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, - * see TileSrouce::getPostData) or null + * see TileSource::getPostData) or null * @param {Boolean} [options.ajaxWithCredentials] - Whether to set withCredentials on AJAX * requests. * @param {Function} [options.callback] - Called once image has been downloaded. * @param {Function} [options.abort] - Called when this image job is aborted. */ 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, complete = function(job) { completeJob(_this, job, options.callback); }, jobOptions = { src: options.src, + tile: options.tile || {}, + source: options.source, loadWithAjax: options.loadWithAjax, ajaxHeaders: options.loadWithAjax ? options.ajaxHeaders : null, crossOriginPolicy: options.crossOriginPolicy, @@ -222,7 +192,7 @@ $.ImageLoader.prototype = { abort: options.abort, timeout: this.timeout }, - newJob = new ImageJob(jobOptions); + newJob = new $.ImageJob(jobOptions); if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) { newJob.start(); @@ -268,7 +238,7 @@ function completeJob(loader, job, callback) { loader.jobsInProgress++; } - callback(job.image, job.errorMsg, job.request); + callback(job.data, job.errorMsg, job.request); } }(OpenSeadragon)); diff --git a/src/openseadragon.js b/src/openseadragon.js index 9cd6ad9f..fded2a1c 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -2327,7 +2327,7 @@ function OpenSeadragon( options ){ * @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.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 * @throws {Error} * @returns {XMLHttpRequest} diff --git a/src/tile.js b/src/tile.js index fc3fe57f..b41aa9e9 100644 --- a/src/tile.js +++ b/src/tile.js @@ -53,7 +53,7 @@ * drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing * with HTML the entire tile is always used. * @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 */ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) { @@ -82,11 +82,11 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja */ this.bounds = bounds; /** - * The portion of the tile to use as the source of the drawing operation, in pixels. Note that - * this only works when drawing with canvas; when drawing with HTML the entire tile is always used. - * @member {OpenSeadragon.Rect} sourceBounds - * @memberof OpenSeadragon.Tile# - */ + * The portion of the tile to use as the source of the drawing operation, in pixels. Note that + * this only works when drawing with canvas; when drawing with HTML the entire tile is always used. + * @member {OpenSeadragon.Rect} sourceBounds + * @memberof OpenSeadragon.Tile# + */ this.sourceBounds = sourceBounds; /** * Is this tile a part of a sparse image? Also has this tile failed to load? @@ -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 * 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, - * see TileSrouce::getPostData) or null + * see TileSource::getPostData) or null * @memberof OpenSeadragon.Tile# */ this.postData = postData; @@ -127,18 +127,18 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @memberof OpenSeadragon.Tile# */ this.ajaxHeaders = ajaxHeaders; - /** - * The unique cache key for this tile. - * @member {String} cacheKey - * @memberof OpenSeadragon.Tile# - */ + if (cacheKey === undefined) { $.console.error("Tile constructor needs 'cacheKey' variable: creation tile cache" + " in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used."); 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; - /** * Is this tile loaded? * @member {Boolean} loaded @@ -164,12 +164,6 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja * @memberof OpenSeadragon.Tile# */ this.imgElement = null; - /** - * The Image object for this tile. - * @member {Object} image - * @memberof OpenSeadragon.Tile# - */ - this.image = null; /** * The alias of this.element.style. @@ -222,6 +216,13 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja */ 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. * @member {Boolean} beingDrawn @@ -266,6 +267,8 @@ $.Tile.prototype = { // private _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'); }, @@ -294,8 +297,13 @@ $.Tile.prototype = { // content during animation of the container size. if ( !this.element ) { + var image = this.getImage(); + if (!image) { + return; + } + this.element = $.makeNeutralElement( "div" ); - this.imgElement = this.cacheImageRecord.getImage().cloneNode(); + this.imgElement = image.cloneNode(); this.imgElement.style.msInterpolationMode = "nearest-neighbor"; this.imgElement.style.width = "100%"; this.imgElement.style.height = "100%"; @@ -322,6 +330,35 @@ $.Tile.prototype = { $.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. * @function @@ -334,8 +371,9 @@ $.Tile.prototype = { * @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. */ - drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize ) { + drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) { var position = this.position.times($.pixelDensityRatio), size = this.size.times($.pixelDensityRatio), @@ -348,7 +386,7 @@ $.Tile.prototype = { return; } - rendered = this.context2D || this.cacheImageRecord.getRenderedContext(); + rendered = this.getCanvasContext(); if ( !this.loaded || !rendered ){ $.console.warn( @@ -360,7 +398,6 @@ $.Tile.prototype = { } context.save(); - context.globalAlpha = this.opacity; 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 //an image with an alpha channel, then the only way //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) { // Round to the nearest whole pixel so we don't get seams from overlap. position.x = Math.round(position.x); diff --git a/src/tilecache.js b/src/tilecache.js index 6221f4e5..cc15a74e 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -46,43 +46,22 @@ var TileRecord = function( options ) { // private class var ImageRecord = function(options) { $.console.assert( options, "[ImageRecord] options is required" ); - $.console.assert( options.image, "[ImageRecord] options.image is required" ); - this._image = options.image; + $.console.assert( options.data, "[ImageRecord] options.data is required" ); 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 = { destroy: function() { - this._image = null; - this._renderedContext = null; + this._destroyImplementation(); 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) { $.console.assert(tile, '[ImageRecord.addTile] tile is required'); this._tiles.push(tile); @@ -158,9 +137,22 @@ $.TileCache.prototype = { var imageRecord = this._imagesLoaded[options.tile.cacheKey]; 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({ - 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++; @@ -196,7 +188,7 @@ $.TileCache.prototype = { worstLevel = worstTile.level; if ( prevTime < worstTime || - ( prevTime === worstTime && prevLevel > worstLevel ) ) { + ( prevTime === worstTime && prevLevel > worstLevel ) ) { worstTile = prevTile; worstTileIndex = i; worstTileRecord = prevTileRecord; diff --git a/src/tiledimage.js b/src/tiledimage.js index d4ee2d08..e768d852 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -1427,8 +1427,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag } else { var imageRecord = this._tileCache.getImageRecord(tile.cacheKey); if (imageRecord) { - var image = imageRecord.getImage(); - this._setTileLoaded(tile, image); + this._setTileLoaded(tile, imageRecord.getData()); } } } @@ -1571,13 +1570,15 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag tile.loading = true; this._imageLoader.addJob({ src: tile.url, + tile: tile, + source: this.source, postData: tile.postData, loadWithAjax: tile.loadWithAjax, ajaxHeaders: tile.ajaxHeaders, crossOriginPolicy: this.crossOriginPolicy, ajaxWithCredentials: this.ajaxWithCredentials, - callback: function( image, errorMsg, tileRequest ){ - _this._onTileLoad( tile, time, image, errorMsg, tileRequest ); + callback: function( data, errorMsg, tileRequest ){ + _this._onTileLoad( tile, time, data, errorMsg, tileRequest ); }, abort: function() { tile.loading = false; @@ -1591,12 +1592,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * Callback fired when a Tile's Image finished downloading. * @param {OpenSeadragon.Tile} tile * @param {Number} time - * @param {Image} image + * @param {*} data image data * @param {String} errorMsg * @param {XMLHttpRequest} tileRequest */ - _onTileLoad: function( tile, time, image, errorMsg, tileRequest ) { - if ( !image ) { + _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) { + if ( !data ) { $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); /** * Triggered when a tile fails to load. @@ -1632,7 +1633,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag finish = function() { var ccc = _this.source; 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 @@ -1649,11 +1650,11 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @private * @inner * @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 {XMLHttpRequest|undefined} tileRequest */ - _setTileLoaded: function(tile, image, cutoff, tileRequest) { + _setTileLoaded: function(tile, data, cutoff, tileRequest) { var increment = 0, _this = this; @@ -1667,9 +1668,12 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag if (increment === 0) { tile.loading = false; tile.loaded = true; + tile.hasTransparency = _this.source.hasTransparency( + tile.context2D, tile.url, tile.ajaxHeaders, tile.postData + ); if (!tile.context2D) { _this._tileCache.cacheTile({ - image: image, + data: data, tile: tile, cutoff: cutoff, tiledImage: _this @@ -1686,7 +1690,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag * @event tile-loaded * @memberof OpenSeadragon.Viewer * @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.Tile} tile - The tile which has been loaded. * @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, tiledImage: this, 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 }); // 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) { useSketch = this.opacity < 1 || (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; @@ -1984,7 +1994,8 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag for (var i = lastDrawn.length - 1; i >= 0; 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; if( this.viewer ){ diff --git a/src/tilesource.js b/src/tilesource.js index 445dab70..cba91511 100644 --- a/src/tilesource.js +++ b/src/tilesource.js @@ -553,7 +553,7 @@ $.TileSource.prototype = { * @property {String} message * @property {String} source * @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. */ _this.raiseEvent( 'open-failed', { @@ -709,6 +709,194 @@ $.TileSource.prototype = { y >= 0 && x < numTiles.x && 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; } }; diff --git a/test/modules/basic.js b/test/modules/basic.js index d5635fcf..457adeab 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -305,7 +305,7 @@ // 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) { var done = assert.async(); diff --git a/test/modules/tilecache.js b/test/modules/tilecache.js index 65fbf431..26ee51ba 100644 --- a/test/modules/tilecache.js +++ b/test/modules/tilecache.js @@ -19,10 +19,12 @@ raiseEvent: function() {} }; var fakeTiledImage0 = { - viewer: fakeViewer + viewer: fakeViewer, + source: OpenSeadragon.TileSource.prototype }; var fakeTiledImage1 = { - viewer: fakeViewer + viewer: fakeViewer, + source: OpenSeadragon.TileSource.prototype }; var fakeTile0 = { @@ -74,7 +76,8 @@ raiseEvent: function() {} }; var fakeTiledImage0 = { - viewer: fakeViewer + viewer: fakeViewer, + source: OpenSeadragon.TileSource.prototype }; var fakeTile0 = {