"use client"; import { t } from "./i18n"; /** Client-side image helpers: validation, resizing and compression. */ export const MAX_IMAGE_DIMENSION = 1600; export const JPEG_QUALITY = 0.82; export const MAX_FILE_BYTES = 20 * 1024 * 1024; // 20 MB per source file export const MAX_IMAGES = 8; export const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif", "image/heic", "image/heif"]; export type PreparedImage = { id: string; dataUrl: string; name: string; width: number; height: number; bytes: number; }; export function isImageFile(file: File): boolean { if (file.type.startsWith("image/")) return true; return /\.(jpe?g|png|webp|gif|heic|heif|bmp)$/i.test(file.name); } export function describeFileError(file: File): string | null { if (!isImageFile(file)) return t("file.notImage", { name: file.name }); if (file.size > MAX_FILE_BYTES) return t("file.imageTooLarge", { name: file.name }); return null; } /** Rough byte length of a base64 data URL. */ export function dataUrlBytes(dataUrl: string): number { const index = dataUrl.indexOf(","); if (index === -1) return dataUrl.length; return Math.floor(((dataUrl.length - index - 1) * 3) / 4); } async function loadBitmap(blob: Blob, fallbackName: string): Promise<{ width: number; height: number; draw: (ctx: CanvasRenderingContext2D, w: number, h: number) => void }> { // Prefer createImageBitmap: it respects EXIF orientation and is fast. if (typeof createImageBitmap === "function") { try { const bitmap = await createImageBitmap(blob, { imageOrientation: "from-image" }); return { width: bitmap.width, height: bitmap.height, draw: (ctx, w, h) => ctx.drawImage(bitmap, 0, 0, w, h), }; } catch { /* fall through to the path (Safari/HEIC edge cases) */ } } const url = URL.createObjectURL(blob); try { const image = await new Promise((resolve, reject) => { const element = new Image(); element.onload = () => resolve(element); element.onerror = () => reject(new Error(`Could not read "${fallbackName}".`)); element.src = url; }); return { width: image.naturalWidth || image.width, height: image.naturalHeight || image.height, draw: (ctx, w, h) => ctx.drawImage(image, 0, 0, w, h), }; } finally { URL.revokeObjectURL(url); } } /** * Resize to max 1600px and re-encode as JPEG to keep uploads small. * Accepts any image blob (photos, canvas output from PDF pages, images * embedded in documents). */ export async function prepareImageFromBlob(blob: Blob, name = "image.jpg"): Promise { const bitmap = await loadBitmap(blob, name); const scale = Math.min(1, MAX_IMAGE_DIMENSION / 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 ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Your browser could not process this image."); ctx.fillStyle = "#ffffff"; // flatten transparency before JPEG encoding ctx.fillRect(0, 0, width, height); bitmap.draw(ctx, width, height); const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY); return { id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, dataUrl, name, width, height, bytes: dataUrlBytes(dataUrl), }; } /** Resize to max 1600px and re-encode as JPEG to keep uploads small. */ export async function prepareImage(file: File): Promise { return prepareImageFromBlob(file, file.name || "photo.jpg"); } /** Extract image files from a paste or drop event. */ export function imageFilesFromDataTransfer(data: DataTransfer | null): File[] { if (!data) return []; const files: File[] = []; if (data.files?.length) { for (const file of Array.from(data.files)) if (isImageFile(file)) files.push(file); } if (data.items?.length) { for (const item of Array.from(data.items)) { if (item.kind !== "file") continue; const file = item.getAsFile(); if (file && isImageFile(file) && !files.some((f) => f.name === file.name && f.size === file.size)) { files.push(file); } } } return files; } export function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }