piclets / src /lib /utils /imageProcessing.ts
Fraser's picture
closr
bf70839
raw
history blame
4.97 kB
/**
* 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<string> {
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<string> {
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;
});
}