118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import { resolveChatContentType } from '@/lib/chat-media';
|
||
|
||
const DEFAULT_MAX_EDGE = 2560;
|
||
const DEFAULT_QUALITY = 0.82;
|
||
|
||
function loadImageFromFile(file: File) {
|
||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||
const url = URL.createObjectURL(file);
|
||
const image = new Image();
|
||
image.onload = () => {
|
||
URL.revokeObjectURL(url);
|
||
resolve(image);
|
||
};
|
||
image.onerror = () => {
|
||
URL.revokeObjectURL(url);
|
||
reject(new Error('Не удалось прочитать изображение'));
|
||
};
|
||
image.src = url;
|
||
});
|
||
}
|
||
|
||
function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number) {
|
||
return new Promise<Blob>((resolve, reject) => {
|
||
canvas.toBlob(
|
||
(blob) => {
|
||
if (!blob) {
|
||
reject(new Error('Не удалось сжать изображение'));
|
||
return;
|
||
}
|
||
resolve(blob);
|
||
},
|
||
type,
|
||
quality
|
||
);
|
||
});
|
||
}
|
||
|
||
export function isCompressibleImageFile(file: File) {
|
||
const type = (file.type || '').toLowerCase();
|
||
if (type.startsWith('image/') && !type.includes('gif') && !type.includes('svg')) {
|
||
return true;
|
||
}
|
||
return /\.(jpe?g|png|webp|heic|heif|bmp)$/i.test(file.name);
|
||
}
|
||
|
||
export async function compressChatImage(
|
||
file: File,
|
||
options?: { maxEdge?: number; quality?: number }
|
||
) {
|
||
const maxEdge = options?.maxEdge ?? DEFAULT_MAX_EDGE;
|
||
const quality = options?.quality ?? DEFAULT_QUALITY;
|
||
|
||
if (!isCompressibleImageFile(file)) {
|
||
return { file, width: undefined, height: undefined, compressed: false };
|
||
}
|
||
|
||
const image = await loadImageFromFile(file);
|
||
const width = image.naturalWidth || image.width;
|
||
const height = image.naturalHeight || image.height;
|
||
if (!width || !height) {
|
||
return { file, width: undefined, height: undefined, compressed: false };
|
||
}
|
||
|
||
const scale = Math.min(1, maxEdge / Math.max(width, height));
|
||
const targetWidth = Math.max(1, Math.round(width * scale));
|
||
const targetHeight = Math.max(1, Math.round(height * scale));
|
||
|
||
if (scale >= 1 && file.size <= 350_000) {
|
||
return { file, width, height, compressed: false };
|
||
}
|
||
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = targetWidth;
|
||
canvas.height = targetHeight;
|
||
const context = canvas.getContext('2d');
|
||
if (!context) {
|
||
return { file, width, height, compressed: false };
|
||
}
|
||
|
||
context.drawImage(image, 0, 0, targetWidth, targetHeight);
|
||
|
||
const outputType = file.type === 'image/png' ? 'image/jpeg' : file.type || 'image/jpeg';
|
||
const blob = await canvasToBlob(canvas, outputType, quality);
|
||
const extension = outputType.includes('webp') ? 'webp' : 'jpg';
|
||
const baseName = file.name.replace(/\.[^.]+$/, '') || 'image';
|
||
const compressedFile = new File([blob], `${baseName}.${extension}`, { type: outputType, lastModified: Date.now() });
|
||
|
||
return {
|
||
file: compressedFile,
|
||
width: targetWidth,
|
||
height: targetHeight,
|
||
compressed: true
|
||
};
|
||
}
|
||
|
||
export async function readImageDimensions(file: File) {
|
||
if (!isCompressibleImageFile(file)) {
|
||
return { width: undefined, height: undefined };
|
||
}
|
||
try {
|
||
const image = await loadImageFromFile(file);
|
||
return {
|
||
width: image.naturalWidth || image.width,
|
||
height: image.naturalHeight || image.height
|
||
};
|
||
} catch {
|
||
return { width: undefined, height: undefined };
|
||
}
|
||
}
|
||
|
||
export function createFilePreviewUrl(file: File) {
|
||
const contentType = resolveChatContentType(file);
|
||
if (contentType.startsWith('image/') || contentType.startsWith('video/')) {
|
||
return URL.createObjectURL(file);
|
||
}
|
||
return null;
|
||
}
|