CenterForDigitalHumanities / CenterForDigitalHumanities/TPEN-interfaces

Continue Working widget shows pixelated thumbnails

Open
#536 0 comments 0 reactions 0 assignees View on GitHub
bug component
Dominant language
JavaScript
Stars
2
Forks
3
PR merge metrics
No merged PRs in 30d

Description

## Summary

The "Continue Working" widget on the home page renders project thumbnails at very low resolution. Images appear visibly pixelated, especially on HiDPI displays.

## Reproduction

1. Sign in and visit the home page (`/`).
2. Observe the project thumbnails in the "Continue Working" card.
3. Thumbnails render blurry/pixelated rather than crisp at the ~200 CSS px display size.

Reproduced against the Italian Paleography manuscript hosted on `iiif.library.utoronto.ca` (IIIF Image API 2 / Presentation API 2.1).

## Root cause

In `components/continue-working/index.js → getProjectThumbnail()`, two stacked bugs combine to deliver a tiny image:

### Bug 1 — `canvas.thumbnail` trusted blindly without checking declared size

Line 194:

```javascript
let thumbnailUrl = canvas.thumbnail?.id ?? canvas.thumbnail?.['@id'] ?? canvas.thumbnail
if (!thumbnailUrl) {
// …construct a sized URL from the IIIF service…
}
```

For the Toronto manifest the canvas advertises:

```json
"thumbnail": {
"@id": ".../full/80,/0/default.jpg",
"width": 80, "height": 120
}
```

So `thumbnailUrl` is set to an **80-pixel-wide** image and the entire fallback path is skipped. The widget displays it in a `flex: 1 1 200px; aspect-ratio: 1; object-fit: cover` section → stretched from 80 → ~200 CSS px (2.5×, worse on HiDPI). That's the pixelation.

The IIIF Presentation spec lets `canvas.thumbnail` be *any* size — it's metadata, not a sizing constraint. Trusting it without checking declared `width`/`height` is the bug.

### Bug 2 — Latent: malformed URLs in the fallback path

Even if `canvas.thumbnail` were missing, the constructed fallback URLs are malformed. Lines 209–213 build:

```javascript
`${baseUrl}/full/${isV3 ? 'max' : 'full'}/200,/0/default.jpg`
```

That produces e.g. `…/full/full/200,/0/default.jpg` — three size-related segments where IIIF wants `region/size/rotation`. Verified against the Toronto server:

```text
GET …/full/full/200,/0/default.jpg → 404
GET …/full/200,/0/default.jpg → 200
```

The `${isV3 ? 'max' : 'full'}/` insert stuffs a size token in front of the actual size `200,`, pushing rotation/quality into invalid path positions. Both the `square` and `full` attempts 404, hitting the final `.catch(() => thumbnailUrl = imageUrl)` — which on Toronto is `…/full/512,/0/default.jpg` (still small).

### Bug 3 — Latent: `thumbnail` as array not handled

[IIIF Presentation 3.0 §5.3.1](https://iiif.io/api/presentation/3.0/#thumbnail) requires `thumbnail` to be an array of objects. The current `canvas.thumbnail?.id ?? canvas.thumbnail?.['@id']` chain returns `undefined` for arrays and falls through to `canvas.thumbnail` itself (the array) as the URL — which renders as `[object Object]` in the ``.

## Suggested fix

Always derive a sized thumbnail from the IIIF Image service when one is available; only fall back to `canvas.thumbnail` when its declared width is sufficient; correctly construct the IIIF URL.

```javascript
async getProjectThumbnail(project, annotationPageId) {
try {
if (!annotationPageId) return this.generateProjectPlaceholder(project)
const annotationPage = await vault.get(
`${TPEN.servicesURL}/project/${project._id}/page/${annotationPageId}`,
'annotationpage', true
)
if (!annotationPage?.target) return this.generateProjectPlaceholder(project)
const canvas = await vault.getWithFallback(annotationPage.target, 'canvas', project.manifest)
if (!canvas) return this.generateProjectPlaceholder(project)

const isV3 = Array.isArray(canvas.items) || canvas.type === "Canvas"
const annotation = isV3 ? canvas.items?.[0]?.items?.[0] : canvas.images?.[0]
const rawService = isV3 ? annotation?.body?.service : annotation?.resource?.service
const service = Array.isArray(rawService) ? rawService[0] : rawService
const serviceBase = service?.id ?? service?.['@id']

// Prefer service-derived sized thumbnail (sharp at ~200 CSS px on HiDPI)
if (serviceBase) {
const base = serviceBase.replace(/\/$/, '')
return `${base}/full/400,/0/default.jpg`
}

// canvas.thumbnail can be array (P3) or object/string (P2/P3)
const t = Array.isArray(canvas.thumbnail) ? canvas.thumbnail[0] : canvas.thumbnail
const tWidth = t?.width ?? 0
if (tWidth >= 200) {
return t?.id ?? t?.['@id'] ?? (typeof t === 'string' ? t : null) ?? this.generateProjectPlaceholder(project)
}

// Last resort — the painting body's image URL
const imageUrl = isV3
? annotation?.body?.id ?? annotation?.body?.['@id']
: annotation?.resource?.['@id'] ?? annotation?.resource?.id
return imageUrl ?? this.generateProjectPlaceholder(project)
} catch (error) {
console.error('Error getting thumbnail:', error)
return this.generateProjectPlaceholder(project)
}
}
```

What this changes:

- Always asks the IIIF service for a 400 px-wide rendition first (sharp on HiDPI 2× displays at 200 CSS px).
- Only uses `canvas.thumbnail` when no service is available **and** the declared thumbnail is at least 200 wide.
- Drops the broken `square`/`max`-prefix attempt chain.
- Handles `thumbnail` as an array (Presentation 3.0 shape).

## Verification

1. `jekyll s`, sign in, visit `/`.
2. Confirm "Continue Working" thumbnails render crisply (no visible pixelation) at the ~200 CSS px display size.
3. Confirm Network panel shows `…/full/400,/0/default.jpg` requests, no 404s on `…/full/full/200,/…` or `…/square/full/200,/…`.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.