export type Area = { x: number; y: number; width: number; height: number; }; function loadImage(src: string): Promise { return new Promise((resolve, reject) => { const image = new Image(); image.addEventListener("load", () => resolve(image)); image.addEventListener("error", (error) => reject(error)); image.crossOrigin = "anonymous"; image.src = src; }); } /** * Crop that may extend outside the source image. * Outside pixels stay transparent (PNG) or fillColor (JPEG). */ export async function getCroppedImageFile( imageSrc: string, pixelCrop: Area, fileName: string, mimeType = "image/jpeg", quality = 0.92, fillColor = "#ffffff", ): Promise { const image = await loadImage(imageSrc); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not crop image."); const outputWidth = Math.max(1, Math.round(pixelCrop.width)); const outputHeight = Math.max(1, Math.round(pixelCrop.height)); canvas.width = outputWidth; canvas.height = outputHeight; if (mimeType !== "image/png") { ctx.fillStyle = fillColor; ctx.fillRect(0, 0, outputWidth, outputHeight); } else { ctx.clearRect(0, 0, outputWidth, outputHeight); } const srcX = Math.max(0, pixelCrop.x); const srcY = Math.max(0, pixelCrop.y); const srcRight = Math.min(image.naturalWidth, pixelCrop.x + pixelCrop.width); const srcBottom = Math.min( image.naturalHeight, pixelCrop.y + pixelCrop.height, ); const srcWidth = srcRight - srcX; const srcHeight = srcBottom - srcY; if (srcWidth > 0 && srcHeight > 0) { const destX = Math.round(srcX - pixelCrop.x); const destY = Math.round(srcY - pixelCrop.y); ctx.drawImage( image, srcX, srcY, srcWidth, srcHeight, destX, destY, srcWidth, srcHeight, ); } const blob = await new Promise((resolve, reject) => { canvas.toBlob( (result) => { if (result) resolve(result); else reject(new Error("Could not export cropped image.")); }, mimeType, quality, ); }); const base = fileName.replace(/\.[^.]+$/, "") || "image"; const extension = mimeType === "image/png" ? "png" : "jpg"; return new File([blob], `${base}-cropped.${extension}`, { type: mimeType }); }