From c00400c500415b60df187e12b625da47f29badf7 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Wed, 13 Mar 2019 14:56:53 +0100 Subject: [PATCH 01/33] Add IIIF Image API tilesource with example src/ol/source/IIIF.js contains a tile source for IIIF Image API services. It supports Image API version 1 and 2 on compliance levels 0, 1 and 2. To get working constructor options for IIIF from an IIIF image info.json, use src/ol/format/IIIFInfo.js. An example is available in examples/iiif.html respectivly examples/iiif.js. --- examples/iiif.html | 16 +++ examples/iiif.js | 48 +++++++ src/ol/format.js | 1 + src/ol/format/IIIFInfo.js | 266 ++++++++++++++++++++++++++++++++++++++ src/ol/source.js | 1 + src/ol/source/IIIF.js | 245 +++++++++++++++++++++++++++++++++++ 6 files changed, 577 insertions(+) create mode 100644 examples/iiif.html create mode 100644 examples/iiif.js create mode 100644 src/ol/format/IIIFInfo.js create mode 100644 src/ol/source/IIIF.js diff --git a/examples/iiif.html b/examples/iiif.html new file mode 100644 index 0000000000..ced6514fb5 --- /dev/null +++ b/examples/iiif.html @@ -0,0 +1,16 @@ +--- +layout: example.html +title: IIIF Image API +shortdesc: Example of a IIIF Image API source. +docs: > + Example of a tile source for an International Image Interoperability Framework (IIIF) Image Service. + Try any Image API version 1 or 2 service. +tags: "IIIF, IIIF Image API, tile source" +--- +
+
+
 
