UNPKG

cozy-iiif

Version:

A developer-friendly collection of abstractions and utilities built on top of @iiif/presentation-3 and @iiif/parser

265 lines (224 loc) 6.7 kB
import type { Canvas, Collection, Manifest, Range } from '@iiif/presentation-3'; import { convertPresentation2 } from '@iiif/parser/presentation-2'; import { Traverse } from '@iiif/parser'; import { getImages, getLabel, getMetadata, getPropertyValue, getTableOfContents, getThumbnailURL, normalizeServiceUrl, parseImageService } from './core'; import type { CozyCanvas, CozyCollection, CozyCollectionItem, CozyManifest, CozyParseResult, CozyRange, ImageServiceResource } from './types'; const parseURL = async (input: string): Promise<CozyParseResult> => { try { new URL(input); } catch { return { type: 'error', code: 'INVALID_URL', message: 'The provided input is not a valid URL' }; } let response: Response; try { response = await fetch(input); if (!response.ok) { return { type: 'error', code: 'INVALID_HTTP_RESPONSE', message: `Server responded: HTTP ${response.status} ${response.statusText ? `(${response.statusText})` : ''}` } } } catch (error) { return { type: 'error', code: 'FETCH_ERROR', message: error instanceof Error ? error.message : 'Failed to fetch resource' }; } const contentType = response.headers.get('content-type'); if (contentType?.startsWith('image/')) { return { type: 'plain-image', url: input }; } if (contentType?.includes('text/html')) { return { type: 'webpage', url: input }; } try { const json = await response.json(); return parse(json, input); } catch { return { type: 'error', code: 'UNSUPPORTED_FORMAT', message: 'Could not parse resource' }; } } const parse = (json: any, url?: string): CozyParseResult => { const context: string = Array.isArray(json['@context']) ? json['@context'].find(str => str.includes('iiif.io/api/presentation') || str.includes('iiif.io/api/image')) : json['@context']; if (!context) { return { type: 'error', code: 'INVALID_MANIFEST', message: 'Missing @context' } }; const id = getPropertyValue<string>(json, 'id'); if (!id) { return { type: 'error', code: 'INVALID_MANIFEST', message: 'Missing id property' } } if (context.includes('presentation/2') || context.includes('presentation/3')) { const majorVersion = context.includes('presentation/2') ? 2 : 3; const type = getPropertyValue(json, 'type'); return type.includes('Collection') ? { type: 'collection', url: url || id, resource: parseCollectionResource(json, majorVersion) } : { type: 'manifest', url: url || id, resource: parseManifestResource(json, majorVersion) }; } if (context.includes('image/2') || context.includes('image/3')) { const resource = parseImageResource(json); return resource ? { type: 'iiif-image', url: url || id, resource } : { type: 'error', code: 'INVALID_MANIFEST', message: 'Invalid image service definition' } } return { type: 'error', code: 'INVALID_MANIFEST', message: 'JSON resource is not a recognized IIIF format' }; } const parseCollectionResource = (resource: any, majorVersion: number): CozyCollection => { const parseV3 = (collection: Collection) => { const items: any[] = []; const modelBuilder = new Traverse({ manifest: [item => items.push(item)] }); modelBuilder.traverseCollection(collection); return items.map(source => ({ id: source.id, type: source.type, getLabel: getLabel(source), source }) as CozyCollectionItem); } const v3: Collection = majorVersion === 2 ? convertPresentation2(resource) : resource; const items = parseV3(v3); return { source: v3, id: v3.id, majorVersion, items, getLabel: getLabel(v3), getMetadata: getMetadata(v3) }; } const parseManifestResource = (resource: any, majorVersion: number): CozyManifest => { const parseV3 = (manifest: Manifest) => { const sourceCanvases: Canvas[] = []; const sourceRanges: Range[] = []; const modelBuilder = new Traverse({ canvas: [canvas => { if (canvas.items) sourceCanvases.push(canvas) }], range: [range => { if (range.type === 'Range') sourceRanges.push(range) }] }); modelBuilder.traverseManifest(manifest); const canvases = sourceCanvases.map((c: Canvas) => { const images = getImages(c); return { source: c, id: c.id, width: c.width, height: c.height, images, annotations: (c.annotations || []), getImageURL: images.length > 0 ? images[0].getImageURL : () => undefined, getLabel: getLabel(c), getMetadata: getMetadata(c), getThumbnailURL: getThumbnailURL(c, images) } as CozyCanvas; }); const toRange = (source: Range): CozyRange => { const items = source.items || []; const nestedCanvases: CozyCanvas[] = items .filter((item: any) => item.type === 'Canvas') .map((item: any) => canvases.find(c => c.id === item.id)!) .filter(Boolean); const nestedRanges = items .filter((item: any) => item.type === 'Range') .map((item: any) => toRange(item)); const nestedItems = [...nestedCanvases, ...nestedRanges]; return { source, id: source.id, // Maintain original order items: items.map((i: any) => nestedItems.find(cozy => cozy.id === i.id)), canvases: nestedCanvases, ranges: nestedRanges, getLabel: getLabel(source) } as CozyRange; } const ranges = sourceRanges.map((source: Range) => toRange(source)); return { canvases, ranges }; } const v3: Manifest = majorVersion === 2 ? convertPresentation2(resource) : resource; const { canvases, ranges } = parseV3(v3); return { source: v3, id: v3.id, majorVersion, canvases, structure: ranges, getLabel: getLabel(v3), getMetadata: getMetadata(v3), getTableOfContents: getTableOfContents(ranges) } } const parseImageResource = (resource: any) => { const { width, height } = resource; const service = parseImageService(resource); if (service) { return { type: service.profileLevel === 0 ? 'level0' : 'dynamic', service: resource, width, height, majorVersion: service.majorVersion, serviceUrl: normalizeServiceUrl(getPropertyValue<string>(resource, 'id')) } as ImageServiceResource; } } export const Cozy = { parse, parseURL };