You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, roiX = 0, roiY = 0, roiWidth = 0, roiHeight = 0, letterColorStr = "#000000", colorTolerance = 50, inpaintingIterations = 10, finalBlurRadius = 1) {
// 0. Parameter validation and setup
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Ensure image has valid dimensions
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
console.error("Image has zero width or height. Returning an empty canvas.");
canvas.width = Math.max(1, imgWidth); // Ensure canvas has at least 1x1 to avoid errors
canvas.height = Math.max(1, imgHeight);
return canvas;
}
canvas.width = imgWidth;
canvas.height = imgHeight;
ctx.drawImage(originalImg, 0, 0);
// Adjust ROI defaults and clamp to image boundaries
let actualRoiX = Math.max(0, roiX);
let actualRoiY = Math.max(0, roiY);
let actualRoiWidth = (roiWidth === 0 || roiWidth > canvas.width - actualRoiX)
? (canvas.width - actualRoiX)
: roiWidth;
let actualRoiHeight = (roiHeight === 0 || roiHeight > canvas.height - actualRoiY)
? (canvas.height - actualRoiY)
: roiHeight;
// Ensure ROI width and height are positive
actualRoiWidth = Math.max(0, actualRoiWidth);
actualRoiHeight = Math.max(0, actualRoiHeight);
if (actualRoiWidth === 0 || actualRoiHeight === 0) {
console.warn("ROI has zero area after clamping. Processing full image instead as fallback.");
actualRoiX = 0;
actualRoiY = 0;
actualRoiWidth = canvas.width;
actualRoiHeight = canvas.height;
// If canvas itself is 0x0 (already checked), this would still be 0.
// Final check if it's still invalid.
if (actualRoiWidth === 0 || actualRoiHeight === 0) {
console.error("Cannot process image with zero dimensions for ROI.");
return canvas; // Return original image drawn on canvas
}
}
// 1. Helper functions
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
let targetLetterRgb = hexToRgb(letterColorStr);
if (!targetLetterRgb) {
console.warn(`Invalid letterColorStr format: "${letterColorStr}". Using black (#000000) as default.`);
targetLetterRgb = { r: 0, g: 0, b: 0 };
}
// Using squared Euclidean distance for efficiency (avoids sqrt)
function colorDistanceSq(r1, g1, b1, r2, g2, b2) {
const dr = r1 - r2;
const dg = g1 - g2;
const db = b1 - b2;
return dr * dr + dg * dg + db * db;
}
const colorToleranceSq = colorTolerance * colorTolerance;
// 2. Image data and Mask Creation
let originalImageData;
try {
originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Could not get ImageData (likely cross-origin issue if image source is external):", e);
// Clear canvas and draw error message
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(230, 230, 230, 0.9)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
ctx.font = '14px Arial';
const messages = [
'Error: Unable to process image data.',
'(This can happen with cross-origin images.)'
];
messages.forEach((msg, i) => {
ctx.fillText(msg, canvas.width / 2, canvas.height / 2 - (messages.length-1)*10 + i*20);
});
return canvas;
}
const data = originalImageData.data;
const width = canvas.width;
const height = canvas.height;
const letterMask = new Uint8Array(width * height); // 1 if letter, 0 if not
for (let y = actualRoiY; y < actualRoiY + actualRoiHeight; y++) {
for (let x = actualRoiX; x < actualRoiX + actualRoiWidth; x++) {
const i = (y * width + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
if (colorDistanceSq(r, g, b, targetLetterRgb.r, targetLetterRgb.g, targetLetterRgb.b) <= colorToleranceSq) {
letterMask[y * width + x] = 1;
}
}
}
// 3. Inpainting
// currentData holds the image data being modified in each iteration.
// nextData is a temporary buffer to write the results of an iteration into.
let currentData = new Uint8ClampedArray(data);
let nextData = new Uint8ClampedArray(data.length);
for (let iter = 0; iter < inpaintingIterations; iter++) {
nextData.set(currentData); // Preserve pixels not being inpainted or if count becomes 0
for (let y = actualRoiY; y < actualRoiY + actualRoiHeight; y++) {
for (let x = actualRoiX; x < actualRoiX + actualRoiWidth; x++) {
if (letterMask[y * width + x] === 1) { // If it's a pixel to inpaint
let sumR = 0, sumG = 0, sumB = 0, sumA = 0, count = 0;
// Iterate 3x3 neighborhood (including center pixel itself for diffusion)
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
// Neighbor is within image bounds
const ni = (ny * width + nx) * 4;
sumR += currentData[ni];
sumG += currentData[ni + 1];
sumB += currentData[ni + 2];
sumA += currentData[ni + 3];
count++;
}
}
}
if (count > 0) {
const targetIdx = (y * width + x) * 4;
nextData[targetIdx] = sumR / count;
nextData[targetIdx + 1] = sumG / count;
nextData[targetIdx + 2] = sumB / count;
nextData[targetIdx + 3] = sumA / count; // Diffuse alpha too
}
}
}
}
// Swap buffers: currentData gets the processed pixels from nextData.
// A direct swap of references is efficient.
[currentData, nextData] = [nextData, currentData];
}
// After the loop, currentData contains the final inpainted result.
// 4. Optional Final Blur on originally masked areas
if (finalBlurRadius > 0) {
const blurredData = new Uint8ClampedArray(currentData); // Start with inpainted data
for (let y = actualRoiY; y < actualRoiY + actualRoiHeight; y++) {
for (let x = actualRoiX; x < actualRoiX + actualRoiWidth; x++) {
if (letterMask[y * width + x] === 1) { // Only blur pixels that were *originally* part of the letter mask
let sumR = 0, sumG = 0, sumB = 0, sumA = 0, count = 0;
// Loop for the blur kernel (e.g., for finalBlurRadius=1, kernel is 3x3)
for (let dy = -finalBlurRadius; dy <= finalBlurRadius; dy++) {
for (let dx = -finalBlurRadius; dx <= finalBlurRadius; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
// Read from currentData (the result of inpainting) for blurring
const ni = (ny * width + nx) * 4;
sumR += currentData[ni];
sumG += currentData[ni + 1];
sumB += currentData[ni + 2];
sumA += currentData[ni + 3];
count++;
}
}
}
if (count > 0) {
const targetIdx = (y * width + x) * 4;
blurredData[targetIdx] = sumR / count;
blurredData[targetIdx + 1] = sumG / count;
blurredData[targetIdx + 2] = sumB / count;
blurredData[targetIdx + 3] = sumA / count;
}
}
}
}
// Put blurred data (which contains all pixels, modified or not) onto the canvas
originalImageData.data.set(blurredData);
} else {
// No final blur, just use the inpainted data from currentData
originalImageData.data.set(currentData);
}
ctx.putImageData(originalImageData, 0, 0);
return canvas;
}
Apply Changes