mirror of
https://github.com/open-webui/open-webui
synced 2024-12-28 06:42:47 +00:00
enh: image compression
This commit is contained in:
parent
591aac5e16
commit
326514be4e
@ -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);
|
||||
|
@ -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}`
|
||||
}
|
||||
];
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user