Gallery thumbnails look soft/poor for large camera images. Two verified causes:
DerivativeGenerator resizes with ImageSharp's DEFAULT Bicubic in one step (20MP → 320px) with no sharpening (DerivativeGenerator.cs:50).
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Gallery thumbnails look soft/poor for large camera images. Two verified causes:
Fix:
Output stays JPEG q90/q80 at 2048/320 — AVIF/WebP format change remains #805.
Resolved in spikersoft-backend PR #506 (merged to
master,6f24333a). Verified againstorigin/master:ResizeMode.Max—DerivativeGenerator.cs:85-86.DerivativeGenerator.cs:91-95, with the thresholded edge-gated unsharp mask inPhotographProcessor/Services/EdgeAwareSharpen.csandEdgeAwareSharpenTests.cscovering it.PreviewMaxEdge = 2048/ThumbnailMaxEdge = 320(:17-18), JPEG q90/q80 (:66,:69). No AVIF/WebP creep here; that stays #805's scope.PhotographConsumers.cs:178-193now runs the extractedPreviewImagethroughDerivativeGenerator.GenerateAsyncto produce both derivatives. The embeddedThumbnailImageis only reached at:208-218whenThumbnailGridFsId is null, i.e. genuine fallback, with a corrupt-embedded-JPEG catch preserving old behavior (:196-205).DerivativeGenerator.CurrentVersion = 2(:26),Photograph.DerivativeVersion(Photograph.cs:110), stamped atPhotographConsumers.cs:193and:237.tools/RegeneratePhotographPreviews/Program.cs, idempotent via theDerivativeVersion >= CurrentVersionskip (:87) with aFORCEoverride, 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 standalonetools/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.