mirror of https://github.com/usememos/memos.git
feat: generate thumbnails for images stored in S3 and generate thumbnails with a maximum size (#5179)
This commit is contained in:
parent
16425ed650
commit
e4f6345342
|
|
@ -79,6 +79,20 @@ func (c *Client) PresignGetObject(ctx context.Context, key string) (string, erro
|
|||
return presignResult.URL, nil
|
||||
}
|
||||
|
||||
// GetObject retrieves an object from S3.
|
||||
func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) {
|
||||
downloader := manager.NewDownloader(c.Client)
|
||||
buffer := manager.NewWriteAtBuffer([]byte{})
|
||||
_, err := downloader.Download(ctx, buffer, &s3.GetObjectInput{
|
||||
Bucket: c.Bucket,
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to download object")
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// DeleteObject deletes an object in S3.
|
||||
func (c *Client) DeleteObject(ctx context.Context, key string) error {
|
||||
_, err := c.Client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
|
|
|
|||
|
|
@ -492,13 +492,40 @@ func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte,
|
|||
}
|
||||
return blob, nil
|
||||
}
|
||||
// For S3 storage, download the file from S3.
|
||||
if attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
||||
if attachment.Payload == nil {
|
||||
return nil, errors.New("attachment payload is missing")
|
||||
}
|
||||
s3Object := attachment.Payload.GetS3Object()
|
||||
if s3Object == nil {
|
||||
return nil, errors.New("S3 object payload is missing")
|
||||
}
|
||||
if s3Object.S3Config == nil {
|
||||
return nil, errors.New("S3 config is missing")
|
||||
}
|
||||
if s3Object.Key == "" {
|
||||
return nil, errors.New("S3 object key is missing")
|
||||
}
|
||||
|
||||
s3Client, err := s3.NewClient(context.Background(), s3Object.S3Config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create S3 client")
|
||||
}
|
||||
|
||||
blob, err := s3Client.GetObject(context.Background(), s3Object.Key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get object from S3")
|
||||
}
|
||||
return blob, nil
|
||||
}
|
||||
// For database storage, return the blob from the database.
|
||||
return attachment.Blob, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// thumbnailRatio is the ratio of the thumbnail image.
|
||||
thumbnailRatio = 0.8
|
||||
// thumbnailMaxSize is the maximum size in pixels for the largest dimension of the thumbnail image.
|
||||
thumbnailMaxSize = 600
|
||||
)
|
||||
|
||||
// getOrGenerateThumbnail returns the thumbnail image of the attachment.
|
||||
|
|
@ -523,9 +550,31 @@ func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]b
|
|||
return nil, errors.Wrap(err, "failed to decode thumbnail image")
|
||||
}
|
||||
|
||||
thumbnailWidth := int(float64(img.Bounds().Dx()) * thumbnailRatio)
|
||||
// Resize the image to the thumbnailWidth.
|
||||
thumbnailImage := imaging.Resize(img, thumbnailWidth, 0, imaging.Lanczos)
|
||||
// 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 {
|
||||
return nil, errors.Wrap(err, "failed to save thumbnail file")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
import React, { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import PreviewImageDialog from "./PreviewImageDialog";
|
||||
import SquareDiv from "./kit/SquareDiv";
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ const AttachmentIcon = (props: Props) => {
|
|||
<SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}>
|
||||
<img
|
||||
className="min-w-full min-h-full object-cover"
|
||||
src={attachment.externalLink ? attachmentUrl : attachmentUrl + "?thumbnail=true"}
|
||||
src={getAttachmentThumbnailUrl(attachment)}
|
||||
onClick={handleImageClick}
|
||||
onError={(e) => {
|
||||
// Fallback to original image if thumbnail fails
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||
import MemoAttachment from "./MemoAttachment";
|
||||
import PreviewImageDialog from "./PreviewImageDialog";
|
||||
|
||||
|
|
@ -35,12 +35,13 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
|
|||
const MediaCard = ({ attachment, className }: { attachment: Attachment; className?: string }) => {
|
||||
const type = getAttachmentType(attachment);
|
||||
const attachmentUrl = getAttachmentUrl(attachment);
|
||||
const attachmentThumbnailUrl = getAttachmentThumbnailUrl(attachment);
|
||||
|
||||
if (type === "image/*") {
|
||||
return (
|
||||
<img
|
||||
className={cn("cursor-pointer h-full w-auto rounded-lg border border-border/60 object-contain transition-colors", className)}
|
||||
src={attachment.externalLink ? attachmentUrl : attachmentUrl + "?thumbnail=true"}
|
||||
src={attachmentThumbnailUrl}
|
||||
onError={(e) => {
|
||||
// Fallback to original image if thumbnail fails
|
||||
const target = e.target as HTMLImageElement;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ export const getAttachmentUrl = (attachment: Attachment) => {
|
|||
return `${window.location.origin}/file/${attachment.name}/${attachment.filename}`;
|
||||
};
|
||||
|
||||
export const getAttachmentThumbnailUrl = (attachment: Attachment) => {
|
||||
return `${window.location.origin}/file/${attachment.name}/${attachment.filename}?thumbnail=true`;
|
||||
};
|
||||
|
||||
export const getAttachmentType = (attachment: Attachment) => {
|
||||
if (isImage(attachment.type)) {
|
||||
return "image/*";
|
||||
|
|
|
|||
Loading…
Reference in New Issue