Please bookmark this page to avoid losing your image tool!

Image Lettering Removal Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Lettering Removal Tool allows users to effectively remove unwanted lettering or text from images. By specifying a region of interest (ROI) and selecting the color of the text to be removed, this tool utilizes inpainting techniques to seamlessly fill in the area where the lettering was, using the surrounding pixels. This can be particularly useful for cleaning up photographs, enhancing images for presentations, or preparing graphics for publishing, where the removal of distracting text is desired.

Leave a Reply

Your email address will not be published. Required fields are marked *