1303c8e26f
- 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
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
// 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)));
|
||
}
|