fix(backend): show whole image in thumbnails and previews (contain)
deploy / deploy (push) Successful in 51s
deploy / deploy (push) Successful in 51s
Both thumbnails and previews went through imaging.Thumbnail, which scales and centre-crops to the exact dimensions — so portrait images lost their top and bottom in the viewer (and the grid). Switch both to imaging.Fit, which scales to fit within the bounds preserving aspect ratio, never cropping or upscaling. The grid cell letterboxes the thumbnail via the existing object-fit: contain. Note: cached *_thumb.jpg / *_preview.jpg are regenerated only when absent, so clear THUMBS_CACHE_PATH after deploying to drop the old cropped renders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -120,16 +120,18 @@ func (s *DiskStorage) InvalidateCache(_ context.Context, id uuid.UUID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thumbnail returns a JPEG that fits within the configured max width×height
|
// Thumbnail returns a JPEG scaled to fit within the configured max width×height,
|
||||||
// (never upscaled, never cropped). Generated on first call and cached.
|
// preserving the original aspect ratio (never upscaled, never cropped); the grid
|
||||||
// Video files are thumbnailed via ffmpeg; other non-image files get a placeholder.
|
// cell letterboxes it as needed. Generated on first call and cached. Video files
|
||||||
|
// are thumbnailed via ffmpeg; other non-image files get a placeholder.
|
||||||
func (s *DiskStorage) Thumbnail(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) {
|
func (s *DiskStorage) Thumbnail(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) {
|
||||||
return s.serveGenerated(ctx, id, s.thumbCachePath(id), s.thumbWidth, s.thumbHeight)
|
return s.serveGenerated(ctx, id, s.thumbCachePath(id), s.thumbWidth, s.thumbHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview returns a JPEG that fits within the configured max width×height
|
// Preview returns a JPEG scaled to fit within the configured max width×height,
|
||||||
// (never upscaled, never cropped). Generated on first call and cached.
|
// preserving the original aspect ratio (never upscaled, never cropped) so the
|
||||||
// Video files are thumbnailed via ffmpeg; other non-image files get a placeholder.
|
// viewer shows the whole image. Generated on first call and cached. Video files
|
||||||
|
// are thumbnailed via ffmpeg; other non-image files get a placeholder.
|
||||||
func (s *DiskStorage) Preview(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) {
|
func (s *DiskStorage) Preview(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) {
|
||||||
return s.serveGenerated(ctx, id, s.previewCachePath(id), s.previewWidth, s.previewHeight)
|
return s.serveGenerated(ctx, id, s.previewCachePath(id), s.previewWidth, s.previewHeight)
|
||||||
}
|
}
|
||||||
@@ -138,8 +140,9 @@ func (s *DiskStorage) Preview(ctx context.Context, id uuid.UUID) (io.ReadCloser,
|
|||||||
// Internal helpers
|
// Internal helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// serveGenerated is the shared implementation for Thumbnail and Preview.
|
// serveGenerated is the shared implementation for Thumbnail and Preview. Both
|
||||||
// imaging.Thumbnail fits the source within maxW×maxH without upscaling or cropping.
|
// fit the source within maxW×maxH with imaging.Fit, preserving the aspect ratio
|
||||||
|
// (no crop, no upscale); they differ only in the configured dimensions.
|
||||||
//
|
//
|
||||||
// Resolution order:
|
// Resolution order:
|
||||||
// 1. Return cached JPEG if present.
|
// 1. Return cached JPEG if present.
|
||||||
@@ -166,9 +169,9 @@ func (s *DiskStorage) serveGenerated(ctx context.Context, id uuid.UUID, cachePat
|
|||||||
// 3. Fall back to placeholder.
|
// 3. Fall back to placeholder.
|
||||||
var img image.Image
|
var img image.Image
|
||||||
if decoded, err := decodeImageLimited(srcPath); err == nil {
|
if decoded, err := decodeImageLimited(srcPath); err == nil {
|
||||||
img = imaging.Thumbnail(decoded, maxW, maxH, imaging.Lanczos)
|
img = imaging.Fit(decoded, maxW, maxH, imaging.Lanczos)
|
||||||
} else if frame, err := extractVideoFrame(ctx, srcPath); err == nil {
|
} else if frame, err := extractVideoFrame(ctx, srcPath); err == nil {
|
||||||
img = imaging.Thumbnail(frame, maxW, maxH, imaging.Lanczos)
|
img = imaging.Fit(frame, maxW, maxH, imaging.Lanczos)
|
||||||
} else {
|
} else {
|
||||||
img = placeholder(maxW, maxH)
|
img = placeholder(maxW, maxH)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user