diff --git a/changelog.txt b/changelog.txt index fef9339f..40178dc2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -8,6 +8,11 @@ OPENSEADRAGON CHANGELOG * Now supporting rotation in the Rect class (#782) * Drag outside of iframe now works better, as long as both pages are on the same domain (#790) * Coordinate conversion now takes rotation into account (#796) +* Support tile-less IIIF as per LegacyTileSource (#816) +* You can now give an empty string to the tabIndex option (#805) +* Fixed issue with rotation and clicking in the navigator (#807) +* Broadened the check for mime type in LegacyTileSource URLs to allow query strings (#819) +* Added globalCompositeOperation option for tiledImage, to allow for different transfer modes (#814) 2.1.0: diff --git a/src/drawer.js b/src/drawer.js index fcfb57b5..cd4c00b0 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -371,10 +371,11 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ * @param {Float} opacity The opacity of the blending. * @param {Float} [scale=1] The scale at which tiles were drawn on the sketch. Default is 1. * Use scale to draw at a lower scale and then enlarge onto the main canvas. - * @param OpenSeadragon.Point} [translate] A translation vector that was used to draw the tiles + * @param {OpenSeadragon.Point} [translate] A translation vector that was used to draw the tiles + * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. * @returns {undefined} */ - blendSketch: function(opacity, scale, translate) { + blendSketch: function(opacity, scale, translate, compositeOperation) { if (!this.useCanvas || !this.sketchCanvas) { return; } @@ -394,6 +395,9 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ this.context.save(); this.context.globalAlpha = opacity; + if (compositeOperation) { + this.context.globalCompositeOperation = compositeOperation; + } this.context.drawImage( this.sketchCanvas, position.x - widthExt * scale, diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index bf3da020..4584f1cd 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -36,8 +36,8 @@ /** * @class IIIFTileSource - * @classdesc A client implementation of the International Image Interoperability - * Format: Image API 1.0 - 2.0 + * @classdesc A client implementation of the International Image Interoperability Framework + * Format: Image API 1.0 - 2.1 * * @memberof OpenSeadragon * @extends OpenSeadragon.TileSource @@ -83,7 +83,7 @@ $.IIIFTileSource = function( options ){ } } } - } else { + } else if ( canBeTiled(options.profile) ) { // use the largest of tileOptions that is smaller than the short dimension var shortDim = Math.min( this.height, this.width ), tileOptions = [256,512,1024], @@ -101,13 +101,32 @@ $.IIIFTileSource = function( options ){ // If we're smaller than 256, just use the short side. options.tileSize = shortDim; } + } else if (this.sizes && this.sizes.length > 0) { + // This info.json can't be tiled, but we can still construct a legacy pyramid from the sizes array. + // In this mode, IIIFTileSource will call functions from the abstract baseTileSource or the + // LegacyTileSource instead of performing IIIF tiling. + this.emulateLegacyImagePyramid = true; + + options.levels = constructLevels( this ); + // use the largest available size to define tiles + $.extend( true, options, { + width: options.levels[ options.levels.length - 1 ].width, + height: options.levels[ options.levels.length - 1 ].height, + tileSize: Math.max( options.height, options.width ), + tileOverlap: 0, + minLevel: 0, + maxLevel: options.levels.length - 1 + }); + this.levels = options.levels; + } else { + $.console.error("Nothing in the info.json to construct image pyramids from"); } - if ( !options.maxLevel ) { - if ( !this.scale_factors ) { - options.maxLevel = Number( Math.ceil( Math.log( Math.max( this.width, this.height ), 2 ) ) ); + if (!options.maxLevel && !this.emulateLegacyImagePyramid) { + if (!this.scale_factors) { + options.maxLevel = Number(Math.ceil(Math.log(Math.max(this.width, this.height), 2))); } else { - options.maxLevel = Math.floor( Math.pow( Math.max.apply(null, this.scale_factors), 0.5) ); + options.maxLevel = Math.floor(Math.pow(Math.max.apply(null, this.scale_factors), 0.5)); } } @@ -192,6 +211,11 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea * @param {Number} level */ getTileWidth: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileWidth.call(this, level); + } + var scaleFactor = Math.pow(2, this.maxLevel - level); if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { @@ -206,6 +230,11 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea * @param {Number} level */ getTileHeight: function( level ) { + + if(this.emulateLegacyImagePyramid) { + return $.TileSource.prototype.getTileHeight.call(this, level); + } + var scaleFactor = Math.pow(2, this.maxLevel - level); if (this.tileSizePerScaleFactor && this.tileSizePerScaleFactor[scaleFactor]) { @@ -214,9 +243,61 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea return this._tileHeight; }, + /** + * @function + * @param {Number} level + */ + getLevelScale: function ( level ) { + + if(this.emulateLegacyImagePyramid) { + var levelScale = NaN; + if (this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel) { + levelScale = + this.levels[level].width / + this.levels[this.maxLevel].width; + } + return levelScale; + } + + return $.TileSource.prototype.getLevelScale.call(this, level); + }, /** - * Responsible for retreiving the url which will return an image for the + * @function + * @param {Number} level + */ + getNumTiles: function( level ) { + + if(this.emulateLegacyImagePyramid) { + var scale = this.getLevelScale(level); + if (scale) { + return new $.Point(1, 1); + } else { + return new $.Point(0, 0); + } + } + + return $.TileSource.prototype.getNumTiles.call(this, level); + }, + + + /** + * @function + * @param {Number} level + * @param {OpenSeadragon.Point} point + */ + getTileAtPoint: function( level, point ) { + + if(this.emulateLegacyImagePyramid) { + return new $.Point(0, 0); + } + + return $.TileSource.prototype.getTileAtPoint.call(this, level, point); + }, + + + /** + * Responsible for retrieving the url which will return an image for the * region specified by the given x, y, and level components. * @function * @param {Number} level - z index @@ -226,6 +307,14 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea */ getTileUrl: function( level, x, y ){ + if(this.emulateLegacyImagePyramid) { + var url = null; + if ( this.levels.length > 0 && level >= this.minLevel && level <= this.maxLevel ) { + url = this.levels[ level ].url; + } + return url; + } + //# constants var IIIF_ROTATION = '0', //## get the scale (level as a decimal) @@ -280,6 +369,40 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea }); + /** + * Determine whether arbitrary tile requests can be made against a service with the given profile + * @function + * @param {object} profile - IIIF profile object + * @throws {Error} + */ + function canBeTiled (profile ) { + var level0Profiles = [ + "http://library.stanford.edu/iiif/image-api/compliance.html#level0", + "http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0", + "http://iiif.io/api/image/2/level0.json" + ]; + var isLevel0 = (level0Profiles.indexOf(profile[0]) != -1); + return !isLevel0 || (profile.indexOf("sizeByW") != -1); + } + + /** + * Build the legacy pyramid URLs (one tile per level) + * @function + * @param {object} options - infoJson + * @throws {Error} + */ + function constructLevels(options) { + var levels = []; + for(var i=0; i= 0, + "[OpenSeadragon.Spring] options.animationTime must be a number greater than or equal to 0"); if (options.exponential) { this._exponential = true; diff --git a/src/tiledimage.js b/src/tiledimage.js index 6a64b0ce..7772c186 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -66,6 +66,7 @@ * @param {Number} [options.minPixelRatio] - See {@link OpenSeadragon.Options}. * @param {Number} [options.smoothTileEdgesMinZoom] - See {@link OpenSeadragon.Options}. * @param {Number} [options.opacity=1] - Opacity the tiled image should be drawn at. + * @param {String} [options.compositeOperation] - How the image is composited onto other images; see compositeOperation in {@link OpenSeadragon.Options} for possible values. * @param {Boolean} [options.debugMode] - See {@link OpenSeadragon.Options}. * @param {String|CanvasGradient|CanvasPattern|Function} [options.placeholderFillStyle] - See {@link OpenSeadragon.Options}. * @param {String|Boolean} [options.crossOriginPolicy] - See {@link OpenSeadragon.Options}. @@ -132,7 +133,6 @@ $.TiledImage = function( options ) { _midDraw: false, // Is the tiledImage currently updating the viewport? _needsDraw: true, // Does the tiledImage need to update the viewport again? _hasOpaqueTile: false, // Do we have even one fully opaque tile? - //configurable settings springStiffness: $.DEFAULT_SETTINGS.springStiffness, animationTime: $.DEFAULT_SETTINGS.animationTime, @@ -147,7 +147,8 @@ $.TiledImage = function( options ) { debugMode: $.DEFAULT_SETTINGS.debugMode, crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy, placeholderFillStyle: $.DEFAULT_SETTINGS.placeholderFillStyle, - opacity: $.DEFAULT_SETTINGS.opacity + opacity: $.DEFAULT_SETTINGS.opacity, + compositeOperation: $.DEFAULT_SETTINGS.compositeOperation }, options ); @@ -176,7 +177,7 @@ $.TiledImage = function( options ) { /** * This event is fired just before the tile is drawn giving the application a chance to alter the image. * - * NOTE: This event is only fired when the drawer is using a . + * NOTE: This event is only fired when the drawer is using a <canvas>. * * @event tile-drawing * @memberof OpenSeadragon.Viewer @@ -585,6 +586,21 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag this._needsDraw = true; }, + /** + * @returns {String} The TiledImage's current compositeOperation. + */ + getCompositeOperation: function() { + return this.compositeOperation; + }, + + /** + * @param {String} compositeOperation the tiled image should be drawn with this globalCompositeOperation. + */ + setCompositeOperation: function(compositeOperation) { + this.compositeOperation = compositeOperation; + this._needsDraw = true; + }, + // private _setScale: function(scale, immediately) { var sameTarget = (this._scaleSpring.target.value === scale); @@ -1313,7 +1329,9 @@ function drawTiles( tiledImage, lastDrawn ) { drawDebugInfo( tiledImage, lastDrawn ); return; } - var useSketch = tiledImage.opacity < 1; + var useSketch = tiledImage.opacity < 1 || + (tiledImage.compositeOperation && tiledImage.compositeOperation !== 'source-over'); + var sketchScale; var sketchTranslate; @@ -1413,7 +1431,7 @@ function drawTiles( tiledImage, lastDrawn ) { if (offsetForRotation) { tiledImage._drawer._offsetForRotation(tiledImage.viewport.degrees, false); } - tiledImage._drawer.blendSketch(tiledImage.opacity, sketchScale, sketchTranslate); + tiledImage._drawer.blendSketch(tiledImage.opacity, sketchScale, sketchTranslate, tiledImage.compositeOperation); if (offsetForRotation) { tiledImage._drawer._restoreRotationChanges(false); } diff --git a/src/viewer.js b/src/viewer.js index 6dfa5c7c..16d5cfa1 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -234,7 +234,7 @@ $.Viewer = function( options ) { style.left = "0px"; }(this.canvas.style)); $.setElementTouchActionNone( this.canvas ); - this.canvas.tabIndex = options.tabIndex || 0; + this.canvas.tabIndex = (options.tabIndex === undefined ? 0 : options.tabIndex); //the container is created through applying the ControlDock constructor above this.container.className = "openseadragon-container"; @@ -1207,6 +1207,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, * (portions of the image outside of this area will not be visible). Only works on * browsers that support the HTML5 canvas. * @param {Number} [options.opacity] Opacity the tiled image should be drawn at by default. + * @param {String} [options.compositeOperation] How the image is composited onto other images. * @param {Function} [options.success] A function that gets called when the image is * successfully added. It's passed the event object which contains a single property: * "item", the resulting TiledImage. @@ -1239,6 +1240,9 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, if (options.opacity === undefined) { options.opacity = this.opacity; } + if (options.compositeOperation === undefined) { + options.compositeOperation = this.compositeOperation; + } var myQueueItem = { options: options @@ -1337,6 +1341,7 @@ $.extend( $.Viewer.prototype, $.EventSource.prototype, $.ControlDock.prototype, clip: queueItem.options.clip, placeholderFillStyle: queueItem.options.placeholderFillStyle, opacity: queueItem.options.opacity, + compositeOperation: queueItem.options.compositeOperation, springStiffness: _this.springStiffness, animationTime: _this.animationTime, minZoomImageRatio: _this.minZoomImageRatio, diff --git a/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg b/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg new file mode 100644 index 00000000..9156c9d8 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg b/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg new file mode 100644 index 00000000..7cad3870 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/400,/0/default.jpg b/test/data/iiif_2_0_sizes/full/400,/0/default.jpg new file mode 100644 index 00000000..6d2433df Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/400,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg b/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg new file mode 100644 index 00000000..8e627bbc Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/full/800,/0/default.jpg b/test/data/iiif_2_0_sizes/full/800,/0/default.jpg new file mode 100644 index 00000000..b574b541 Binary files /dev/null and b/test/data/iiif_2_0_sizes/full/800,/0/default.jpg differ diff --git a/test/data/iiif_2_0_sizes/info.json b/test/data/iiif_2_0_sizes/info.json new file mode 100644 index 00000000..c78e059b --- /dev/null +++ b/test/data/iiif_2_0_sizes/info.json @@ -0,0 +1,15 @@ +{ + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "http://localhost:8000/test/data/iiif_2_0_sizes", + "protocol": "http://iiif.io/api/image", + "width": 6976, + "height": 5074, + "profile": ["http://iiif.io/api/image/2/level0.json"], + "sizes" : [ + {"width" : 400, "height" : 291}, + {"width" : 800, "height" : 582}, + {"width" : 1600, "height" : 1164}, + {"width" : 3200, "height": 2328}, + {"width" : 6976, "height": 5074} + ] +} diff --git a/test/demo/iiif-sizes.html b/test/demo/iiif-sizes.html new file mode 100644 index 00000000..e9785556 --- /dev/null +++ b/test/demo/iiif-sizes.html @@ -0,0 +1,51 @@ + + + + OpenSeadragon Demo - IIIF emulation of legacy image pyramid + + + + + +
+

Default OpenSeadragon viewer from IIIF Source

+

This allows IIIF even if you only have a handful of static image sizes.

+
+ + +
+ + + diff --git a/test/demo/iiif.html b/test/demo/iiif.html new file mode 100644 index 00000000..5d978df3 --- /dev/null +++ b/test/demo/iiif.html @@ -0,0 +1,34 @@ + + + + OpenSeadragon Demo - IIIF Tiled + + + + + +
+

Default OpenSeadragon viewer from IIIF Tile Source.

+

This depends on a remote server providing a IIIF Image API endpoint.

+
+
+ + + diff --git a/test/demo/legacy.html b/test/demo/legacy.html new file mode 100644 index 00000000..0c13a2b4 --- /dev/null +++ b/test/demo/legacy.html @@ -0,0 +1,56 @@ + + + + OpenSeadragon Demo - Legacy image pyramid + + + + + +
+ Use an array of full images at different sizes. +
+
+ + + diff --git a/test/modules/formats.js b/test/modules/formats.js index 5166f796..f0edea75 100644 --- a/test/modules/formats.js +++ b/test/modules/formats.js @@ -114,6 +114,11 @@ testOpenUrl('iiif_2_0_tiled/info.json'); }); + // ---------- + asyncTest('IIIF 2.0 JSON, sizes array only', function() { + testOpenUrl('iiif_2_0_sizes/info.json'); + }); + // ---------- asyncTest('IIIF 2.0 JSON String', function() { testOpen( @@ -149,5 +154,38 @@ url: "/test/data/A.png" }); }); + + + // ---------- + asyncTest('Legacy Image Pyramid', function() { + // Although it is using image paths that happen to be in IIIF format, this is not a IIIFTileSource. + // The url values are opaque, just image locations. + // When emulating a legacy pyramid, IIIFTileSource calls functions from LegacyTileSource, so this + // adds a test for the legacy functionality too. + testOpen({ + type: 'legacy-image-pyramid', + levels:[{ + url: '/test/data/iiif_2_0_sizes/full/400,/0/default.jpg', + height: 291, + width: 400 + },{ + url: '/test/data/iiif_2_0_sizes/full/800,/0/default.jpg', + height: 582, + width: 800 + },{ + url: '/test/data/iiif_2_0_sizes/full/1600,/0/default.jpg', + height: 1164, + width: 1600 + },{ + url: '/test/data/iiif_2_0_sizes/full/3200,/0/default.jpg', + height: 2328, + width: 3200 + },{ + url: '/test/data/iiif_2_0_sizes/full/6976,/0/default.jpg', + height: 5074, + width: 6976 + }] + }); + }); })();