Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 24 additions & 32 deletions components/annotorious-annotator/line-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 = `
<h3>Canvas Error</h3>
<p>The Canvas within this Page could not be loaded.</p>
<p> ${e.status ?? e.code}: ${e.statusText ?? e.message} </p>
`
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 = `
<h3>Canvas Error</h3>
<p>The Canvas within this Page could not be loaded.</p>
<p style="word-break: break-all; font-size: 0.875rem; color: #666;">${uri}</p>
`
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 = `
<h3>Image Error</h3>
<p>The Canvas loaded but the image could not be extracted.</p>
<p style="font-size: 0.875rem; color: #666;">${err.message}</p>
`
}
// Use the Annotations and Image on the Canvas for inititalizing the Annotorious portion of the component.
this.loadAnnotorious(resolvedCanvas)
}

async validateImageUrl(url) {
Expand Down Expand Up @@ -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]
Expand Down
69 changes: 38 additions & 31 deletions components/annotorious-annotator/plain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = `
<div style="padding: 2rem; text-align: center; background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; margin: 1rem;">
<h3 style="color: #856404; margin-bottom: 1rem;">Canvas Not Available</h3>
<p style="color: #856404; margin-bottom: 0.5rem;">${message}</p>
<p style="color: #666; font-size: 0.875rem; word-break: break-all;">${uri}</p>
</div>
`
}
this.renderCanvas(resolvedCanvas)
}

/**
Expand Down
14 changes: 8 additions & 6 deletions components/continue-working/index.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 16 additions & 24 deletions components/legacy-annotator/plain.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = `
<h3>Canvas Error</h3>
<p>${err.message}</p>
`
}
// Note this does not load and draw the existing Annotations. That functionality was not present at the time of componentizing.
}

Expand Down Expand Up @@ -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."})
Expand Down
26 changes: 14 additions & 12 deletions components/line-image/index.js
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
})
}

Expand Down
26 changes: 21 additions & 5 deletions components/read-only-transcribe/index.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 = `
<div style="padding: 2rem; text-align: center;">
<h3>Canvas Not Available</h3>
<p>The canvas image could not be loaded.</p>
</div>
`
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]

Expand Down
2 changes: 1 addition & 1 deletion interfaces/manage-project/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h2 slot="header">This Project</h2>
<div slot="body">
<tpen-project-details></tpen-project-details>
<div class="action-links">
<tpen-can tpen-view="*_SELECTOR_LINE">
<tpen-can tpen-view="ANY_SELECTOR_LINE">
<a id="goParse" title="Go Parse Lines" class="left"><img src="../../assets/icons/parse-lines.svg" alt="icon"/>
<span>Parse Lines</span>
</a>
Expand Down
2 changes: 1 addition & 1 deletion interfaces/project/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ <h2 slot="header">Project Details <a id="projectManagementBtn" title="Manage Pro
<div slot="body">
<tpen-project-details class="card"></tpen-project-details>
<div class="action-links">
<tpen-can tpen-view="*_SELECTOR_LINE">
<tpen-can tpen-view="ANY_SELECTOR_LINE">
<a id="goParse" title="Go Parse Lines" class="left"><img src="../../assets/icons/parse-lines.svg"/>
<span>Parse Lines</span>
</a>
Expand Down
Loading