Files
familienarchiv/docs/adr/005-thumbnail-aspect-and-page-count.md
Marcel d6b1949c84 docs(adr): ADR-005 thumbnailAspect + pageCount alongside the thumbnail
Captures the reasoning behind persisting two scalar columns on
documents rather than deriving aspect client-side or standing up a
thumbnail_metadata table. Also documents the 1.1 landscape threshold,
the null-during-rollout state, and the ordering invariants inside
ThumbnailService.generate().

Refs #305

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:38:56 +02:00

4.3 KiB

ADR-005: thumbnailAspect + pageCount alongside the thumbnail

Status

Accepted

Context

Issue #305 rebalances the /briefwechsel correspondence list into PDF-thumbnail rows. Two pieces of metadata are needed at row-render time:

  • Aspect ratio — postcards are landscape (7:5), letters are portrait (5:7). Forcing landscape scans into a portrait tile crops away the signature; forcing portrait scans into a landscape tile wastes horizontal real estate.
  • Page count — multi-page letters should show a "N" badge on their thumbnail so the reader can tell a single-page note from a seven-page letter without clicking in.

Both values are cheap to derive at the point the thumbnail is generated (the source image is already decoded; the PDF is already loaded) and impossible to derive cheaply later (requires re-reading the S3 object).

Decision

Persist both values as columns on documents and populate them inside ThumbnailService.generate() — the same code path that writes the JPEG to S3 and stamps thumbnail_generated_at.

  • thumbnail_aspect VARCHAR(16) mapped to a Java enum ThumbnailAspect with two values: PORTRAIT, LANDSCAPE.
  • page_count INTEGERPDDocument.getNumberOfPages() for PDFs, 1 for image uploads.
  • Aspect threshold is source.width / source.height > 1.1LANDSCAPE; everything else (including near-square A4 scans at ratio ≈ 1.0) stays PORTRAIT. The 1.1 margin keeps borderline scans from flipping across the threshold on a rounding error.
  • Both columns are nullable and remain null for historical documents until the existing /api/admin/generate-thumbnails backfill rerun populates them.

Alternatives Considered

Alternative Why rejected
Derive aspect client-side after image load First-paint would have all tiles in portrait, then reshuffle into landscape when the JPEG decodes — a visible jank on slow networks. The backend already has the dimensions; client-side recomputation is a waste.
Store full width / height columns Not needed anywhere — consumers want the categorical answer. If a future feature needs exact dimensions, they can be added later without migrating existing rows.
A separate thumbnail_metadata table Two scalar nullable columns aren't worth a join. See ADR-004 — thumbnails are modeled as a cross-cutting aspect of Document, not a sub-domain.
Derive page count from the existing PDF at render time on the frontend Duplicates work already done on the backend and requires a separate byte-range fetch of the PDF header. Frontend already gets pageCount "for free" via the Document response.

Consequences

Easier:

  • ConversationThumbnail.svelte picks the tile dimensions from thumbnailAspect directly — no async measurement, no layout shift.
  • ThumbnailRow reads pageCount synchronously for the badge. Multi-page letters are distinguishable at first paint.
  • Backfill runs the same migration path for every old document — re-executing generates the aspect + pageCount columns along with the JPEG, so operators don't have a second admin button to click.

Harder:

  • Both columns are null for every document until the backfill runs on a given instance. Frontend components guard with ?? 'PORTRAIT' / ?? 1 so the UI stays sensible during the rollout window. The backfill is idempotent and cheap (reuses existing S3 object), so re-running it is the simplest recovery path.
  • The aspect threshold is a single constant in Java. A future need to tune per-type (e.g. postcards vs photos) means a code change, not a configuration change — acceptable for a single-operator archive.

Ordering inside ThumbnailService.generate()

Aspect computation happens AFTER the JPEG upload succeeds but BEFORE the entity save — if the save throws, the columns rewind with it. Page count is captured while the PDDocument is still open; the SourcePreview record carries both the rendered first-page image and the page count back to the top of the pipeline so the PDF isn't reopened later.

Future Direction

  • If a postcard-specific "photo" chip is ever reintroduced, reuse thumbnailAspect === 'LANDSCAPE' && pageCount === 1 rather than adding a new kind column.
  • If multi-size thumbnails are introduced (per ADR-004's future note), the aspect + pageCount are per-document and do not need to be duplicated per size.