diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 65b7bad9..a14e68bc 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -391,6 +391,7 @@ TPEN.eventDispatcher.on('tpen-project-loaded', (ev) => { // - tpen-project-load-failed: Project load failed // - tpen-page-selected: Page selected in page selector ({ pageId, pageIndex, page }) // - tpen-toast: Show toast message ({ status, message }) +// - tpen-canvas-resolution-failed: Canvas fetch failed ({ errorType, message, httpStatus, canvasUri, component }) // - token-expiration: Auth token expired // DOM custom events for component communication diff --git a/components/annotorious-annotator/line-parser.js b/components/annotorious-annotator/line-parser.js index d816c78e..381c5c63 100644 --- a/components/annotorious-annotator/line-parser.js +++ b/components/annotorious-annotator/line-parser.js @@ -18,6 +18,7 @@ import { detectTextLinesCombined } from "./detect-lines.js" import { v4 as uuidv4 } from "https://cdn.skypack.dev/uuid@9.0.1" import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' import { onProjectReady } from '../../utilities/projectReady.js' +import vault from '../../js/vault.js' import '../page-selector/index.js' class AnnotoriousAnnotator extends HTMLElement { @@ -104,7 +105,7 @@ class AnnotoriousAnnotator extends HTMLElement { // Initialize HTML after loading in a TPEN3 Project render() { // Check that user can create AND update selectors on lines (required for the annotator) - if (!(CheckPermissions.checkEditAccess("LINE", "SELECTOR") && CheckPermissions.checkCreateAccess("LINE", "SELECTOR"))) { + if (!(CheckPermissions.checkEditAccess("LINE", "SELECTOR") || CheckPermissions.checkCreateAccess("LINE", "SELECTOR"))) { this.shadowRoot.innerHTML = "You do not have the proper project permissions to use this interface." return } @@ -553,43 +554,34 @@ class AnnotoriousAnnotator extends HTMLElement { /** * Fetch a Canvas URI and check that it is a Canvas object. Pass it forward to render the Image into the interface. - * Be prepared to recieve presentation api 2+ - * - * FIXME - * Give users a path when Canvas URIs do not resolve or resolve to something unexpected. + * Be prepared to receive presentation api 2+. + * Uses vault for consistent caching and error handling. * * @param uri A String Canvas URI */ async processCanvas(uri) { if (!uri) return - // TODO Vault me? - const resolvedCanvas = await fetch(uri) - .then(r => { - if (!r.ok) throw r - return r.json() - }) - .catch(e => { - this.shadowRoot.innerHTML = ` -

Canvas Error

-

The Canvas within this Page could not be loaded.

-

${e.status ?? e.code}: ${e.statusText ?? e.message}

- ` - throw e - }) - const context = resolvedCanvas["@context"] - if (!context?.includes("iiif.io/api/presentation/3/context.json")) { - console.warn("The Canvas object did not have the IIIF Presentation API 3 context and may not be parseable.") - } - const id = resolvedCanvas["@id"] ?? resolvedCanvas.id - if (!id) { - throw new Error("Cannot Resolve Canvas or Image", { "cause": "The Canvas is 404 or unresolvable." }) + const resolvedCanvas = await vault.get(uri, 'canvas', false, 'tpen-line-parser') + if (!resolvedCanvas) { + // Canvas resolution failed - event already dispatched by vault + this.shadowRoot.innerHTML = ` +

Canvas Error

+

The Canvas within this Page could not be loaded.

+

${uri}

+ ` + return } - const type = resolvedCanvas["@type"] ?? resolvedCanvas.type - if (!(type === "Canvas" || type === "sc:Canvas")) { - throw new Error(`Provided URI did not resolve a 'Canvas'. It resolved a '${type}'`, { "cause": "URI must point to a Canvas." }) + // Use the Annotations and Image on the Canvas for initializing the Annotorious portion of the component. + try { + await this.loadAnnotorious(resolvedCanvas) + } catch (err) { + // Canvas resolved but image extraction failed + this.shadowRoot.innerHTML = ` +

Image Error

+

The Canvas loaded but the image could not be extracted.

+

${err.message}

+ ` } - // Use the Annotations and Image on the Canvas for inititalizing the Annotorious portion of the component. - this.loadAnnotorious(resolvedCanvas) } async validateImageUrl(url) { @@ -619,7 +611,7 @@ class AnnotoriousAnnotator extends HTMLElement { throw new Error("Cannot Resolve Canvas Image", { "cause": "The Image is 404 or unresolvable." }) } let imgx = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.width - if (!imgx) imgx = resolvedCanvas?.images[0]?.resource?.width + if (!imgx) imgx = resolvedCanvas?.images?.[0]?.resource?.width let imgy = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.height if (!imgy) imgy = resolvedCanvas?.images?.[0]?.resource?.height this.#imageDims = [imgx, imgy] diff --git a/components/annotorious-annotator/plain.js b/components/annotorious-annotator/plain.js index 85cbe1a2..df4cdd62 100644 --- a/components/annotorious-annotator/plain.js +++ b/components/annotorious-annotator/plain.js @@ -14,6 +14,7 @@ import TPEN from '../../api/TPEN.js' import User from '../../api/User.js' import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' +import vault from '../../js/vault.js' class AnnotoriousAnnotator extends HTMLElement { #osd @@ -147,17 +148,18 @@ class AnnotoriousAnnotator extends HTMLElement { async renderCanvas(resolvedCanvas) { this.shadowRoot.getElementById('annotator-container').innerHTML = "" const canvasID = resolvedCanvas["@id"] ?? resolvedCanvas.id - const fullImage = resolvedCanvas?.items[0]?.items[0]?.body?.id - const imageService = resolvedCanvas?.items[0]?.items[0]?.body?.service?.id - + // Handle both IIIF v3 (items) and v2 (images) formats + const fullImage = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.id ?? resolvedCanvas?.images?.[0]?.resource?.id ?? resolvedCanvas?.images?.[0]?.resource?.['@id'] + const imageService = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.service?.id ?? resolvedCanvas?.images?.[0]?.resource?.service?.['@id'] + if(!fullImage) { - throw new Error("Cannot Resolve Canvas Image", + throw new Error("Cannot Resolve Canvas Image", {"cause":"The Image is 404 or unresolvable."}) } this.#imageDims = [ - resolvedCanvas?.items[0]?.items[0]?.body?.width, - resolvedCanvas?.items[0]?.items[0]?.body?.height + resolvedCanvas?.items?.[0]?.items?.[0]?.body?.width ?? resolvedCanvas?.images?.[0]?.resource?.width, + resolvedCanvas?.items?.[0]?.items?.[0]?.body?.height ?? resolvedCanvas?.images?.[0]?.resource?.height ] this.#canvasDims = [ resolvedCanvas?.width, @@ -325,38 +327,43 @@ class AnnotoriousAnnotator extends HTMLElement { /** * Fetch a Canvas URI and check that it is a Canvas object. Pass it forward to render the Image into the interface. - * - * FIXME - * Give users a path when Canvas URIs do not resolve or resolve to something unexpected. + * Uses vault for consistent caching and error handling. * * @param uri A String Canvas URI */ async processCanvas(uri) { - const canvas = uri - if(!canvas) return - const resolvedCanvas = await fetch(canvas) - .then(r => { - if(!r.ok) throw r - return r.json() - }) - .catch(e => { - throw e - }) - const context = resolvedCanvas["@context"] - if(!context.includes("iiif.io/api/presentation/3/context.json")) { - console.warn("The Canvas object did not have the IIIF Presentation API 3 context and may not be parseable.") + if(!uri) return + const resolvedCanvas = await vault.get(uri, 'canvas', false, 'tpen-plain-annotator') + if(!resolvedCanvas) { + // Canvas resolution failed - event already dispatched by vault + this.renderCanvasError(uri) + return } - const id = resolvedCanvas["@id"] ?? resolvedCanvas.id - if(!id) { - throw new Error("Cannot Resolve Canvas or Image", - {"cause":"The Canvas is 404 or unresolvable."}) + try { + await this.renderCanvas(resolvedCanvas) + } catch (err) { + // Canvas resolved but image extraction failed + this.renderCanvasError(uri, err.message) } - const type = resolvedCanvas["@type"] ?? resolvedCanvas.type - if(type !== "Canvas") { - throw new Error(`Provided URI did not resolve a 'Canvas'. It resolved a '${type}'`, - {"cause":"URI must point to a Canvas."}) + } + + /** + * Renders an error message when canvas resolution fails. + * @param {string} uri - The canvas URI that failed to resolve + * @param {string} [errorMessage] - Optional error message to display + */ + renderCanvasError(uri, errorMessage) { + const container = this.shadowRoot.querySelector('#annotoriousContainer') ?? this.shadowRoot + if (container) { + const message = errorMessage ?? 'The canvas image could not be loaded.' + container.innerHTML = ` +
+

Canvas Not Available

+

${message}

+

${uri}

+
+ ` } - this.renderCanvas(resolvedCanvas) } /** diff --git a/components/continue-working/index.js b/components/continue-working/index.js index 7dc5ef73..7e906ac9 100644 --- a/components/continue-working/index.js +++ b/components/continue-working/index.js @@ -1,6 +1,7 @@ import TPEN from '../../api/TPEN.js' import { stringFromDate } from '/js/utils.js' import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' +import vault from '../../js/vault.js' /** * ContinueWorking - Displays recent projects with thumbnails for quick access. @@ -175,19 +176,20 @@ class ContinueWorking extends HTMLElement { const annotationPage = await fetch(`${TPEN.servicesURL}/project/${project._id}/page/${annotationPageId}`).then(r => r.json()) const canvasId = annotationPage.target if (!canvasId) return this.generateProjectPlaceholder(project) - + let canvas, isV3 - try { - canvas = await fetch(canvasId).then(r => r.json()) + // Use vault for canvas fetching - silent failure for thumbnails + canvas = await vault.get(canvasId, 'canvas', false, 'tpen-continue-working') + if (canvas) { const context = canvas['@context'] isV3 = Array.isArray(context) ? context.some(ctx => typeof ctx === 'string' && ctx.includes('iiif.io/api/presentation/3')) : typeof context === 'string' && context.includes('iiif.io/api/presentation/3') - } catch { - // Fetch manifest + } else { + // Canvas fetch failed - fallback to manifest const manifestUrl = project.manifest?.[0] if (!manifestUrl) return this.generateProjectPlaceholder(project) - + const manifest = await fetch(manifestUrl).then(r => r.json()) const context = manifest['@context'] isV3 = Array.isArray(context) diff --git a/components/legacy-annotator/plain.js b/components/legacy-annotator/plain.js index e147e437..7aa04707 100644 --- a/components/legacy-annotator/plain.js +++ b/components/legacy-annotator/plain.js @@ -12,6 +12,7 @@ import { eventDispatcher } from '../../api/events.js' import TPEN from '../../api/TPEN.js' import User from '../../api/User.js' import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' +import vault from '../../js/vault.js' class LegacyAnnotator extends HTMLElement { #isDrawing = false @@ -340,7 +341,15 @@ class LegacyAnnotator extends HTMLElement { } // Note this will process the id from embedded Canvas objects to pass forward and be resolved. const canvasURI = this.processPageTarget(targetCanvas) - this.loadCanvas(canvasURI) + try { + await this.loadCanvas(canvasURI) + } catch (err) { + // Canvas or image loading failed + this.shadowRoot.innerHTML = ` +

Canvas Error

+

${err.message}

+ ` + } // Note this does not load and draw the existing Annotations. That functionality was not present at the time of componentizing. } @@ -394,31 +403,14 @@ class LegacyAnnotator extends HTMLElement { const imageCanvas = this.shadowRoot.getElementById("imageCanvas") const uploadedImage = this.shadowRoot.getElementById("uploadedImage") const ctx = imageCanvas.getContext("2d") - let err if(!canvas) return - const resolvedCanvas = await fetch(canvas) - .then(r => { - if(!r.ok) throw r - return r.json() - }) - .catch(e => { - throw e - }) - const context = resolvedCanvas["@context"] - if(!context.includes("iiif.io/api/presentation/3/context.json")){ - console.warn("The Canvas object did not have the IIIF Presentation API 3 context and may not be parseable.") - } - const id = resolvedCanvas["@id"] ?? resolvedCanvas.id - if(!id) { - throw new Error("Cannot Resolve Canvas or Image", - {"cause":"The Canvas is 404 or unresolvable."}) - } - const type = resolvedCanvas["@type"] ?? resolvedCanvas.type - if(type !== "Canvas"){ - throw new Error(`Provided URI did not resolve a 'Canvas'. It resolved a '${type}'`, - {"cause":"URI must point to a Canvas."}) + const resolvedCanvas = await vault.get(canvas, 'canvas', false, 'tpen-legacy-annotator') + if (!resolvedCanvas) { + // Canvas resolution failed - event already dispatched by vault + throw new Error("Cannot Resolve Canvas or Image", {"cause":"The Canvas is 404 or unresolvable."}) } - let image = resolvedCanvas?.items[0]?.items[0]?.body?.id + // Handle both IIIF v3 and v2 image formats + let image = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.id ?? resolvedCanvas?.images?.[0]?.resource?.id ?? resolvedCanvas?.images?.[0]?.resource?.['@id'] if(!image){ throw new Error("Cannot Resolve Canvas or Image", {"cause":"The Image is 404 or unresolvable."}) diff --git a/components/line-image/index.js b/components/line-image/index.js index ba192ccb..8e6b632b 100644 --- a/components/line-image/index.js +++ b/components/line-image/index.js @@ -1,5 +1,6 @@ import { decodeContentState } from '../iiif-tools/index.js' import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' +import vault from '../../js/vault.js' const CANVAS_PANEL_SCRIPT = document.createElement('script') CANVAS_PANEL_SCRIPT.src = "https://cdn.jsdelivr.net/npm/@digirati/canvas-panel-web-components@latest" @@ -204,18 +205,19 @@ class TpenImageFragment extends HTMLElement { } connectedCallback() { - this.cleanup.onDocument('canvas-change', (event) => { - fetch(event.detail.canvasId) - .then(res => res.json()) - .then(canvas => { - this.#canvas = canvas - this.setContainerStyle() - const imageResource = canvas?.items?.[0]?.items?.[0]?.body?.id ?? canvas?.images?.[0]?.resource?.id - if (imageResource) { - this.#lineImage.src = imageResource - } - }) - .catch(console.error) + this.cleanup.onDocument('canvas-change', async (event) => { + const canvas = await vault.get(event.detail.canvasId, 'canvas', false, 'tpen-image-fragment') + if (!canvas) { + // Canvas resolution failed - event dispatched by vault, parent handles errors + return + } + this.#canvas = canvas + this.setContainerStyle() + // Handle both IIIF v3 (items) and v2 (images) formats + const imageResource = canvas?.items?.[0]?.items?.[0]?.body?.id ?? canvas?.images?.[0]?.resource?.id ?? canvas?.images?.[0]?.resource?.['@id'] + if (imageResource) { + this.#lineImage.src = imageResource + } }) } diff --git a/components/read-only-transcribe/index.js b/components/read-only-transcribe/index.js index 6ded6e03..efda0497 100644 --- a/components/read-only-transcribe/index.js +++ b/components/read-only-transcribe/index.js @@ -1,5 +1,6 @@ import { checkIfUrlExists } from '../../utilities/checkIfUrlExists.js' import { CleanupRegistry } from '../../utilities/CleanupRegistry.js' +import vault from '../../js/vault.js' /** * ReadOnlyViewTranscribe - Public read-only view of transcription data from static manifest. @@ -411,12 +412,27 @@ class ReadOnlyViewTranscribe extends HTMLElement { async processCanvas(uri) { if (!uri) return - let embeddedCanvas = this.#staticManifest?.items.find(c => (c.id ?? c['@id']) === uri) + // Try to find canvas embedded in manifest first + let embeddedCanvas = this.#staticManifest?.items?.find(c => (c.id ?? c['@id']) === uri) + // Fallback to vault fetch if not found in manifest + if (!embeddedCanvas) { + embeddedCanvas = await vault.get(uri, 'canvas', false, 'tpen-read-only-view-transcribe') + } + if (!embeddedCanvas) { + // Canvas resolution failed - show error message + this.shadowRoot.getElementById('annotator-container').innerHTML = ` +
+

Canvas Not Available

+

The canvas image could not be loaded.

+
+ ` + return + } // Handle both Presentation API v3 (items) and v2 (images) formats - let fullImage = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.id ?? embeddedCanvas?.images?.[0]?.resource?.id - let imageService = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.service?.id - let imgx = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.width - let imgy = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.height + let fullImage = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.id ?? embeddedCanvas?.images?.[0]?.resource?.id ?? embeddedCanvas?.images?.[0]?.resource?.['@id'] + let imageService = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.service?.id ?? embeddedCanvas?.images?.[0]?.resource?.service?.['@id'] + let imgx = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.width ?? embeddedCanvas?.images?.[0]?.resource?.width + let imgy = embeddedCanvas?.items?.[0]?.items?.[0]?.body?.height ?? embeddedCanvas?.images?.[0]?.resource?.height this.#imageDims = [imgx || 0, imgy || 0] this.#canvasDims = [embeddedCanvas?.width || 0, embeddedCanvas?.height || 0] diff --git a/interfaces/manage-project/index.html b/interfaces/manage-project/index.html index 3120bc83..cb6c7079 100644 --- a/interfaces/manage-project/index.html +++ b/interfaces/manage-project/index.html @@ -22,7 +22,7 @@

This Project