UNPKG

mirador

Version:

An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.

320 lines (261 loc) 10.8 kB
import { Utils } from 'manifesto.js'; import MiradorManifest from './MiradorManifest'; import MiradorCanvas from './MiradorCanvas'; import asArray from './asArray'; /** */ function isLevel0ImageProfile(service) { const profile = service.getProfile(); // work around a bug in manifesto with normalized urls that strip # values. if (profile.endsWith('#level1') || profile.endsWith('#level2')) return false; // support IIIF v3-style profiles if (profile === 'level0') return true; return Utils.isLevel0ImageProfile(profile); } /** */ function isLevel2ImageProfile(service) { const profile = service.getProfile(); // work around a bug in manifesto with normalized urls that strip # values. if (profile.endsWith('#level0') || profile.endsWith('#level1')) return false; // support IIIF v3-style profiles if (profile === 'level2') return true; return Utils.isLevel2ImageProfile(profile); } /** */ function iiifv3ImageServiceType(service) { const type = service.getProperty('type') || []; return asArray(type).some(v => v.startsWith('ImageService')); } /** */ function iiifImageService(resource) { const service = resource && resource.getServices().find(s => ( iiifv3ImageServiceType(s) || Utils.isImageProfile(s.getProfile()) )); if (!(service)) return undefined; return service; } /** */ class ThumbnailFactory { /** */ constructor(iiifOpts = {}, { getMiradorCanvas, getMiradorManifest }) { this.iiifOpts = iiifOpts; this.getMiradorCanvas = getMiradorCanvas; this.getMiradorManifest = getMiradorManifest; } /** */ static staticImageUrl(resource) { return { height: resource.getProperty('height'), url: resource.id, width: resource.getProperty('width') }; } /** * Selects the image resource that is representative of the given canvas. * @param {Object} canvas A Manifesto Canvas * @return {Object} A Manifesto Image Resource */ static getPreferredImage(miradorCanvas) { return miradorCanvas.iiifImageResources[0] || miradorCanvas.imageResource; } /** * Chooses the best available image size based on a target area (w x h) value. * @param {Object} service A IIIF Image API service that has a `sizes` array * @param {Number} targetArea The target area value to compare potential sizes against * @return {Object|undefined} The best size, or undefined if none are acceptable */ static selectBestImageSize(service, targetArea) { const sizes = asArray(service.getProperty('sizes')); let closestSize = { default: true, height: service.getProperty('height') || Number.MAX_SAFE_INTEGER, width: service.getProperty('width') || Number.MAX_SAFE_INTEGER, }; /** Compare the total image area to our target */ const imageFitness = (test) => test.width * test.height - targetArea; /** Look for the size that's just bigger than we prefer... */ closestSize = sizes.reduce((best, test) => { const score = imageFitness(test); if (score < 0) return best; return Math.abs(score) < Math.abs(imageFitness(best)) ? test : best; }, closestSize); /** .... but not "too" big; we'd rather scale up an image than download too much */ if (closestSize.width * closestSize.height > targetArea * 6) { closestSize = sizes.reduce((best, test) => ( Math.abs(imageFitness(test)) < Math.abs(imageFitness(best)) ? test : best ), closestSize); } if (closestSize.default) return undefined; return closestSize; } /** * Determines the appropriate thumbnail to use to represent an Image Resource. * @param {Object} resource The Image Resource from which to derive a thumbnail * @return {Object} The thumbnail URL and any spatial dimensions that can be determined */ iiifThumbnailUrl(resource) { let size; let width; let height; const minDimension = 120; let maxHeight = minDimension; let maxWidth = minDimension; const { maxHeight: requestedMaxHeight, maxWidth: requestedMaxWidth } = this.iiifOpts; if (requestedMaxHeight) maxHeight = Math.max(requestedMaxHeight, minDimension); if (requestedMaxWidth) maxWidth = Math.max(requestedMaxWidth, minDimension); const service = iiifImageService(resource); if (!service) return ThumbnailFactory.staticImageUrl(resource); const aspectRatio = resource.getWidth() && resource.getHeight() && (resource.getWidth() / resource.getHeight()); const target = (requestedMaxWidth && requestedMaxHeight) ? requestedMaxWidth * requestedMaxHeight : maxHeight * maxWidth; const closestSize = ThumbnailFactory.selectBestImageSize(service, target); if (closestSize) { // Embedded service advertises an appropriate size width = closestSize.width; height = closestSize.height; size = `${width},${height}`; } else if (isLevel0ImageProfile(service)) { /** Bail if the best available size is the full size.. maybe we'll get lucky with the @id */ if (!service.getProperty('height') && !service.getProperty('width')) { return ThumbnailFactory.staticImageUrl(resource); } } else if (requestedMaxHeight && requestedMaxWidth) { // IIIF level 2, no problem. if (isLevel2ImageProfile(service)) { size = `!${maxWidth},${maxHeight}`; width = maxWidth; height = maxHeight; if (aspectRatio && aspectRatio > 1) height = Math.round(maxWidth / aspectRatio); if (aspectRatio && aspectRatio < 1) width = Math.round(maxHeight * aspectRatio); } else if ((maxWidth / maxHeight) < aspectRatio) { size = `${maxWidth},`; width = maxWidth; if (aspectRatio) height = Math.round(maxWidth / aspectRatio); } else { size = `,${maxHeight}`; height = maxHeight; if (aspectRatio) width = Math.round(maxHeight * aspectRatio); } } else if (requestedMaxHeight && !requestedMaxWidth) { size = `,${maxHeight}`; height = maxHeight; if (aspectRatio) width = Math.round(maxHeight * aspectRatio); } else if (!requestedMaxHeight && requestedMaxWidth) { size = `${maxWidth},`; width = maxWidth; if (aspectRatio) height = Math.round(maxWidth / aspectRatio); } else { size = `,${minDimension}`; height = minDimension; if (aspectRatio) width = Math.round(height * aspectRatio); } const region = 'full'; const quality = Utils.getImageQuality(service.getProfile()); const id = service.id.replace(/\/+$/, ''); const format = this.getFormat(service); return { height, url: [id, region, size, 0, `${quality}.${format}`].join('/'), width, }; } /** * Figure out what format thumbnail to use by looking at the preferred formats * on offer, and selecting a format shared in common with the application's * preferred format list. * * Fall back to jpg, which is required to work for all IIIF services. */ getFormat(service) { const { preferredFormats = [] } = this.iiifOpts; const servicePreferredFormats = service.getProperty('preferredFormats'); if (!servicePreferredFormats) return 'jpg'; const filteredFormats = servicePreferredFormats.filter( value => preferredFormats.includes(value), ); // this is a format found in common between the preferred formats of the service // and the application if (filteredFormats[0]) return filteredFormats[0]; // IIIF Image API guarantees jpg support; if it wasn't provided by the service // but the application is fine with it, we might as well try it. if (!servicePreferredFormats.includes('jpg') && preferredFormats.includes('jpg')) { return 'jpg'; } // there were no formats in common, and the application didn't want jpg... so // just trust that the IIIF service is advertising something useful? if (servicePreferredFormats[0]) return servicePreferredFormats[0]; // JPG support is guaranteed by the spec, so it's a good worst-case fallback return 'jpg'; } /** * Determines the content resource from which to derive a thumbnail to represent a given resource. * This method is recursive. * @param {Object} resource A IIIF resource to derive a thumbnail from * @return {Object|undefined} The Image Resource to derive a thumbnail from, or undefined * if no appropriate resource exists */ getSourceContentResource(resource) { const thumbnail = resource.getThumbnail(); // Any resource type may have a thumbnail if (thumbnail) { if (typeof thumbnail.__jsonld === 'string') return thumbnail.__jsonld; // Prefer an image's ImageService over its image's thumbnail // Note that Collection, Manifest, and Canvas don't have `getType()` if (!resource.isCollection() && !resource.isManifest() && !resource.isCanvas()) { if (resource.getType() === 'image' && iiifImageService(resource) && !iiifImageService(thumbnail)) { return resource; } } return thumbnail; } if (resource.isCollection()) { const firstManifest = resource.getManifests()[0]; if (firstManifest) return this.getSourceContentResource(firstManifest); return undefined; } if (resource.isManifest()) { const miradorManifest = this.getMiradorManifest(resource); const canvas = miradorManifest.startCanvas || miradorManifest.canvasAt(0); if (canvas) return this.getSourceContentResource(canvas); return undefined; } if (resource.isCanvas()) { const image = ThumbnailFactory.getPreferredImage(this.getMiradorCanvas(resource)); if (image) return this.getSourceContentResource(image); return undefined; } if (resource.getType() === 'image') { return resource; } return undefined; } /** * Gets a thumbnail representing the resource. * @return {Object|undefined} A thumbnail representing the resource, or undefined if none could * be determined */ get(resource) { if (!resource) return undefined; // Determine which content resource we should use to derive a thumbnail const sourceContentResource = this.getSourceContentResource(resource); if (!sourceContentResource) return undefined; // Special treatment for external resources if (typeof sourceContentResource === 'string') return { url: sourceContentResource }; return this.iiifThumbnailUrl(sourceContentResource); } } /** */ function getBestThumbnail(resource, iiifOpts = {}) { return new ThumbnailFactory( iiifOpts, { getMiradorCanvas: (r) => new MiradorCanvas(r), getMiradorManifest: (r) => new MiradorManifest(r), }, ).get(resource); } export { ThumbnailFactory }; export default getBestThumbnail;