import { encodeToBase64 } from "$lib/modules/crypto"; const scaleSize = (width: number, height: number, targetSize: number) => { if (width <= targetSize || height <= targetSize) { return { width, height }; } const scale = targetSize / Math.min(width, height); return { width: Math.round(width * scale), height: Math.round(height * scale), }; }; const capture = ( width: number, height: number, drawer: (context: CanvasRenderingContext2D, width: number, height: number) => void, targetSize = 250, ) => { return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const { width: scaledWidth, height: scaledHeight } = scaleSize(width, height, targetSize); canvas.width = scaledWidth; canvas.height = scaledHeight; const context = canvas.getContext("2d"); if (!context) { return reject(new Error("Failed to generate thumbnail")); } drawer(context, scaledWidth, scaledHeight); canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error("Failed to generate thumbnail")); } }, "image/webp"); }); }; const generateImageThumbnail = (imageUrl: string) => { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { capture(image.width, image.height, (context, width, height) => { context.drawImage(image, 0, 0, width, height); }) .then(resolve) .catch(reject); }; image.onerror = reject; image.src = imageUrl; }); }; export const captureVideoThumbnail = (video: HTMLVideoElement) => { return capture(video.videoWidth, video.videoHeight, (context, width, height) => { context.drawImage(video, 0, 0, width, height); }); }; const generateVideoThumbnail = (videoUrl: string, time = 0) => { return new Promise((resolve, reject) => { const video = document.createElement("video"); video.onloadedmetadata = () => { video.currentTime = Math.min(time, video.duration); video.requestVideoFrameCallback(() => { captureVideoThumbnail(video).then(resolve).catch(reject); }); }; video.onerror = reject; video.muted = true; video.playsInline = true; video.src = videoUrl; }); }; export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => { let url; try { if (fileType === "image/heic") { const { default: heic2any } = await import("heic2any"); url = URL.createObjectURL( (await heic2any({ blob: new Blob([fileBuffer], { type: fileType }), toType: "image/png", })) as Blob, ); return await generateImageThumbnail(url); } else if (fileType.startsWith("image/")) { url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); return await generateImageThumbnail(url); } else if (fileType.startsWith("video/")) { url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); return await generateVideoThumbnail(url); } return null; } catch { return null; } finally { if (url) { URL.revokeObjectURL(url); } } }; export const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => { return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`; };