CenterForDigitalHumanities / CenterForDigitalHumanities/TPEN-interfaces
Continue Working widget shows pixelated thumbnails
- 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.