cozy-iiif
Version:
A developer-friendly collection of abstractions and utilities built on top of @iiif/presentation-3 and @iiif/parser
162 lines (128 loc) • 5.24 kB
text/typescript
import { imageSize } from 'image-size';
import type { ImageService2, ImageService3, Service } from '@iiif/presentation-3';
import { getPropertyValue } from './resource';
import type { Bounds, CozyImageResource, GetRegionURLOpts } from '../types';
type ImageService = ImageService2 | ImageService3;
export const normalizeServiceUrl = (url: string) =>
url.endsWith('/info.json') ? url : `${url.endsWith('/') ? url : `${url}/`}info.json`;
export const isImageService = (data: any): data is ImageService => {
const t = getPropertyValue<string>(data, 'type');
return t?.startsWith('ImageService') || (
data.profile?.toString().includes('iiif.io/api/image/')
);
}
export const parseImageService = (service: Service) => {
const t = getPropertyValue<string>(service, 'type');
const context = getPropertyValue<string>(service, 'context');
if (t === 'ImageService2' || context?.includes('image/2')) {
const service2 = service as ImageService2;
const p = getPropertyValue<string>(service2, 'profile');
const labels = ['level0', 'level1', 'level2'];
const profiles = Array.isArray(p) ? p: p ? [p] : [];
const levels = profiles
.map(profile => labels.findIndex(level => profile.toString().includes(level)))
.filter(l => l > -1)
.sort((a, b) => b - a); // Sort descending
return { majorVersion: 2, profileLevel: levels[0] };
} else if (t || context) {
// Image API 3
const service3 = service as ImageService3;
return { majorVersion: 3, profileLevel: parseInt(service3.profile)}
}
}
export const getStaticImagePixelSize = (url: string) => () => {
const isBrowser = typeof window !== 'undefined';
return fetch(url).then(res => res.blob()).then(blob => {
if (isBrowser) {
return createImageBitmap(blob).then(bitmap => {
const { width, height } = bitmap;
bitmap.close();
return { width, height }
});
} else { return blob.arrayBuffer().then(buffer =>
imageSize(new Uint8Array(buffer)));
}
});
}
export const getImageURLFromService = (
service: Service,
width: number,
height: number,
rotation: number = 0
): string => {
const id = getPropertyValue(service, 'id');
const compliance = service.profile || '';
const isLevel0 = typeof compliance === 'string' &&
(compliance.includes('level0') || compliance.includes('level:0'));
if (isLevel0) {
// For level 0, find the closest pre-defined size
if ('sizes' in service && Array.isArray(service.sizes)) {
const suitableSize = service.sizes
.sort((a, b) => (b.width * b.height) - (a.width * a.height))
.filter(s => (s.width * s.height) >= width * height)[0];
if (suitableSize)
return `${id}/full/${suitableSize.width},${suitableSize.height}/0/default.jpg`;
}
// Fallback: full image
return `${id}/full/full/0/default.jpg`;
}
return `${id}/full/!${width},${height}/${rotation}/default.jpg`;
}
export const getRegionURLFromService = (
service: Service,
bounds: Bounds,
rotation = 0, // 0, 90, 180, 270
opts: GetRegionURLOpts = { minSize: 400 }
): string | undefined => {
const id = getPropertyValue(service, 'id');
const compliance = service.profile || '';
const isLevel0 = typeof compliance === 'string' &&
(compliance.includes('level0') || compliance.includes('level:0'));
if (isLevel0) {
console.warn(`Level 0 image service does not support custom region URLs: ${id}`);
return;
}
const { x, y, w , h } = bounds;
const { minSize = 400, maxSize } = opts;
// Find minimum scale required to keep shortest edge >= minSize
const minRequiredScale = minSize / Math.min(w, h);
const scale = maxSize
? Math.max(minRequiredScale, maxSize / Math.max(w, h))
: minRequiredScale;
const width = Math.round(w * scale);
const height = Math.round(h * scale);
const normalizedRotation = ((rotation % 360) + 360) % 360;
const regionParam = `${Math.round(x)},${Math.round(y)},${Math.round(w)},${Math.round(h)}`;
return `${id}/${regionParam}/!${width},${height}/${normalizedRotation}/default.jpg`;
}
export const getRegionURL = (
image: CozyImageResource
) => (
bounds: Bounds,
rotation = 0,
opts: GetRegionURLOpts = { minSize: 400 }
): string | undefined => {
if (image.type === 'dynamic') {
return getRegionURLFromService(image.service, bounds, rotation, opts);
} else {
console.error('Level 0 or static image canvas: unsupported');
}
}
export const getImageURL = (
width: number | undefined,
height: number | undefined,
service: Service
) => (minSize = 800, rotation = 0) => {
if (!width || !height) return;
const aspect = width / height;
const isPortrait = aspect < 1;
const h = Math.ceil(isPortrait ? minSize / aspect : minSize);
const w = Math.ceil(isPortrait ? minSize : minSize / aspect);
return getImageURLFromService(service!, w, h, rotation);
}
export const getPixelSizeFromServiceUrl = (serviceUrl: string) => () =>
fetch(serviceUrl).then(res => res.json()).then(data => {
const width: number = data.width;
const height: number = data.height;
return (width !== undefined && height !== undefined) ? { width, height } : undefined;
});