/** * Converts an image URL to a base64 data URL with white background made transparent * Uses flood-fill from edges to only remove background white, preserving internal white */ export async function makeWhiteTransparent(imageUrl: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Failed to get canvas context')); return; } canvas.width = img.width; canvas.height = img.height; // Draw the image ctx.drawImage(img, 0, 0); // Get image data const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const width = canvas.width; const height = canvas.height; // Create a mask to track which pixels to make transparent const mask = new Uint8Array(width * height); // Define white threshold with some tolerance const whiteThreshold = 240; const tolerance = 20; // Helper function to check if a pixel is white-ish const isWhite = (index: number): boolean => { const i = index * 4; const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; return r > whiteThreshold && g > whiteThreshold && b > whiteThreshold; }; // Helper function to check if colors are similar const colorSimilar = (i1: number, i2: number): boolean => { const idx1 = i1 * 4; const idx2 = i2 * 4; return Math.abs(data[idx1] - data[idx2]) < tolerance && Math.abs(data[idx1 + 1] - data[idx2 + 1]) < tolerance && Math.abs(data[idx1 + 2] - data[idx2 + 2]) < tolerance; }; // Flood fill from edges const queue: number[] = []; // Add all edge pixels that are white to the queue // Top and bottom edges for (let x = 0; x < width; x++) { if (isWhite(x)) { queue.push(x); mask[x] = 1; } const bottomIdx = (height - 1) * width + x; if (isWhite(bottomIdx)) { queue.push(bottomIdx); mask[bottomIdx] = 1; } } // Left and right edges for (let y = 1; y < height - 1; y++) { const leftIdx = y * width; if (isWhite(leftIdx)) { queue.push(leftIdx); mask[leftIdx] = 1; } const rightIdx = y * width + width - 1; if (isWhite(rightIdx)) { queue.push(rightIdx); mask[rightIdx] = 1; } } // Flood fill while (queue.length > 0) { const idx = queue.pop()!; const x = idx % width; const y = Math.floor(idx / width); // Check 4 neighbors const neighbors = [ { dx: -1, dy: 0 }, // left { dx: 1, dy: 0 }, // right { dx: 0, dy: -1 }, // up { dx: 0, dy: 1 } // down ]; for (const { dx, dy } of neighbors) { const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const nIdx = ny * width + nx; // If not already marked and color is similar to current pixel if (!mask[nIdx] && isWhite(nIdx) && colorSimilar(idx, nIdx)) { mask[nIdx] = 1; queue.push(nIdx); } } } } // Apply transparency based on mask for (let i = 0; i < mask.length; i++) { if (mask[i]) { data[i * 4 + 3] = 0; // Set alpha to 0 } } // Put the modified image data back ctx.putImageData(imageData, 0, 0); // Convert to base64 const base64 = canvas.toDataURL('image/png'); resolve(base64); }; img.onerror = () => { reject(new Error('Failed to load image')); }; img.src = imageUrl; }); } /** * Fetches an image from URL and converts it to base64 */ export async function imageUrlToBase64(imageUrl: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Failed to get canvas context')); return; } canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const base64 = canvas.toDataURL('image/png'); resolve(base64); }; img.onerror = () => { reject(new Error('Failed to load image')); }; img.src = imageUrl; }); }