enh: image compression

This commit is contained in:
Timothy Jaeryang Baek 2024-12-24 23:28:14 -07:00
parent 591aac5e16
commit 326514be4e
4 changed files with 160 additions and 7 deletions

View File

@ -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);

View File

@ -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}`
}
];
};

View File

@ -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>

View File

@ -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');