/** * Сжатие картинки фона перед отправкой на сервер. * * «При добавлении картинок ужимать их до приемлемого размера, чтобы не грузили * БД» — требование задачи. Жмём именно на КЛИЕНТЕ: фон всё равно рендерится в * браузере, а на backend нет Pillow, и ставить его ради одной операции незачем. * Серверная валидация (тип по магическим байтам, лимит размера) при этом * остаётся — клиенту верить нельзя, запрос может прийти и мимо интерфейса. */ /** Больше 1280 по длинной стороне фону не нужно — столько же у дефолтных сцен (1280×720). */ const MAX_SIDE_PX = 1280 /** Ступени качества WebP: жмём сильнее, только если с прошлой ступени не уложились. */ const QUALITY_STEPS = [0.82, 0.7, 0.6] /** Цель по весу — 600 КБ. Серверный лимит вдвое больше (2 МБ), запас на подстраховку. */ const TARGET_BYTES = 600 * 1024 export class ImageDecodeError extends Error {} /** * Ужать картинку до `MAX_SIDE_PX` по длинной стороне и вернуть WebP-Blob. * * Пропорции сохраняются: кадрировать под 16:9 здесь нельзя — какая часть * картинки важна, знает только пользователь, а сама библиотека замены фона * вписывает изображение в кадр сама. */ export async function resizeImageForBackground(file: File): Promise { const bitmap = await decode(file) try { const scale = Math.min(1, MAX_SIDE_PX / Math.max(bitmap.width, bitmap.height)) const width = Math.max(1, Math.round(bitmap.width * scale)) const height = Math.max(1, Math.round(bitmap.height * scale)) const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const context = canvas.getContext('2d') if (!context) throw new ImageDecodeError('canvas 2d context unavailable') context.drawImage(bitmap, 0, 0, width, height) let result: Blob | null = null for (const quality of QUALITY_STEPS) { result = await toBlob(canvas, quality) if (result.size <= TARGET_BYTES) return result } // Даже на самом низком качестве не уложились (огромная детализованная // картинка) — отдаём как есть: серверный лимит вдвое выше цели, и шанс // пройти его остаётся; иначе пользователь получит честную 413. return result! } finally { bitmap.close() } } /** Декодировать файл в `ImageBitmap`; битый/не-картинка → `ImageDecodeError`. */ async function decode(file: File): Promise { try { return await createImageBitmap(file) } catch (err) { throw new ImageDecodeError(String(err)) } } function toBlob(canvas: HTMLCanvasElement, quality: number): Promise { return new Promise((resolve, reject) => { canvas.toBlob( (blob) => (blob ? resolve(blob) : reject(new ImageDecodeError('canvas.toBlob returned null'))), 'image/webp', quality, ) }) }