/** سقف حجم فایل فیلد فرم عمومی پس از فشرده‌سازی */ export const FORM_FILE_MAX_BYTES = 200 * 1024; export class FormFileTooLargeError extends Error { constructor(message = 'حجم فایل زیاد است') { super(message); this.name = 'FormFileTooLargeError'; } } function toBlob(canvas: HTMLCanvasElement, type: string, quality?: number): Promise { return new Promise((resolve, reject) => { canvas.toBlob( (b) => (b ? resolve(b) : reject(new Error('فشرده‌سازی تصویر ناموفق بود.'))), type, quality, ); }); } function fileToDataUrl(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(new Error('خواندن فایل ناموفق بود.')); reader.readAsDataURL(file); }); } async function compressImageUnder(file: File, maxBytes: number): Promise { if (file.size <= maxBytes) return file; let bitmap: ImageBitmap; try { bitmap = await createImageBitmap(file); } catch { throw new FormFileTooLargeError(); } let edge = Math.max(bitmap.width, bitmap.height); edge = Math.min(edge, 1600); let quality = 0.85; let best: File | null = null; try { for (let attempt = 0; attempt < 14; attempt += 1) { const ratio = Math.min(edge / bitmap.width, edge / bitmap.height, 1); const width = Math.max(1, Math.round(bitmap.width * ratio)); const height = Math.max(1, Math.round(bitmap.height * ratio)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) throw new FormFileTooLargeError(); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(bitmap, 0, 0, width, height); let blob: Blob; try { blob = await toBlob(canvas, 'image/jpeg', quality); } catch { blob = await toBlob(canvas, 'image/webp', quality); } const base = file.name.replace(/\.[^/.]+$/, '') || 'file'; const ext = blob.type === 'image/webp' ? 'webp' : 'jpg'; const out = new File([blob], `${base}.${ext}`, { type: blob.type, lastModified: Date.now(), }); if (!best || out.size < best.size) best = out; if (out.size <= maxBytes) return out; if (quality > 0.45) { quality = Math.max(0.4, quality - 0.1); } else { quality = 0.72; edge = Math.round(edge * 0.72); if (edge < 320) break; } } } finally { bitmap.close(); } if (best && best.size <= maxBytes) return best; throw new FormFileTooLargeError(); } /** * فایل را تا سقف ۲۰۰ کیلوبایت فشرده می‌کند (تصاویر با کاهش کیفیت/ابعاد). * اگر ممکن نباشد، خطای «حجم فایل زیاد است» می‌دهد. */ export async function prepareFormFieldFile(file: File): Promise<{ name: string; mime: string; size: number; dataUrl: string; file: File; }> { let prepared = file; if (file.type.startsWith('image/')) { prepared = await compressImageUnder(file, FORM_FILE_MAX_BYTES); } else if (file.size > FORM_FILE_MAX_BYTES) { throw new FormFileTooLargeError(); } if (prepared.size > FORM_FILE_MAX_BYTES) { throw new FormFileTooLargeError(); } const dataUrl = await fileToDataUrl(prepared); return { name: prepared.name || file.name || 'file', mime: prepared.type || file.type || 'application/octet-stream', size: prepared.size, dataUrl, file: prepared, }; } /** مقدار ذخیره‌شده در پاسخ فرم: نام + data URL */ export function encodeFormFileValue(meta: { name: string; mime: string; dataUrl: string; }): string { return JSON.stringify({ name: meta.name, mime: meta.mime, dataUrl: meta.dataUrl, }); } export function formFileDisplayName(raw: string | undefined | null): string { if (!raw) return ''; try { const parsed = JSON.parse(raw) as { name?: string }; if (parsed?.name) return parsed.name; } catch { /* plain string */ } if (raw.startsWith('data:')) return 'فایل پیوست'; return raw.length > 48 ? `${raw.slice(0, 48)}…` : raw; }