Explorar o código

enh: image compression

Timothy Jaeryang Baek hai 4 meses
pai
achega
326514be4e

+ 16 - 3
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);

+ 14 - 3
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}`
 						}
 					];
 				};

+ 63 - 1
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 @@
 					</button>
 				</div>
 			</div>
+
+			<div class=" my-1.5 text-sm font-medium">{$i18n.t('File')}</div>
+
+			<div>
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs">{$i18n.t('Image Compression')}</div>
+
+					<button
+						class="p-1 px-3 text-xs flex rounded transition"
+						on:click={() => {
+							toggleImageCompression();
+						}}
+						type="button"
+					>
+						{#if imageCompression === true}
+							<span class="ml-2 self-center">{$i18n.t('On')}</span>
+						{:else}
+							<span class="ml-2 self-center">{$i18n.t('Off')}</span>
+						{/if}
+					</button>
+				</div>
+			</div>
+
+			{#if imageCompression}
+				<div>
+					<div class=" py-0.5 flex w-full justify-between text-xs">
+						<div class=" self-center text-xs">{$i18n.t('Image Max Compression Size')}</div>
+
+						<div>
+							<input
+								bind:value={imageCompressionSize.width}
+								type="number"
+								class="w-20 bg-transparent outline-none text-center"
+								min="0"
+								placeholder="Width"
+							/>x
+							<input
+								bind:value={imageCompressionSize.height}
+								type="number"
+								class="w-20 bg-transparent outline-none text-center"
+								min="0"
+								placeholder="Height"
+							/>
+						</div>
+					</div>
+				</div>
+			{/if}
 		</div>
 	</div>
 

+ 67 - 0
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');