Skip to content

Commit 086f6d1

Browse files
Algorithm5838github-actions[bot]
authored andcommitted
feat: convert user-attached chat images to WebP at 0.9 quality
1 parent 414ab87 commit 086f6d1

2 files changed

Lines changed: 74 additions & 71 deletions

File tree

src/lib/components/chat/MessageInput.svelte

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -740,47 +740,35 @@
740740
}
741741
742742
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
743-
// Quick shortcut so we don’t do unnecessary work.
744743
const settingsCompression = settings?.imageCompression ?? false;
745744
const configWidth = config?.file?.image_compression?.width ?? null;
746745
const configHeight = config?.file?.image_compression?.height ?? null;
747746
748-
// If neither settings nor config wants compression, return original URL.
749-
if (!settingsCompression && !configWidth && !configHeight) {
750-
return imageUrl;
751-
}
752-
753-
// Default to null (no compression unless set)
754747
let width = null;
755748
let height = null;
756749
757-
// If user/settings want compression, pick their preferred size.
758750
if (settingsCompression) {
759751
width = settings?.imageCompressionSize?.width ?? null;
760752
height = settings?.imageCompressionSize?.height ?? null;
761753
}
762754
763-
// Apply config limits as an upper bound if any
764755
if (configWidth && (width === null || width > configWidth)) {
765756
width = configWidth;
766757
}
767758
if (configHeight && (height === null || height > configHeight)) {
768759
height = configHeight;
769760
}
770761
771-
// Do the compression if required
772-
if (width || height) {
773-
return await compressImage(imageUrl, width, height);
774-
}
775-
return imageUrl;
762+
// Single-pass: resize (if needed) and convert to WebP
763+
return await compressImage(imageUrl, width, height, 'image/webp', 0.9);
776764
};
777765
778766
let reader = new FileReader();
779767
780768
reader.onload = async (event) => {
781769
let imageUrl = event.target.result;
782770
783-
// Compress the image if settings or config require it
771+
// Compress and convert to WebP in a single canvas pass
784772
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
785773
786774
if ($temporaryChatEnabled) {
@@ -793,9 +781,13 @@
793781
];
794782
} else {
795783
const blob = await (await fetch(imageUrl)).blob();
796-
const compressedFile = new File([blob], file.name, { type: file.type });
784+
const webpFile = new File(
785+
[blob],
786+
file.name.replace(/\.[^.]+$/, '.webp'),
787+
{ type: 'image/webp' }
788+
);
797789
798-
uploadFileHandler(compressedFile, false);
790+
uploadFileHandler(webpFile, false);
799791
}
800792
};
801793

