diff --git a/.gitignore b/.gitignore index aaaabf8c..282b4dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ build/ sftp-config.json coverage/ temp/ -.idea \ No newline at end of file +.idea +/nbproject/private/ +.directory diff --git a/Gruntfile.js b/Gruntfile.js index 06511e31..8c7ff60a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -39,6 +39,7 @@ module.exports = function(grunt) { "src/osmtilesource.js", "src/tmstilesource.js", "src/legacytilesource.js", + "src/imagetilesource.js", "src/tilesourcecollection.js", "src/button.js", "src/buttongroup.js", diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 00000000..422675b4 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,18 @@ +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=4 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=80 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project +auxiliary.org-netbeans-modules-editor-indent.text.javascript.CodeStyle.project.indent-shift-width=4 +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.indent-shift-width=2 +auxiliary.org-netbeans-modules-editor-indent.text.x-json.CodeStyle.project.spaces-per-tab=2 +auxiliary.org-netbeans-modules-web-clientproject-api.js_2e_libs_2e_folder=src +browser.autorefresh.Chromium.INTEGRATED=true +browser.highlightselection.Chromium.INTEGRATED=true +file.reference.openseadragon-openseadragon=. +files.encoding=UTF-8 +site.root.folder=${file.reference.openseadragon-openseadragon} +start.file=test/demo/basic.html +web.context.root=/ diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 00000000..53613f5c --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.web.clientproject + + + openseadragon + + + diff --git a/src/imagetilesource.js b/src/imagetilesource.js new file mode 100644 index 00000000..98f6c500 --- /dev/null +++ b/src/imagetilesource.js @@ -0,0 +1,287 @@ +/* + * OpenSeadragon - ImageTileSource + * + * Copyright (C) 2009 CodePlex Foundation + * Copyright (C) 2010-2013 OpenSeadragon contributors + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of CodePlex Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +(function ($) { + + /** + * @class ImageTileSource + * @classdesc The ImageTileSource allows a simple image to be loaded + * into an OpenSeadragon Viewer. + * There are 2 ways to open an ImageTileSource: + * 1. viewer.open({type: 'image', url: fooUrl}); + * 2. viewer.open(new OpenSeadragon.ImageTileSource({url: fooUrl})); + * + * With the first syntax, the crossOriginPolicy, ajaxWithCredentials and + * useCanvas options are inherited from the viewer if they are not + * specified directly in the options object. + * + * @memberof OpenSeadragon + * @extends OpenSeadragon.TileSource + * @param {Object} options Options object. + * @param {String} options.url URL of the image + * @param {Boolean} [options.buildPyramid=true] If set to true (default), a + * pyramid will be built internally to provide a better downsampling. + * @param {String|Boolean} [options.crossOriginPolicy=false] Valid values are + * 'Anonymous', 'use-credentials', and false. If false, image requests will + * not use CORS preventing internal pyramid building for images from other + * domains. + * @param {String|Boolean} [options.ajaxWithCredentials=false] Whether to set + * the withCredentials XHR flag for AJAX requests (when loading tile sources). + * @param {Boolean} [options.useCanvas=true] Set to false to prevent any use + * of the canvas API. + */ + $.ImageTileSource = function (options) { + + options = $.extend({ + buildPyramid: true, + crossOriginPolicy: false, + ajaxWithCredentials: false, + useCanvas: true + }, options); + $.TileSource.apply(this, [options]); + + }; + + $.extend($.ImageTileSource.prototype, $.TileSource.prototype, /** @lends OpenSeadragon.ImageTileSource.prototype */{ + /** + * Determine if the data and/or url imply the image service is supported by + * this tile source. + * @function + * @param {Object|Array} data + * @param {String} optional - url + */ + supports: function (data, url) { + return data.type && data.type === "image"; + }, + /** + * + * @function + * @param {Object} options - the options + * @param {String} dataUrl - the url the image was retreived from, if any. + * @return {Object} options - A dictionary of keyword arguments sufficient + * to configure this tile sources constructor. + */ + configure: function (options, dataUrl) { + return options; + }, + /** + * Responsible for retrieving, and caching the + * image metadata pertinent to this TileSources implementation. + * @function + * @param {String} url + * @throws {Error} + */ + getImageInfo: function (url) { + var image = this._image = new Image(); + var _this = this; + + if (this.crossOriginPolicy) { + image.crossOrigin = this.crossOriginPolicy; + } + if (this.ajaxWithCredentials) { + image.useCredentials = this.ajaxWithCredentials; + } + + $.addEvent(image, 'load', function () { + _this.width = image.naturalWidth; + _this.height = image.naturalHeight; + _this.aspectRatio = _this.width / _this.height; + _this.dimensions = new $.Point(_this.width, _this.height); + _this._tileWidth = _this.width; + _this._tileHeight = _this.height; + _this.tileOverlap = 0; + _this.minLevel = 0; + _this.levels = _this._buildLevels(); + _this.maxLevel = _this.levels.length - 1; + + _this.ready = true; + /** + * Raised when a TileSource is opened and initialized. + * + * @event ready + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference + * to the TileSource which raised the event. + * @property {Object} tileSource + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent('ready', {tileSource: _this}); + }); + + $.addEvent(image, 'error', function () { + /*** + * Raised when an error occurs loading a TileSource. + * + * @event open-failed + * @memberof OpenSeadragon.TileSource + * @type {object} + * @property {OpenSeadragon.TileSource} eventSource - A reference + * to the TileSource which raised the event. + * @property {String} message + * @property {String} source + * @property {?Object} userData - Arbitrary subscriber-defined object. + */ + _this.raiseEvent('open-failed', { + message: "Error loading image at " + url, + source: url + }); + }); + + image.src = url; + }, + /** + * @function + * @param {Number} level + */ + getLevelScale: function (level) { + var levelScale = NaN; + if (level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + }, + /** + * @function + * @param {Number} level + */ + getNumTiles: function (level) { + var scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + }, + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function (level, point) { + return new $.Point(0, 0); + }, + /** + * Retrieves a tile url + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getTileUrl: function (level, x, y) { + var url = null; + if (level >= this.minLevel && level <= this.maxLevel) { + url = this.levels[level].url; + } + return url; + }, + /** + * Retrieves a tile context 2D + * @function + * @param {Number} level Level of the tile + * @param {Number} x x coordinate of the tile + * @param {Number} y y coordinate of the tile + */ + getContext2D: function (level, x, y) { + var context = null; + if (level >= this.minLevel && level <= this.maxLevel) { + context = this.levels[level].context2D; + } + return context; + }, + /** + * @private Build the differents levels of the pyramid if possible + * (canvas API enabled and no canvas tainting issue) + */ + _buildLevels: function () { + var levels = [{ + url: this._image.src, + width: this._image.naturalWidth, + height: this._image.naturalHeight + }]; + + if (!this.buildPyramid || !$.supportsCanvas || !this.useCanvas) { + // We don't need the image anymore. Allows it to be GC. + delete this._image; + return levels; + } + + var currentWidth = this._image.naturalWidth; + var currentHeight = this._image.naturalHeight; + + var bigCanvas = document.createElement("canvas"); + var bigContext = bigCanvas.getContext("2d"); + + bigCanvas.width = currentWidth; + bigCanvas.height = currentHeight; + bigContext.drawImage(this._image, 0, 0, currentWidth, currentHeight); + // We cache the context of the highest level because the browser + // is a lot faster at downsampling something it already has + // downsampled before. + levels[0].context2D = bigContext; + // We don't need the image anymore. Allows it to be GC. + delete this._image; + + if ($.isCanvasTainted(bigCanvas)) { + // If the canvas is tainted, we can't compute the pyramid. + return levels; + } + + // We build smaller levels until either width or height becomes + // 1 pixel wide. + while (currentWidth >= 2 && currentHeight >= 2) { + currentWidth = Math.floor(currentWidth / 2); + currentHeight = Math.floor(currentHeight / 2); + var smallCanvas = document.createElement("canvas"); + var smallContext = smallCanvas.getContext("2d"); + smallCanvas.width = currentWidth; + smallCanvas.height = currentHeight; + smallContext.drawImage(bigCanvas, 0, 0, currentWidth, currentHeight); + + levels.splice(0, 0, { + context2D: smallContext, + width: currentWidth, + height: currentHeight + }); + + bigCanvas = smallCanvas; + bigContext = smallContext; + } + return levels; + } + }); + +}(OpenSeadragon)); diff --git a/src/openseadragon.js b/src/openseadragon.js index c0111b2b..c9504183 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -844,6 +844,23 @@ if (typeof define === 'function' && define.amd) { canvasElement.getContext( '2d' ) ); }()); + /** + * Test whether the submitted canvas is tainted or not. + * @argument {Canvas} canvas The canvas to test. + * @returns {Boolean} True if the canvas is tainted. + */ + $.isCanvasTainted = function(canvas) { + var isTainted = false; + try { + // We test if the canvas is tainted by retrieving data from it. + // An exception will be raised if the canvas is tainted. + var data = canvas.getContext('2d').getImageData(0, 0, 1, 1); + } catch (e) { + isTainted = true; + } + return isTainted; + }; + /** * A ratio comparing the device screen's pixel density to the canvas's backing store pixel density. Defaults to 1 if canvas isn't supported by the browser. * @member {Number} pixelDensityRatio diff --git a/src/tile.js b/src/tile.js index ad018ba7..97d88e17 100644 --- a/src/tile.js +++ b/src/tile.js @@ -45,8 +45,10 @@ * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has * this tile failed to load? ) * @param {String} url The URL of this tile's image. + * @param {CanvasRenderingContext2D} context2D The context2D of this tile if it + * is provided directly by the tile source. */ -$.Tile = function(level, x, y, bounds, exists, url) { +$.Tile = function(level, x, y, bounds, exists, url, context2D) { /** * The zoom level this tile belongs to. * @member {Number} level @@ -83,6 +85,12 @@ $.Tile = function(level, x, y, bounds, exists, url) { * @memberof OpenSeadragon.Tile# */ this.url = url; + /** + * The context2D of this tile if it is provided directly by the tile source. + * @member {CanvasRenderingContext2D} context2D + * @memberOf OpenSeadragon.Tile# + */ + this.context2D = context2D; /** * Is this tile loaded? * @member {Boolean} loaded @@ -247,14 +255,14 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ size = this.size, rendered; - if (!this.cacheImageRecord) { + if (!this.context2D && !this.cacheImageRecord) { $.console.warn( '[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString()); return; } - rendered = this.cacheImageRecord.getRenderedContext(); + rendered = this.context2D || this.cacheImageRecord.getRenderedContext(); if ( !this.loaded || !rendered ){ $.console.warn( @@ -273,7 +281,8 @@ $.Tile.prototype = /** @lends OpenSeadragon.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.url.match('.png') ){ + if (context.globalAlpha === 1 && + (this.context2D || this.url.match('.png'))) { //clearing only the inside of the rectangle occupied //by the png prevents edge flikering context.clearRect( diff --git a/src/tiledimage.js b/src/tiledimage.js index 1731d472..76e113a9 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -795,7 +795,7 @@ function updateViewport( tiledImage ) { drawTiles( tiledImage, tiledImage.lastDrawn ); // Load the new 'best' tile - if ( best ) { + if (best && !best.context2D) { loadTile( tiledImage, best, currentTime ); } @@ -940,10 +940,14 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity ); if (!tile.loaded) { - var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); - if (imageRecord) { - var image = imageRecord.getImage(); - setTileLoaded(tiledImage, tile, image); + if (tile.context2D) { + setTileLoaded(tiledImage, tile); + } else { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + if (imageRecord) { + var image = imageRecord.getImage(); + setTileLoaded(tiledImage, tile, image); + } } } @@ -976,6 +980,7 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds, exists, url, + context2D, tile; if ( !tilesMatrix[ level ] ) { @@ -991,6 +996,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid bounds = tileSource.getTileBounds( level, xMod, yMod ); exists = tileSource.tileExists( level, xMod, yMod ); url = tileSource.getTileUrl( level, xMod, yMod ); + context2D = tileSource.getContext2D ? + tileSource.getContext2D(level, xMod, yMod) : undefined; bounds.x += ( x - xMod ) / numTiles.x; bounds.y += (worldHeight / worldWidth) * (( y - yMod ) / numTiles.y); @@ -1001,7 +1008,8 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWid y, bounds, exists, - url + url, + context2D ); } @@ -1080,12 +1088,14 @@ function setTileLoaded(tiledImage, tile, image, cutoff) { if (increment === 0) { tile.loading = false; tile.loaded = true; - tiledImage._tileCache.cacheTile({ - image: image, - tile: tile, - cutoff: cutoff, - tiledImage: tiledImage - }); + if (!tile.context2D) { + tiledImage._tileCache.cacheTile({ + image: image, + tile: tile, + cutoff: cutoff, + tiledImage: tiledImage + }); + } tiledImage._needsDraw = true; } } diff --git a/src/viewer.js b/src/viewer.js index 0890a55d..b484364c 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1288,19 +1288,21 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, } } + if ($.isArray(options.tileSource)) { + setTimeout(function() { + raiseAddItemFailed({ + message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", + source: options.tileSource, + options: options + }); + }); + return; + } + this._loadQueue.push(myQueueItem); getTileSourceImplementation( this, options.tileSource, function( tileSource ) { - if ( tileSource instanceof Array ) { - raiseAddItemFailed({ - message: "[Viewer.addTiledImage] Sequences can not be added; add them one at a time instead.", - source: tileSource, - options: options - }); - return; - } - myQueueItem.tileSource = tileSource; // add everybody at the front of the queue that's ready to go @@ -2042,12 +2044,30 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, } } + function waitUntilReady(tileSource, originalTileSource) { + if (tileSource.ready) { + successCallback(tileSource); + } else { + tileSource.addHandler('ready', function () { + successCallback(tileSource); + }); + tileSource.addHandler('open-failed', function (event) { + failCallback({ + message: event.message, + source: originalTileSource + }); + }); + } + } + setTimeout( function() { if ( $.type( tileSource ) == 'string' ) { //If its still a string it means it must be a url at this point tileSource = new $.TileSource({ url: tileSource, + crossOriginPolicy: viewer.crossOriginPolicy, ajaxWithCredentials: viewer.ajaxWithCredentials, + useCanvas: viewer.useCanvas, success: function( event ) { successCallback( event.tileSource ); } @@ -2056,10 +2076,16 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, failCallback( event ); } ); - } else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ) { + } else if ($.isPlainObject(tileSource) || tileSource.nodeType) { + if (!tileSource.crossOriginPolicy && viewer.crossOriginPolicy) { + tileSource.crossOriginPolicy = viewer.crossOriginPolicy; + } if (tileSource.ajaxWithCredentials === undefined) { tileSource.ajaxWithCredentials = viewer.ajaxWithCredentials; } + if (tileSource.useCanvas === undefined) { + tileSource.useCanvas = viewer.useCanvas; + } if ( $.isFunction( tileSource.getTileUrl ) ) { //Custom tile source @@ -2077,14 +2103,13 @@ function getTileSourceImplementation( viewer, tileSource, successCallback, return; } var options = $TileSource.prototype.configure.apply( _this, [ tileSource ] ); - var readySource = new $TileSource( options ); - successCallback( readySource ); + waitUntilReady(new $TileSource(options), tileSource); } } else { //can assume it's already a tile source implementation - successCallback( tileSource ); + waitUntilReady(tileSource, tileSource); } - }, 1 ); + }); } function getOverlayObject( viewer, overlay ) { diff --git a/test/coverage.html b/test/coverage.html index 65cc2818..dc857202 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -32,6 +32,7 @@ + diff --git a/test/modules/basic.js b/test/modules/basic.js index 7f095777..8b7a513b 100644 --- a/test/modules/basic.js +++ b/test/modules/basic.js @@ -314,23 +314,11 @@ var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - callback(!isCanvasTainted(ctx)); + callback(!OpenSeadragon.isCanvasTainted(canvas)); }; img.src = corsImg; } - function isCanvasTainted(context) { - var isTainted = false; - try { - // We test if the canvas is tainted by retrieving data from it. - // An exception will be raised if the canvas is tainted. - var url = context.getImageData(0, 0, 1, 1); - } catch (e) { - isTainted = true; - } - return isTainted; - } - asyncTest( 'CrossOriginPolicyMissing', function () { viewer.crossOriginPolicy = false; @@ -343,7 +331,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(isCanvasTainted(viewer.drawer.context), "Canvas should be tainted."); + ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should be tainted."); start(); }); @@ -366,7 +355,8 @@ } ] } ); viewer.addHandler('tile-drawn', function() { - ok(!isCanvasTainted(viewer.drawer.context), "Canvas should not be tainted."); + ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas), + "Canvas should not be tainted."); start(); }); } diff --git a/test/modules/formats.js b/test/modules/formats.js index 2cef3576..5166f796 100644 --- a/test/modules/formats.js +++ b/test/modules/formats.js @@ -142,4 +142,12 @@ '}'); }); + // ---------- + asyncTest('ImageTileSource', function () { + testOpen({ + type: "image", + url: "/test/data/A.png" + }); + }); + })(); diff --git a/test/modules/navigator.js b/test/modules/navigator.js index c6381a20..e300b4a0 100644 --- a/test/modules/navigator.js +++ b/test/modules/navigator.js @@ -801,7 +801,6 @@ }); asyncTest('Item positions including collection mode', function() { - var navAddCount = 0; viewer = OpenSeadragon({ id: 'example', @@ -815,16 +814,16 @@ var openHandler = function() { viewer.removeHandler('open', openHandler); viewer.navigator.world.addHandler('add-item', navOpenHandler); + // The navigator may already have added the items. + navOpenHandler(); }; var navOpenHandler = function(event) { - navAddCount++; - if (navAddCount === 2) { + if (viewer.navigator.world.getItemCount() === 2) { viewer.navigator.world.removeHandler('add-item', navOpenHandler); setTimeout(function() { // Test initial formation - equal(viewer.navigator.world.getItemCount(), 2, 'navigator has both items'); for (var i = 0; i < 2; i++) { propEqual(viewer.navigator.world.getItemAt(i).getBounds(), viewer.world.getItemAt(i).getBounds(), 'bounds are the same');