From 5612fb8f41e5f9d7bb31137bfcbeda51620a0d6e Mon Sep 17 00:00:00 2001 From: Johnny Date: Sat, 3 Jan 2026 13:16:56 +0800 Subject: [PATCH] feat: add HDR image and video support - Add HDR detection utilities for wide color gamut formats (HEIC, HEIF, WebP) - Apply colorSpace attribute to image/video elements for HDR-capable files - Update frontend components (AttachmentCard, PreviewImageDialog, AttachmentList) - Expand backend thumbnail generation to support HEIC, HEIF, WebP formats - Add Color-Gamut response headers to advertise wide gamut support - Extend avatar MIME type validation for HDR formats Supported formats: - Images: HEIC, HEIF, WebP, PNG, JPEG - Videos: MP4, QuickTime, Matroska, WebM (VP9 Profile 2) Browser support: - Safari 14.1+, Chrome 118+, Edge 118+ - Gracefully degrades to sRGB on unsupported browsers --- server/router/fileserver/fileserver.go | 20 +++++++++++- .../components/metadata/AttachmentCard.tsx | 14 +++++++-- .../components/metadata/AttachmentList.tsx | 12 ++++--- web/src/components/PreviewImageDialog.tsx | 6 +++- web/src/utils/attachment.ts | 31 +++++++++++++++++++ 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/server/router/fileserver/fileserver.go b/server/router/fileserver/fileserver.go index bac7a46e1..c49d92e3a 100644 --- a/server/router/fileserver/fileserver.go +++ b/server/router/fileserver/fileserver.go @@ -36,6 +36,9 @@ const ( var SupportedThumbnailMimeTypes = []string{ "image/png", "image/jpeg", + "image/heic", + "image/heif", + "image/webp", } // FileServerService handles HTTP file serving with proper range request support. @@ -143,6 +146,10 @@ func (s *FileServerService) serveAttachmentFile(c echo.Context) error { // Defense-in-depth: prevent embedding in frames and restrict content loading c.Response().Header().Set("X-Frame-Options", "DENY") c.Response().Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';") + // Support HDR/wide color gamut display for capable browsers + if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { + c.Response().Header().Set("Color-Gamut", "srgb, p3, rec2020") + } // Force download for non-media files to prevent XSS execution if !strings.HasPrefix(contentType, "image/") && @@ -194,12 +201,15 @@ func (s *FileServerService) serveUserAvatar(c echo.Context) error { } // Validate avatar MIME type to prevent XSS + // Supports standard formats and HDR-capable formats allowedAvatarTypes := map[string]bool{ "image/png": true, "image/jpeg": true, "image/jpg": true, "image/gif": true, "image/webp": true, + "image/heic": true, + "image/heif": true, } if !allowedAvatarTypes[imageType] { return echo.NewHTTPError(http.StatusBadRequest, "invalid avatar image type") @@ -336,8 +346,16 @@ func (s *FileServerService) getCurrentUser(ctx context.Context, c echo.Context) } // isImageType checks if the mime type is an image that supports thumbnails. +// Supports standard formats (PNG, JPEG) and HDR-capable formats (HEIC, HEIF, WebP). func (*FileServerService) isImageType(mimeType string) bool { - return mimeType == "image/png" || mimeType == "image/jpeg" + supportedTypes := map[string]bool{ + "image/png": true, + "image/jpeg": true, + "image/heic": true, + "image/heif": true, + "image/webp": true, + } + return supportedTypes[mimeType] } // getAttachmentReader returns a reader for the attachment content. diff --git a/web/src/components/MemoView/components/metadata/AttachmentCard.tsx b/web/src/components/MemoView/components/metadata/AttachmentCard.tsx index 2aadfb76c..1fae54fd3 100644 --- a/web/src/components/MemoView/components/metadata/AttachmentCard.tsx +++ b/web/src/components/MemoView/components/metadata/AttachmentCard.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; -import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; +import { getAttachmentType, getAttachmentUrl, getColorspace } from "@/utils/attachment"; interface AttachmentCardProps { attachment: Attachment; @@ -11,6 +11,7 @@ interface AttachmentCardProps { const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) => { const attachmentType = getAttachmentType(attachment); const sourceUrl = getAttachmentUrl(attachment); + const colorspace = getColorspace(attachment.type); if (attachmentType === "image/*") { return ( @@ -20,12 +21,21 @@ const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) className={cn("w-full h-full object-cover rounded-lg cursor-pointer", className)} onClick={onClick} loading="lazy" + {...(colorspace && { colorSpace: colorspace as unknown as string })} /> ); } if (attachmentType === "video/*") { - return