src/lib/utils/index.ts

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -363,14 +363,15 @@ async function resizeImageToDataURL(
363363
img: HTMLImageElement,
364364
width: number,
365365
height: number,
366-
mimeType = 'image/jpeg'
366+
mimeType = 'image/jpeg',
367+
quality?: number
367368
): Promise<string> {
368369
const canvas = document.createElement('canvas');
369370
canvas.width = width;
370371
canvas.height = height;
371372
canvas.getContext('2d')?.drawImage(img, 0, 0, width, height);
372373

373-
const toDataURL = () => canvas.toDataURL(mimeType);
374+
const toDataURL = () => canvas.toDataURL(mimeType, quality);
374375

375376
if (
376377
!resizeImageWarmupDone &&
@@ -379,74 +380,84 @@ async function resizeImageToDataURL(
379380
) {
380381
resizeImageWarmupDone = true;
381382
return new Promise((resolve) => {
382-
canvas.toBlob((blob) => {
383-
if (!blob) {
384-
resolve(toDataURL());
385-
return;
386-
}
387-
const reader = new FileReader();
388-
reader.onload = () => resolve(String(reader.result));
389-
reader.onerror = () => resolve(toDataURL());
390-
reader.readAsDataURL(blob);
391-
}, mimeType);
383+
canvas.toBlob(
384+
(blob) => {
385+
if (!blob) {
386+
resolve(toDataURL());
387+
return;
388+
}
389+
const reader = new FileReader();
390+
reader.onload = () => resolve(String(reader.result));
391+
reader.onerror = () => resolve(toDataURL());
392+
reader.readAsDataURL(blob);
393+
},
394+
mimeType,
395+
quality
396+
);
392397
});
393398
}
394399
return Promise.resolve(toDataURL());
395400
}
396401

397-
export const compressImage = async (imageUrl, maxWidth, maxHeight) => {
402+
/**
403+
* Calculate constrained dimensions preserving aspect ratio (pure function).
404+
*/
405+
function calcResize(
406+
srcW: number,
407+
srcH: number,
408+
maxW?: number | null,
409+
maxH?: number | null
410+
): { width: number; height: number; resized: boolean } {
411+
let w = srcW,
412+
h = srcH;
413+
if (maxW && maxH) {
414+
if (w > maxW || h > maxH) {
415+
if (w / h > maxW / maxH) {
416+
h = Math.round((maxW * h) / w);
417+
w = maxW;
418+
} else {
419+
w = Math.round((maxH * w) / h);
420+
h = maxH;
421+
}
422+
return { width: w, height: h, resized: true };
423+
}
424+
} else if (maxW && w > maxW) {
425+
h = Math.round((maxW * h) / w);
426+
w = maxW;
427+
return { width: w, height: h, resized: true };
428+
} else if (maxH && h > maxH) {
429+
w = Math.round((maxH * w) / h);
430+
h = maxH;
431+
return { width: w, height: h, resized: true };
432+
}
433+
return { width: w, height: h, resized: false };
434+
}
435+
436+
export const compressImage = async (
437+
imageUrl: string,
438+
maxWidth?: number | null,
439+
maxHeight?: number | null,
440+
outputFormat?: string,
441+
quality?: number
442+
) => {
398443
return new Promise((resolve, reject) => {
399444
const img = new Image();
400445
img.onload = async () => {
401-
let width = img.width;
402-
let height = img.height;
403-
404-
// Maintain aspect ratio while resizing
446+
const { width, height, resized } = calcResize(img.width, img.height, maxWidth, maxHeight);
405447

406-
if (maxWidth && maxHeight) {
407-
// Resize with both dimensions defined (preserves aspect ratio)
408-
409-
if (width <= maxWidth && height <= maxHeight) {
410-
resolve(imageUrl);
411-
return;
412-
}
413-
414-
if (width / height > maxWidth / maxHeight) {
415-
height = Math.round((maxWidth * height) / width);
416-
width = maxWidth;
417-
} else {
418-
width = Math.round((maxHeight * width) / height);
419-
height = maxHeight;
420-
}
421-
} else if (maxWidth) {
422-
// Only maxWidth defined
423-
424-
if (width <= maxWidth) {
425-
resolve(imageUrl);
426-
return;
427-
}
428-
429-
height = Math.round((maxWidth * height) / width);
430-
width = maxWidth;
431-
} else if (maxHeight) {
432-
// Only maxHeight defined
433-
434-
if (height <= maxHeight) {
435-
resolve(imageUrl);
436-
return;
437-
}
438-
439-
width = Math.round((maxHeight * width) / height);
440-
height = maxHeight;
448+
if (!resized && !outputFormat) {
449+
resolve(imageUrl);
450+
return;
441451
}
442452

443-
const mimeType = imageUrl.match(/^data:([^;]+);/)?.[1] ?? 'image/jpeg';
444-
resolve(await resizeImageToDataURL(img, width, height, mimeType));
453+
const mimeType = outputFormat || (imageUrl.match(/^data:([^;]+);/)?.[1] ?? 'image/jpeg');
454+
resolve(await resizeImageToDataURL(img, width, height, mimeType, quality));
445455
};
446456
img.onerror = (error) => reject(error);
447457
img.src = imageUrl;
448458
});
449459
};
460+
450461
export const generateInitialsImage = (name) => {
451462
const canvas = document.createElement('canvas');
452463
const ctx = canvas.getContext('2d');

0 commit comments

Comments
 (0)