| |
| |
| |
| |
| |
| export async function makeWhiteTransparent(imageUrl: string, largeSegmentThreshold: number = 0.013): 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 imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| const data = imageData.data; |
| const width = canvas.width; |
| const height = canvas.height; |
| |
| |
| const mask = new Uint8Array(width * height); |
| |
| |
| const whiteThreshold = 240; |
| const tolerance = 20; |
| |
| |
| 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; |
| }; |
| |
| |
| 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; |
| }; |
| |
| |
| const queue: number[] = []; |
| |
| |
| |
| 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; |
| } |
| } |
| |
| |
| 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; |
| } |
| } |
| |
| |
| while (queue.length > 0) { |
| const idx = queue.pop()!; |
| const x = idx % width; |
| const y = Math.floor(idx / width); |
| |
| |
| const neighbors = [ |
| { dx: -1, dy: 0 }, |
| { dx: 1, dy: 0 }, |
| { dx: 0, dy: -1 }, |
| { dx: 0, dy: 1 } |
| ]; |
| |
| 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 (!mask[nIdx] && isWhite(nIdx) && colorSimilar(idx, nIdx)) { |
| mask[nIdx] = 1; |
| queue.push(nIdx); |
| } |
| } |
| } |
| } |
| |
| |
| for (let i = 0; i < mask.length; i++) { |
| if (mask[i]) { |
| data[i * 4 + 3] = 0; |
| } |
| } |
| |
| |
| const totalPixels = width * height; |
| const segmentMask = new Uint8Array(width * height); |
| const visited = new Uint8Array(width * height); |
| |
| |
| for (let y = 0; y < height; y++) { |
| for (let x = 0; x < width; x++) { |
| const idx = y * width + x; |
| |
| |
| if (mask[idx] || visited[idx] || !isWhite(idx)) continue; |
| |
| |
| const segmentPixels: number[] = []; |
| const segmentQueue: number[] = [idx]; |
| visited[idx] = 1; |
| |
| while (segmentQueue.length > 0) { |
| const currentIdx = segmentQueue.pop()!; |
| segmentPixels.push(currentIdx); |
| |
| const cx = currentIdx % width; |
| const cy = Math.floor(currentIdx / width); |
| |
| |
| const neighbors = [ |
| { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, |
| { dx: 0, dy: -1 }, { dx: 0, dy: 1 } |
| ]; |
| |
| for (const { dx, dy } of neighbors) { |
| const nx = cx + dx; |
| const ny = cy + dy; |
| |
| if (nx >= 0 && nx < width && ny >= 0 && ny < height) { |
| const nIdx = ny * width + nx; |
| |
| if (!visited[nIdx] && !mask[nIdx] && isWhite(nIdx) && colorSimilar(currentIdx, nIdx)) { |
| visited[nIdx] = 1; |
| segmentQueue.push(nIdx); |
| } |
| } |
| } |
| } |
| |
| |
| const segmentSize = segmentPixels.length / totalPixels; |
| if (segmentSize > largeSegmentThreshold) { |
| |
| for (const pixelIdx of segmentPixels) { |
| segmentMask[pixelIdx] = 1; |
| } |
| } |
| } |
| } |
| |
| |
| for (let i = 0; i < segmentMask.length; i++) { |
| if (segmentMask[i]) { |
| data[i * 4 + 3] = 0; |
| } |
| } |
| |
| |
| ctx.putImageData(imageData, 0, 0); |
| |
| |
| const base64 = canvas.toDataURL('image/png'); |
| resolve(base64); |
| }; |
| |
| img.onerror = () => { |
| reject(new Error('Failed to load image')); |
| }; |
| |
| img.src = imageUrl; |
| }); |
| } |
|
|
| |
| |
| |
| 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; |
| }); |
| } |