Continue Working widget shows pixelated thumbnails
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- javascript
- Domain
- frontend
Research direction
Start in components/continue-working/index.js at getProjectThumbnail() and review how IIIF canvas thumbnails and image services are resolved for Presentation 2 and 3. Run jekyll s, sign in, and visit / to verify crisp ~200 CSS px thumbnails, 400 px IIIF requests, and no malformed URL 404s in the Network panel.
Written by the indexing model from the issue text.
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
- Sign in and visit the home page (
/). - Observe the project thumbnails in the "Continue Working" card.
- 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:
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:
"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:
`${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:
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 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 <img src>.
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.
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.thumbnailwhen no service is available and the declared thumbnail is at least 200 wide. - Drops the broken
square/max-prefix attempt chain. - Handles
thumbnailas an array (Presentation 3.0 shape).
Verification
jekyll s, sign in, visit/.- Confirm "Continue Working" thumbnails render crisply (no visible pixelation) at the ~200 CSS px display size.
- Confirm Network panel shows
…/full/400,/0/default.jpgrequests, no 404s on…/full/full/200,/…or…/square/full/200,/….
- Dominant language
- JavaScript
- Stars
- 2
- Forks
- 3
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from CenterForDigitalHumanities/TPEN-interfaces
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
License link borkedOpen
Difficulty 1/5 Under an hour Newbie friendliness 68/100
All issues in CenterForDigitalHumanities/TPEN-interfaces
Similar issues
-
release-followup
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
area: right sidebar
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
zulip/zulip#40449 · 3 comments ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
cleanup
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
uclaacm/membership-portal-ui#409 ·
Maintainers usually reply within 1 day