From 0c358c140dae8bcd5cbc568ef6236a840af84efe Mon Sep 17 00:00:00 2001 From: Ruven Date: Mon, 17 Apr 2023 21:08:18 +0200 Subject: [PATCH 1/4] Use resolution level dimensions provided in the info.json "sizes" field to determine tile sizes as well as the number of tiles that exist at a particular resolution. Fall back to calculation using ceil() if no resolution sizes provided. Avoids rounding errors for edge tiles and fixes https://github.com/openseadragon/openseadragon/issues/2321 --- src/iiiftilesource.js | 63 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index 08932b56..8e151cbe 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -2,7 +2,7 @@ * OpenSeadragon - IIIFTileSource * * Copyright (C) 2009 CodePlex Foundation - * Copyright (C) 2010-2022 OpenSeadragon contributors + * Copyright (C) 2010-2023 OpenSeadragon contributors * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -333,7 +333,45 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea } } - return $.TileSource.prototype.getNumTiles.call(this, level); + // Use supplied list of scaled resolution sizes if these exist + var levelSize = this.getLevelSize(level); + if( levelSize ) { + var x = Math.ceil( levelSize.width / this.getTileWidth(level) ), + y = Math.ceil( levelSize.height / this.getTileHeight(level) ); + return new $.Point( x, y ); + } + // Otherwise call default TileSource->getNumTiles() function + else { + return $.TileSource.prototype.getNumTiles.call(this, level); + } + }, + + + /** + * Determine image size at a given resolution level using the info.json "sizes" field + * Returns null if this information is not present + * @function {Number} level + */ + getLevelSize: function( level ) { + + var numLevels = this.maxLevel - this.minLevel; + // Need to take into account that the list may or may not include the full resolution size + if( this.sizes && ((this.sizes.length === numLevels) || + (this.sizes.length === numLevels + 1)) ) { + var levelWidth, levelHeight; + if( this.sizes.length === numLevels ) { + levelWidth = (level === this.sizes.length) ? this.width : this.sizes[level].width; + levelHeight = (level === this.sizes.length) ? this.height : this.sizes[level].height; + } + else { + levelWidth = this.sizes[level].width; + levelHeight = this.sizes[level].height; + } + return { width: levelWidth, height: levelHeight }; + } + else { + return null; + } }, @@ -375,10 +413,9 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea var IIIF_ROTATION = '0', //## get the scale (level as a decimal) scale = Math.pow( 0.5, this.maxLevel - level ), - //# image dimensions at this level - levelWidth = Math.round( this.width * scale ), - levelHeight = Math.round( this.height * scale ), + levelWidth, + levelHeight, //## iiif region tileWidth, @@ -396,6 +433,18 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea iiifQuality, uri; + // Use supplied list of scaled resolution sizes if these exist + var levelSize = this.getLevelSize( level ); + if( levelSize ) { + levelWidth = levelSize.width; + levelHeight = levelSize.height; + } + // Otherwise calculate the sizes ourselves + else { + levelWidth = Math.ceil( this.width * scale ); + levelHeight = Math.ceil( this.height * scale ); + } + tileWidth = this.getTileWidth(level); tileHeight = this.getTileHeight(level); iiifTileSizeWidth = Math.round( tileWidth / scale ); @@ -426,8 +475,8 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea } else { iiifRegion = [ iiifTileX, iiifTileY, iiifTileW, iiifTileH ].join( ',' ); } - iiifSizeW = Math.round( iiifTileW * scale ); - iiifSizeH = Math.round( iiifTileH * scale ); + iiifSizeW = Math.min( tileWidth, levelWidth - (x * tileWidth) ); + iiifSizeH = Math.min( tileHeight, levelHeight - (y * tileHeight) ); if ( this.version === 2 && iiifSizeW === this.width ) { iiifSize = "full"; } else if ( this.version === 3 && iiifSizeW === this.width && iiifSizeH === this.height ) { From 877c3b68ed7f84c9c6a930e25689d43174cf0e37 Mon Sep 17 00:00:00 2001 From: Ruven Date: Mon, 24 Apr 2023 17:24:18 +0200 Subject: [PATCH 2/4] Refactored code to take into account optimization suggestions (https://github.com/openseadragon/openseadragon/pull/2337#discussion_r1170931340) --- src/iiiftilesource.js | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index 8e151cbe..0ba5a6f4 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -354,24 +354,27 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea */ getLevelSize: function( level ) { - var numLevels = this.maxLevel - this.minLevel; - // Need to take into account that the list may or may not include the full resolution size - if( this.sizes && ((this.sizes.length === numLevels) || - (this.sizes.length === numLevels + 1)) ) { - var levelWidth, levelHeight; - if( this.sizes.length === numLevels ) { - levelWidth = (level === this.sizes.length) ? this.width : this.sizes[level].width; - levelHeight = (level === this.sizes.length) ? this.height : this.sizes[level].height; - } - else { - levelWidth = this.sizes[level].width; - levelHeight = this.sizes[level].height; - } - return { width: levelWidth, height: levelHeight }; - } - else { + if (!this.sizes) { return null; } + + var levelWidth, levelHeight; + var numLevels = this.maxLevel - this.minLevel; + var sizeLength = this.sizes.length; + + // Need to take into account that the list may or may not include the full resolution size + if (sizeLength === numLevels) { + levelWidth = (level === sizeLength) ? this.width : this.sizes[level].width; + levelHeight = (level === sizeLength) ? this.height : this.sizes[level].height; + } else if ( sizeLength === numLevels + 1 ) { + levelWidth = this.sizes[level].width; + levelHeight = this.sizes[level].height; + } else { + // Sizes field doesn't contain resolution level sizes, so discard + return null; + } + + return {width: levelWidth, height: levelHeight}; }, From 5fd125dc92cd95461f2e033af64b19f6a620a681 Mon Sep 17 00:00:00 2001 From: Ruven Date: Mon, 24 Apr 2023 22:44:46 +0200 Subject: [PATCH 3/4] Added implementation of getTileAtPoint() function. This eliminates flickering at level transitions caused by mis-match in resolution size calculation between the getTileAtPoint() and getNumTiles() functions. --- src/iiiftilesource.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index 0ba5a6f4..94385dc3 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -336,13 +336,13 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea // Use supplied list of scaled resolution sizes if these exist var levelSize = this.getLevelSize(level); if( levelSize ) { - var x = Math.ceil( levelSize.width / this.getTileWidth(level) ), - y = Math.ceil( levelSize.height / this.getTileHeight(level) ); - return new $.Point( x, y ); + var x = Math.ceil( levelSize.width / this.getTileWidth(level) ), + y = Math.ceil( levelSize.height / this.getTileHeight(level) ); + return new $.Point( x, y ); } // Otherwise call default TileSource->getNumTiles() function else { - return $.TileSource.prototype.getNumTiles.call(this, level); + return $.TileSource.prototype.getNumTiles.call(this, level); } }, @@ -389,6 +389,35 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea return new $.Point(0, 0); } + // Use supplied list of scaled resolution sizes if these exist + var levelSize = this.getLevelSize(level); + if( levelSize ) { + + var validPoint = point.x >= 0 && point.x <= 1 && + point.y >= 0 && point.y <= 1 / this.aspectRatio; + $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); + + var widthScaled = levelSize.width; + var pixelX = point.x * widthScaled; + var pixelY = point.y * widthScaled; + + var x = Math.floor(pixelX / this.getTileWidth(level)); + var y = Math.floor(pixelY / this.getTileHeight(level)); + + // When point.x == 1 or point.y == 1 / this.aspectRatio we want to + // return the last tile of the row/column + if (point.x >= 1) { + x = this.getNumTiles(level).x - 1; + } + var EPSILON = 1e-15; + if (point.y >= 1 / this.aspectRatio - EPSILON) { + y = this.getNumTiles(level).y - 1; + } + + return new $.Point(x, y); + } + + // Otherwise call default TileSource->getTileAtPoint() function return $.TileSource.prototype.getTileAtPoint.call(this, level, point); }, From c5404006b26119b17bd6408a89b7534c4bbbcfb5 Mon Sep 17 00:00:00 2001 From: Ruven Date: Tue, 25 Apr 2023 12:06:27 +0200 Subject: [PATCH 4/4] Further optimization: code moved into constructor thereby eliminating need for getLevelSize() function. --- src/iiiftilesource.js | 63 +++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/src/iiiftilesource.js b/src/iiiftilesource.js index 94385dc3..6db4e321 100644 --- a/src/iiiftilesource.js +++ b/src/iiiftilesource.js @@ -141,6 +141,18 @@ $.IIIFTileSource = function( options ){ } } + // Create an array with our exact resolution sizes if these have been supplied + if( this.sizes ) { + var sizeLength = this.sizes.length; + if ( (sizeLength === options.maxLevel) || (sizeLength === options.maxLevel + 1) ) { + this.levelSizes = this.sizes; + // Need to take into account that the list may or may not include the full resolution size + if( sizeLength === options.maxLevel ) { + this.levelSizes.push( {width: this.width, height: this.height} ); + } + } + } + $.TileSource.apply( this, [ options ] ); }; @@ -334,8 +346,8 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea } // Use supplied list of scaled resolution sizes if these exist - var levelSize = this.getLevelSize(level); - if( levelSize ) { + if( this.levelSizes ) { + var levelSize = this.levelSizes[level]; var x = Math.ceil( levelSize.width / this.getTileWidth(level) ), y = Math.ceil( levelSize.height / this.getTileHeight(level) ); return new $.Point( x, y ); @@ -347,37 +359,6 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea }, - /** - * Determine image size at a given resolution level using the info.json "sizes" field - * Returns null if this information is not present - * @function {Number} level - */ - getLevelSize: function( level ) { - - if (!this.sizes) { - return null; - } - - var levelWidth, levelHeight; - var numLevels = this.maxLevel - this.minLevel; - var sizeLength = this.sizes.length; - - // Need to take into account that the list may or may not include the full resolution size - if (sizeLength === numLevels) { - levelWidth = (level === sizeLength) ? this.width : this.sizes[level].width; - levelHeight = (level === sizeLength) ? this.height : this.sizes[level].height; - } else if ( sizeLength === numLevels + 1 ) { - levelWidth = this.sizes[level].width; - levelHeight = this.sizes[level].height; - } else { - // Sizes field doesn't contain resolution level sizes, so discard - return null; - } - - return {width: levelWidth, height: levelHeight}; - }, - - /** * @function * @param {Number} level @@ -390,14 +371,13 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea } // Use supplied list of scaled resolution sizes if these exist - var levelSize = this.getLevelSize(level); - if( levelSize ) { + if( this.levelSizes ) { var validPoint = point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1 / this.aspectRatio; $.console.assert(validPoint, "[TileSource.getTileAtPoint] must be called with a valid point."); - var widthScaled = levelSize.width; + var widthScaled = this.levelSizes[level].width; var pixelX = point.x * widthScaled; var pixelY = point.y * widthScaled; @@ -466,15 +446,14 @@ $.extend( $.IIIFTileSource.prototype, $.TileSource.prototype, /** @lends OpenSea uri; // Use supplied list of scaled resolution sizes if these exist - var levelSize = this.getLevelSize( level ); - if( levelSize ) { - levelWidth = levelSize.width; - levelHeight = levelSize.height; + if( this.levelSizes ) { + levelWidth = this.levelSizes[level].width; + levelHeight = this.levelSizes[level].height; } // Otherwise calculate the sizes ourselves else { - levelWidth = Math.ceil( this.width * scale ); - levelHeight = Math.ceil( this.height * scale ); + levelWidth = Math.ceil( this.width * scale ); + levelHeight = Math.ceil( this.height * scale ); } tileWidth = this.getTileWidth(level);