Photo gallery: poor thumbnail quality — Lanczos3 + edge-aware sharpening + fix ORF 160×120 thumbs #892

Closed
opened 2026-07-29 05:48:12 +00:00 by spikerj · 1 comment
Owner

Gallery thumbnails look soft/poor for large camera images. Two verified causes:

  1. DerivativeGenerator resizes with ImageSharp's DEFAULT Bicubic in one step (20MP → 320px) with no sharpening (DerivativeGenerator.cs:50).
  2. Worse, for ORF raws the served thumbnail is the camera's embedded ThumbnailImage — a 160×120 JPEG uploaded verbatim and upscaled by the browser (PhotographConsumers.cs L166-180; DerivativeGenerator is skipped because PreviewGridFsId is already set).

Fix:

  • Lanczos3 resampling (ResizeOptions Mode=Max, precedent ArtImageDerivativeGenerator.cs:90-95) + new EdgeAwareSharpen helper (thresholded edge-gated unsharp mask) applied to the 320 thumb.
  • ORF path: run the embedded PreviewImage through the upgraded generator to produce BOTH derivatives; embedded ThumbnailImage becomes fallback-only.
  • Photograph.DerivativeVersion marker (v2) + new tools/RegeneratePhotographPreviews to regenerate all existing previews/thumbs idempotently (runs inside the PhotographProcessor image — needs exiftool).

Output stays JPEG q90/q80 at 2048/320 — AVIF/WebP format change remains #805.

Gallery thumbnails look soft/poor for large camera images. Two verified causes: 1. DerivativeGenerator resizes with ImageSharp's DEFAULT Bicubic in one step (20MP → 320px) with no sharpening (DerivativeGenerator.cs:50). 2. Worse, for ORF raws the served thumbnail is the camera's embedded ThumbnailImage — a 160×120 JPEG uploaded verbatim and upscaled by the browser (PhotographConsumers.cs L166-180; DerivativeGenerator is skipped because PreviewGridFsId is already set). Fix: - Lanczos3 resampling (ResizeOptions Mode=Max, precedent ArtImageDerivativeGenerator.cs:90-95) + new EdgeAwareSharpen helper (thresholded edge-gated unsharp mask) applied to the 320 thumb. - ORF path: run the embedded PreviewImage through the upgraded generator to produce BOTH derivatives; embedded ThumbnailImage becomes fallback-only. - Photograph.DerivativeVersion marker (v2) + new tools/RegeneratePhotographPreviews to regenerate all existing previews/thumbs idempotently (runs inside the PhotographProcessor image — needs exiftool). Output stays JPEG q90/q80 at 2048/320 — AVIF/WebP format change remains #805.
Author
Owner

Resolved in spikersoft-backend PR #506 (merged to master, 6f24333a). Verified against origin/master:

  • Lanczos3 + ResizeMode.MaxDerivativeGenerator.cs:85-86.
  • Edge-aware sharpening on the 320 thumb onlyDerivativeGenerator.cs:91-95, with the thresholded edge-gated unsharp mask in PhotographProcessor/Services/EdgeAwareSharpen.cs and EdgeAwareSharpenTests.cs covering it.
  • Sizes/quality unchanged as intendedPreviewMaxEdge = 2048 / ThumbnailMaxEdge = 320 (:17-18), JPEG q90/q80 (:66, :69). No AVIF/WebP creep here; that stays #805's scope.
  • ORF 160×120 fixedPhotographConsumers.cs:178-193 now runs the extracted PreviewImage through DerivativeGenerator.GenerateAsync to produce both derivatives. The embedded ThumbnailImage is only reached at :208-218 when ThumbnailGridFsId is null, i.e. genuine fallback, with a corrupt-embedded-JPEG catch preserving old behavior (:196-205).
  • Re-derivation markerDerivativeGenerator.CurrentVersion = 2 (:26), Photograph.DerivativeVersion (Photograph.cs:110), stamped at PhotographConsumers.cs:193 and :237.
  • Regeneration tooltools/RegeneratePhotographPreviews/Program.cs, idempotent via the DerivativeVersion >= CurrentVersion skip (:87) with a FORCE override, and save-then-delete ordering so the read path never window-404s.

Note for whoever runs the regen: like BackfillPhotographBursts/BackfillPhotographTags, it ships as a standalone tools/ csproj with no Dockerfile — run it inside the PhotographProcessor image per its header comment (Program.cs:18-21). That's the existing convention, not a gap.

Closing.

Resolved in spikersoft-backend PR #506 (merged to `master`, `6f24333a`). Verified against `origin/master`: - **Lanczos3 + `ResizeMode.Max`** — `DerivativeGenerator.cs:85-86`. - **Edge-aware sharpening on the 320 thumb only** — `DerivativeGenerator.cs:91-95`, with the thresholded edge-gated unsharp mask in `PhotographProcessor/Services/EdgeAwareSharpen.cs` and `EdgeAwareSharpenTests.cs` covering it. - **Sizes/quality unchanged as intended** — `PreviewMaxEdge = 2048` / `ThumbnailMaxEdge = 320` (`:17-18`), JPEG q90/q80 (`:66`, `:69`). No AVIF/WebP creep here; that stays #805's scope. - **ORF 160×120 fixed** — `PhotographConsumers.cs:178-193` now runs the extracted `PreviewImage` through `DerivativeGenerator.GenerateAsync` to produce both derivatives. The embedded `ThumbnailImage` is only reached at `:208-218` when `ThumbnailGridFsId is null`, i.e. genuine fallback, with a corrupt-embedded-JPEG catch preserving old behavior (`:196-205`). - **Re-derivation marker** — `DerivativeGenerator.CurrentVersion = 2` (`:26`), `Photograph.DerivativeVersion` (`Photograph.cs:110`), stamped at `PhotographConsumers.cs:193` and `:237`. - **Regeneration tool** — `tools/RegeneratePhotographPreviews/Program.cs`, idempotent via the `DerivativeVersion >= CurrentVersion` skip (`:87`) with a `FORCE` override, and save-then-delete ordering so the read path never window-404s. Note for whoever runs the regen: like `BackfillPhotographBursts`/`BackfillPhotographTags`, it ships as a standalone `tools/` csproj with no Dockerfile — run it inside the PhotographProcessor image per its header comment (`Program.cs:18-21`). That's the existing convention, not a gap. Closing.
Sign in to join this conversation.