cozy-iiif
Version:
A developer-friendly collection of abstractions and utilities built on top of @iiif/presentation-3 and @iiif/parser
110 lines (87 loc) • 3.45 kB
text/typescript
import type { Bounds, Level0ImageServiceResource } from '../types';
import { fetchImageInfo } from './fetch-image-info';
import { getThrottledLoader } from './throttled-loader';
import type { ImageInfo, Tile } from './types';
const getTileUrl = (info: ImageInfo, bounds: Bounds): string => {
const { x, y, w, h } = bounds;
const tileWidth = info.tiles[0].width;
const tileHeight = info.tiles[0].height|| info.tiles[0].width;
return `${info.id}/${x * tileWidth},${y * tileHeight},${w},${h}/${tileWidth},/0/default.jpg`;
}
const getTilesForRegion = (info: ImageInfo, bounds: Bounds): Tile[] => {
const tileWidth = info.tiles[0].width;
const tileHeight = info.tiles[0].height || info.tiles[0].width; // fallback for square tiles
const maxWidth = info.width;
const maxHeight = info.height;
const startTileX = Math.floor(bounds.x / tileWidth);
const startTileY = Math.floor(bounds.y / tileHeight);
const endTileX = Math.ceil((bounds.x + bounds.w) / tileWidth);
const endTileY = Math.ceil((bounds.y + bounds.h) / tileHeight);
const tiles: Tile[] = [];
for (let y = startTileY; y < endTileY; y++) {
for (let x = startTileX; x < endTileX; x++) {
// Skip tiles outside image bounds
if (x * tileWidth >= maxWidth || y * tileHeight >= maxHeight) {
continue;
}
// Calculate actual tile dimensions (might be smaller at edges)
const effectiveWidth = Math.min(tileWidth, maxWidth - (x * tileWidth));
const effectiveHeight = Math.min(tileHeight, maxHeight - (y * tileHeight));
tiles.push({
x,
y,
width: effectiveWidth,
height: effectiveHeight,
url: getTileUrl(info, { x, y, w: effectiveWidth, h: effectiveHeight })
});
}
}
return tiles;
}
export const cropRegion = async (resource: Level0ImageServiceResource, bounds: Bounds): Promise<Blob> => {
const info = await fetchImageInfo(resource);
const tiles = getTilesForRegion(info, bounds);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx)
// Should never happen
throw new Error('Error initializing canvas context');
const tileWidth = info.tiles[0].width;
const tileHeight = info.tiles[0].height || info.tiles[0].width;
const tilesWidth = (Math.ceil(bounds.w / tileWidth) + 1) * tileWidth;
const tilesHeight = (Math.ceil(bounds.h / tileHeight) + 1) * tileHeight;
canvas.width = tilesWidth;
canvas.height = tilesHeight;
const loader = getThrottledLoader({ callsPerSecond: 20 });
// TODO implement polite harvesting!
await Promise.all(tiles.map(async (tile) => {
const img = await loader.loadImage(tile.url);
const x = (tile.x * tileWidth) - bounds.x;
const y = (tile.y * tileHeight) - bounds.y;
ctx.drawImage(img, x, y);
}));
const cropCanvas = document.createElement('canvas');
cropCanvas.width = bounds.w;
cropCanvas.height = bounds.h;
const cropCtx = cropCanvas.getContext('2d');
if (!cropCtx)
throw new Error('Error initializing canvas context');
// Copy cropped region
cropCtx.drawImage(canvas,
0, 0, bounds.w, bounds.h,
0, 0, bounds.w, bounds.h
);
return new Promise((resolve, reject) => {
cropCanvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to create blob'));
}
},
'image/jpeg',
0.95
);
});
}