Files
ChatApp/apps/desktop/src/lib/imageCompress.ts
T
byGalax 1303c8e26f feat: backup/restore, user profile popover, image compress, video blur, wake lock
- backup/restore dialog + user profile popover components
- image compression, video blur, wake lock utilities
- message cache + conversation messages hook refinements
- call context, active speakers, screen share dialog tweaks
- audio + screen share settings persistence
- refreshed app icons (smaller sizes) across all platforms
2026-04-21 12:11:09 +02:00

58 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Client-side image compression before upload. Downscales huge photos
// (phone cameras regularly emit 4000×3000+ at 5MB+) to a size that
// actually makes sense for chat display. Preserves aspect ratio.
//
// Skips animated formats (gif/apng/webp) to keep motion, and skips
// already-small files to avoid useless re-encode overhead.
const MAX_DIM = 2048;
const TARGET_QUALITY = 0.85;
const SKIP_BELOW_BYTES = 512 * 1024; // 512KB — not worth re-encoding
const ANIMATED_MIME = /^image\/(gif|apng|webp)$/;
export async function compressImage(file: File): Promise<File> {
if (!file.type.startsWith('image/')) return file;
if (ANIMATED_MIME.test(file.type)) return file;
if (file.size < SKIP_BELOW_BYTES) return file;
if (typeof createImageBitmap !== 'function') return file;
if (typeof OffscreenCanvas !== 'function') return file;
try {
const bitmap = await createImageBitmap(file);
const largest = Math.max(bitmap.width, bitmap.height);
const scale = largest > MAX_DIM ? MAX_DIM / largest : 1;
const w = Math.max(1, Math.round(bitmap.width * scale));
const h = Math.max(1, Math.round(bitmap.height * scale));
const canvas = new OffscreenCanvas(w, h);
const ctx = canvas.getContext('2d');
if (!ctx) {
bitmap.close();
return file;
}
ctx.drawImage(bitmap, 0, 0, w, h);
bitmap.close();
const blob = await canvas.convertToBlob({
type: 'image/webp',
quality: TARGET_QUALITY,
});
// If the re-encoded blob is actually larger (small PNGs can expand as
// WebP), keep the original.
if (blob.size >= file.size) return file;
const name = renameToWebp(file.name);
return new File([blob], name, { type: 'image/webp', lastModified: file.lastModified });
} catch (err: unknown) {
console.warn('compressImage failed — keeping original', err);
return file;
}
}
function renameToWebp(original: string): string {
const dot = original.lastIndexOf('.');
const base = dot > 0 ? original.slice(0, dot) : original;
return base + '.webp';
}
export async function compressImages(files: File[]): Promise<File[]> {
return Promise.all(files.map((f) => compressImage(f)));
}