+ Enter info.json URL: + + +
diff --git a/examples/iiif.js b/examples/iiif.js new file mode 100644 index 0000000000..e184ca037f --- /dev/null +++ b/examples/iiif.js @@ -0,0 +1,48 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import IIIF from '../src/ol/source/IIIF.js'; +import IIIFInfo from '../src/ol/format/IIIFInfo.js'; + +const layer = new TileLayer(), + map = new Map({ + layers: [layer], + target: 'map' + }); + +const notifyDiv = document.getElementById('iiif-notification'); + +function refreshMap(imageInfoUrl) { + fetch(imageInfoUrl).then(function(response) { + response.json().then(function(imageInfo) { + const options = new IIIFInfo().readFromJson(imageInfo); + if (options === undefined || options.version === undefined) { + notifyDiv.textContent = 'Data seems to be no valid IIIF image information.'; + return; + } + const extent = [0, -options.size[1], options.size[0], 0]; + layer.setSource(new IIIF(options)); + map.setView(new View({ + resolutions: layer.getSource().getTileGrid().getResolutions(), + extent: extent, + constrainOnlyCenter: true + })); + map.getView().fit(extent); + notifyDiv.textContent = ''; + }).catch(function(body) { + notifyDiv.textContent = 'Could not read image info json. ' + body; + }); + }).catch(function() { + notifyDiv.textContent = 'Could not read data from URL.'; + }); +} + +const urlInput = document.getElementById('imageInfoUrl'); +const displayButton = document.getElementById('display'); + +displayButton.addEventListener('click', function() { + const imageInfoUrl = urlInput.value; + refreshMap(imageInfoUrl); +}); + +refreshMap(urlInput.value); diff --git a/src/ol/format.js b/src/ol/format.js index a151e9e415..e18ba77069 100644 --- a/src/ol/format.js +++ b/src/ol/format.js @@ -7,6 +7,7 @@ export {default as GeoJSON} from './format/GeoJSON.js'; export {default as GML} from './format/GML.js'; export {default as GPX} from './format/GPX.js'; export {default as IGC} from './format/IGC.js'; +export {default as IIIFInfo} from './format/IIIFInfo.js'; export {default as KML} from './format/KML.js'; export {default as MVT} from './format/MVT.js'; export {default as OWS} from './format/OWS.js'; diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js new file mode 100644 index 0000000000..d88db27d22 --- /dev/null +++ b/src/ol/format/IIIFInfo.js @@ -0,0 +1,266 @@ +/** + * @module ol/format/IIIFInfo + */ + +/** + * Supported image formats, qualities and region / size calculation features + * for different image API versions and compliance levels + * @const + * @type {Object>>} + */ +const IIIF_PROFILE_VALUES = { + version1: { + level0: { + features: [], + formats: [], + qualities: ['native'] + }, + level1: { + features: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + formats: ['jpg'], + qualities: ['native'] + }, + level2: { + features: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + 'sizeByConfinedWh', 'sizeByWh'], + formats: ['jpg', 'png'], + qualities: ['native', 'color', 'grey', 'bitonal'] + } + }, + version2: { + level0: { + features: [], + formats: ['jpg'], + qualities: ['default'] + }, + level1: { + features: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + formats: ['jpg'], + qualities: ['default'] + }, + level2: { + features: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + 'sizeByConfinedWh', 'sizeByDistortedWh', 'sizeByWh'], + formats: ['jpg', 'png'], + qualities: ['default', 'bitonal'] + } + }, + version3: { + level0: { + features: [], + formats: ['jpg'], + qualities: ['default'] + }, + level1: { + features: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'], + formats: ['jpg'], + qualities: ['default'] + }, + level2: { + features: ['regionByPx', 'regionSquare', 'regionByPct', + 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'], + formats: ['jpg'], + qualities: ['default', 'bitonal'] + } + }, + none: { + features: [], + formats: [], + qualities: [] + } +}; + +export const Versions = { + VERSION1: 'version1', + VERSION2: 'version2', + VERSION3: 'version3' +}; + +function getComplianceLevelOfImageInfoForVersion(imageInfo, version) { + switch (version) { + case Versions.VERSION1: + case Versions.VERSION3: + return imageInfo.profile; + case Versions.VERSION2: + if (typeof imageInfo.profile === 'string') { + return imageInfo.profile; + } + if (Array.isArray(imageInfo.profile) && imageInfo.profile.length > 0 + && typeof imageInfo.profile[0] === 'string') { + return imageInfo.profile[0]; + } + // TODO error: cannot get compliance level URL / string + break; + default: + // TODO error: invalid Image API version + } +} + +function getVersionOfImageInfo(imageInfo) { + const context = imageInfo['@context'] || undefined; + switch (context) { + case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': + case 'http://iiif.io/api/image/1/context.json': + return Versions.VERSION1; + case 'http://iiif.io/api/image/2/context.json': + return Versions.VERSION2; + case 'http://iiif.io/api/image/3/context.json': + return Versions.VERSION3; + case undefined: + // Image API 1.0 has no '@context' + if (getComplianceLevelOfImageInfoForVersion(imageInfo, Versions.VERSION1)) { + return Versions.VERSION1; + } + // TODO error: can't detect Image API version + break; + default: + // TODO error: can't detect Image API version + } +} + +function getLevelProfileForImageInfo(imageInfo) { + const version = getVersionOfImageInfo(imageInfo), + complianceLevel = getComplianceLevelOfImageInfoForVersion(imageInfo, version); + let level; + if (version === undefined || complianceLevel === undefined) { + return IIIF_PROFILE_VALUES.none; + } + level = complianceLevel.match(/level[0-2](\.json)?$/g); + level = Array.isArray(level) ? level[0].replace('.json', '') : 'none'; + return IIIF_PROFILE_VALUES[version][level]; +} + +function generateVersion1Options(imageInfo) { + const levelProfile = getLevelProfileForImageInfo(imageInfo); + return { + url: imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + features: levelProfile.features, + formats: [...levelProfile.formats, imageInfo.formats === undefined ? + [] : imageInfo.formats + ], + qualities: [...levelProfile.qualities, imageInfo.qualities === undefined ? + [] : imageInfo.qualities + ], + resolutions: imageInfo.scale_factors, + tileSize: imageInfo.tile_width !== undefined ? imageInfo.tile_height != undefined ? + [imageInfo.tile_width, imageInfo.tile_height] : [imageInfo.tile_width, imageInfo.tile_width] : + imageInfo.tile_height != undefined ? [imageInfo.tile_height, imageInfo.tile_height] : undefined + }; +} + +function generateVersion2Options(imageInfo) { + const levelProfile = getLevelProfileForImageInfo(imageInfo), + additionalProfile = Array.isArray(imageInfo.profile) && imageInfo.profile.length > 1, + profileFeatures = additionalProfile && imageInfo.profile[1].supports ? imageInfo.profile[1].supports : [], + profileFormats = additionalProfile && imageInfo.profile[1].formats ? imageInfo.profile[1].formats : [], + profileQualities = additionalProfile && imageInfo.profile[1].qualities ? imageInfo.profile[1].qualities : [], + attributions = []; + if (imageInfo.attribution !== undefined) { + // TODO potentially dangerous + attributions.push(imageInfo.attribution); + } + if (imageInfo.license !== undefined) { + let license = imageInfo.license; + if (license.match(/^http(s)?:\/\//g)) { + license = '' + encodeURI(license) + ''; + } + // TODO potentially dangerous + attributions.push(license); + } + return { + url: imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + sizes: imageInfo.sizes === undefined ? undefined : imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: imageInfo.tiles === undefined ? undefined : [ + imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + imageInfo.tiles.map(function(tile) { + return tile.height; + })[0] + ], + resolutions: imageInfo.tiles === undefined ? undefined : + imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + features: [...levelProfile.features, ...profileFeatures], + formats: [...levelProfile.formats, ...profileFormats], + qualities: [...levelProfile.qualities, ...profileQualities], + attributions: attributions.length == 0 ? undefined : attributions + }; +} + +function generateVersion3Options(imageInfo) { + const levelProfile = getLevelProfileForImageInfo(imageInfo); + return { + url: imageInfo['id'], + sizes: imageInfo.sizes === undefined ? undefined : imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: imageInfo.tiles === undefined ? undefined : [ + imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + imageInfo.tiles.map(function(tile) { + return tile.height; + })[0] + ], + resolutions: imageInfo.tiles === undefined ? undefined : + imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + features: imageInfo.extraFeatures === undefined ? levelProfile.features : + [...levelProfile.features, ...imageInfo.extraFeatures], + formats: imageInfo.extraFormats === undefined ? levelProfile.formats : + [...levelProfile.formats, ...imageInfo.extraFormats], + qualities: imageInfo.extraQualities === undefined ? levelProfile.qualities : + [...levelProfile.extraQualities, ...imageInfo.extraQualities], + maxWidth: undefined, + maxHeight: undefined, + maxArea: undefined, + attributions: undefined + }; +} + +const versionFunctions = {}; +versionFunctions[Versions.VERSION1] = generateVersion1Options; +versionFunctions[Versions.VERSION2] = generateVersion2Options; +versionFunctions[Versions.VERSION3] = generateVersion3Options; + +function getOptionsForImageInformation(imageInfo, preferredOptions) { + const options = preferredOptions || {}, + version = getVersionOfImageInfo(imageInfo), + optionAttributions = options.attributions ? options.attributions : [], + imageOptions = version === undefined ? undefined : versionFunctions[version](imageInfo); + if (imageOptions === undefined) { + return; + } + return { + url: options.url ? options.url : imageOptions.url, + version: version, + size: [imageInfo.width, imageInfo.height], + sizes: imageOptions.sizes, + format: imageOptions.formats.includes(options.format) ? options.format : 'jpg', + features: imageOptions.features, + quality: options.quality && imageOptions.qualities.includes(options.quality) ? + options.quality : imageOptions.qualities.includes('native') ? 'native' : 'default', + resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) { + return b - a; + }) : undefined, + tileSize: imageOptions.tileSize, + attributions: [ + ...optionAttributions, + ...(imageOptions.attributions === undefined ? [] : imageOptions.attributions) + ] + }; +} + +// TODO at the moment, this does not need to be a class. +class IIIFInfo { + readFromJson(imageInfo, preferredOptions) { + return getOptionsForImageInformation(imageInfo, preferredOptions); + } +} + +export default IIIFInfo; diff --git a/src/ol/source.js b/src/ol/source.js index bbf77333ea..155d039c5b 100644 --- a/src/ol/source.js +++ b/src/ol/source.js @@ -5,6 +5,7 @@ export {default as BingMaps} from './source/BingMaps.js'; export {default as CartoDB} from './source/CartoDB.js'; export {default as Cluster} from './source/Cluster.js'; +export {default as IIIF} from './source/IIIF.js'; export {default as Image} from './source/Image.js'; export {default as ImageArcGISRest} from './source/ImageArcGISRest.js'; export {default as ImageCanvas} from './source/ImageCanvas.js'; diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js new file mode 100644 index 0000000000..e862c5d917 --- /dev/null +++ b/src/ol/source/IIIF.js @@ -0,0 +1,245 @@ +/** + * @module ol/source/IIIF + */ + +import {DEFAULT_TILE_SIZE} from '../tilegrid/common.js'; +import {getTopLeft} from '../extent.js'; +import {CustomTile} from './Zoomify.js'; +import {Versions} from '../format/IIIFInfo.js'; +import {assert} from '../asserts.js'; +import TileGrid from '../tilegrid/TileGrid.js'; +import TileImage from './TileImage.js'; + +/** + * @typedef {Object} Options + * @property {import("./Source.js").AttributionLike} [attributions] Attributions. + * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible. + * @property {number} [cacheSize] + * @property {null|string} [crossOrigin] + * @property {import("../proj.js").ProjectionLike} [projection] + * @property {number} [tilePixelRatio] + * @property {number} [reprojectionErrorThreshold=0.5] + * @property {string} [url] Base URL of the IIIF Image service. + * This shoulf be the same as the IIIF Image @id. + * @property {import("../size.js").Size} [size] Size of the image [width, height]. + * @property {import("../size.js").Size[]} [sizes] Supported scaled image sizes. + * Content of the IIIF info.json 'sizes' property, but as array of Size objects. + * @property {import("../extent.js").Extent} [extent=[0, -height, width, 0]] + * @property {number} [transition] + * @property {number|import("../size.js").Size} [tileSize] Tile size. + * Same tile size is used for all zoom levels. If tile size is a number, + * a square tile is assumed. If the IIIF image service supports arbitrary + * tiling (sizeByH, sizeByW or sizeByPct as well as regionByPx and regionByPct + * are supported), the default tilesize is 256. + * @property {boolean} [wrapX=false] + */ + + +/** + * @classdesc + * Layer source for tile data in IIIF format. + * @api + */ +class IIIF extends TileImage { + + constructor(opt_options) { + + const options = opt_options || {}; + + let baseUrl = options.url || ''; + baseUrl = baseUrl + (baseUrl.lastIndexOf('/') === baseUrl.length - 1 || baseUrl === '' ? '' : '/'); + const version = options.version || Versions.VERSION2; + const sizes = options.sizes || []; + const size = options.size; + // TODO Appropriate error code + assert(size != undefined && Array.isArray(size) && size.length == 2 && + !isNaN(size[0]) && size[0] > 0 && !isNaN(size[1] && size[1] > 0), 999); + const width = size[0]; + const height = size[1]; + const tileSize = options.tileSize; + const format = options.format || 'jpg'; + const quality = options.quality || (options.version == Versions.VERSION1 ? 'native' : 'default'); + let resolutions = options.resolutions || []; + const features = options.features || []; + const extent = options.extent || [0, -height, width, 0]; + + const supportsListedSizes = sizes != undefined && Array.isArray(sizes) && sizes.length > 0; + const supportsListedTiles = tileSize != undefined && (Number.isInteger(tileSize) && tileSize > 0 || Array.isArray(tileSize) && tileSize.length > 0); + const supportsArbitraryTiling = features != undefined && Array.isArray(features) && + (features.includes('regionByPx') || features.includes('regionByPct')) && + (features.includes('sizeByWh') || features.includes('sizeByH') || + features.includes('sizeByW') || features.includes('sizeByPct')); + + let tileWidth, + tileHeight, + maxZoom; + + resolutions.sort(function(a, b) { + return b - a; + }); + + if (supportsListedTiles || supportsArbitraryTiling) { + if (tileSize != undefined) { + if (Number.isInteger(tileSize) && tileSize > 0) { + tileWidth = tileSize; + tileHeight = tileSize; + } else if (Array.isArray(tileSize) && tileSize.length > 0) { + if (tileSize.length == 1 || tileSize[1] == undefined && Number.isInteger(tileSize[0])) { + tileWidth = tileSize[0]; + tileHeight = tileSize[0]; + } + if (tileSize.length == 2) { + if (Number.isInteger(tileSize[0]) && Number.isInteger(tileSize[1])) { + tileWidth = tileSize[0]; + tileHeight = tileSize[1]; + } else if (tileSize[0] == undefined && Number.isInteger(tileSize[1])) { + tileWidth = tileSize[1]; + tileHeight = tileSize[1]; + } + } + } + } + if (tileWidth === undefined || tileHeight === undefined) { + tileWidth = DEFAULT_TILE_SIZE; + tileHeight = DEFAULT_TILE_SIZE; + } + if (resolutions.length == 0) { + maxZoom = Math.max( + Math.ceil(Math.log(width / tileWidth) / Math.LN2), + Math.ceil(Math.log(height / tileHeight) / Math.LN2) + ); + for (let i = maxZoom; i >= 0; i--) { + resolutions.push(Math.pow(2, i)); + } + } else { + const maxScaleFactor = Math.max([...resolutions]); + // TODO maxScaleFactor might not be a power to 2 + maxZoom = Math.round(Math.log(maxScaleFactor) / Math.LN2); + } + } else { + // No tile support. + tileWidth = width; + tileHeight = height; + resolutions = []; + if (supportsListedSizes) { + /* + * 'sizes' provided. Use full region in different resolutions. Every + * resolution has only one tile. + */ + sizes.sort(function(a, b) { + return a[0] - b[0]; + }); + for (let i = 0; i < sizes.length; i++) { + const resolution = width / sizes[i][0]; + resolutions.push(resolution); + maxZoom = i; + } + } else { + // No useful image information at all. Try pseudo tile with full image. + resolutions.push(1); + sizes.push([width, height]); + maxZoom = 0; + } + } + + const tileGrid = new TileGrid({ + tileSize: [tileWidth, tileHeight], + extent: extent, + origin: getTopLeft(extent), + resolutions: resolutions + }); + + const tileUrlFunction = function(tileCoord, pixelRatio, projection) { + let regionParam, + sizeParam; + const zoom = tileCoord[0]; + if (maxZoom < zoom) { + return; + } + const tileX = tileCoord[1], + tileY = tileCoord[2], + scale = resolutions[zoom]; + if (tileX < 0 || Math.ceil(width / scale / tileWidth) <= tileX || + tileY < 0 || Math.ceil(height / scale / tileHeight) <= tileY) { + return; + } + if (supportsArbitraryTiling || supportsListedTiles) { + const regionX = tileX * tileWidth * scale, + regionY = tileY * tileHeight * scale; + let regionW = tileWidth * scale, + regionH = tileHeight * scale, + sizeW = tileWidth, + sizeH = tileHeight; + if (regionX + regionW > width) { + regionW = width - regionX; + } + if (regionY + regionH > height) { + regionH = height - regionY; + } + if (regionX + tileWidth * scale > width) { + sizeW = Math.floor((width - regionX + scale - 1) / scale); + } + if (regionY + tileHeight * scale > height) { + sizeH = Math.floor((height - regionY + scale - 1) / scale); + } + const sizeHBySizeW = Math.round(sizeW / regionW * regionH), + sizeWBySizeH = Math.round(sizeH / regionH * regionW), + preferSizeByH = (sizeHBySizeW > sizeH) && (sizeW == sizeWBySizeH); + if (regionX == 0 && regionW == width && regionY == 0 && regionH == height) { + // canonical full image region parameter is 'full', not 'x,y,w,h' + regionParam = 'full'; + } else if (!supportsArbitraryTiling || features.includes('regionByPx')) { + regionParam = regionX + ',' + regionY + ',' + regionW + ',' + regionH; + } else if (features.includes('regionByPct')) { + const pctX = regionX / width * 100, + pctY = regionY / height * 100, + pctW = regionW / width * 100, + pctH = regionH / height * 100; + regionParam = 'pct:' + pctX + ',' + pctY + ',' + pctW + ',' + pctH; + } + if (version == Versions.VERSION3 && (!supportsArbitraryTiling || features.includes('sizeByWh'))) { + sizeParam = sizeW + ',' + sizeH; + } else if (!supportsArbitraryTiling || features.includes('sizeByW') && (!preferSizeByH || !(features.includes('sizeByH')))) { + sizeParam = sizeW + ','; + } else if (features.includes('sizeByH')) { + sizeParam = ',' + sizeH; + } else if (features.includes('sizeByWh')) { + sizeParam = sizeW + ',' + sizeH; + } else if (features.includes('sizeByPct')) { + sizeParam = 'pct:' + (100 / scale); + } + } else { + regionParam = 'full'; + if (supportsListedSizes) { + sizeParam = sizes[zoom][0] + ',' + (version == Versions.VERSION3 ? sizes[zoom][0] : ''); + } else { + sizeParam = version == Versions.VERSION3 ? 'max' : 'full'; + } + } + return baseUrl + regionParam + '/' + sizeParam + '/0/' + quality + '.' + format; + }; + + const IiifTileClass = CustomTile.bind(null, tileGrid); + + super({ + attributions: options.attributions, + attributionsCollapsible: options.attributionsCollapsible, + cacheSize: options.cacheSize, + crossOrigin: options.crossOrigin, + opaque: options.opaque, + projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, + state: options.state, + tileClass: IiifTileClass, + transition: options.transition, + wrapX: options.wrapX !== undefined ? options.wrapX : false, + tileGrid: tileGrid, + tilePixelRatio: options.tilePixelRatio, + tileUrlFunction: tileUrlFunction + }); + + } + +} + +export default IIIF; From 66b41a53b8d8b97954993b82640557dde670c924 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 14 Mar 2019 09:16:42 +0100 Subject: [PATCH 02/33] Fix error in IIIF v3 level0 size paramter Size request parameter in w,h form for level 0 services that only support listed sizes had been using erroneous w,w instead of w,h. --- src/ol/source/IIIF.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index e862c5d917..4748ccd0d3 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -211,7 +211,7 @@ class IIIF extends TileImage { } else { regionParam = 'full'; if (supportsListedSizes) { - sizeParam = sizes[zoom][0] + ',' + (version == Versions.VERSION3 ? sizes[zoom][0] : ''); + sizeParam = sizes[zoom][0] + ',' + (version == Versions.VERSION3 ? sizes[zoom][1] : ''); } else { sizeParam = version == Versions.VERSION3 ? 'max' : 'full'; } From 58efe1f850d5c2d613a97339ad4caad0af1cc661 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 14 Mar 2019 11:29:14 +0100 Subject: [PATCH 03/33] Limit IIIF tile source percentages to 10 decimals max IIIF Image API versions 1 and 2 recommend limiting the number of decimals digits to 10 at most. --- src/ol/source/IIIF.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 4748ccd0d3..ec49e7a9b7 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -34,6 +34,9 @@ import TileImage from './TileImage.js'; * @property {boolean} [wrapX=false] */ +function formatPercentage(percentage) { + return percentage.toLocaleString('en', {maximumFractionDigits: 10}); +} /** * @classdesc @@ -191,10 +194,10 @@ class IIIF extends TileImage { } else if (!supportsArbitraryTiling || features.includes('regionByPx')) { regionParam = regionX + ',' + regionY + ',' + regionW + ',' + regionH; } else if (features.includes('regionByPct')) { - const pctX = regionX / width * 100, - pctY = regionY / height * 100, - pctW = regionW / width * 100, - pctH = regionH / height * 100; + const pctX = formatPercentage(regionX / width * 100), + pctY = formatPercentage(regionY / height * 100), + pctW = formatPercentage(regionW / width * 100), + pctH = formatPercentage(regionH / height * 100); regionParam = 'pct:' + pctX + ',' + pctY + ',' + pctW + ',' + pctH; } if (version == Versions.VERSION3 && (!supportsArbitraryTiling || features.includes('sizeByWh'))) { @@ -206,7 +209,7 @@ class IIIF extends TileImage { } else if (features.includes('sizeByWh')) { sizeParam = sizeW + ',' + sizeH; } else if (features.includes('sizeByPct')) { - sizeParam = 'pct:' + (100 / scale); + sizeParam = 'pct:' + formatPercentage(100 / scale); } } else { regionParam = 'full'; From 0ab7ad741f935872b5d0b6d540fa371a79d24421 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 15 Mar 2019 16:02:29 +0100 Subject: [PATCH 04/33] Add error codes for IIIF errors 60: "Missing or invalid `size`." Without at least a size, no IIIF can be displayed. 61: "Cannot determine IIIF Image API version from provided image information JSON." Without finding out the version information, one could only guess how to use the image info JSON. --- doc/errors/index.md | 8 ++++++++ src/ol/format/IIIFInfo.js | 7 ++----- src/ol/source/IIIF.js | 3 +-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/errors/index.md b/doc/errors/index.md index d783685479..687d9efbf1 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -220,3 +220,11 @@ Duplicate item added to a unique collection. For example, it may be that you tr ### 59 Invalid command found in the PBF. This indicates that the loaded vector tile may be corrupt. + +### 60 + +Missing or invalid `size`. + +### 61 + +Cannot determine IIIF Image API version from provided image information JSON. diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index d88db27d22..f6ec81d261 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -89,10 +89,8 @@ function getComplianceLevelOfImageInfoForVersion(imageInfo, version) { && typeof imageInfo.profile[0] === 'string') { return imageInfo.profile[0]; } - // TODO error: cannot get compliance level URL / string - break; + return; default: - // TODO error: invalid Image API version } } @@ -111,11 +109,10 @@ function getVersionOfImageInfo(imageInfo) { if (getComplianceLevelOfImageInfoForVersion(imageInfo, Versions.VERSION1)) { return Versions.VERSION1; } - // TODO error: can't detect Image API version break; default: - // TODO error: can't detect Image API version } + assert(false, 61); } function getLevelProfileForImageInfo(imageInfo) { diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index ec49e7a9b7..05babdd608 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -54,9 +54,8 @@ class IIIF extends TileImage { const version = options.version || Versions.VERSION2; const sizes = options.sizes || []; const size = options.size; - // TODO Appropriate error code assert(size != undefined && Array.isArray(size) && size.length == 2 && - !isNaN(size[0]) && size[0] > 0 && !isNaN(size[1] && size[1] > 0), 999); + !isNaN(size[0]) && size[0] > 0 && !isNaN(size[1] && size[1] > 0), 60); const width = size[0]; const height = size[1]; const tileSize = options.tileSize; From 01a6381756795bb17370709d42f34573ed156660 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 15 Mar 2019 16:03:08 +0100 Subject: [PATCH 05/33] Change the IIIF image example image --- examples/iiif.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/iiif.html b/examples/iiif.html index ced6514fb5..8d8fadcad9 100644 --- a/examples/iiif.html +++ b/examples/iiif.html @@ -11,6 +11,6 @@ tags: "IIIF, IIIF Image API, tile source"
 
Enter info.json URL: - +
From 3f3fbe7e4b63c19a1a8f5baa3850a5c2c3e26e0f Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 21 Mar 2019 15:54:08 +0100 Subject: [PATCH 06/33] Remove IIIF source rounding workarounds Always try to use canonical URI form. If the server serves wrong tile heights for given w, shaped size request URI parameters, so be it. --- src/ol/source/IIIF.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 05babdd608..d0f4754767 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -184,9 +184,6 @@ class IIIF extends TileImage { if (regionY + tileHeight * scale > height) { sizeH = Math.floor((height - regionY + scale - 1) / scale); } - const sizeHBySizeW = Math.round(sizeW / regionW * regionH), - sizeWBySizeH = Math.round(sizeH / regionH * regionW), - preferSizeByH = (sizeHBySizeW > sizeH) && (sizeW == sizeWBySizeH); if (regionX == 0 && regionW == width && regionY == 0 && regionH == height) { // canonical full image region parameter is 'full', not 'x,y,w,h' regionParam = 'full'; @@ -201,7 +198,7 @@ class IIIF extends TileImage { } if (version == Versions.VERSION3 && (!supportsArbitraryTiling || features.includes('sizeByWh'))) { sizeParam = sizeW + ',' + sizeH; - } else if (!supportsArbitraryTiling || features.includes('sizeByW') && (!preferSizeByH || !(features.includes('sizeByH')))) { + } else if (!supportsArbitraryTiling || features.includes('sizeByW')) { sizeParam = sizeW + ','; } else if (features.includes('sizeByH')) { sizeParam = ',' + sizeH; From 063bc51c595eb457fbadc75e023ae7d74dbe84a9 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 21 Mar 2019 16:10:34 +0100 Subject: [PATCH 07/33] Rename IIIF features to supports Avoid terminology confusion between OpenLayers map features and IIIF region/size calculation capability features. --- src/ol/format/IIIFInfo.js | 34 +++++++++++++++++----------------- src/ol/source/IIIF.js | 26 ++++++++++++++------------ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index f6ec81d261..fd27433e07 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -3,7 +3,7 @@ */ /** - * Supported image formats, qualities and region / size calculation features + * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels * @const * @type {Object>>} @@ -11,17 +11,17 @@ const IIIF_PROFILE_VALUES = { version1: { level0: { - features: [], + supports: [], formats: [], qualities: ['native'] }, level1: { - features: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], formats: ['jpg'], qualities: ['native'] }, level2: { - features: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'], formats: ['jpg', 'png'], qualities: ['native', 'color', 'grey', 'bitonal'] @@ -29,17 +29,17 @@ const IIIF_PROFILE_VALUES = { }, version2: { level0: { - features: [], + supports: [], formats: ['jpg'], qualities: ['default'] }, level1: { - features: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], formats: ['jpg'], qualities: ['default'] }, level2: { - features: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByDistortedWh', 'sizeByWh'], formats: ['jpg', 'png'], qualities: ['default', 'bitonal'] @@ -47,24 +47,24 @@ const IIIF_PROFILE_VALUES = { }, version3: { level0: { - features: [], + supports: [], formats: ['jpg'], qualities: ['default'] }, level1: { - features: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'], + supports: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'], formats: ['jpg'], qualities: ['default'] }, level2: { - features: ['regionByPx', 'regionSquare', 'regionByPct', + supports: ['regionByPx', 'regionSquare', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'], formats: ['jpg'], qualities: ['default', 'bitonal'] } }, none: { - features: [], + supports: [], formats: [], qualities: [] } @@ -131,7 +131,7 @@ function generateVersion1Options(imageInfo) { const levelProfile = getLevelProfileForImageInfo(imageInfo); return { url: imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), - features: levelProfile.features, + supports: levelProfile.supports, formats: [...levelProfile.formats, imageInfo.formats === undefined ? [] : imageInfo.formats ], @@ -148,7 +148,7 @@ function generateVersion1Options(imageInfo) { function generateVersion2Options(imageInfo) { const levelProfile = getLevelProfileForImageInfo(imageInfo), additionalProfile = Array.isArray(imageInfo.profile) && imageInfo.profile.length > 1, - profileFeatures = additionalProfile && imageInfo.profile[1].supports ? imageInfo.profile[1].supports : [], + profileSupports = additionalProfile && imageInfo.profile[1].supports ? imageInfo.profile[1].supports : [], profileFormats = additionalProfile && imageInfo.profile[1].formats ? imageInfo.profile[1].formats : [], profileQualities = additionalProfile && imageInfo.profile[1].qualities ? imageInfo.profile[1].qualities : [], attributions = []; @@ -181,7 +181,7 @@ function generateVersion2Options(imageInfo) { imageInfo.tiles.map(function(tile) { return tile.scaleFactors; })[0], - features: [...levelProfile.features, ...profileFeatures], + supports: [...levelProfile.supports, ...profileSupports], formats: [...levelProfile.formats, ...profileFormats], qualities: [...levelProfile.qualities, ...profileQualities], attributions: attributions.length == 0 ? undefined : attributions @@ -207,8 +207,8 @@ function generateVersion3Options(imageInfo) { imageInfo.tiles.map(function(tile) { return tile.scaleFactors; })[0], - features: imageInfo.extraFeatures === undefined ? levelProfile.features : - [...levelProfile.features, ...imageInfo.extraFeatures], + supports: imageInfo.extraFeatures === undefined ? levelProfile.supports : + [...levelProfile.supports, ...imageInfo.extraFeatures], formats: imageInfo.extraFormats === undefined ? levelProfile.formats : [...levelProfile.formats, ...imageInfo.extraFormats], qualities: imageInfo.extraQualities === undefined ? levelProfile.qualities : @@ -239,7 +239,7 @@ function getOptionsForImageInformation(imageInfo, preferredOptions) { size: [imageInfo.width, imageInfo.height], sizes: imageOptions.sizes, format: imageOptions.formats.includes(options.format) ? options.format : 'jpg', - features: imageOptions.features, + supports: imageOptions.supports, quality: options.quality && imageOptions.qualities.includes(options.quality) ? options.quality : imageOptions.qualities.includes('native') ? 'native' : 'default', resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) { diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index d0f4754767..bf9efd93dd 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -25,6 +25,8 @@ import TileImage from './TileImage.js'; * @property {import("../size.js").Size[]} [sizes] Supported scaled image sizes. * Content of the IIIF info.json 'sizes' property, but as array of Size objects. * @property {import("../extent.js").Extent} [extent=[0, -height, width, 0]] + * @property {Array} [supports=[]] Supported IIIF region and size calculation + * features. * @property {number} [transition] * @property {number|import("../size.js").Size} [tileSize] Tile size. * Same tile size is used for all zoom levels. If tile size is a number, @@ -62,15 +64,15 @@ class IIIF extends TileImage { const format = options.format || 'jpg'; const quality = options.quality || (options.version == Versions.VERSION1 ? 'native' : 'default'); let resolutions = options.resolutions || []; - const features = options.features || []; + const supports = options.supports || []; const extent = options.extent || [0, -height, width, 0]; const supportsListedSizes = sizes != undefined && Array.isArray(sizes) && sizes.length > 0; const supportsListedTiles = tileSize != undefined && (Number.isInteger(tileSize) && tileSize > 0 || Array.isArray(tileSize) && tileSize.length > 0); - const supportsArbitraryTiling = features != undefined && Array.isArray(features) && - (features.includes('regionByPx') || features.includes('regionByPct')) && - (features.includes('sizeByWh') || features.includes('sizeByH') || - features.includes('sizeByW') || features.includes('sizeByPct')); + const supportsArbitraryTiling = supports != undefined && Array.isArray(supports) && + (supports.includes('regionByPx') || supports.includes('regionByPct')) && + (supports.includes('sizeByWh') || supports.includes('sizeByH') || + supports.includes('sizeByW') || supports.includes('sizeByPct')); let tileWidth, tileHeight, @@ -187,24 +189,24 @@ class IIIF extends TileImage { if (regionX == 0 && regionW == width && regionY == 0 && regionH == height) { // canonical full image region parameter is 'full', not 'x,y,w,h' regionParam = 'full'; - } else if (!supportsArbitraryTiling || features.includes('regionByPx')) { + } else if (!supportsArbitraryTiling || supports.includes('regionByPx')) { regionParam = regionX + ',' + regionY + ',' + regionW + ',' + regionH; - } else if (features.includes('regionByPct')) { + } else if (supports.includes('regionByPct')) { const pctX = formatPercentage(regionX / width * 100), pctY = formatPercentage(regionY / height * 100), pctW = formatPercentage(regionW / width * 100), pctH = formatPercentage(regionH / height * 100); regionParam = 'pct:' + pctX + ',' + pctY + ',' + pctW + ',' + pctH; } - if (version == Versions.VERSION3 && (!supportsArbitraryTiling || features.includes('sizeByWh'))) { + if (version == Versions.VERSION3 && (!supportsArbitraryTiling || supports.includes('sizeByWh'))) { sizeParam = sizeW + ',' + sizeH; - } else if (!supportsArbitraryTiling || features.includes('sizeByW')) { + } else if (!supportsArbitraryTiling || supports.includes('sizeByW')) { sizeParam = sizeW + ','; - } else if (features.includes('sizeByH')) { + } else if (supports.includes('sizeByH')) { sizeParam = ',' + sizeH; - } else if (features.includes('sizeByWh')) { + } else if (supports.includes('sizeByWh')) { sizeParam = sizeW + ',' + sizeH; - } else if (features.includes('sizeByPct')) { + } else if (supports.includes('sizeByPct')) { sizeParam = 'pct:' + formatPercentage(100 / scale); } } else { From e4a531de8ced9db645b86b05fcef89904231b1da Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 22 Mar 2019 09:54:33 +0100 Subject: [PATCH 08/33] Declare IIIF versions as enum --- src/ol/format/IIIFInfo.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index fd27433e07..064333ad1b 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -70,6 +70,9 @@ const IIIF_PROFILE_VALUES = { } }; +/** + * @enum {string} + */ export const Versions = { VERSION1: 'version1', VERSION2: 'version2', From f61562a51a9f1bbbc1226261b49d1bbb667b1abb Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 22 Mar 2019 09:55:22 +0100 Subject: [PATCH 09/33] Remove unsupported options, document options --- src/ol/source/IIIF.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index bf9efd93dd..2d5343436d 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -16,24 +16,29 @@ import TileImage from './TileImage.js'; * @property {boolean} [attributionsCollapsible=true] Attributions are collapsible. * @property {number} [cacheSize] * @property {null|string} [crossOrigin] + * @property {import("../extent.js").Extent} [extent=[0, -height, width, 0]] + * @property {string} [format='jpg'] Requested image format. * @property {import("../proj.js").ProjectionLike} [projection] - * @property {number} [tilePixelRatio] - * @property {number} [reprojectionErrorThreshold=0.5] - * @property {string} [url] Base URL of the IIIF Image service. - * This shoulf be the same as the IIIF Image @id. + * @property {string} [quality] Requested IIIF image quality. Default is 'native' + * for version 1, 'default' for versions 2 and 3. + * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). + * Higher values can increase reprojection performance, but decrease precision. * @property {import("../size.js").Size} [size] Size of the image [width, height]. * @property {import("../size.js").Size[]} [sizes] Supported scaled image sizes. * Content of the IIIF info.json 'sizes' property, but as array of Size objects. - * @property {import("../extent.js").Extent} [extent=[0, -height, width, 0]] + * @property {import("./State.js").default} [state] Source state. * @property {Array} [supports=[]] Supported IIIF region and size calculation * features. - * @property {number} [transition] + * @property {number} [tilePixelRatio] * @property {number|import("../size.js").Size} [tileSize] Tile size. * Same tile size is used for all zoom levels. If tile size is a number, * a square tile is assumed. If the IIIF image service supports arbitrary * tiling (sizeByH, sizeByW or sizeByPct as well as regionByPx and regionByPct * are supported), the default tilesize is 256. - * @property {boolean} [wrapX=false] + * @property {number} [transition] + * @property {string} [url] Base URL of the IIIF Image service. + * This shoulf be the same as the IIIF Image @id. + * @property {Versions} [version=Versions.VERSION2] Service's IIIF Image API version. */ function formatPercentage(percentage) { @@ -42,7 +47,7 @@ function formatPercentage(percentage) { /** * @classdesc - * Layer source for tile data in IIIF format. + * Layer source for IIIF Image API services. * @api */ class IIIF extends TileImage { @@ -227,16 +232,14 @@ class IIIF extends TileImage { attributionsCollapsible: options.attributionsCollapsible, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, - opaque: options.opaque, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: options.state, tileClass: IiifTileClass, - transition: options.transition, - wrapX: options.wrapX !== undefined ? options.wrapX : false, tileGrid: tileGrid, tilePixelRatio: options.tilePixelRatio, tileUrlFunction: tileUrlFunction + transition: options.transition, }); } From b77177ed74f3e54fc20583faea888d2e844ea09c Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 22 Mar 2019 11:21:14 +0100 Subject: [PATCH 10/33] Fix errors in IIIF code - missing import in IIIFInfo - syntax errors in IIIF --- src/ol/format/IIIFInfo.js | 2 ++ src/ol/source/IIIF.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 064333ad1b..ded0a658df 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -2,6 +2,8 @@ * @module ol/format/IIIFInfo */ +import {assert} from '../asserts.js'; + /** * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 2d5343436d..63efbdf9a8 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -238,8 +238,8 @@ class IIIF extends TileImage { tileClass: IiifTileClass, tileGrid: tileGrid, tilePixelRatio: options.tilePixelRatio, - tileUrlFunction: tileUrlFunction - transition: options.transition, + tileUrlFunction: tileUrlFunction, + transition: options.transition }); } From 3cf9b5aa2867632f5b6eda088497cbfcaad47a26 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 22 Mar 2019 12:04:56 +0100 Subject: [PATCH 11/33] Add zDirection option to IIIF tile source --- examples/iiif.js | 1 + src/ol/source/IIIF.js | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/examples/iiif.js b/examples/iiif.js index e184ca037f..845b3da75c 100644 --- a/examples/iiif.js +++ b/examples/iiif.js @@ -21,6 +21,7 @@ function refreshMap(imageInfoUrl) { return; } const extent = [0, -options.size[1], options.size[0], 0]; + options.zDirection = -1; layer.setSource(new IIIF(options)); map.setView(new View({ resolutions: layer.getSource().getTileGrid().getResolutions(), diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 63efbdf9a8..6da518cb57 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -39,6 +39,10 @@ import TileImage from './TileImage.js'; * @property {string} [url] Base URL of the IIIF Image service. * This shoulf be the same as the IIIF Image @id. * @property {Versions} [version=Versions.VERSION2] Service's IIIF Image API version. + * @property {number} [zDirection] Indicate which resolution should be used + * by a renderer if the views resolution does not match any resolution of the tile source. + * If 0, the nearest resolution will be used. If 1, the nearest lower resolution + * will be used. If -1, the nearest higher resolution will be used. */ function formatPercentage(percentage) { @@ -242,6 +246,11 @@ class IIIF extends TileImage { transition: options.transition }); + /** + * @inheritDoc + */ + this.zDirection = options.zDirection; + } } From 66b5b5d7e1f539cb60a7d8d62e0f3cc67f551ec2 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Wed, 27 Mar 2019 16:14:22 +0100 Subject: [PATCH 12/33] Cleanup IIIF example code --- examples/iiif.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/iiif.js b/examples/iiif.js index 845b3da75c..95fdf3b9f8 100644 --- a/examples/iiif.js +++ b/examples/iiif.js @@ -8,9 +8,10 @@ const layer = new TileLayer(), map = new Map({ layers: [layer], target: 'map' - }); - -const notifyDiv = document.getElementById('iiif-notification'); + }), + notifyDiv = document.getElementById('iiif-notification'), + urlInput = document.getElementById('imageInfoUrl'), + displayButton = document.getElementById('display'); function refreshMap(imageInfoUrl) { fetch(imageInfoUrl).then(function(response) { @@ -38,12 +39,8 @@ function refreshMap(imageInfoUrl) { }); } -const urlInput = document.getElementById('imageInfoUrl'); -const displayButton = document.getElementById('display'); - displayButton.addEventListener('click', function() { - const imageInfoUrl = urlInput.value; - refreshMap(imageInfoUrl); + refreshMap(urlInput.value); }); refreshMap(urlInput.value); From 27d943dcc30f28055e9d06986aa397b0ba2a64d6 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 28 Mar 2019 17:45:05 +0100 Subject: [PATCH 13/33] Add tests for IIIF tile source and fix source The tests are still incomplete. This commit also corrects the IIIF source where tests have failed. --- src/ol/source/IIIF.js | 36 +++- test/spec/ol/source/iiif.test.js | 271 +++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 test/spec/ol/source/iiif.test.js diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 6da518cb57..1dfa88dd31 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -66,7 +66,7 @@ class IIIF extends TileImage { const sizes = options.sizes || []; const size = options.size; assert(size != undefined && Array.isArray(size) && size.length == 2 && - !isNaN(size[0]) && size[0] > 0 && !isNaN(size[1] && size[1] > 0), 60); + !isNaN(size[0]) && size[0] > 0 && !isNaN(size[1]) && size[1] > 0, 60); const width = size[0]; const height = size[1]; const tileSize = options.tileSize; @@ -142,10 +142,21 @@ class IIIF extends TileImage { sizes.sort(function(a, b) { return a[0] - b[0]; }); + maxZoom = -1; + const ignoredSizesIndex = []; for (let i = 0; i < sizes.length; i++) { const resolution = width / sizes[i][0]; + if (resolutions.length > 0 && resolutions[resolutions.length - 1] == resolution) { + ignoredSizesIndex.push(i); + continue; + } resolutions.push(resolution); - maxZoom = i; + maxZoom++; + } + if (ignoredSizesIndex.length > 0) { + for (let i = 0; i < ignoredSizesIndex.length; i++) { + sizes.splice(ignoredSizesIndex[i] - i, 1); + } } } else { // No useful image information at all. Try pseudo tile with full image. @@ -166,13 +177,14 @@ class IIIF extends TileImage { let regionParam, sizeParam; const zoom = tileCoord[0]; - if (maxZoom < zoom) { + if (zoom > maxZoom) { return; } const tileX = tileCoord[1], tileY = tileCoord[2], scale = resolutions[zoom]; - if (tileX < 0 || Math.ceil(width / scale / tileWidth) <= tileX || + if (tileX === undefined || tileY === undefined || scale === undefined || + tileX < 0 || Math.ceil(width / scale / tileWidth) <= tileX || tileY < 0 || Math.ceil(height / scale / tileHeight) <= tileY) { return; } @@ -221,7 +233,21 @@ class IIIF extends TileImage { } else { regionParam = 'full'; if (supportsListedSizes) { - sizeParam = sizes[zoom][0] + ',' + (version == Versions.VERSION3 ? sizes[zoom][1] : ''); + const regionWidth = sizes[zoom][0], + regionHeight = sizes[zoom][1]; + if (version == Versions.VERSION3) { + if (regionWidth == width && regionHeight == height) { + sizeParam = 'max'; + } else { + sizeParam = regionWidth + ',' + regionHeight; + } + } else { + if (regionWidth == width) { + sizeParam = 'full'; + } else { + sizeParam = regionWidth + ','; + } + } } else { sizeParam = version == Versions.VERSION3 ? 'max' : 'full'; } diff --git a/test/spec/ol/source/iiif.test.js b/test/spec/ol/source/iiif.test.js new file mode 100644 index 0000000000..2fdd37a026 --- /dev/null +++ b/test/spec/ol/source/iiif.test.js @@ -0,0 +1,271 @@ +// import {DEFAULT_TILE_SIZE} from '../../../../src/ol/tilegrid/common.js'; +import IIIF from '../../../../src/ol/source/IIIF.js'; +import {Versions} from '../../../../src/ol/format/IIIFInfo.js'; +// import {CustomTile} from '../../../../src/ol/source/Zoomify.js'; +// import TileGrid from '../../../../src/ol/tilegrid/TileGrid.js'; + + +describe('ol.source.IIIF', function() { + const width = 2000, + height = 1500, + size = [width, height], + url = 'http://iiif.test/image-id'; + + function getMinimalSource() { + return new IIIF({ + size: size + }); + } + + function getSource(additionalOptions) { + const options = Object.assign({}, { + size: size, + url: url + }, additionalOptions === undefined ? {} : additionalOptions); + return new IIIF(options); + } + + describe('constructor', function() { + + it('requires valid size option', function() { + + expect(function() { + new IIIF(); + }).to.throwException(); + + expect(function() { + new IIIF({}); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: 100 + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [100] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [null, 100] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: ['very wide', 100] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [0, 100] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [100, null] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [100, 0] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [100, 'not that high'] + }); + }).to.throwException(); + + expect(function() { + new IIIF({ + size: [100, 200, 300] + }); + }).to.throwException(); + + let source; + + expect(function() { + source = new IIIF({ + size: [100, 200] + }); + }).to.not.throwException(); + + expect(source).to.be.a(IIIF); + + expect(function() { + getMinimalSource(); + }).to.not.throwException(); + + }); + + it('uses empty base URL, default quality, jpg format as default', function() { + + const tileUrlFunction = getMinimalSource().getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('full/full/0/default.jpg'); + + }); + + it('uses native as default quality for version 1', function() { + + const tileUrlFunction = new IIIF({ + size: size, + version: Versions.VERSION1 + }).getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('full/full/0/native.jpg'); + + }); + + it('corrects non empty base URL if trailing slash is missing', function() { + + // missing trailing slash is added + let tileUrlFunction = getSource().getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg'); + + // existent trailing slash isn't doubled + tileUrlFunction = getSource({ + url: 'http://iiif.test/other-image-id/' + }).getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/other-image-id/full/full/0/default.jpg'); + + }); + + }); + + describe('tileUrlFunction', function() { + + it('has only one resolution and one tile if no tiles, resolutions, sizes and supported features are given', function() { + + let tileUrlFunction = getSource().getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg'); + expect(tileUrlFunction([-1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([0, 1, 0])).to.be(undefined); + expect(tileUrlFunction([0, 0, 1])).to.be(undefined); + + tileUrlFunction = getSource({ + version: Versions.VERSION1 + }).getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/native.jpg'); + + tileUrlFunction = getSource({ + version: Versions.VERSION3 + }).getTileUrlFunction(); + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/max/0/default.jpg'); + + }); + + it('constructs the same number of resolutions as sizes are given', function() { + + let tileUrlFunction = getSource({ + sizes: [[2000, 1500], [1000, 750], [500, 375]] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be(undefined); + expect(tileUrlFunction([-1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([0, 1, 0])).to.be(undefined); + expect(tileUrlFunction([0, 0, 1])).to.be(undefined); + expect(tileUrlFunction([1, 1, 0])).to.be(undefined); + expect(tileUrlFunction([1, 0, 1])).to.be(undefined); + + tileUrlFunction = getSource({ + sizes: [[2000, 1500], [1000, 750], [500, 375]], + version: Versions.VERSION3 + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,750/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/max/0/default.jpg'); + + tileUrlFunction = getSource({ + sizes: [[2000, 1500], [1000, 749], [1000, 750], [500, 375], [500, 374]] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be(undefined); + + }); + + it('given resolutions without tilesize or supported features do not result in tiling', function() { + + const tileUrlFunction = getSource({ + resolutions: [16, 8, 4, 2, 1] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg'); + expect(tileUrlFunction([-1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([0, 1, 0])).to.be(undefined); + expect(tileUrlFunction([0, 0, 1])).to.be(undefined); + + }); + + it('given tilesize results in tiling with minimal resolutions set and canonical URLs', function() { + + let tileUrlFunction = getSource({ + tileSize: 512 + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg'); + expect(tileUrlFunction([-1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([0, 1, 0])).to.be(undefined); + expect(tileUrlFunction([0, 0, 1])).to.be(undefined); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,/0/default.jpg'); + expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,/0/default.jpg'); + expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg'); + expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,/0/default.jpg'); + expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be(undefined); + + tileUrlFunction = getSource({ + tileSize: 512, + version: Versions.VERSION3 + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,512/0/default.jpg'); + expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,512/0/default.jpg'); + expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,238/0/default.jpg'); + expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,238/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,512/0/default.jpg'); + expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,512/0/default.jpg'); + expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,476/0/default.jpg'); + expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,476/0/default.jpg'); + + }); + + it('', function() { + //expect(tileUrlFunction([])); + }); + + // canonical tiles + // unsufficient features, no tilesize, but resolution given: static + // sufficient features, no tilesize: default tilesize + // sufficient features, tilesize: this one + // sufficient features, all teh combinations + + }); + +}); From c25dba415fcc42a5637a6d658075a0eaf50e01a0 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 29 Mar 2019 12:19:25 +0100 Subject: [PATCH 14/33] Add more IIIF tile source tests --- test/spec/ol/source/iiif.test.js | 142 ++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 13 deletions(-) diff --git a/test/spec/ol/source/iiif.test.js b/test/spec/ol/source/iiif.test.js index 2fdd37a026..93a33e9526 100644 --- a/test/spec/ol/source/iiif.test.js +++ b/test/spec/ol/source/iiif.test.js @@ -1,8 +1,6 @@ -// import {DEFAULT_TILE_SIZE} from '../../../../src/ol/tilegrid/common.js'; +import {DEFAULT_TILE_SIZE} from '../../../../src/ol/tilegrid/common.js'; import IIIF from '../../../../src/ol/source/IIIF.js'; import {Versions} from '../../../../src/ol/format/IIIFInfo.js'; -// import {CustomTile} from '../../../../src/ol/source/Zoomify.js'; -// import TileGrid from '../../../../src/ol/tilegrid/TileGrid.js'; describe('ol.source.IIIF', function() { @@ -169,7 +167,7 @@ describe('ol.source.IIIF', function() { }); - it('constructs the same number of resolutions as sizes are given', function() { + it('constructs the same number of resolutions as distinguishable sizes are given', function() { let tileUrlFunction = getSource({ sizes: [[2000, 1500], [1000, 750], [500, 375]] @@ -205,7 +203,7 @@ describe('ol.source.IIIF', function() { }); - it('given resolutions without tilesize or supported features do not result in tiling', function() { + it('cannot provide scaled tiles sizes without provieded tilesize or supported features', function() { const tileUrlFunction = getSource({ resolutions: [16, 8, 4, 2, 1] @@ -219,7 +217,7 @@ describe('ol.source.IIIF', function() { }); - it('given tilesize results in tiling with minimal resolutions set and canonical URLs', function() { + it('provides canonical tile URLs for all necessary resolutions if only a tileSize exists', function() { let tileUrlFunction = getSource({ tileSize: 512 @@ -256,15 +254,133 @@ describe('ol.source.IIIF', function() { }); - it('', function() { - //expect(tileUrlFunction([])); + it('provides canonical tile URLs for all provided resolutions if a tileSize also exists', function() { + + const tileUrlFunction = getSource({ + tileSize: 512, + resolutions: [8, 4, 2, 1] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/250,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,/0/default.jpg'); + expect(tileUrlFunction([2, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,/0/default.jpg'); + expect(tileUrlFunction([2, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg'); + expect(tileUrlFunction([3, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,/0/default.jpg'); + expect(tileUrlFunction([3, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,/0/default.jpg'); + expect(tileUrlFunction([3, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,/0/default.jpg'); + expect(tileUrlFunction([4, 0, 0])).to.be(undefined); + }); - // canonical tiles - // unsufficient features, no tilesize, but resolution given: static - // sufficient features, no tilesize: default tilesize - // sufficient features, tilesize: this one - // sufficient features, all teh combinations + it('supports non square tiles', function() { + + let tileUrlFunction = getSource({ + tileSize: [1024, 512] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,2000,1024/1000,/0/default.jpg'); + expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,2000,476/1000,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,512/1024,/0/default.jpg'); + expect(tileUrlFunction([2, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,512/976,/0/default.jpg'); + expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,1024,476/1024,/0/default.jpg'); + expect(tileUrlFunction([2, 1, 2])).to.be('http://iiif.test/image-id/1024,1024,976,476/976,/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be(undefined); + + tileUrlFunction = getSource({ + tileSize: [1024, 512], + version: Versions.VERSION3 + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg'); + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,512/1024,512/0/default.jpg'); + + }); + + it('provides tile URLs with default tile size if sufficient supported features are provided', function() { + + let tileUrlFunction = getSource({ + supports: ['regionByPx', 'sizeByW'] + }).getTileUrlFunction(); + + const maxZoom = Math.ceil(Math.log2(width / DEFAULT_TILE_SIZE)); + + expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/' + DEFAULT_TILE_SIZE + ',/0/default.jpg'); + expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined); + + tileUrlFunction = getSource({ + supports: ['regionByPx', 'sizeByH'] + }).getTileUrlFunction(); + + expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/,' + DEFAULT_TILE_SIZE + '/0/default.jpg'); + expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined); + + tileUrlFunction = getSource({ + supports: ['regionByPx', 'sizeByWh'] + }).getTileUrlFunction(); + + expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/0/default.jpg'); + expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined); + + tileUrlFunction = getSource({ + supports: ['regionByPct', 'sizeByPct'] + }).getTileUrlFunction(); + + const tileWPct = (DEFAULT_TILE_SIZE / width * 100).toLocaleString('en', {maximumFractionDigits: 10}), + tileHPct = (DEFAULT_TILE_SIZE / height * 100).toLocaleString('en', {maximumFractionDigits: 10}); + + expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,' + tileWPct + ',' + tileHPct + '/pct:100/0/default.jpg'); + expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined); + + }); + + it('prefers canonical tile URLs', function() { + + let tileUrlFunction = getSource({ + tileSize: 512, + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByWh', 'sizeByPct'] + }).getTileUrlFunction(); + + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg'); + + tileUrlFunction = getSource({ + tileSize: 512, + version: Versions.VERSION3, + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByWh', 'sizeByPct'] + }).getTileUrlFunction(); + + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,512/0/default.jpg'); + + }); + + + it('provides correct tile URLs for percentage URL parameter values', function() { + + const tileUrlFunction = getSource({ + tileSize: 512, + supports: ['regionByPct', 'sizeByPct'] + }).getTileUrlFunction(); + + expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/pct:25/0/default.jpg'); + expect(tileUrlFunction([-1, 0, 0])).to.be(undefined); + expect(tileUrlFunction([0, 1, 0])).to.be(undefined); + expect(tileUrlFunction([0, 0, 1])).to.be(undefined); + + expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,51.2,68.2666666667/pct:50/0/default.jpg'); + expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/pct:51.2,0,48.8,68.2666666667/pct:50/0/default.jpg'); + expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/pct:0,68.2666666667,51.2,31.7333333333/pct:50/0/default.jpg'); + expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/pct:51.2,68.2666666667,48.8,31.7333333333/pct:50/0/default.jpg'); + + expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,25.6,34.1333333333/pct:100/0/default.jpg'); + expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/pct:76.8,0,23.2,34.1333333333/pct:100/0/default.jpg'); + expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/pct:0,68.2666666667,25.6,31.7333333333/pct:100/0/default.jpg'); + expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/pct:76.8,68.2666666667,23.2,31.7333333333/pct:100/0/default.jpg'); + expect(tileUrlFunction([3, 0, 0])).to.be(undefined); + + }); }); From 12154d98b467614939e45b5adbcd549f0607d49b Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 29 Mar 2019 13:16:11 +0100 Subject: [PATCH 15/33] Improve IIIF tile source documentation --- src/ol/source/IIIF.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 1dfa88dd31..9fd7d355af 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -33,11 +33,11 @@ import TileImage from './TileImage.js'; * @property {number|import("../size.js").Size} [tileSize] Tile size. * Same tile size is used for all zoom levels. If tile size is a number, * a square tile is assumed. If the IIIF image service supports arbitrary - * tiling (sizeByH, sizeByW or sizeByPct as well as regionByPx and regionByPct + * tiling (sizeByH, sizeByW, sizeByWh or sizeByPct as well as regionByPx or regionByPct * are supported), the default tilesize is 256. * @property {number} [transition] * @property {string} [url] Base URL of the IIIF Image service. - * This shoulf be the same as the IIIF Image @id. + * This should be the same as the IIIF Image @id. * @property {Versions} [version=Versions.VERSION2] Service's IIIF Image API version. * @property {number} [zDirection] Indicate which resolution should be used * by a renderer if the views resolution does not match any resolution of the tile source. From a2b39c9c535a6d1e703f14f4d8a98e24825e9d0d Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 29 Mar 2019 15:44:17 +0100 Subject: [PATCH 16/33] Clean up and document IIIFInfo parser Optional preferred options are now reduced to the two options that depend on the image service's supported features: format and quality. --- src/ol/format/IIIFInfo.js | 44 ++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index ded0a658df..ee8304d51b 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -4,6 +4,15 @@ import {assert} from '../asserts.js'; + +/** + * @typedef {Object} PreferredOptions + * @property {string} [format] Preferred image format. Will be used if the image information + * indicates support for that format. + * @property {string} [quality] IIIF image qualitiy. Will be used if the image information + * indicates support for that quality. + */ + /** * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels @@ -233,13 +242,12 @@ versionFunctions[Versions.VERSION3] = generateVersion3Options; function getOptionsForImageInformation(imageInfo, preferredOptions) { const options = preferredOptions || {}, version = getVersionOfImageInfo(imageInfo), - optionAttributions = options.attributions ? options.attributions : [], imageOptions = version === undefined ? undefined : versionFunctions[version](imageInfo); if (imageOptions === undefined) { return; } return { - url: options.url ? options.url : imageOptions.url, + url: imageOptions.url, version: version, size: [imageInfo.width, imageInfo.height], sizes: imageOptions.sizes, @@ -251,18 +259,38 @@ function getOptionsForImageInformation(imageInfo, preferredOptions) { return b - a; }) : undefined, tileSize: imageOptions.tileSize, - attributions: [ - ...optionAttributions, - ...(imageOptions.attributions === undefined ? [] : imageOptions.attributions) - ] + attributions: imageOptions.attributions }; } +/** + * @classdesc + * Format for transforming IIIF Image API image information responses into + * IIIF tile source ready options + * + * @api + */ // TODO at the moment, this does not need to be a class. class IIIFInfo { - readFromJson(imageInfo, preferredOptions) { - return getOptionsForImageInformation(imageInfo, preferredOptions); + + /** + * @param {Object} imageInfo Deserialized image information JSON response object + * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. + * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. + */ + readFromJson(imageInfo, opt_preferredOptions) { + return getOptionsForImageInformation(imageInfo, opt_preferredOptions); } + + /** + * @param {string} imageInfo Image information JSON response as string serialization. + * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. + * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. + */ + readFromJsonString(imageInfo, opt_preferredOptions) { + return getOptionsForImageInformation(JSON.parse(imageInfo), opt_preferredOptions); + } + } export default IIIFInfo; From 3895a59c5efac692e5cc66a2e50037e5afb487b8 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Wed, 10 Apr 2019 09:11:45 +0200 Subject: [PATCH 17/33] Fix typo --- test/spec/ol/source/iiif.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ol/source/iiif.test.js b/test/spec/ol/source/iiif.test.js index 93a33e9526..29c1d46955 100644 --- a/test/spec/ol/source/iiif.test.js +++ b/test/spec/ol/source/iiif.test.js @@ -203,7 +203,7 @@ describe('ol.source.IIIF', function() { }); - it('cannot provide scaled tiles sizes without provieded tilesize or supported features', function() { + it('cannot provide scaled tiles without provided tilesize or supported features', function() { const tileUrlFunction = getSource({ resolutions: [16, 8, 4, 2, 1] From 0cffee6f83987e485dcdd410afe7256b48b931d8 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Wed, 10 Apr 2019 09:24:07 +0200 Subject: [PATCH 18/33] Rename IIIFInfo format methods for more clarity --- examples/iiif.js | 2 +- src/ol/format/IIIFInfo.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/iiif.js b/examples/iiif.js index 95fdf3b9f8..3c9439166a 100644 --- a/examples/iiif.js +++ b/examples/iiif.js @@ -16,7 +16,7 @@ const layer = new TileLayer(), function refreshMap(imageInfoUrl) { fetch(imageInfoUrl).then(function(response) { response.json().then(function(imageInfo) { - const options = new IIIFInfo().readFromJson(imageInfo); + const options = new IIIFInfo().readOptionsFromJson(imageInfo); if (options === undefined || options.version === undefined) { notifyDiv.textContent = 'Data seems to be no valid IIIF image information.'; return; diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index ee8304d51b..cb5e4ccbd8 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -278,7 +278,7 @@ class IIIFInfo { * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. */ - readFromJson(imageInfo, opt_preferredOptions) { + readOptionsFromJson(imageInfo, opt_preferredOptions) { return getOptionsForImageInformation(imageInfo, opt_preferredOptions); } @@ -287,7 +287,7 @@ class IIIFInfo { * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. */ - readFromJsonString(imageInfo, opt_preferredOptions) { + readOptionsFromJsonString(imageInfo, opt_preferredOptions) { return getOptionsForImageInformation(JSON.parse(imageInfo), opt_preferredOptions); } From 7bfaa3b6ad7cc3a9b0ca35d7d7805dce0bfe255b Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 11 Apr 2019 17:17:21 +0200 Subject: [PATCH 19/33] Refactor IIIFInfo and add tests --- examples/iiif.js | 2 +- src/ol/format/IIIFInfo.js | 441 ++++++++++++++++++------------- test/spec/ol/format/iiif.test.js | 278 +++++++++++++++++++ 3 files changed, 538 insertions(+), 183 deletions(-) create mode 100644 test/spec/ol/format/iiif.test.js diff --git a/examples/iiif.js b/examples/iiif.js index 3c9439166a..23b6ce2822 100644 --- a/examples/iiif.js +++ b/examples/iiif.js @@ -16,7 +16,7 @@ const layer = new TileLayer(), function refreshMap(imageInfoUrl) { fetch(imageInfoUrl).then(function(response) { response.json().then(function(imageInfo) { - const options = new IIIFInfo().readOptionsFromJson(imageInfo); + const options = new IIIFInfo(imageInfo).getTileSourceOptions(); if (options === undefined || options.version === undefined) { notifyDiv.textContent = 'Data seems to be no valid IIIF image information.'; return; diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index cb5e4ccbd8..8b865a2def 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -13,11 +13,19 @@ import {assert} from '../asserts.js'; * indicates support for that quality. */ +/** + * @typedef {Object} SupportedFeatures + * @property {Array} [supports] Supported IIIF image size and region + * calculation features. + * @property {Array} [formats] Supported image formats. + * @property {Array} [qualities] Supported IIIF image qualities. + */ + /** * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels * @const - * @type {Object>>} + * @type {Object} */ const IIIF_PROFILE_VALUES = { version1: { @@ -90,178 +98,9 @@ export const Versions = { VERSION3: 'version3' }; -function getComplianceLevelOfImageInfoForVersion(imageInfo, version) { - switch (version) { - case Versions.VERSION1: - case Versions.VERSION3: - return imageInfo.profile; - case Versions.VERSION2: - if (typeof imageInfo.profile === 'string') { - return imageInfo.profile; - } - if (Array.isArray(imageInfo.profile) && imageInfo.profile.length > 0 - && typeof imageInfo.profile[0] === 'string') { - return imageInfo.profile[0]; - } - return; - default: - } -} - -function getVersionOfImageInfo(imageInfo) { - const context = imageInfo['@context'] || undefined; - switch (context) { - case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': - case 'http://iiif.io/api/image/1/context.json': - return Versions.VERSION1; - case 'http://iiif.io/api/image/2/context.json': - return Versions.VERSION2; - case 'http://iiif.io/api/image/3/context.json': - return Versions.VERSION3; - case undefined: - // Image API 1.0 has no '@context' - if (getComplianceLevelOfImageInfoForVersion(imageInfo, Versions.VERSION1)) { - return Versions.VERSION1; - } - break; - default: - } - assert(false, 61); -} - -function getLevelProfileForImageInfo(imageInfo) { - const version = getVersionOfImageInfo(imageInfo), - complianceLevel = getComplianceLevelOfImageInfoForVersion(imageInfo, version); - let level; - if (version === undefined || complianceLevel === undefined) { - return IIIF_PROFILE_VALUES.none; - } - level = complianceLevel.match(/level[0-2](\.json)?$/g); - level = Array.isArray(level) ? level[0].replace('.json', '') : 'none'; - return IIIF_PROFILE_VALUES[version][level]; -} - -function generateVersion1Options(imageInfo) { - const levelProfile = getLevelProfileForImageInfo(imageInfo); - return { - url: imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), - supports: levelProfile.supports, - formats: [...levelProfile.formats, imageInfo.formats === undefined ? - [] : imageInfo.formats - ], - qualities: [...levelProfile.qualities, imageInfo.qualities === undefined ? - [] : imageInfo.qualities - ], - resolutions: imageInfo.scale_factors, - tileSize: imageInfo.tile_width !== undefined ? imageInfo.tile_height != undefined ? - [imageInfo.tile_width, imageInfo.tile_height] : [imageInfo.tile_width, imageInfo.tile_width] : - imageInfo.tile_height != undefined ? [imageInfo.tile_height, imageInfo.tile_height] : undefined - }; -} - -function generateVersion2Options(imageInfo) { - const levelProfile = getLevelProfileForImageInfo(imageInfo), - additionalProfile = Array.isArray(imageInfo.profile) && imageInfo.profile.length > 1, - profileSupports = additionalProfile && imageInfo.profile[1].supports ? imageInfo.profile[1].supports : [], - profileFormats = additionalProfile && imageInfo.profile[1].formats ? imageInfo.profile[1].formats : [], - profileQualities = additionalProfile && imageInfo.profile[1].qualities ? imageInfo.profile[1].qualities : [], - attributions = []; - if (imageInfo.attribution !== undefined) { - // TODO potentially dangerous - attributions.push(imageInfo.attribution); - } - if (imageInfo.license !== undefined) { - let license = imageInfo.license; - if (license.match(/^http(s)?:\/\//g)) { - license = '' + encodeURI(license) + ''; - } - // TODO potentially dangerous - attributions.push(license); - } - return { - url: imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), - sizes: imageInfo.sizes === undefined ? undefined : imageInfo.sizes.map(function(size) { - return [size.width, size.height]; - }), - tileSize: imageInfo.tiles === undefined ? undefined : [ - imageInfo.tiles.map(function(tile) { - return tile.width; - })[0], - imageInfo.tiles.map(function(tile) { - return tile.height; - })[0] - ], - resolutions: imageInfo.tiles === undefined ? undefined : - imageInfo.tiles.map(function(tile) { - return tile.scaleFactors; - })[0], - supports: [...levelProfile.supports, ...profileSupports], - formats: [...levelProfile.formats, ...profileFormats], - qualities: [...levelProfile.qualities, ...profileQualities], - attributions: attributions.length == 0 ? undefined : attributions - }; -} - -function generateVersion3Options(imageInfo) { - const levelProfile = getLevelProfileForImageInfo(imageInfo); - return { - url: imageInfo['id'], - sizes: imageInfo.sizes === undefined ? undefined : imageInfo.sizes.map(function(size) { - return [size.width, size.height]; - }), - tileSize: imageInfo.tiles === undefined ? undefined : [ - imageInfo.tiles.map(function(tile) { - return tile.width; - })[0], - imageInfo.tiles.map(function(tile) { - return tile.height; - })[0] - ], - resolutions: imageInfo.tiles === undefined ? undefined : - imageInfo.tiles.map(function(tile) { - return tile.scaleFactors; - })[0], - supports: imageInfo.extraFeatures === undefined ? levelProfile.supports : - [...levelProfile.supports, ...imageInfo.extraFeatures], - formats: imageInfo.extraFormats === undefined ? levelProfile.formats : - [...levelProfile.formats, ...imageInfo.extraFormats], - qualities: imageInfo.extraQualities === undefined ? levelProfile.qualities : - [...levelProfile.extraQualities, ...imageInfo.extraQualities], - maxWidth: undefined, - maxHeight: undefined, - maxArea: undefined, - attributions: undefined - }; -} - -const versionFunctions = {}; -versionFunctions[Versions.VERSION1] = generateVersion1Options; -versionFunctions[Versions.VERSION2] = generateVersion2Options; -versionFunctions[Versions.VERSION3] = generateVersion3Options; - -function getOptionsForImageInformation(imageInfo, preferredOptions) { - const options = preferredOptions || {}, - version = getVersionOfImageInfo(imageInfo), - imageOptions = version === undefined ? undefined : versionFunctions[version](imageInfo); - if (imageOptions === undefined) { - return; - } - return { - url: imageOptions.url, - version: version, - size: [imageInfo.width, imageInfo.height], - sizes: imageOptions.sizes, - format: imageOptions.formats.includes(options.format) ? options.format : 'jpg', - supports: imageOptions.supports, - quality: options.quality && imageOptions.qualities.includes(options.quality) ? - options.quality : imageOptions.qualities.includes('native') ? 'native' : 'default', - resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) { - return b - a; - }) : undefined, - tileSize: imageOptions.tileSize, - attributions: imageOptions.attributions - }; -} +const COMPLIANCE_VERSION1 = new RegExp('^https?\:\/\/library\.stanford\.edu\/iiif\/image-api\/(1\.1\/)?compliance\.html#level[0-2]$'); +const COMPLIANCE_VERSION2 = new RegExp('^https?\:\/\/iiif\.io\/api\/image\/2\/level[0-2](\.json)?$'); +const COMPLIANCE_VERSION3 = new RegExp('(^https?\:\/\/iiif\.io\/api\/image\/3\/level[0-2](\.json)?$)|(^level[0-2]$)'); /** * @classdesc @@ -270,25 +109,263 @@ function getOptionsForImageInformation(imageInfo, preferredOptions) { * * @api */ -// TODO at the moment, this does not need to be a class. class IIIFInfo { /** - * @param {Object} imageInfo Deserialized image information JSON response object - * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. - * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. + * @param {Object|string} imageInfo Deserialized image information JSON response + * object or JSON response as string */ - readOptionsFromJson(imageInfo, opt_preferredOptions) { - return getOptionsForImageInformation(imageInfo, opt_preferredOptions); + constructor(imageInfo) { + this.setImageInfo(imageInfo); + this.versionFunctions = {}; + this.versionFunctions[Versions.VERSION1] = this.generateVersion1Options.bind(this); + this.versionFunctions[Versions.VERSION2] = this.generateVersion2Options.bind(this); + this.versionFunctions[Versions.VERSION3] = this.generateVersion3Options.bind(this); + } + + /** + * @param {Object|string} imageInfo Deserialized image information JSON response + * object or JSON response as string + */ + setImageInfo(imageInfo) { + if (typeof imageInfo == 'string') { + this.imageInfo = JSON.parse(imageInfo); + } else { + this.imageInfo = imageInfo; + } + } + + /** + * @returns {Versions} Major IIIF version. + */ + getImageApiVersion() { + if (this.imageInfo === undefined) { + return; + } + const context = this.imageInfo['@context'] || undefined; + switch (context) { + case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': + case 'http://iiif.io/api/image/1/context.json': + return Versions.VERSION1; + case 'http://iiif.io/api/image/2/context.json': + return Versions.VERSION2; + case 'http://iiif.io/api/image/3/context.json': + return Versions.VERSION3; + case undefined: + // Image API 1.0 has no '@context' + if (this.getComplianceLevelEntryFromProfile(Versions.VERSION1) && this.imageInfo.identifier) { + return Versions.VERSION1; + } + break; + default: + } + assert(false, 61); + } + + /** + * @param {Versions} version Optional IIIF image API version + * @returns {string} Compliance level as it appears in the IIIF image information + * response. + */ + getComplianceLevelEntryFromProfile(version) { + if (this.imageInfo === undefined || this.imageInfo.profile === undefined) { + return; + } + if (version === undefined) { + version = this.getImageApiVersion(); + } + switch (version) { + case Versions.VERSION1: + if (COMPLIANCE_VERSION1.test(this.imageInfo.profile)) { + return this.imageInfo.profile; + } + break; + case Versions.VERSION3: + if (COMPLIANCE_VERSION3.test(this.imageInfo.profile)) { + return this.imageInfo.profile; + } + break; + case Versions.VERSION2: + if (typeof this.imageInfo.profile === 'string' && COMPLIANCE_VERSION2.test(this.imageInfo.profile)) { + return this.imageInfo.profile; + } + if (Array.isArray(this.imageInfo.profile) && this.imageInfo.profile.length > 0 + && typeof this.imageInfo.profile[0] === 'string' && COMPLIANCE_VERSION2.test(this.imageInfo.profile[0])) { + return this.imageInfo.profile[0]; + } + break; + default: + } + } + + /** + * @param {Versions} version Optional IIIF image API version + * @returns {string} Compliance level, on of 'level0', 'level1' or 'level2' or undefined + */ + getComplianceLevelFromProfile(version) { + const complianceLevel = this.getComplianceLevelEntryFromProfile(version); + if (complianceLevel === undefined) { + return undefined; + } + const level = complianceLevel.match(/level[0-2](\.json)?$/g); + return Array.isArray(level) ? level[0].replace('.json', '') : 'none'; + } + + /** + * @returns {SupportedFeatures} Image formats, qualities and region / size calculation + * methods that are supported by the IIIF service. + */ + getComplianceLevelSupportedFeatures() { + if (this.imageInfo === undefined) { + return; + } + const version = this.getImageApiVersion(); + const level = this.getComplianceLevelFromProfile(version); + if (level === undefined) { + return IIIF_PROFILE_VALUES.none; + } + return IIIF_PROFILE_VALUES[version][level]; } /** - * @param {string} imageInfo Image information JSON response as string serialization. * @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality. * @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options. */ - readOptionsFromJsonString(imageInfo, opt_preferredOptions) { - return getOptionsForImageInformation(JSON.parse(imageInfo), opt_preferredOptions); + getTileSourceOptions(opt_preferredOptions) { + const options = opt_preferredOptions || {}, + version = this.getImageApiVersion(); + if (version === undefined) { + return; + } + const imageOptions = version === undefined ? undefined : this.versionFunctions[version](); + if (imageOptions === undefined) { + return; + } + return { + url: imageOptions.url, + version: version, + size: [this.imageInfo.width, this.imageInfo.height], + sizes: imageOptions.sizes, + format: imageOptions.formats.includes(options.format) ? options.format : 'jpg', + supports: imageOptions.supports, + quality: options.quality && imageOptions.qualities.includes(options.quality) ? + options.quality : imageOptions.qualities.includes('native') ? 'native' : 'default', + resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) { + return b - a; + }) : undefined, + tileSize: imageOptions.tileSize, + attributions: imageOptions.attributions + }; + } + + /** + * @private + * @returns {object} Available options + */ + generateVersion1Options() { + let levelProfile = this.getComplianceLevelSupportedFeatures(); + // Version 1.0 and 1.1 do not require a profile. + if (levelProfile === undefined) { + levelProfile = IIIF_PROFILE_VALUES.version1.level0; + } + return { + url: this.imageInfo['@id'] === undefined ? undefined : this.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + supports: levelProfile.supports, + formats: [...levelProfile.formats, this.imageInfo.formats === undefined ? + [] : this.imageInfo.formats + ], + qualities: [...levelProfile.qualities, this.imageInfo.qualities === undefined ? + [] : this.imageInfo.qualities + ], + resolutions: this.imageInfo.scale_factors, + tileSize: this.imageInfo.tile_width !== undefined ? this.imageInfo.tile_height != undefined ? + [this.imageInfo.tile_width, this.imageInfo.tile_height] : [this.imageInfo.tile_width, this.imageInfo.tile_width] : + this.imageInfo.tile_height != undefined ? [this.imageInfo.tile_height, this.imageInfo.tile_height] : undefined + }; + } + + /** + * @private + * @returns {object} Available options + */ + generateVersion2Options() { + const levelProfile = this.getComplianceLevelSupportedFeatures(), + additionalProfile = Array.isArray(this.imageInfo.profile) && this.imageInfo.profile.length > 1, + profileSupports = additionalProfile && this.imageInfo.profile[1].supports ? this.imageInfo.profile[1].supports : [], + profileFormats = additionalProfile && this.imageInfo.profile[1].formats ? this.imageInfo.profile[1].formats : [], + profileQualities = additionalProfile && this.imageInfo.profile[1].qualities ? this.imageInfo.profile[1].qualities : [], + attributions = []; + if (this.imageInfo.attribution !== undefined) { + // TODO potentially dangerous + attributions.push(this.imageInfo.attribution); + } + if (this.imageInfo.license !== undefined) { + let license = this.imageInfo.license; + if (license.match(/^http(s)?:\/\//g)) { + license = '' + encodeURI(license) + ''; + } + // TODO potentially dangerous + attributions.push(license); + } + return { + url: this.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + sizes: this.imageInfo.sizes === undefined ? undefined : this.imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: this.imageInfo.tiles === undefined ? undefined : [ + this.imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + this.imageInfo.tiles.map(function(tile) { + return tile.height; + })[0] + ], + resolutions: this.imageInfo.tiles === undefined ? undefined : + this.imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + supports: [...levelProfile.supports, ...profileSupports], + formats: [...levelProfile.formats, ...profileFormats], + qualities: [...levelProfile.qualities, ...profileQualities], + attributions: attributions.length == 0 ? undefined : attributions + }; + } + + /** + * @ignore + * @private + * @returns {object} Available options + */ + generateVersion3Options() { + const levelProfile = this.getComplianceLevelSupportedFeatures(); + return { + url: this.imageInfo['id'], + sizes: this.imageInfo.sizes === undefined ? undefined : this.imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: this.imageInfo.tiles === undefined ? undefined : [ + this.imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + this.imageInfo.tiles.map(function(tile) { + return tile.height; + })[0] + ], + resolutions: this.imageInfo.tiles === undefined ? undefined : + this.imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + supports: this.imageInfo.extraFeatures === undefined ? levelProfile.supports : + [...levelProfile.supports, ...this.imageInfo.extraFeatures], + formats: this.imageInfo.extraFormats === undefined ? levelProfile.formats : + [...levelProfile.formats, ...this.imageInfo.extraFormats], + qualities: this.imageInfo.extraQualities === undefined ? levelProfile.qualities : + [...levelProfile.extraQualities, ...this.imageInfo.extraQualities], + maxWidth: undefined, + maxHeight: undefined, + maxArea: undefined, + attributions: undefined + }; } } diff --git a/test/spec/ol/format/iiif.test.js b/test/spec/ol/format/iiif.test.js new file mode 100644 index 0000000000..ff5759d8c1 --- /dev/null +++ b/test/spec/ol/format/iiif.test.js @@ -0,0 +1,278 @@ +import IIIFInfo from '../../../../src/ol/format/IIIFInfo.js'; +import {Versions} from '../../../../src/ol/format/IIIFInfo.js'; + +describe('ol.format.IIIF', function() { + + const iiifInfo = new IIIFInfo(), + imageInfoVersion1_0Level0 = { + identifier: 'identifier-version-1.0', + width: 2000, + height: 1500, + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }, + imageInfoVersion2Level1 = { + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/version2/id', + width: 2000, + height: 1500, + profile: [ + 'http://iiif.io/api/image/2/level1.json' + ] + }, + imageInfoVersion2Level2 = { + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/version2/id', + width: 2000, + height: 1500, + profile: [ + 'http://iiif.io/api/image/2/level2.json' + ] + }; + + describe('constructor', function() { + + }); + + describe('setImageInfo', function() { + + it('can handle image info JSON as object or as string serialization', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + '@id': 'http://iiif.test/id' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION3); + + iiifInfo.setImageInfo('{"@context": "http://iiif.io/api/image/2/context.json","@id":"http://iiif.test/id"}'); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION2); + + }); + + }); + + describe('getImageApiVersion', function() { + + it('provides the correct Image API version', function() { + + iiifInfo.setImageInfo({ + '@id': 'http://iiif.test/id' + }); + expect(function() { + iiifInfo.getImageApiVersion(); + }).to.throwException(); + + iiifInfo.setImageInfo({ + 'identifier': 'http://iiif.test/id', + 'profile': 'this is no valid profile' + }); + expect(function() { + iiifInfo.getImageApiVersion(); + }).to.throwException(); + + iiifInfo.setImageInfo({ + '@context': 'this is no valid context', + '@id': 'http://iiif.test/id' + }); + expect(function() { + iiifInfo.getImageApiVersion(); + }).to.throwException(); + + iiifInfo.setImageInfo({ + 'identifier': 'http://iiif.test/id', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1); + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + '@id': 'http://iiif.test/id' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/1/context.json', + 'identifier': 'http://iiif.test/id' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION2); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'id': 'http://iiif.test/id' + }); + expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION3); + + }); + + }); + + describe('getComplianceLevelFromProfile', function() { + + it('detects the correct compliance level', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + 'profile': 'level0' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + 'profile': 'http://iiif.io/api/image/level3.json' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + 'profile': 'level1' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + 'profile': 'http://iiif.io/api/image/2/level2.json' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2'); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + 'profile': ['http://iiif.io/api/image/2/level1.json'] + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1'); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'profile': 'level4' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'profile': 'http://iiif.io/api/image/3/level3.json' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'profile': 'http://iiif.io/api/image/2/level1.json' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'profile': 'level2' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2'); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + 'profile': 'http://iiif.io/api/image/3/level1.json' + }); + expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1'); + }); + + }); + + describe('getComplianceLevelSupportedFeatures', function() { + + it('provides the correct features for given versions and compliance levels', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + 'profile': 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }); + expect(); + + }); + + }); + + describe('getTileSourceOptions', function() { + + it('produces options from minimal information responses', function() { + + let imageInfo = { + width: 2000, + height: 1500 + }; + + expect(function() { + iiifInfo.setImageInfo(imageInfo); + iiifInfo.getTileSourceOptions(); + }).to.throwException(); + + imageInfo = { + identifier: 'id', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }; + + iiifInfo.setImageInfo(imageInfo); + let options = iiifInfo.getTileSourceOptions(imageInfo); + + expect(options).to.be.an('object'); + expect(options).to.have.property('version', Versions.VERSION1); + + iiifInfo.setImageInfo(imageInfoVersion1_0Level0); + options = iiifInfo.getTileSourceOptions(); + + expect(options).to.not.be(undefined); + expect(options).to.not.be(null); + expect(options).to.have.property('version', Versions.VERSION1); + expect(options).to.have.property('size'); + expect(options.size).to.be.an('array'); + expect(options.size.length).to.be(2); + expect(options.size[0]).to.be(2000); + expect(options.size[1]).to.be(1500); + expect(options.quality).to.be('native'); + expect(options.url).to.be(undefined); + expect(options.sizes).to.be(undefined); + expect(options.tileSize).to.be(undefined); + expect(options.format).to.be('jpg'); + expect(options.supports).to.be.empty(); + + imageInfo = { + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/version2/id' + }; + iiifInfo.setImageInfo(imageInfo); + options = iiifInfo.getTileSourceOptions(); + + expect(options).to.be.an('object'); + expect(options).to.have.property('version', Versions.VERSION2); + expect(options).to.have.property('url', 'http://iiif.test/version2/id'); + expect(options).to.have.property('format', 'jpg'); + + }); + + it('uses preferred options if applicable', function() { + + iiifInfo.setImageInfo(imageInfoVersion2Level2); + const options = iiifInfo.getTileSourceOptions({ + quality: 'bitonal', + format: 'png' + }); + expect(options).to.have.property('quality', 'bitonal'); + expect(options).to.have.property('format', 'png'); + + }); + + it('ignores preferred options that are not supported', function() { + + iiifInfo.setImageInfo(imageInfoVersion2Level1); + const options = iiifInfo.getTileSourceOptions({ + quality: 'bitonal', + format: 'png' + }); + expect(options).to.have.property('quality', 'default'); + expect(options).to.have.property('format', 'jpg'); + + }); + + }); + +}); From b7c004f95c865eaa68ff55557e48c9b2177cd6a3 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 12 Apr 2019 11:13:34 +0200 Subject: [PATCH 20/33] Remove version specific IIIFInfo methods from class --- src/ol/format/IIIFInfo.js | 218 ++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 115 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 8b865a2def..2d2c384c88 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -102,6 +102,108 @@ const COMPLIANCE_VERSION1 = new RegExp('^https?\:\/\/library\.stanford\.edu\/iii const COMPLIANCE_VERSION2 = new RegExp('^https?\:\/\/iiif\.io\/api\/image\/2\/level[0-2](\.json)?$'); const COMPLIANCE_VERSION3 = new RegExp('(^https?\:\/\/iiif\.io\/api\/image\/3\/level[0-2](\.json)?$)|(^level[0-2]$)'); +function generateVersion1Options(iiifInfo) { + let levelProfile = iiifInfo.getComplianceLevelSupportedFeatures(); + // Version 1.0 and 1.1 do not require a profile. + if (levelProfile === undefined) { + levelProfile = IIIF_PROFILE_VALUES.version1.level0; + } + return { + url: iiifInfo.imageInfo['@id'] === undefined ? undefined : iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + supports: levelProfile.supports, + formats: [...levelProfile.formats, iiifInfo.imageInfo.formats === undefined ? + [] : iiifInfo.imageInfo.formats + ], + qualities: [...levelProfile.qualities, iiifInfo.imageInfo.qualities === undefined ? + [] : iiifInfo.imageInfo.qualities + ], + resolutions: iiifInfo.imageInfo.scale_factors, + tileSize: iiifInfo.imageInfo.tile_width !== undefined ? (iiifInfo.imageInfo.tile_height !== undefined ? + [iiifInfo.imageInfo.tile_width, iiifInfo.imageInfo.tile_height] : [iiifInfo.imageInfo.tile_width, iiifInfo.imageInfo.tile_width]) : + (iiifInfo.imageInfo.tile_height != undefined ? [iiifInfo.imageInfo.tile_height, iiifInfo.imageInfo.tile_height] : undefined) + }; +} + +function generateVersion2Options(iiifInfo) { + const levelProfile = iiifInfo.getComplianceLevelSupportedFeatures(), + additionalProfile = Array.isArray(iiifInfo.imageInfo.profile) && iiifInfo.imageInfo.profile.length > 1, + profileSupports = additionalProfile && iiifInfo.imageInfo.profile[1].supports ? iiifInfo.imageInfo.profile[1].supports : [], + profileFormats = additionalProfile && iiifInfo.imageInfo.profile[1].formats ? iiifInfo.imageInfo.profile[1].formats : [], + profileQualities = additionalProfile && iiifInfo.imageInfo.profile[1].qualities ? iiifInfo.imageInfo.profile[1].qualities : [], + attributions = []; + if (iiifInfo.imageInfo.attribution !== undefined) { + // TODO potentially dangerous + attributions.push(iiifInfo.imageInfo.attribution); + } + if (iiifInfo.imageInfo.license !== undefined) { + let license = iiifInfo.imageInfo.license; + if (license.match(/^http(s)?:\/\//g)) { + license = '' + encodeURI(license) + ''; + } + // TODO potentially dangerous + attributions.push(license); + } + return { + url: iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), + sizes: iiifInfo.imageInfo.sizes === undefined ? undefined : iiifInfo.imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: iiifInfo.imageInfo.tiles === undefined ? undefined : [ + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.height === undefined ? tile.width : tile.height; + })[0] + ], + resolutions: iiifInfo.imageInfo.tiles === undefined ? undefined : + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + supports: [...levelProfile.supports, ...profileSupports], + formats: [...levelProfile.formats, ...profileFormats], + qualities: [...levelProfile.qualities, ...profileQualities], + attributions: attributions.length == 0 ? undefined : attributions + }; +} + +function generateVersion3Options(iiifInfo) { + const levelProfile = iiifInfo.getComplianceLevelSupportedFeatures(); + return { + url: iiifInfo.imageInfo['id'], + sizes: iiifInfo.imageInfo.sizes === undefined ? undefined : iiifInfo.imageInfo.sizes.map(function(size) { + return [size.width, size.height]; + }), + tileSize: iiifInfo.imageInfo.tiles === undefined ? undefined : [ + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.width; + })[0], + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.height; + })[0] + ], + resolutions: iiifInfo.imageInfo.tiles === undefined ? undefined : + iiifInfo.imageInfo.tiles.map(function(tile) { + return tile.scaleFactors; + })[0], + supports: iiifInfo.imageInfo.extraFeatures === undefined ? levelProfile.supports : + [...levelProfile.supports, ...iiifInfo.imageInfo.extraFeatures], + formats: iiifInfo.imageInfo.extraFormats === undefined ? levelProfile.formats : + [...levelProfile.formats, ...iiifInfo.imageInfo.extraFormats], + qualities: iiifInfo.imageInfo.extraQualities === undefined ? levelProfile.qualities : + [...levelProfile.extraQualities, ...iiifInfo.imageInfo.extraQualities], + maxWidth: undefined, + maxHeight: undefined, + maxArea: undefined, + attributions: undefined + }; +} + +const versionFunctions = {}; +versionFunctions[Versions.VERSION1] = generateVersion1Options; +versionFunctions[Versions.VERSION2] = generateVersion2Options; +versionFunctions[Versions.VERSION3] = generateVersion3Options; + /** * @classdesc * Format for transforming IIIF Image API image information responses into @@ -117,10 +219,6 @@ class IIIFInfo { */ constructor(imageInfo) { this.setImageInfo(imageInfo); - this.versionFunctions = {}; - this.versionFunctions[Versions.VERSION1] = this.generateVersion1Options.bind(this); - this.versionFunctions[Versions.VERSION2] = this.generateVersion2Options.bind(this); - this.versionFunctions[Versions.VERSION3] = this.generateVersion3Options.bind(this); } /** @@ -237,7 +335,7 @@ class IIIFInfo { if (version === undefined) { return; } - const imageOptions = version === undefined ? undefined : this.versionFunctions[version](); + const imageOptions = version === undefined ? undefined : versionFunctions[version](this); if (imageOptions === undefined) { return; } @@ -258,116 +356,6 @@ class IIIFInfo { }; } - /** - * @private - * @returns {object} Available options - */ - generateVersion1Options() { - let levelProfile = this.getComplianceLevelSupportedFeatures(); - // Version 1.0 and 1.1 do not require a profile. - if (levelProfile === undefined) { - levelProfile = IIIF_PROFILE_VALUES.version1.level0; - } - return { - url: this.imageInfo['@id'] === undefined ? undefined : this.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), - supports: levelProfile.supports, - formats: [...levelProfile.formats, this.imageInfo.formats === undefined ? - [] : this.imageInfo.formats - ], - qualities: [...levelProfile.qualities, this.imageInfo.qualities === undefined ? - [] : this.imageInfo.qualities - ], - resolutions: this.imageInfo.scale_factors, - tileSize: this.imageInfo.tile_width !== undefined ? this.imageInfo.tile_height != undefined ? - [this.imageInfo.tile_width, this.imageInfo.tile_height] : [this.imageInfo.tile_width, this.imageInfo.tile_width] : - this.imageInfo.tile_height != undefined ? [this.imageInfo.tile_height, this.imageInfo.tile_height] : undefined - }; - } - - /** - * @private - * @returns {object} Available options - */ - generateVersion2Options() { - const levelProfile = this.getComplianceLevelSupportedFeatures(), - additionalProfile = Array.isArray(this.imageInfo.profile) && this.imageInfo.profile.length > 1, - profileSupports = additionalProfile && this.imageInfo.profile[1].supports ? this.imageInfo.profile[1].supports : [], - profileFormats = additionalProfile && this.imageInfo.profile[1].formats ? this.imageInfo.profile[1].formats : [], - profileQualities = additionalProfile && this.imageInfo.profile[1].qualities ? this.imageInfo.profile[1].qualities : [], - attributions = []; - if (this.imageInfo.attribution !== undefined) { - // TODO potentially dangerous - attributions.push(this.imageInfo.attribution); - } - if (this.imageInfo.license !== undefined) { - let license = this.imageInfo.license; - if (license.match(/^http(s)?:\/\//g)) { - license = '' + encodeURI(license) + ''; - } - // TODO potentially dangerous - attributions.push(license); - } - return { - url: this.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), - sizes: this.imageInfo.sizes === undefined ? undefined : this.imageInfo.sizes.map(function(size) { - return [size.width, size.height]; - }), - tileSize: this.imageInfo.tiles === undefined ? undefined : [ - this.imageInfo.tiles.map(function(tile) { - return tile.width; - })[0], - this.imageInfo.tiles.map(function(tile) { - return tile.height; - })[0] - ], - resolutions: this.imageInfo.tiles === undefined ? undefined : - this.imageInfo.tiles.map(function(tile) { - return tile.scaleFactors; - })[0], - supports: [...levelProfile.supports, ...profileSupports], - formats: [...levelProfile.formats, ...profileFormats], - qualities: [...levelProfile.qualities, ...profileQualities], - attributions: attributions.length == 0 ? undefined : attributions - }; - } - - /** - * @ignore - * @private - * @returns {object} Available options - */ - generateVersion3Options() { - const levelProfile = this.getComplianceLevelSupportedFeatures(); - return { - url: this.imageInfo['id'], - sizes: this.imageInfo.sizes === undefined ? undefined : this.imageInfo.sizes.map(function(size) { - return [size.width, size.height]; - }), - tileSize: this.imageInfo.tiles === undefined ? undefined : [ - this.imageInfo.tiles.map(function(tile) { - return tile.width; - })[0], - this.imageInfo.tiles.map(function(tile) { - return tile.height; - })[0] - ], - resolutions: this.imageInfo.tiles === undefined ? undefined : - this.imageInfo.tiles.map(function(tile) { - return tile.scaleFactors; - })[0], - supports: this.imageInfo.extraFeatures === undefined ? levelProfile.supports : - [...levelProfile.supports, ...this.imageInfo.extraFeatures], - formats: this.imageInfo.extraFormats === undefined ? levelProfile.formats : - [...levelProfile.formats, ...this.imageInfo.extraFormats], - qualities: this.imageInfo.extraQualities === undefined ? levelProfile.qualities : - [...levelProfile.extraQualities, ...this.imageInfo.extraQualities], - maxWidth: undefined, - maxHeight: undefined, - maxArea: undefined, - attributions: undefined - }; - } - } export default IIIFInfo; From b36ad87cb51112ad05f1eae889a65d8bb40e68bc Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 12 Apr 2019 11:13:50 +0200 Subject: [PATCH 21/33] Extend IIIFInfo tests --- test/spec/ol/format/iiif.test.js | 337 +++++++++++++++++++++++++------ 1 file changed, 273 insertions(+), 64 deletions(-) diff --git a/test/spec/ol/format/iiif.test.js b/test/spec/ol/format/iiif.test.js index ff5759d8c1..ce05f1e9c1 100644 --- a/test/spec/ol/format/iiif.test.js +++ b/test/spec/ol/format/iiif.test.js @@ -1,37 +1,9 @@ import IIIFInfo from '../../../../src/ol/format/IIIFInfo.js'; import {Versions} from '../../../../src/ol/format/IIIFInfo.js'; -describe('ol.format.IIIF', function() { +describe('ol.format.IIIFInfo', function() { - const iiifInfo = new IIIFInfo(), - imageInfoVersion1_0Level0 = { - identifier: 'identifier-version-1.0', - width: 2000, - height: 1500, - profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' - }, - imageInfoVersion2Level1 = { - '@context': 'http://iiif.io/api/image/2/context.json', - '@id': 'http://iiif.test/version2/id', - width: 2000, - height: 1500, - profile: [ - 'http://iiif.io/api/image/2/level1.json' - ] - }, - imageInfoVersion2Level2 = { - '@context': 'http://iiif.io/api/image/2/context.json', - '@id': 'http://iiif.test/version2/id', - width: 2000, - height: 1500, - profile: [ - 'http://iiif.io/api/image/2/level2.json' - ] - }; - - describe('constructor', function() { - - }); + const iiifInfo = new IIIFInfo(); describe('setImageInfo', function() { @@ -62,8 +34,8 @@ describe('ol.format.IIIF', function() { }).to.throwException(); iiifInfo.setImageInfo({ - 'identifier': 'http://iiif.test/id', - 'profile': 'this is no valid profile' + identifier: 'http://iiif.test/id', + profile: 'this is no valid profile' }); expect(function() { iiifInfo.getImageApiVersion(); @@ -78,7 +50,7 @@ describe('ol.format.IIIF', function() { }).to.throwException(); iiifInfo.setImageInfo({ - 'identifier': 'http://iiif.test/id', + identifier: 'http://iiif.test/id', profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' }); expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1); @@ -91,7 +63,7 @@ describe('ol.format.IIIF', function() { iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/1/context.json', - 'identifier': 'http://iiif.test/id' + identifier: 'http://iiif.test/id' }); expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1); @@ -103,7 +75,7 @@ describe('ol.format.IIIF', function() { iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'id': 'http://iiif.test/id' + id: 'http://iiif.test/id' }); expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION3); @@ -117,61 +89,61 @@ describe('ol.format.IIIF', function() { iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', - 'profile': 'level0' + profile: 'level0' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', - 'profile': 'http://iiif.io/api/image/level3.json' + profile: 'http://iiif.io/api/image/level3.json' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', - 'profile': 'level1' + profile: 'level1' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', - 'profile': 'http://iiif.io/api/image/2/level2.json' + profile: 'http://iiif.io/api/image/2/level2.json' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2'); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', - 'profile': ['http://iiif.io/api/image/2/level1.json'] + profile: ['http://iiif.io/api/image/2/level1.json'] }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1'); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'profile': 'level4' + profile: 'level4' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'profile': 'http://iiif.io/api/image/3/level3.json' + profile: 'http://iiif.io/api/image/3/level3.json' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'profile': 'http://iiif.io/api/image/2/level1.json' + profile: 'http://iiif.io/api/image/2/level1.json' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'profile': 'level2' + profile: 'level2' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2'); iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/3/context.json', - 'profile': 'http://iiif.io/api/image/3/level1.json' + profile: 'http://iiif.io/api/image/3/level1.json' }); expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1'); }); @@ -184,10 +156,70 @@ describe('ol.format.IIIF', function() { iiifInfo.setImageInfo({ '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', - 'profile': 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' }); - expect(); + let level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.be.empty(); + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level1' + }); + level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.have.length(4); + expect(level.supports).to.contain('regionByPx'); + expect(level.supports).to.contain('sizeByW'); + expect(level.supports).to.contain('sizeByH'); + expect(level.supports).to.contain('sizeByPct'); + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level2' + }); + level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.have.length(7); + expect(level.supports).to.contain('regionByPx'); + expect(level.supports).to.contain('regionByPct'); + expect(level.supports).to.contain('sizeByW'); + expect(level.supports).to.contain('sizeByH'); + expect(level.supports).to.contain('sizeByPct'); + expect(level.supports).to.contain('sizeByConfinedWh'); + expect(level.supports).to.contain('sizeByWh'); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + profile: 'http://iiif.io/api/image/2/level0.json' + }); + level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.be.empty(); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + profile: 'http://iiif.io/api/image/2/level1.json' + }); + level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.have.length(4); + expect(level.supports).to.contain('regionByPx'); + expect(level.supports).to.contain('sizeByW'); + expect(level.supports).to.contain('sizeByH'); + expect(level.supports).to.contain('sizeByPct'); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + profile: 'http://iiif.io/api/image/2/level2.json' + }); + level = iiifInfo.getComplianceLevelSupportedFeatures(); + expect(level.supports).to.have.length(8); + expect(level.supports).to.contain('regionByPx'); + expect(level.supports).to.contain('regionByPct'); + expect(level.supports).to.contain('sizeByW'); + expect(level.supports).to.contain('sizeByH'); + expect(level.supports).to.contain('sizeByPct'); + expect(level.supports).to.contain('sizeByConfinedWh'); + expect(level.supports).to.contain('sizeByDistortedWh'); + expect(level.supports).to.contain('sizeByWh'); + + // TODO test version 3 compliance level features once version 3 is final }); }); @@ -196,28 +228,29 @@ describe('ol.format.IIIF', function() { it('produces options from minimal information responses', function() { - let imageInfo = { - width: 2000, - height: 1500 - }; - expect(function() { - iiifInfo.setImageInfo(imageInfo); + iiifInfo.setImageInfo({ + width: 2000, + height: 1500 + }); iiifInfo.getTileSourceOptions(); }).to.throwException(); - imageInfo = { + iiifInfo.setImageInfo({ identifier: 'id', profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' - }; - - iiifInfo.setImageInfo(imageInfo); - let options = iiifInfo.getTileSourceOptions(imageInfo); + }); + let options = iiifInfo.getTileSourceOptions(); expect(options).to.be.an('object'); expect(options).to.have.property('version', Versions.VERSION1); - iiifInfo.setImageInfo(imageInfoVersion1_0Level0); + iiifInfo.setImageInfo({ + identifier: 'identifier-version-1.0', + width: 2000, + height: 1500, + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }); options = iiifInfo.getTileSourceOptions(); expect(options).to.not.be(undefined); @@ -235,11 +268,10 @@ describe('ol.format.IIIF', function() { expect(options.format).to.be('jpg'); expect(options.supports).to.be.empty(); - imageInfo = { + iiifInfo.setImageInfo({ '@context': 'http://iiif.io/api/image/2/context.json', '@id': 'http://iiif.test/version2/id' - }; - iiifInfo.setImageInfo(imageInfo); + }); options = iiifInfo.getTileSourceOptions(); expect(options).to.be.an('object'); @@ -251,7 +283,13 @@ describe('ol.format.IIIF', function() { it('uses preferred options if applicable', function() { - iiifInfo.setImageInfo(imageInfoVersion2Level2); + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/version2/id', + width: 2000, + height: 1500, + profile: ['http://iiif.io/api/image/2/level2.json'] + }); const options = iiifInfo.getTileSourceOptions({ quality: 'bitonal', format: 'png' @@ -263,7 +301,13 @@ describe('ol.format.IIIF', function() { it('ignores preferred options that are not supported', function() { - iiifInfo.setImageInfo(imageInfoVersion2Level1); + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/version2/id', + width: 2000, + height: 1500, + profile: ['http://iiif.io/api/image/2/level1.json'] + }); const options = iiifInfo.getTileSourceOptions({ quality: 'bitonal', format: 'png' @@ -273,6 +317,171 @@ describe('ol.format.IIIF', function() { }); + it('combines supported features indicated by compliance level and explicitly stated in image info', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id', + profile: ['http://iiif.io/api/image/2/level1.json', { + supports: ['regionByPct', 'sizeByWh'] + }], + }); + + let options = iiifInfo.getTileSourceOptions(); + expect(options.supports).to.contain('regionByPct'); + expect(options.supports).to.contain('sizeByWh'); + expect(options.supports).to.contain('regionByPx'); + expect(options.supports).to.contain('sizeByW'); + expect(options.supports).to.contain('sizeByH'); + expect(options.supports).to.contain('sizeByPct'); + expect(options.supports).to.have.length(6); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/3/context.json', + id: 'http://iiif.test/id', + profile: 'level1', + extraFeatures: ['regionByPct', 'sizeByPct'] + }); + + options = iiifInfo.getTileSourceOptions(); + expect(options.supports).to.contain('regionByPct'); + expect(options.supports).to.contain('sizeByPct'); + expect(options.supports).to.contain('regionByPx'); + expect(options.supports).to.contain('regionSquare'); + expect(options.supports).to.contain('sizeByW'); + expect(options.supports).to.contain('sizeByH'); + expect(options.supports).to.have.length(6); + + }); + + it('uses the first available scale factors and tile sizes', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0' + }); + let options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.be(undefined); + expect(options.tileSize).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0', + scale_factors: [1, 2, 4], + tile_width: 512 + }); + options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.have.length(3); + expect(options.resolutions).to.contain(1); + expect(options.resolutions).to.contain(2); + expect(options.resolutions).to.contain(4); + expect(options.tileSize).to.have.length(2); + expect(options.tileSize[0]).to.be(512); + expect(options.tileSize[1]).to.be(512); + + iiifInfo.setImageInfo({ + '@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0', + scale_factors: [1, 2, 4], + tile_width: 512, + tile_height: 1024 + }); + options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.have.length(3); + expect(options.resolutions).to.contain(1); + expect(options.resolutions).to.contain(2); + expect(options.resolutions).to.contain(4); + expect(options.tileSize).to.have.length(2); + expect(options.tileSize[0]).to.be(512); + expect(options.tileSize[1]).to.be(1024); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://iiif.io/api/image/2/level0.json' + }); + options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.be(undefined); + expect(options.tileSize).to.be(undefined); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://iiif.io/api/image/2/level0.json', + tiles: [{ + scaleFactors: [1,2,4], + width: 512 + }, + { + scaleFactors: [1,2,4,8,16], + width: 256 + }] + }); + options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.have.length(3); + expect(options.resolutions).to.contain(1); + expect(options.resolutions).to.contain(2); + expect(options.resolutions).to.contain(4); + expect(options.tileSize).to.have.length(2); + expect(options.tileSize[0]).to.be(512); + expect(options.tileSize[1]).to.be(512); + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id', + profile: 'http://iiif.io/api/image/2/level0.json', + tiles: [{ + scaleFactors: [1,2,4], + width: 512, + height: 1024 + }] + }); + options = iiifInfo.getTileSourceOptions(); + expect(options.resolutions).to.have.length(3); + expect(options.resolutions).to.contain(1); + expect(options.resolutions).to.contain(2); + expect(options.resolutions).to.contain(4); + expect(options.tileSize).to.have.length(2); + expect(options.tileSize[0]).to.be(512); + expect(options.tileSize[1]).to.be(1024); + + }); + + }); + + it('provides each given size in sizes as OpenLayers Size', function() { + + iiifInfo.setImageInfo({ + '@context': 'http://iiif.io/api/image/2/context.json', + '@id': 'http://iiif.test/id', + 'sizes': [{ + width: 2000, + height: 1000 + }, + { + width: 1000, + height: 500 + }, + { + width: 500, + height: 250 + }] + }); + let options = iiifInfo.getTileSourceOptions(); + expect(options.sizes).to.have.length(3); + expect(options.sizes[0]).to.have.length(2); + expect(options.sizes[0][0]).to.be(2000); + expect(options.sizes[0][1]).to.be(1000); + expect(options.sizes[1]).to.have.length(2); + expect(options.sizes[1][0]).to.be(1000); + expect(options.sizes[1][1]).to.be(500); + expect(options.sizes[2]).to.have.length(2); + expect(options.sizes[2][0]).to.be(500); + expect(options.sizes[2][1]).to.be(250); + }); }); From c2cbae95c6ab349672ff527ed174ec106a6775a2 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 12 Apr 2019 11:21:15 +0200 Subject: [PATCH 22/33] Fix codestyle in IIIFInfo tests --- test/spec/ol/format/iiif.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/spec/ol/format/iiif.test.js b/test/spec/ol/format/iiif.test.js index ce05f1e9c1..49f14fe561 100644 --- a/test/spec/ol/format/iiif.test.js +++ b/test/spec/ol/format/iiif.test.js @@ -324,7 +324,7 @@ describe('ol.format.IIIFInfo', function() { '@id': 'http://iiif.test/id', profile: ['http://iiif.io/api/image/2/level1.json', { supports: ['regionByPct', 'sizeByWh'] - }], + }] }); let options = iiifInfo.getTileSourceOptions(); @@ -412,11 +412,11 @@ describe('ol.format.IIIFInfo', function() { '@id': 'http://iiif.test/id', profile: 'http://iiif.io/api/image/2/level0.json', tiles: [{ - scaleFactors: [1,2,4], + scaleFactors: [1, 2, 4], width: 512 }, { - scaleFactors: [1,2,4,8,16], + scaleFactors: [1, 2, 4, 8, 16], width: 256 }] }); @@ -434,7 +434,7 @@ describe('ol.format.IIIFInfo', function() { '@id': 'http://iiif.test/id', profile: 'http://iiif.io/api/image/2/level0.json', tiles: [{ - scaleFactors: [1,2,4], + scaleFactors: [1, 2, 4], width: 512, height: 1024 }] @@ -470,7 +470,7 @@ describe('ol.format.IIIFInfo', function() { height: 250 }] }); - let options = iiifInfo.getTileSourceOptions(); + const options = iiifInfo.getTileSourceOptions(); expect(options.sizes).to.have.length(3); expect(options.sizes[0]).to.have.length(2); expect(options.sizes[0][0]).to.be(2000); From f68b8d8df936ce02acd4a28ee2c103b5b2cdfeda Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 12 Apr 2019 13:23:50 +0200 Subject: [PATCH 23/33] Make IIIF_PROFILE_VALUES consistent with it's doc --- src/ol/format/IIIFInfo.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 2d2c384c88..cfd15909e9 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -83,9 +83,11 @@ const IIIF_PROFILE_VALUES = { } }, none: { - supports: [], - formats: [], - qualities: [] + none: { + supports: [], + formats: [], + qualities: [] + } } }; @@ -306,7 +308,7 @@ class IIIFInfo { return undefined; } const level = complianceLevel.match(/level[0-2](\.json)?$/g); - return Array.isArray(level) ? level[0].replace('.json', '') : 'none'; + return Array.isArray(level) ? level[0].replace('.json', '') : undefined; } /** @@ -320,7 +322,7 @@ class IIIFInfo { const version = this.getImageApiVersion(); const level = this.getComplianceLevelFromProfile(version); if (level === undefined) { - return IIIF_PROFILE_VALUES.none; + return IIIF_PROFILE_VALUES.none.none; } return IIIF_PROFILE_VALUES[version][level]; } From d332b6a0f4ad664b670f14987eaa2011dc157ec1 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Tue, 16 Apr 2019 08:48:17 +0200 Subject: [PATCH 24/33] Prepare IIIFInfo for JSON-LD context extensions IIIF Image API 3 allows context extensions that should be added to the info.json's @context property. Therefore, '@context' might be an array instead of a string. --- src/ol/format/IIIFInfo.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index cfd15909e9..89a21ccd5f 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -242,22 +242,27 @@ class IIIFInfo { if (this.imageInfo === undefined) { return; } - const context = this.imageInfo['@context'] || undefined; - switch (context) { - case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': - case 'http://iiif.io/api/image/1/context.json': - return Versions.VERSION1; - case 'http://iiif.io/api/image/2/context.json': - return Versions.VERSION2; - case 'http://iiif.io/api/image/3/context.json': - return Versions.VERSION3; - case undefined: - // Image API 1.0 has no '@context' - if (this.getComplianceLevelEntryFromProfile(Versions.VERSION1) && this.imageInfo.identifier) { + let context = this.imageInfo['@context'] || 'ol-no-context'; + if (typeof context == 'string') { + context = [context]; + } + for (let i = 0; i < context.length; i++) { + switch (context[i]) { + case 'http://library.stanford.edu/iiif/image-api/1.1/context.json': + case 'http://iiif.io/api/image/1/context.json': return Versions.VERSION1; - } - break; - default: + case 'http://iiif.io/api/image/2/context.json': + return Versions.VERSION2; + case 'http://iiif.io/api/image/3/context.json': + return Versions.VERSION3; + case 'ol-no-context': + // Image API 1.0 has no '@context' + if (this.getComplianceLevelEntryFromProfile(Versions.VERSION1) && this.imageInfo.identifier) { + return Versions.VERSION1; + } + break; + default: + } } assert(false, 61); } From 67757238409d8ae8cf7ade4f2cf442199585ad19 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Tue, 16 Apr 2019 08:53:03 +0200 Subject: [PATCH 25/33] Fix error in IIIFInfo IIIF Image API v3 handling --- src/ol/format/IIIFInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 89a21ccd5f..6d23f237f9 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -193,7 +193,7 @@ function generateVersion3Options(iiifInfo) { formats: iiifInfo.imageInfo.extraFormats === undefined ? levelProfile.formats : [...levelProfile.formats, ...iiifInfo.imageInfo.extraFormats], qualities: iiifInfo.imageInfo.extraQualities === undefined ? levelProfile.qualities : - [...levelProfile.extraQualities, ...iiifInfo.imageInfo.extraQualities], + [...levelProfile.supports, ...iiifInfo.imageInfo.extraQualities], maxWidth: undefined, maxHeight: undefined, maxArea: undefined, From 72e41d37037f911cc393194c22b7a40c8a154f72 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 15:02:11 +0200 Subject: [PATCH 26/33] Remove attribution generation from IIIFInfo Generating attributions by injecting 3rd party HTML content introduces XSS vulnerabilities, so with regard to upcoming Image API changes this functionality is removed. See also https://github.com/openlayers/openlayers/pull/9430#issuecomment-482610729 --- src/ol/format/IIIFInfo.js | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 6d23f237f9..6c05e66ef0 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -131,20 +131,7 @@ function generateVersion2Options(iiifInfo) { additionalProfile = Array.isArray(iiifInfo.imageInfo.profile) && iiifInfo.imageInfo.profile.length > 1, profileSupports = additionalProfile && iiifInfo.imageInfo.profile[1].supports ? iiifInfo.imageInfo.profile[1].supports : [], profileFormats = additionalProfile && iiifInfo.imageInfo.profile[1].formats ? iiifInfo.imageInfo.profile[1].formats : [], - profileQualities = additionalProfile && iiifInfo.imageInfo.profile[1].qualities ? iiifInfo.imageInfo.profile[1].qualities : [], - attributions = []; - if (iiifInfo.imageInfo.attribution !== undefined) { - // TODO potentially dangerous - attributions.push(iiifInfo.imageInfo.attribution); - } - if (iiifInfo.imageInfo.license !== undefined) { - let license = iiifInfo.imageInfo.license; - if (license.match(/^http(s)?:\/\//g)) { - license = '' + encodeURI(license) + ''; - } - // TODO potentially dangerous - attributions.push(license); - } + profileQualities = additionalProfile && iiifInfo.imageInfo.profile[1].qualities ? iiifInfo.imageInfo.profile[1].qualities : []; return { url: iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), sizes: iiifInfo.imageInfo.sizes === undefined ? undefined : iiifInfo.imageInfo.sizes.map(function(size) { @@ -164,8 +151,7 @@ function generateVersion2Options(iiifInfo) { })[0], supports: [...levelProfile.supports, ...profileSupports], formats: [...levelProfile.formats, ...profileFormats], - qualities: [...levelProfile.qualities, ...profileQualities], - attributions: attributions.length == 0 ? undefined : attributions + qualities: [...levelProfile.qualities, ...profileQualities] }; } @@ -196,8 +182,7 @@ function generateVersion3Options(iiifInfo) { [...levelProfile.supports, ...iiifInfo.imageInfo.extraQualities], maxWidth: undefined, maxHeight: undefined, - maxArea: undefined, - attributions: undefined + maxArea: undefined }; } @@ -358,8 +343,7 @@ class IIIFInfo { resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) { return b - a; }) : undefined, - tileSize: imageOptions.tileSize, - attributions: imageOptions.attributions + tileSize: imageOptions.tileSize }; } From 84f1e0c66edf3cdebb4aa793cddd1dc772986189 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 15:26:33 +0200 Subject: [PATCH 27/33] Avoid minification/compilation errors in IIIFInfo Unquoted properties might be renamed, resulting in errors while accessing these properties via intented string values. See also: https://github.com/openlayers/openlayers/pull/9430#pullrequestreview-232622522 https://github.com/openlayers/openlayers/pull/9430#pullrequestreview-232622575 --- src/ol/format/IIIFInfo.js | 135 +++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 6c05e66ef0..9159bf5d42 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -21,83 +21,82 @@ import {assert} from '../asserts.js'; * @property {Array} [qualities] Supported IIIF image qualities. */ +/** +* @enum {string} +*/ +export const Versions = { + VERSION1: 'version1', + VERSION2: 'version2', + VERSION3: 'version3' +}; + /** * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels * @const * @type {Object} */ -const IIIF_PROFILE_VALUES = { - version1: { - level0: { - supports: [], - formats: [], - qualities: ['native'] - }, - level1: { - supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], - formats: ['jpg'], - qualities: ['native'] - }, - level2: { - supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', - 'sizeByConfinedWh', 'sizeByWh'], - formats: ['jpg', 'png'], - qualities: ['native', 'color', 'grey', 'bitonal'] - } +const IIIF_PROFILE_VALUES = {}; +IIIF_PROFILE_VALUES[Versions.VERSION1] = { + 'level0': { + supports: [], + formats: [], + qualities: ['native'] }, - version2: { - level0: { - supports: [], - formats: ['jpg'], - qualities: ['default'] - }, - level1: { - supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], - formats: ['jpg'], - qualities: ['default'] - }, - level2: { - supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', - 'sizeByConfinedWh', 'sizeByDistortedWh', 'sizeByWh'], - formats: ['jpg', 'png'], - qualities: ['default', 'bitonal'] - } + 'level1': { + supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + formats: ['jpg'], + qualities: ['native'] }, - version3: { - level0: { - supports: [], - formats: ['jpg'], - qualities: ['default'] - }, - level1: { - supports: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'], - formats: ['jpg'], - qualities: ['default'] - }, - level2: { - supports: ['regionByPx', 'regionSquare', 'regionByPct', - 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'], - formats: ['jpg'], - qualities: ['default', 'bitonal'] - } - }, - none: { - none: { - supports: [], - formats: [], - qualities: [] - } + 'level2': { + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + 'sizeByConfinedWh', 'sizeByWh'], + formats: ['jpg', 'png'], + qualities: ['native', 'color', 'grey', 'bitonal'] } }; - -/** - * @enum {string} - */ -export const Versions = { - VERSION1: 'version1', - VERSION2: 'version2', - VERSION3: 'version3' +IIIF_PROFILE_VALUES[Versions.VERSION2] = { + 'level0': { + supports: [], + formats: ['jpg'], + qualities: ['default'] + }, + 'level1': { + supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'], + formats: ['jpg'], + qualities: ['default'] + }, + 'level2': { + supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct', + 'sizeByConfinedWh', 'sizeByDistortedWh', 'sizeByWh'], + formats: ['jpg', 'png'], + qualities: ['default', 'bitonal'] + } +}; +IIIF_PROFILE_VALUES[Versions.VERSION3] = { + 'level0': { + supports: [], + formats: ['jpg'], + qualities: ['default'] + }, + 'level1': { + supports: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'], + formats: ['jpg'], + qualities: ['default'] + }, + 'level2': { + supports: ['regionByPx', 'regionSquare', 'regionByPct', + 'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'], + formats: ['jpg'], + qualities: ['default', 'bitonal'] + } +}; +IIIF_PROFILE_VALUES['none'] = { + 'none': { + supports: [], + formats: [], + qualities: [] + } }; const COMPLIANCE_VERSION1 = new RegExp('^https?\:\/\/library\.stanford\.edu\/iiif\/image-api\/(1\.1\/)?compliance\.html#level[0-2]$'); @@ -108,7 +107,7 @@ function generateVersion1Options(iiifInfo) { let levelProfile = iiifInfo.getComplianceLevelSupportedFeatures(); // Version 1.0 and 1.1 do not require a profile. if (levelProfile === undefined) { - levelProfile = IIIF_PROFILE_VALUES.version1.level0; + levelProfile = IIIF_PROFILE_VALUES[Versions.VERSION1]['level0']; } return { url: iiifInfo.imageInfo['@id'] === undefined ? undefined : iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''), From ba2c558b728ebc8a79acbf61fe344569f7225eb2 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 16:39:59 +0200 Subject: [PATCH 28/33] Add typedefs for IIIF info.json responses --- src/ol/format/IIIFInfo.js | 79 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 9159bf5d42..b917249ce2 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -21,6 +21,81 @@ import {assert} from '../asserts.js'; * @property {Array} [qualities] Supported IIIF image qualities. */ +/** + * @typedef {Object} ImageInformationResponse1_0 + * @property {string} identifier + * @property {number} width + * @property {number} height + * @property {Array} [scale_factors] Resolution scaling factors. + * @property {number} [tile_width] + * @property {number} [tile_height] + * @property {Array} [formats] Supported image formats. + * @property {string} [profile] Compliance level URI. + */ + +/** + * @typedef {Object} ImageInformationResponse1_1 + * @property {string} "@id" The base URI of the image service. + * @property {string} "@context" JSON-LD context URI. + * @property {number} width Full image width. + * @property {number} height Full image height. + * @property {Array} [scale_factors] Resolution scaling factors. + * @property {number} [tile_width] + * @property {number} [tile_height] + * @property {Array} [formats] Supported image formats. + * @property {string} [profile] Compliance level URI. + */ + +/** + * @typedef {Object} TileInfo + * @property {Array} scaleFactors Supported resolution scaling factors. + * @property {number} width Tile width in pixels. + * @property {number} [height] Tile height in pixels. Same as tile width if height is + * not given. + */ + +/** + * @typedef {Object} IiifProfile + * @property {Array} [formats] Supported image formats for the image service. + * @property {Array} [qualities] Supported IIIF image qualities. + * @property {Array} [supports] Supported features. + * @property {number} [maxArea] Maximum area (pixels) available for this image service. + * @property {number} [maxHeight] Maximum height. + * @property {number} [maxWidth] Maximum width. + */ + +/** + * @typedef {Object} ImageInformationResponse2 + * @property {string} "@id" The base URI of the image service. + * @property {string} "@context" JSON-LD context IRI + * @property {number} width Full image width. + * @property {number} height Full image height. + * @property {Array} profile Additional informations about the image + * service's capabilities. + * @property {Array>} [sizes] Supported full image dimensions. + * @property {Array} [tiles] Supported tile sizes and resolution scaling factors. + */ + +/** + * @typedef {Object} ImageInformationResponse3 + * @property {string} id The base URI of the image service. + * @property {string} "@context" JSON-LD context IRI + * @property {number} width Full image width. + * @property {number} height Full image height. + * @property {string} profile Compliance level, one of 'level0', 'level1' or 'level2' + * @property {Array>} [sizes] Supported full image dimensions. + * @property {Array} [tiles] Supported tile sizes and resolution scaling factors. + * @property {number} [maxArea] Maximum area (pixels) available for this image service. + * @property {number} [maxHeight] Maximum height. + * @property {number} [maxWidth] Maximum width. + * @property {Array} [extraQualities] IIIF image qualities supported by the + * image service additional to the ones indicated by the compliance level. + * @property {Array} [extraFormats] Image formats supported by the + * image service additional to the ones indicated by the compliance level. + * @property {Array} [extraFeatures] Additional supported features whose support + * is not indicated by the compliance level. + */ + /** * @enum {string} */ @@ -200,8 +275,8 @@ versionFunctions[Versions.VERSION3] = generateVersion3Options; class IIIFInfo { /** - * @param {Object|string} imageInfo Deserialized image information JSON response - * object or JSON response as string + * @param {ImageInformationResponse1_0|ImageInformationResponse1_1|ImageInformationResponse2|ImageInformationResponse3|string} imageInfo + * Deserialized image information JSON response object or JSON response as string */ constructor(imageInfo) { this.setImageInfo(imageInfo); From 4629fe502815d4a427e1db4106bbca86b79039ce Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 16:55:01 +0200 Subject: [PATCH 29/33] Add @api annotation for TileGrid#getExtent --- src/ol/tilegrid/TileGrid.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ol/tilegrid/TileGrid.js b/src/ol/tilegrid/TileGrid.js index 9b391c8725..a375e47749 100644 --- a/src/ol/tilegrid/TileGrid.js +++ b/src/ol/tilegrid/TileGrid.js @@ -237,6 +237,7 @@ class TileGrid { /** * Get the extent for this tile grid, if it was configured. * @return {import("../extent.js").Extent} Extent. + * @api */ getExtent() { return this.extent_; From 40ea2a8b7ecbd520f025e982a46e6a523f3733fa Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 16:55:49 +0200 Subject: [PATCH 30/33] Remove extent definition from IIIF example See https://github.com/openlayers/openlayers/pull/9430#pullrequestreview-232616993 --- examples/iiif.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/iiif.js b/examples/iiif.js index 23b6ce2822..b2fc4e4e59 100644 --- a/examples/iiif.js +++ b/examples/iiif.js @@ -21,15 +21,15 @@ function refreshMap(imageInfoUrl) { notifyDiv.textContent = 'Data seems to be no valid IIIF image information.'; return; } - const extent = [0, -options.size[1], options.size[0], 0]; options.zDirection = -1; - layer.setSource(new IIIF(options)); + const iiifTileSource = new IIIF(options); + layer.setSource(iiifTileSource); map.setView(new View({ - resolutions: layer.getSource().getTileGrid().getResolutions(), - extent: extent, + resolutions: iiifTileSource.getTileGrid().getResolutions(), + extent: iiifTileSource.getTileGrid().getExtent(), constrainOnlyCenter: true })); - map.getView().fit(extent); + map.getView().fit(iiifTileSource.getTileGrid().getExtent()); notifyDiv.textContent = ''; }).catch(function(body) { notifyDiv.textContent = 'Could not read image info json. ' + body; From 64e67ae3510865cedcf803c57707ee024a56a694 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Fri, 3 May 2019 16:59:00 +0200 Subject: [PATCH 31/33] Correct IIIF tile source doc --- src/ol/source/IIIF.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/source/IIIF.js b/src/ol/source/IIIF.js index 9fd7d355af..e165db78bb 100644 --- a/src/ol/source/IIIF.js +++ b/src/ol/source/IIIF.js @@ -23,7 +23,7 @@ import TileImage from './TileImage.js'; * for version 1, 'default' for versions 2 and 3. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. - * @property {import("../size.js").Size} [size] Size of the image [width, height]. + * @property {import("../size.js").Size} size Size of the image [width, height]. * @property {import("../size.js").Size[]} [sizes] Supported scaled image sizes. * Content of the IIIF info.json 'sizes' property, but as array of Size objects. * @property {import("./State.js").default} [state] Source state. From 7daba05548c31b315f65228c5464de939d4873d3 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Mon, 6 May 2019 14:23:12 +0200 Subject: [PATCH 32/33] Use quoted strings for accessing IIIF_PROFILE_VALUES --- src/ol/format/IIIFInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index b917249ce2..5a76f46cbd 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -386,7 +386,7 @@ class IIIFInfo { const version = this.getImageApiVersion(); const level = this.getComplianceLevelFromProfile(version); if (level === undefined) { - return IIIF_PROFILE_VALUES.none.none; + return IIIF_PROFILE_VALUES['none']['none']; } return IIIF_PROFILE_VALUES[version][level]; } From bd235b7b495ed4c7cdb1c6c3261b04000d584179 Mon Sep 17 00:00:00 2001 From: Lutz Helm Date: Thu, 9 May 2019 17:56:09 +0200 Subject: [PATCH 33/33] Fix JSDoc error in IIIFInfo --- src/ol/format/IIIFInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/format/IIIFInfo.js b/src/ol/format/IIIFInfo.js index 5a76f46cbd..2d12a8073c 100644 --- a/src/ol/format/IIIFInfo.js +++ b/src/ol/format/IIIFInfo.js @@ -109,7 +109,7 @@ export const Versions = { * Supported image formats, qualities and supported region / size calculation features * for different image API versions and compliance levels * @const - * @type {Object} + * @type {Object>} */ const IIIF_PROFILE_VALUES = {}; IIIF_PROFILE_VALUES[Versions.VERSION1] = {