/** * Process image data to crop whitespace */ export async function cropImage( imageSrc: string, options: { threshold?: number; // 0-255, higher means more colors are considered "white" padding?: number; // padding in pixels to keep around content } = {} ): Promise { const { threshold = 252, padding = 20 } = options; // 252 covers pure white and very light compression artifacts return new Promise((resolve, reject) => { const img = new Image(); // Enable CORS if needed, though usually data URLs don't need it img.crossOrigin = "anonymous"; img.onload = () => { try { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); if (!ctx) { resolve(imageSrc); return; } ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const { width, height } = canvas; // Check if pixel is "content" (darker than threshold) // Returns true if it is CONTENT const isContent = (r: number, g: number, b: number) => { return r < threshold || g < threshold || b < threshold; }; let top = -1, bottom = -1, left = -1, right = -1; // Scan Top-down for top boundary for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; if (isContent(data[idx], data[idx+1], data[idx+2])) { top = y; break; } } if (top !== -1) break; } // If top is still -1, the image is empty (all white) if (top === -1) { resolve(imageSrc); return; } // Scan Bottom-up for bottom boundary for (let y = height - 1; y >= top; y--) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; if (isContent(data[idx], data[idx+1], data[idx+2])) { bottom = y + 1; break; } } if (bottom !== -1) break; } // Scan Left-right for left boundary // We only need to scan within the vertical bounds we found for (let x = 0; x < width; x++) { for (let y = top; y < bottom; y++) { const idx = (y * width + x) * 4; if (isContent(data[idx], data[idx+1], data[idx+2])) { left = x; break; } } if (left !== -1) break; } // Scan Right-left for right boundary for (let x = width - 1; x >= left; x--) { for (let y = top; y < bottom; y++) { const idx = (y * width + x) * 4; if (isContent(data[idx], data[idx+1], data[idx+2])) { right = x + 1; break; } } if (right !== -1) break; } // Calculate crop coordinates with padding const cropX = Math.max(0, left - padding); const cropY = Math.max(0, top - padding); // Calculate width/height ensuring we don't go out of bounds // right is exclusive index + 1, so width is right - left. // content width = right - left // target width = content width + 2 * padding // but limited by image bounds const contentWidth = right - left; const contentHeight = bottom - top; // Check if we found valid bounds if (contentWidth <= 0 || contentHeight <= 0) { resolve(imageSrc); return; } // Adjust padding to not exceed original image // Actually, we want the crop rect to include padding if it exists in source, // or maybe just white pad if it doesn't? // The current logic `Math.max(0, left - padding)` keeps padding INSIDE the original image. // If the content is at the edge, we might want to ADD whitespace? // Usually cropping means reducing size. Let's stick to keeping what's in the image. const rectRight = Math.min(width, right + padding); const rectBottom = Math.min(height, bottom + padding); const finalWidth = rectRight - cropX; const finalHeight = rectBottom - cropY; const finalCanvas = document.createElement('canvas'); finalCanvas.width = finalWidth; finalCanvas.height = finalHeight; const finalCtx = finalCanvas.getContext('2d'); if (!finalCtx) { resolve(imageSrc); return; } // Fill with white in case of transparency (though we are copying from opaque usually) finalCtx.fillStyle = '#ffffff'; finalCtx.fillRect(0, 0, finalWidth, finalHeight); finalCtx.drawImage( canvas, cropX, cropY, finalWidth, finalHeight, // Source rect 0, 0, finalWidth, finalHeight // Dest rect ); resolve(finalCanvas.toDataURL('image/png')); } catch (e) { console.error("Image processing failed", e); resolve(imageSrc); } }; img.onerror = (e) => { console.error("Image loading failed", e); resolve(imageSrc); }; img.src = imageSrc; }); }