From 326514be4e6eb3c416c5180b63cf4012ea90d703 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 24 Dec 2024 23:28:14 -0700 Subject: [PATCH] enh: image compression --- .../components/channel/MessageInput.svelte | 19 +++++- src/lib/components/chat/MessageInput.svelte | 17 ++++- .../components/chat/Settings/Interface.svelte | 64 +++++++++++++++++- src/lib/utils/index.ts | 67 +++++++++++++++++++ 4 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index e9f9852b9..3f1bd3d3d 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -7,7 +7,7 @@ const i18n = getContext('i18n'); import { config, mobile, settings } from '$lib/stores'; - import { blobToFile } from '$lib/utils'; + import { blobToFile, compressImage } from '$lib/utils'; import Tooltip from '../common/Tooltip.svelte'; import RichTextInput from '../common/RichTextInput.svelte'; @@ -100,15 +100,28 @@ if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { let reader = new FileReader(); - reader.onload = (event) => { + + reader.onload = async (event) => { + let imageUrl = event.target.result; + + if ($settings?.imageCompression ?? false) { + const width = $settings?.imageCompressionSize?.width ?? null; + const height = $settings?.imageCompressionSize?.height ?? null; + + if (width || height) { + imageUrl = await compressImage(imageUrl, width, height); + } + } + files = [ ...files, { type: 'image', - url: `${event.target.result}` + url: `${imageUrl}` } ]; }; + reader.readAsDataURL(file); } else { uploadFileHandler(file); diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c2fef4ff2..a65150ec4 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -19,7 +19,7 @@ showControls } from '$lib/stores'; - import { blobToFile, createMessagesList, findWordIndices } from '$lib/utils'; + import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils'; import { transcribeAudio } from '$lib/apis/audio'; import { uploadFile } from '$lib/apis/files'; import { getTools } from '$lib/apis/tools'; @@ -244,12 +244,23 @@ return; } let reader = new FileReader(); - reader.onload = (event) => { + reader.onload = async (event) => { + let imageUrl = event.target.result; + + if ($settings?.imageCompression ?? false) { + const width = $settings?.imageCompressionSize?.width ?? null; + const height = $settings?.imageCompressionSize?.height ?? null; + + if (width || height) { + imageUrl = await compressImage(imageUrl, width, height); + } + } + files = [ ...files, { type: 'image', - url: `${event.target.result}` + url: `${imageUrl}` } ]; }; diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index 68d585f3e..02e87c251 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -37,6 +37,12 @@ let chatBubble = true; let chatDirection: 'LTR' | 'RTL' = 'LTR'; + let imageCompression = false; + let imageCompressionSize = { + width: '', + height: '' + }; + // Admin - Show Update Available Toast let showUpdateToast = true; let showChangelog = true; @@ -95,6 +101,11 @@ saveSettings({ voiceInterruption: voiceInterruption }); }; + const toggleImageCompression = async () => { + imageCompression = !imageCompression; + saveSettings({ imageCompression }); + }; + const toggleHapticFeedback = async () => { hapticFeedback = !hapticFeedback; saveSettings({ hapticFeedback: hapticFeedback }); @@ -176,7 +187,8 @@ const updateInterfaceHandler = async () => { saveSettings({ - models: [defaultModelId] + models: [defaultModelId], + imageCompressionSize: imageCompressionSize }); }; @@ -206,6 +218,9 @@ hapticFeedback = $settings.hapticFeedback ?? false; + imageCompression = $settings.imageCompression ?? false; + imageCompressionSize = $settings.imageCompressionSize ?? { width: '', height: '' }; + defaultModelId = $settings?.models?.at(0) ?? ''; if ($config?.default_models) { defaultModelId = $config.default_models.split(',')[0]; @@ -662,6 +677,53 @@ + +
{$i18n.t('File')}
+ +
+
+
{$i18n.t('Image Compression')}
+ + +
+
+ + {#if imageCompression} +
+
+
{$i18n.t('Image Max Compression Size')}
+ +
+ x + +
+
+
+ {/if} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 91c3ce871..51deca306 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -14,6 +14,7 @@ function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + export const replaceTokens = (content, sourceIds, char, user) => { const charToken = /{{char}}/gi; const userToken = /{{user}}/gi; @@ -189,6 +190,72 @@ export const canvasPixelTest = () => { return true; }; + +export const compressImage = async (imageUrl, maxWidth, maxHeight) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + + // Maintain aspect ratio while resizing + + + + if (maxWidth && maxHeight) { + // Resize with both dimensions defined (preserves aspect ratio) + + if (width <= maxWidth && height <= maxHeight) { + resolve(imageUrl); + return; + } + + + if (width / height > maxWidth / maxHeight) { + height = Math.round((maxWidth * height) / width); + width = maxWidth; + } else { + width = Math.round((maxHeight * width) / height); + height = maxHeight; + } + } else if (maxWidth) { + // Only maxWidth defined + + if (width <= maxWidth) { + resolve(imageUrl); + return; + } + + + height = Math.round((maxWidth * height) / width); + width = maxWidth; + } else if (maxHeight) { + // Only maxHeight defined + + if (height <= maxHeight) { + resolve(imageUrl); + return; + } + + width = Math.round((maxHeight * width) / height); + height = maxHeight; + } + + canvas.width = width; + canvas.height = height; + + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0, width, height); + + // Get compressed image URL + const compressedUrl = canvas.toDataURL(); + resolve(compressedUrl); + }; + img.onerror = (error) => reject(error); + img.src = imageUrl; + }); +} export const generateInitialsImage = (name) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');