From 476ab5da2d720d564b54b2424cdf4fe48f28f4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84=E5=A7=93=E5=90=8D?= Date: Fri, 19 Jun 2026 21:23:45 +0800 Subject: [PATCH] feat(desktop): auto-compress pasted/dropped images before upload Images are now resized to max 1568px longest edge and re-encoded as JPEG (quality 0.85) before being attached. This prevents oversized base64 payloads hitting provider limits and fixes broken preview thumbnails caused by large objectURLs exhausting renderer memory. GIF images pass through unchanged (would lose animation). Compression applies to both paste and file-drop/select paths. --- desktop/src/components/chat/ChatInput.tsx | 26 +++++++++++++---------- desktop/src/lib/composerAttachments.ts | 6 ++++-- desktop/src/lib/imageCompress.ts | 13 +++++++++--- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 746f619e..5cc4bdfc 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -42,6 +42,7 @@ import { selectNativeFileAttachments, type ComposerAttachment, } from '../../lib/composerAttachments' +import { compressDataUrl } from '../../lib/imageCompress' import { useComposerFileDrop } from './useComposerFileDrop' import { shouldSubmitOnEnter } from './sendShortcut' import { @@ -903,17 +904,20 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro const id = `att-${Date.now()}-${Math.random().toString(36).slice(2)}` const reader = new FileReader() reader.onload = () => { - setComposerAttachments((prev) => [ - ...prev, - { - id, - name: `pasted-image-${Date.now()}.png`, - type: 'image', - mimeType: file.type || 'image/png', - previewUrl: reader.result as string, - data: reader.result as string, - }, - ]) + const rawDataUrl = reader.result as string + void compressDataUrl(rawDataUrl).then((compressed) => { + setComposerAttachments((prev) => [ + ...prev, + { + id, + name: `pasted-image-${Date.now()}.png`, + type: 'image', + mimeType: 'image/jpeg', + previewUrl: compressed, + data: compressed, + }, + ]) + }) } reader.readAsDataURL(file) } diff --git a/desktop/src/lib/composerAttachments.ts b/desktop/src/lib/composerAttachments.ts index 8ba948f2..b6e6a8ac 100644 --- a/desktop/src/lib/composerAttachments.ts +++ b/desktop/src/lib/composerAttachments.ts @@ -1,6 +1,7 @@ import { getApiUrl } from '../api/client' import { isDesktopRuntime } from './desktopRuntime' import { getDesktopHost } from './desktopHost' +import { compressDataUrl } from './imageCompress' export type ComposerAttachment = { id: string @@ -121,12 +122,13 @@ async function fileToComposerAttachment(file: File): Promise { +/** + * Compress a data:URL image by resizing to maxEdge and re-encoding as JPEG. + * GIFs pass through unchanged (would lose animation). + * Small images (≤ maxEdge both sides) still re-encode to reduce payload. + */ +export async function compressDataUrl(dataUrl: string, maxEdge = 1568, quality = 0.85): Promise { + if (!dataUrl.startsWith('data:')) return dataUrl + if (dataUrl.startsWith('data:image/gif')) return dataUrl + const img = new Image() await new Promise((res, rej) => { img.onload = () => res(); img.onerror = () => rej(new Error('load')); img.src = dataUrl }) const { width, height } = planResize(img.naturalWidth, img.naturalHeight, maxEdge) const canvas = document.createElement('canvas') canvas.width = width; canvas.height = height canvas.getContext('2d')!.drawImage(img, 0, 0, width, height) - return canvas.toDataURL('image/png', quality) + return canvas.toDataURL('image/jpeg', quality) }