@allmaps/iiif-parser
Version:
Allmaps IIIF parser
317 lines (316 loc) • 12.9 kB
JavaScript
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, getIiifTile } from '../lib/tiles.js';
import { getThumbnail } from '../lib/thumbnails.js';
import { getProfileProperties, getMajorIiifVersionFromImageService } from '../lib/profile.js';
const ImageTypeString = 'image';
/**
* Parsed IIIF Image, embedded in a Canvas
* @class EmbeddedImage
* @property {boolean} embedded - Whether the Image is embedded in a Canvas
* @property {string} [type] - Resource type, equals 'image'
* @property {string} uri - URI of Image
* @property {MajorVersion} majorVersion - IIIF API version of Image
* @property {boolean} supportsAnyRegionAndSize - Whether the associated Image Service supports any region and size
* @property {number} [maxWidth] - Maximum width of the associated Image Service
* @property {number} [maxHeight] - Maximum height of the associated Image Service
* @property {number} [maxArea] - Maximum area of the associated Image Service
* @property {number} width - Width of Image
* @property {number} height - Height of Image
*/
export class EmbeddedImage {
embedded = true;
uri;
type = ImageTypeString;
maxWidth;
maxHeight;
maxArea;
supportsAnyRegionAndSize;
width;
height;
majorVersion;
constructor(parsedImage, parsedCanvas) {
if (parsedCanvas) {
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 (err) {
// 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)) {
profile = imageService.profile[0];
}
else {
profile = imageService.profile;
}
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} imageRequest - Image request object containing the desired region and size of the requested image
* @returns {string} 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;
// Is this really the right way to do it?
// 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`;
}
getThumbnail(size, mode = 'cover') {
return getThumbnail({ width: this.width, height: this.height }, size, mode, {
supportsAnyRegionAndSize: this.supportsAnyRegionAndSize,
maxWidth: this.maxWidth,
maxHeight: this.maxHeight,
maxArea: this.maxArea
});
}
}
/**
* Parsed IIIF Image
* @class Image
* @extends EmbeddedImage
* @property {TileZoomLevel[]} tileZoomLevels - Array of parsed tile zoom levels
* @property {Size[]} [sizes] - Array of parsed sizes
*/
export class Image extends EmbeddedImage {
tileZoomLevels;
sizes;
embedded = false;
constructor(parsedImage) {
super(parsedImage);
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 {any} iiifImage - Source data of IIIF Image
* @param {MajorVersion} [majorVersion=null] - IIIF API version of Image. If not provided, it will be determined automatically
* @returns {Image} Parsed IIIF Image
* @static
*/
static parse(iiifImage, majorVersion = null) {
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);
}
// TODO: rename this to getImageRequest
/**
* Returns a Image request object for a tile with the requested zoom level, column, and row
* @param {TileZoomLevel} zoomLevel - Desired zoom level of the requested tile
* @param {number} column - Column of the requested tile
* @param {number} row - Row of the requested tile
* @returns {ImageRequest} Image request object that can be used to fetch the requested tile
*/
getIiifTile(zoomLevel, column, row) {
return getIiifTile({ width: this.width, height: this.height }, zoomLevel, column, row);
}
/**
* Returns a Image request object for the requested region and size
* @param {SizeObject} size - Size of the requested thumbnail
* @param {'cover' | 'contain'} mode - Desired fit mode of the requested thumbnail
* @returns {ImageRequest} Image request object that can be used to fetch the requested thumbnail
*/
getThumbnail(size, mode = 'cover') {
return getThumbnail({ 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
});
}
}