UNPKG

tify

Version:

A slim and mobile-friendly IIIF document viewer

152 lines (124 loc) 4.69 kB
import fs from 'node:fs'; import path from 'node:path'; import chalk from 'chalk'; import sharp from 'sharp'; import { upgrade } from '@iiif/parser/upgrader'; // eslint-disable-next-line import/extensions import { filenamifyUrl } from '../src/demo/modules/filenamify.js'; // eslint-disable-next-line import/extensions import sampleManifests from '../src/demo/manifests.js'; const thumbnailsDir = path.resolve(path.dirname(import.meta.url.replace('file://', '')), '../public/thumbnails'); const thumbnailWidth = 240; const thumbnailHeight = 240; const thumbnailOverflow = 8; if (!fs.existsSync(thumbnailsDir)) { fs.mkdirSync(thumbnailsDir, { recursive: true }); } async function saveThumbnail(manifestUrl, parentUrl = '') { const thumbnailFilename = `${thumbnailsDir}/${filenamifyUrl(parentUrl || manifestUrl)}.avif`; if (fs.existsSync(thumbnailFilename)) { return { alreadyExists: true, thumbnailFilename, }; } // Fetch IIIF manifest let manifestRes; try { manifestRes = await fetch(manifestUrl); if (!manifestRes.ok) { return { error: `Failed to fetch manifest: ${manifestRes.statusText}` }; } } catch (error) { return { error: 'Failed to fetch manifest' }; } const originalManifest = await manifestRes.json(); // Upgrade to IIIF 3 manifest using @iiif/parser/upgrader const manifest = upgrade(originalManifest); if (manifest.type === 'Collection' || manifest['@type'] === 'sc:Collection') { const manifests = manifest.items || manifest.manifests || []; for (let i = 0; i < manifests.length; i += 1) { // eslint-disable-next-line no-await-in-loop const result = await saveThumbnail(manifests[i].id || manifests[i]['@id'], parentUrl || manifestUrl); if (!result.error) { return result; } } return { error: `Failed loading thumbnail for any manifest in collection of ${manifests.length}` }; } let imageUrl = null; if (manifest.thumbnail && manifest.thumbnail.length) { const thumbnail = manifest.thumbnail[0]; if (thumbnail.width >= thumbnailWidth + thumbnailOverflow * 2 && thumbnail.height >= thumbnailHeight + thumbnailOverflow * 2 ) { imageUrl = thumbnail.id || thumbnail['@id']; } } if (!imageUrl) { const startCanvasId = manifest.start?.id || manifest.items?.[0]?.id; if (!startCanvasId) { return { error: 'No thumbnail found, and no canvases in this manifest' }; } const startCanvas = manifest.items?.find((c) => c.id === startCanvasId); if (!startCanvas?.items?.length) { return { error: 'Canvas found but no annotation pages present' }; } const annotationPage = startCanvas.items[0]; if (!annotationPage?.items?.length) { return { error: 'No annotations found on this canvas annotation page' }; } const annotation = annotationPage.items[0]; const resource = annotation?.body || annotation?.body; if (resource?.service) { const service = [].concat(resource.service)[0]; const quality = ['ImageService2', 'ImageService3'].includes(service.type || service['@type']) ? 'default' : 'native'; const id = service.id || service['@id']; // NOTE: Using "full" instead of "square" because the latter is not supported by all APIs imageUrl = `${id}${id.at(-1) === '/' ? '' : '/'}full/${thumbnailWidth + thumbnailOverflow * 2},/0/${quality}.jpg`; } else if (resource.format.startsWith('image/')) { // Filter out non-image resources like audio imageUrl = resource?.id; } } if (!imageUrl) { return { error: 'No image URL found on the annotation body' }; } // Fetch and save the image const imageRes = await fetch(imageUrl); if (!imageRes.ok) { return { error: `Failed to fetch image from ${imageUrl} (${imageRes.statusText})` }; } const imageBuffer = await imageRes.arrayBuffer(); await sharp(Buffer.from(imageBuffer)) .resize(thumbnailWidth + thumbnailOverflow * 2, thumbnailHeight + thumbnailOverflow * 2) .extract({ width: thumbnailWidth, height: thumbnailHeight, left: thumbnailOverflow, top: thumbnailOverflow, }) .toFile(thumbnailFilename); return { thumbnailFilename, }; } console.log(); if (!sampleManifests?.length) { console.warn(chalk.redBright('No manifests found\n')); process.exit(); } sampleManifests.forEach((manifest) => { saveThumbnail(manifest.url).then((result) => { console.log(`Manifest: ${chalk.bold(manifest.url)}`); if (result.error) { console.warn(chalk.redBright(`❌ Failed: ${result.error}\n`)); } else if (result.alreadyExists) { console.log(`🤷 Thumbnail already exists: ${chalk.dim('file://')}${result.thumbnailFilename}\n`); } else { console.log(`✅ Thumbnail saved: ${chalk.dim('file://')}${result.thumbnailFilename}\n`); } }); });