@allmaps/iiif-parser
Version:
Allmaps IIIF parser
328 lines (327 loc) • 13.3 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, 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(...args) {
const parsedImage = args[0];
const parsedCanvas = args[1];
if (args.length === 2) {
const parsedEmbeddedImage = args[0];
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 {
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 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, 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 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
});
}
}