UNPKG

@allmaps/iiif-parser

Version:

Allmaps IIIF parser

330 lines (329 loc) 13.5 kB
import { Image1Schema, Image2Schema, Image3Schema, ImageSchema } from '../schemas/iiif.js'; import { Image1ContextString, Image1ContextStringIncorrect, image1ProfileUriRegex } from '../schemas/image.1.js'; import { Image2ContextString, image2ProfileUriRegex } from '../schemas/image.2.js'; import { getTileZoomLevels, getTileImageRequest } from '../lib/tiles.js'; import { getImageRequest } from '../lib/image-requests.js'; import { getProfileProperties, getMajorIiifVersionFromImageService } from '../lib/profile.js'; const ImageTypeString = 'image'; /** * Parsed IIIF Image, embedded in a Canvas * * @property embedded - Whether the Image is embedded in a Canvas * @property type - Resource type, equals 'image' * @property uri - URI of Image * @property majorVersion - IIIF API version of Image * @property supportsAnyRegionAndSize - Whether the associated Image Service supports any region and size * @property maxWidth - Maximum width of the associated Image Service * @property maxHeight - Maximum height of the associated Image Service * @property maxArea - Maximum area of the associated Image Service * @property width - Width of Image * @property height - Height of Image */ export class EmbeddedImage { embedded = true; uri; type = ImageTypeString; maxWidth; maxHeight; maxArea; supportsAnyRegionAndSize; width; height; majorVersion; constructor(parsedImage, options) { const { parsedCanvas } = options || {}; if (parsedCanvas && 'service' in parsedImage) { const parsedEmbeddedImage = parsedImage; let imageService; let majorVersion; if (Array.isArray(parsedEmbeddedImage.service)) { parsedEmbeddedImage.service.forEach((currentImageService) => { try { const currentMajorVersion = getMajorIiifVersionFromImageService(currentImageService); if (!majorVersion || currentMajorVersion > majorVersion) { majorVersion = currentMajorVersion; imageService = currentImageService; } } catch { // Ignore this error, throw error later if no valid image service is found } }); } else { imageService = parsedEmbeddedImage.service; } if (!imageService) { throw new Error('Unsupported IIIF Image Service'); } if ('@id' in imageService) { this.uri = imageService['@id']; } else if ('id' in imageService) { this.uri = imageService.id; } else { throw new Error('Unsupported IIIF Image Service'); } if ('type' in imageService && imageService.type === 'ImageService3') { this.majorVersion = 3; } else if (('type' in imageService && imageService.type === 'ImageService2') || ('@type' in imageService && imageService['@type'] === 'ImageService2') || ('@context' in imageService && imageService['@context'] === Image2ContextString)) { this.majorVersion = 2; } else if ('@context' in imageService && (imageService['@context'] === Image1ContextString || imageService['@context'] === Image1ContextStringIncorrect)) { this.majorVersion = 1; } else if ('profile' in imageService) { let profile; if (Array.isArray(imageService.profile) && imageService.profile.length > 0) { profile = imageService.profile[0]; } else if (typeof imageService.profile === 'string') { profile = imageService.profile; } else { throw new Error('Unsupported IIIF Image Service'); } if (profile.match(image1ProfileUriRegex)) { this.majorVersion = 1; } else if (profile.match(image2ProfileUriRegex)) { this.majorVersion = 2; } else { this.majorVersion = 3; } } else { throw new Error('Unsupported IIIF Image Service'); } if ('profile' in imageService) { const profileProperties = getProfileProperties(imageService); this.supportsAnyRegionAndSize = profileProperties.supportsAnyRegionAndSize; this.maxWidth = profileProperties.maxWidth; this.maxHeight = profileProperties.maxHeight; this.maxArea = profileProperties.maxArea; } else { this.supportsAnyRegionAndSize = false; } } else { if ('@id' in parsedImage) { this.uri = parsedImage['@id']; } else if ('id' in parsedImage) { this.uri = parsedImage.id; } else { throw new Error('Unsupported IIIF Image'); } if ('type' in parsedImage && parsedImage.type === 'ImageService3') { this.majorVersion = 3; } else if (('@type' in parsedImage && parsedImage['@type'] === 'iiif:Image') || ('@context' in parsedImage && parsedImage['@context'] === Image2ContextString)) { this.majorVersion = 2; } else if ('@context' in parsedImage && parsedImage['@context'] === Image1ContextString) { this.majorVersion = 1; } else { throw new Error('Unsupported IIIF Image'); } if ('profile' in parsedImage) { const profileProperties = getProfileProperties(parsedImage); this.supportsAnyRegionAndSize = profileProperties.supportsAnyRegionAndSize; this.maxWidth = profileProperties.maxWidth; this.maxHeight = profileProperties.maxHeight; this.maxArea = profileProperties.maxArea; } else { this.supportsAnyRegionAndSize = false; } } if (parsedImage.width !== undefined) { this.width = parsedImage.width; } else if (parsedCanvas) { this.width = parsedCanvas.width; } else { throw new Error('Width not present on either Canvas or Image Resource'); } if (parsedImage.height !== undefined) { this.height = parsedImage.height; } else if (parsedCanvas) { this.height = parsedCanvas.height; } else { throw new Error('Height not present on either Canvas or Image Resource'); } } /** * Generates a IIIF Image API URL for the requested region and size * @param imageRequest - Image request object containing the desired region and size of the requested image * @returns Image API URL that can be used to fetch the requested image */ getImageUrl(imageRequest) { const { region, size } = imageRequest; let width; let height; let regionHeight; let regionWidth; let urlRegion; if (region) { urlRegion = `${region.x},${region.y},${region.width},${region.height}`; regionHeight = region.height; regionWidth = region.width; } else { urlRegion = 'full'; regionHeight = this.height; regionWidth = this.width; } let urlSize; if (size) { width = Math.round(size.width); height = Math.round(size.height); const widthStr = String(width); let heightStr = String(height); const aspectRatio = regionWidth / regionHeight; const aspectRatioWidth = height * aspectRatio; const aspectRatioHeight = aspectRatioWidth / aspectRatio; if (this.majorVersion <= 2) { // In IIIF Image API 2.1 and below, use // "the w, syntax for images that should be scaled maintaining the aspect ratio" // In version 3, use "w,h if the size requested does not require upscaling" // See: // - https://iiif.io/api/image/2.1/#canonical-uri-syntax // - https://iiif.io/api/image/3.0/#48-canonical-uri-syntax // And see also: // - https://www.jack-reed.com/2016/10/14/rounding-strategies-used-in-iiif.html if (height === Math.round(aspectRatioHeight)) { heightStr = ''; } } urlSize = `${widthStr},${heightStr}`; } else { width = this.width; height = this.height; urlSize = this.majorVersion === 2 ? 'full' : 'max'; } const area = width * height; if (this.maxWidth !== undefined) { if (width > this.maxWidth) { throw new Error(`Width of requested image is too large: ${width} > ${this.maxWidth}`); } } if (this.maxHeight !== undefined) { if (height > this.maxHeight) { throw new Error(`Height of requested image is too large: ${height} > ${this.maxHeight}`); } } if (this.maxArea !== undefined) { if (area > this.maxArea) { throw new Error(`Area of requested image is too large: ${area} > ${this.maxArea}`); } } const quality = this.majorVersion === 1 ? 'native' : 'default'; return `${this.uri}/${urlRegion}/${urlSize}/0/${quality}.jpg`; } getImageRequest(size, mode = 'cover') { return getImageRequest({ width: this.width, height: this.height }, size, mode, { supportsAnyRegionAndSize: this.supportsAnyRegionAndSize, maxWidth: this.maxWidth, maxHeight: this.maxHeight, maxArea: this.maxArea }); } } /** * Parsed IIIF Image * @property tileZoomLevels - Array of parsed tile zoom levels * @property sizes - Array of parsed sizes */ export class Image extends EmbeddedImage { source; tileZoomLevels; sizes; embedded = false; constructor(parsedImage, options) { super(parsedImage); this.source = options?.source; const profileProperties = getProfileProperties(parsedImage); let tilesets; if ('tiles' in parsedImage) { tilesets = parsedImage.tiles; } this.tileZoomLevels = getTileZoomLevels({ width: this.width, height: this.height }, tilesets, profileProperties.supportsAnyRegionAndSize); if ('sizes' in parsedImage) { this.sizes = parsedImage.sizes; } } /** * Parses a IIIF image and returns a [Image](#image) containing the parsed version * @param iiifImage - Source data of IIIF Image * @param majorVersion - IIIF API version of Image. If not provided, it will be determined automatically * @returns Parsed IIIF Image * @static */ static parse(iiifImage, parseOptions) { const { majorVersion, keepSource } = parseOptions || {}; let parsedImage; if (majorVersion === 1) { parsedImage = Image1Schema.parse(iiifImage); } else if (majorVersion === 2) { parsedImage = Image2Schema.parse(iiifImage); } else if (majorVersion === 3) { parsedImage = Image3Schema.parse(iiifImage); } else { parsedImage = ImageSchema.parse(iiifImage); } return new Image(parsedImage, keepSource ? { source: iiifImage } : {}); } // TODO: rename this to getImageRequest /** * Returns a Image request object for a tile with the requested zoom level, column, and row * @param zoomLevel - Desired zoom level of the requested tile * @param column - Column of the requested tile * @param row - Row of the requested tile * @returns Image request object that can be used to fetch the requested tile */ getTileImageRequest(zoomLevel, column, row) { return getTileImageRequest({ width: this.width, height: this.height }, zoomLevel, column, row); } /** * Returns a Image request object for the requested region and size * @param size - Size of the requested thumbnail * @param mode - Desired fit mode of the requested thumbnail * @returns Image request object that can be used to fetch the requested thumbnail */ getImageRequest(size, mode = 'cover') { return getImageRequest({ width: this.width, height: this.height }, size, mode, { supportsAnyRegionAndSize: this.supportsAnyRegionAndSize, sizes: this.sizes, tileZoomLevels: this.tileZoomLevels, maxWidth: this.maxWidth, maxHeight: this.maxHeight, maxArea: this.maxArea }); } }