Skip to content
File

Blob: src/lib/image.ts

typescript72 lines
1import type { ExportFormat } from "../types";
2 
3export interface LoadedImage {
4 file: File;
5 /** Object URL for display in the cropper. Revoke when replaced. */
6 url: string;
7 width: number;
8 height: number;
9}
10 
11const ACCEPTED = /^image\/(jpeg|png|webp|avif|gif|bmp|x-icon|svg\+xml)$/;
12 
13export function isImageFile(file: File): boolean {
14 return file.type ? ACCEPTED.test(file.type) : /\.(jpe?g|png|webp|avif|gif|bmp|ico|svg)$/i.test(file.name);
15}
16 
17function readSize(url: string): Promise<{ width: number; height: number }> {
18 return new Promise((resolve, reject) => {
19 const img = new Image();
20 img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
21 img.onerror = () => reject(new Error("Could not decode this image."));
22 img.src = url;
23 });
24}
25 
26export async function loadImage(file: File): Promise<LoadedImage> {
27 const url = URL.createObjectURL(file);
28 try {
29 const { width, height } = await readSize(url);
30 return { file, url, width, height };
31 } catch (err) {
32 URL.revokeObjectURL(url);
33 throw err;
34 }
35}
36 
37const EXT: Record<ExportFormat, string> = {
38 jpeg: "jpg",
39 webp: "webp",
40 png: "png",
41 avif: "avif",
42};
43 
44export function buildFilename(sourceName: string, templateId: string, format: ExportFormat): string {
45 const base = sourceName.replace(/\.[^./\\]+$/, "") || "image";
46 const suffix = templateId === "original" ? "edited" : templateId;
47 return `${base}-${suffix}.${EXT[format]}`;
48}
49 
50export function triggerDownload(blob: Blob, filename: string): void {
51 const url = URL.createObjectURL(blob);
52 const a = document.createElement("a");
53 a.href = url;
54 a.download = filename;
55 document.body.appendChild(a);
56 a.click();
57 a.remove();
58 // Defer revocation so the download has time to start.
59 setTimeout(() => URL.revokeObjectURL(url), 10_000);
60}
61 
62export function formatBytes(bytes: number): string {
63 if (bytes < 1024) return `${bytes} B`;
64 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
65 return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
66}
67 
68export function megapixels(width: number, height: number): string {
69 const mp = (width * height) / 1_000_000;
70 return mp >= 1 ? `${mp.toFixed(1)} MP` : `${(mp * 1000).toFixed(0)} KP`;
71}