Please bookmark this page to avoid losing your image tool!

Image Braille Text Filter Effect

(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, cellSize = 8, threshold = 128, invertColors = "false", fontFamily = "monospace", foregroundColor = "black", backgroundColor = "white") {

    // 1. Parameter parsing and validation
    let cellSizeNum = Number(cellSize);
    if (isNaN(cellSizeNum) || cellSizeNum <= 0) {
        cellSizeNum = 8; // Default cellSize
    }
    // Braille uses 2x4 dots. Minimum 1 pixel per dot segment.
    // Cell needs to be at least 2 wide for 2 columns, and 4 high for 4 rows of dots.
    // We take Math.max of these minimums if user provides smaller. So, effectively min cell size is 4x4.
    cellSizeNum = Math.max(4, Math.floor(cellSizeNum));

    let thresholdNum = Number(threshold);
    if (isNaN(thresholdNum)) {
        thresholdNum = 128;
    }
    thresholdNum = Math.max(0, Math.min(255, thresholdNum)); // Clamp to 0-255

    const invert = String(invertColors).toLowerCase() === 'true';
    const fontFamilyStr = String(fontFamily).trim() || "monospace";
    const fgColorStr = String(foregroundColor).trim() || "black";
    const bgColorStr = String(backgroundColor).trim() || "white";

    // 2. Source canvas setup (to get pixel data)
    const srcCanvas = document.createElement('canvas');
    const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
    srcCanvas.width = originalImg.width;
    srcCanvas.height = originalImg.height;

    if (originalImg.width === 0 || originalImg.height === 0) {
        const zeroSizeCanvas = document.createElement('canvas');
        zeroSizeCanvas.width = 100; // Minimal display canvas
        zeroSizeCanvas.height = 50;
        const zsCtx = zeroSizeCanvas.getContext('2d');
        zsCtx.fillStyle = bgColorStr;
        zsCtx.fillRect(0, 0, zeroSizeCanvas.width, zeroSizeCanvas.height);
        zsCtx.fillStyle = fgColorStr;
        zsCtx.font = "12px sans-serif";
        zsCtx.textAlign = "center";
        zsCtx.textBaseline = "middle";
        zsCtx.fillText("Original image is empty.", zeroSizeCanvas.width / 2, zeroSizeCanvas.height / 2);
        return zeroSizeCanvas;
    }
    
    srcCtx.drawImage(originalImg, 0, 0);
    
    let imgData;
    try {
        imgData = srcCtx.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
    } catch (e) {
        // Handle potential security exception if image is cross-origin and canvas is tainted
        console.error("Error getting ImageData:", e);
        const errorCanvas = document.createElement('canvas');
        // Use a somewhat fixed size for error message or try to match original if very large.
        errorCanvas.width = Math.min(300, Math.max(100, originalImg.width)); 
        errorCanvas.height = Math.min(150, Math.max(50, originalImg.height));
        const errCtx = errorCanvas.getContext('2d');
        errCtx.fillStyle = bgColorStr;
        errCtx.fillRect(0,0, errorCanvas.width, errorCanvas.height);
        errCtx.fillStyle = fgColorStr;
        errCtx.font = "12px sans-serif";
        errCtx.textAlign = "center";
        errCtx.textBaseline = "middle";
        errCtx.fillText("Error: Cannot process cross-origin image.", errorCanvas.width / 2, errorCanvas.height / 2);
        return errorCanvas;
    }
    const pixels = imgData.data;

    // 3. Output canvas setup
    const numCharsX = Math.floor(originalImg.width / cellSizeNum);
    const numCharsY = Math.floor(originalImg.height / cellSizeNum);

    if (numCharsX === 0 || numCharsY === 0) {
        const fallbackCanvas = document.createElement('canvas');
        fallbackCanvas.width = Math.max(cellSizeNum * 5, originalImg.width, 150); 
        fallbackCanvas.height = Math.max(cellSizeNum * 2, originalImg.height, 50);
        
        const fbCtx = fallbackCanvas.getContext('2d');
        fbCtx.fillStyle = bgColorStr;
        fbCtx.fillRect(0, 0, fallbackCanvas.width, fallbackCanvas.height);
        fbCtx.fillStyle = fgColorStr;
        const fbFontSize = Math.max(10, Math.min(12, fallbackCanvas.height / 3, fallbackCanvas.width / 15));
        fbCtx.font = `${fbFontSize}px sans-serif`;
        fbCtx.textAlign = "center";
        fbCtx.textBaseline = "middle";
        fbCtx.fillText("Image too small or cell size too large for Braille grid.", fallbackCanvas.width / 2, fallbackCanvas.height / 2);
        return fallbackCanvas;
    }

    const outCanvas = document.createElement('canvas');
    const outCtx = outCanvas.getContext('2d');
    outCanvas.width = numCharsX * cellSizeNum;
    outCanvas.height = numCharsY * cellSizeNum;

    outCtx.fillStyle = bgColorStr;
    outCtx.fillRect(0, 0, outCanvas.width, outCanvas.height);
    
    outCtx.fillStyle = fgColorStr;
    const fontSize = cellSizeNum; 
    outCtx.font = `${fontSize}px ${fontFamilyStr}`; // Using cellSizeNum as font size
    outCtx.textAlign = "center";
    outCtx.textBaseline = "middle";

    // 4. Main processing loop
    for (let cy = 0; cy < numCharsY; cy++) {
        for (let cx = 0; cx < numCharsX; cx++) {
            let brailleValue = 0; // Accumulator for Braille dot bits

            // Iterate over the 8 conceptual dots in a 2-column x 4-row grid for the Braille character
            for (let dotIndex = 0; dotIndex < 8; dotIndex++) {
                const dotCol = dotIndex % 2; // 0 for left column, 1 for right column
                const dotRow = Math.floor(dotIndex / 2); // 0, 1, 2, 3 for dot row

                // Define the sub-region in the original image for this specific dot
                const subCellNominalWidth = cellSizeNum / 2;
                const subCellNominalHeight = cellSizeNum / 4;

                const imgRegionX = Math.floor((cx * cellSizeNum) + (dotCol * subCellNominalWidth));
                const imgRegionY = Math.floor((cy * cellSizeNum) + (dotRow * subCellNominalHeight));
                
                // Ensure sub-region dimensions are at least 1x1 pixel for sampling
                const imgRegionW = Math.max(1, Math.floor(subCellNominalWidth));
                const imgRegionH = Math.max(1, Math.floor(subCellNominalHeight));

                // Calculate average grayscale intensity for this sub-region
                let sumIntensity = 0;
                let pixelCount = 0;
                for (let srY = 0; srY < imgRegionH; srY++) {
                    for (let srX = 0; srX < imgRegionW; srX++) {
                        const currentPixelX = imgRegionX + srX;
                        const currentPixelY = imgRegionY + srY;
                        
                        // Ensure pixel coordinates are within the original image bounds
                        if (currentPixelX >= 0 && currentPixelX < originalImg.width &&
                            currentPixelY >= 0 && currentPixelY < originalImg.height) {
                            
                            const pixelStartIndex = (currentPixelY * originalImg.width + currentPixelX) * 4;
                            const r = pixels[pixelStartIndex];
                            const g = pixels[pixelStartIndex + 1];
                            const b = pixels[pixelStartIndex + 2];
                            const gray = (r * 0.299 + g * 0.587 + b * 0.114); // Luminosity method
                            sumIntensity += gray;
                            pixelCount++;
                        }
                    }
                }

                // Default intensity if no pixels found (e.g. region slightly outside image boundaries)
                // Set to max or min intensity depending on invert to make it a non-dot.
                const avgIntensity = (pixelCount > 0) ? (sumIntensity / pixelCount) : (invert ? 0 : 255); 

                // Determine if this dot should be active
                let dotIsActive;
                if (invert) { // Lighter parts of the image become dots
                    dotIsActive = avgIntensity > thresholdNum;
                } else { // Darker parts of the image become dots (default behavior)
                    dotIsActive = avgIntensity < thresholdNum;
                }

                if (dotIsActive) {
                    let bitPosition = -1;
                    // Map visual dotIndex (0-7, our 2x4 grid) to Braille Unicode bit positions (0-7)
                    // Visual dot arrangement:   Braille dot numbers:   Corresponding bit:
                    // (0,0) dotIndex=0         dot 1 (top-left)       bit 0
                    // (1,0) dotIndex=1         dot 4 (top-right)      bit 3
                    // (0,1) dotIndex=2         dot 2                  bit 1
                    // (1,1) dotIndex=3         dot 5                  bit 4
                    // (0,2) dotIndex=4         dot 3                  bit 2
                    // (1,2) dotIndex=5         dot 6                  bit 5
                    // (0,3) dotIndex=6         dot 7 (bottom-left)    bit 6
                    // (1,3) dotIndex=7         dot 8 (bottom-right)   bit 7
                    if (dotIndex === 0) bitPosition = 0; 
                    else if (dotIndex === 1) bitPosition = 3;
                    else if (dotIndex === 2) bitPosition = 1;
                    else if (dotIndex === 3) bitPosition = 4;
                    else if (dotIndex === 4) bitPosition = 2;
                    else if (dotIndex === 5) bitPosition = 5;
                    else if (dotIndex === 6) bitPosition = 6;
                    else if (dotIndex === 7) bitPosition = 7;
                    
                    if (bitPosition !== -1) {
                        brailleValue |= (1 << bitPosition);
                    }
                }
            }

            const brailleChar = String.fromCharCode(0x2800 + brailleValue);
            
            // Calculate position to draw the character, centered in its allocated cell
            const drawX = (cx + 0.5) * cellSizeNum;
            const drawY = (cy + 0.5) * cellSizeNum; 
            
            outCtx.fillText(brailleChar, drawX, drawY);
        }
    }

    return outCanvas;
}

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 Braille Text Filter Effect tool converts standard images into a format that mimics Braille text, allowing users to create tactile representations of images. By adjusting parameters like cell size and threshold for dot activation, it can provide customizable outputs suited for various visual styles. This tool can be particularly useful for educators, accessibility advocates, and artists, enabling them to create Braille art, facilitate learning experiences for visually impaired individuals, or enhance educational materials with tactile elements.

Leave a Reply

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