From 8f410d710505f583e3384a55f864e56c56ec9a0b Mon Sep 17 00:00:00 2001 From: Florian Dewald Date: Thu, 23 Oct 2025 17:53:55 +0100 Subject: [PATCH] Extract functionality to scale images into separate function --- server/router/api/v1/attachment_service.go | 105 +++++++++++++++------ 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index 64f342904..029d48213 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -5,6 +5,9 @@ import ( "context" "encoding/binary" "fmt" + "image" + _ "image/jpeg" + _ "image/png" "io" "log/slog" "os" @@ -526,8 +529,74 @@ func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte, const ( // thumbnailMaxSize is the maximum size in pixels for the largest dimension of the thumbnail image. thumbnailMaxSize = 600 + // defaultJPEGQuality is the default JPEG quality for downscaling images. + defaultJPEGQuality = 85 ) +// downscaleImage takes an image blob and returns a downscaled version as a blob. +// The maxDimension parameter specifies the maximum size in pixels for the largest dimension. +// The quality parameter specifies the JPEG encoding quality (1-100, where 100 is best quality). +// PNG images are preserved as PNG, other formats are encoded as JPEG. +// Images smaller than maxDimension are not enlarged. +func downscaleImage(imageBlob []byte, maxDimension int, quality int) ([]byte, error) { + // Detect the image format before decoding + reader := bytes.NewReader(imageBlob) + _, formatName, err := image.DecodeConfig(reader) + if err != nil { + return nil, errors.Wrap(err, "failed to detect image format") + } + + // Reset reader position for actual decoding + reader.Seek(0, 0) + + // Decode the image with auto-orientation support + img, err := imaging.Decode(reader, imaging.AutoOrientation(true)) + if err != nil { + return nil, errors.Wrap(err, "failed to decode image") + } + + // Get original dimensions + width := img.Bounds().Dx() + height := img.Bounds().Dy() + var targetWidth, targetHeight int + + // Only resize if the image is larger than maxDimension + if max(width, height) > maxDimension { + if width >= height { + // Landscape or square - constrain width, maintain aspect ratio for height + targetWidth = maxDimension + targetHeight = 0 + } else { + // Portrait - constrain height, maintain aspect ratio for width + targetWidth = 0 + targetHeight = maxDimension + } + } else { + // Keep original dimensions for small images + targetWidth = width + targetHeight = height + } + + // Resize the image to the calculated dimensions + resizedImage := imaging.Resize(img, targetWidth, targetHeight, imaging.Lanczos) + + // Encode the image based on the original format + var buf bytes.Buffer + if formatName == "png" { + // Preserve PNG format for PNG images + if err := imaging.Encode(&buf, resizedImage, imaging.PNG); err != nil { + return nil, errors.Wrap(err, "failed to encode PNG image") + } + } else { + // Encode as JPEG for all other formats + if err := imaging.Encode(&buf, resizedImage, imaging.JPEG, imaging.JPEGQuality(quality)); err != nil { + return nil, errors.Wrap(err, "failed to encode JPEG image") + } + } + + return buf.Bytes(), nil +} + // getOrGenerateThumbnail returns the thumbnail image of the attachment. func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) { thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder) @@ -545,39 +614,19 @@ func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]b if err != nil { return nil, errors.Wrap(err, "failed to get attachment blob") } - img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true)) + + // Downscale the image + thumbnailBlob, err := downscaleImage(blob, thumbnailMaxSize, defaultJPEGQuality) if err != nil { - return nil, errors.Wrap(err, "failed to decode thumbnail image") + return nil, errors.Wrap(err, "failed to downscale image") } - // The largest dimension is set to thumbnailMaxSize and the smaller dimension is scaled proportionally. - // Small images are not enlarged. - width := img.Bounds().Dx() - height := img.Bounds().Dy() - var thumbnailWidth, thumbnailHeight int - - // Only resize if the image is larger than thumbnailMaxSize - if max(width, height) > thumbnailMaxSize { - if width >= height { - // Landscape or square - constrain width, maintain aspect ratio for height - thumbnailWidth = thumbnailMaxSize - thumbnailHeight = 0 - } else { - // Portrait - constrain height, maintain aspect ratio for width - thumbnailWidth = 0 - thumbnailHeight = thumbnailMaxSize - } - } else { - // Keep original dimensions for small images - thumbnailWidth = width - thumbnailHeight = height - } - - // Resize the image to the calculated dimensions. - thumbnailImage := imaging.Resize(img, thumbnailWidth, thumbnailHeight, imaging.Lanczos) - if err := imaging.Save(thumbnailImage, filePath); err != nil { + // Save the thumbnail to disk + if err := os.WriteFile(filePath, thumbnailBlob, 0644); err != nil { return nil, errors.Wrap(err, "failed to save thumbnail file") } + + return thumbnailBlob, nil } thumbnailFile, err := os.Open(filePath)