cozy-iiif
Version:
A developer-friendly collection of abstractions and utilities built on top of @iiif/presentation-3 and @iiif/parser
114 lines (85 loc) • 3.68 kB
text/typescript
import type { Level0ImageServiceResource } from '../types';
import { fetchImageInfo } from './fetch-image-info';
import { getThrottledLoader } from './throttled-loader';
import type { ImageInfo, Size, Tile } from './types';
const getBestScaleFactor = (info: ImageInfo, minSize?: Partial<Size>): number => {
// Sort descending
const scaleFactors = info.tiles[0].scaleFactors.sort((a, b) => b - a);
if (!minSize)
// Just return highest scale factor
return scaleFactors[0];
const scaleX = minSize.width ? info.width / minSize.width : Infinity;
const scaleY = minSize.height ? info.height / minSize.height : Infinity;
const scale = Math.min(scaleX, scaleY);
// Find the smallest scale factor that still meets our minimum size requirements
for (const factor of scaleFactors) {
if (factor <= scale)
return factor;
}
// If no scale factor is small enough, return the smallest available
return scaleFactors[scaleFactors.length - 1];
};
const getThumbnailDimensions = (info: ImageInfo, minSize?: Partial<Size>): Size => {
const scaleFactor = getBestScaleFactor(info, minSize);
let width = Math.ceil(info.width / scaleFactor);
let height = Math.ceil(info.height / scaleFactor);
if (minSize) {
const aspectRatio = info.width / info.height;
if (minSize.width && width < minSize.width) {
width = minSize.width;
height = Math.ceil(width / aspectRatio);
}
if (minSize.height && height < minSize.height) {
height = minSize.height;
width = Math.ceil(height * aspectRatio);
}
}
return { width, height };
}
const getThumbnailTiles = (info: ImageInfo, minSize?: Partial<Size>): Tile[] => {
const scaleFactor = getBestScaleFactor(info, minSize);
const tileWidth = info.tiles[0].width;
const tileHeight = info.tiles[0].height || info.tiles[0].width;
const cols = Math.ceil(info.width / (tileWidth * scaleFactor));
const rows = Math.ceil(info.height / (tileHeight * scaleFactor));
const tiles = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const actualWidth = Math.min(tileWidth,
(info.width - x * tileWidth * scaleFactor) / scaleFactor);
const actualHeight = Math.min(tileHeight,
(info.height - y * tileHeight * scaleFactor) / scaleFactor);
if (actualWidth <= 0 || actualHeight <= 0) continue;
tiles.push({
url: `${info.id}/${x * tileWidth * scaleFactor},${y * tileHeight * scaleFactor},${actualWidth * scaleFactor},${actualHeight * scaleFactor}/${Math.ceil(actualWidth)},/0/default.jpg`,
width: Math.ceil(actualWidth),
height: Math.ceil(actualHeight),
x: x * tileWidth,
y: y * tileHeight
});
}
}
return tiles;
}
export const getThumbnail = async (resource: Level0ImageServiceResource, minSize?: Partial<Size>): Promise<Blob> => {
const info = await fetchImageInfo(resource);
const tiles = getThumbnailTiles(info, minSize);
const dimensions = getThumbnailDimensions(info, minSize);
const canvas = document.createElement('canvas');
canvas.width = dimensions.width;
canvas.height = dimensions.height;
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('Error creating canvas context');
const loader = getThrottledLoader();
await Promise.all(tiles.map(async (tile) => {
const img = await loader.loadImage(tile.url);
ctx.drawImage(img, tile.x, tile.y);
}));
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject(new Error('Failed to create blob'));
}, 'image/jpeg', 0.85);
});
}