Please bookmark this page to avoid losing your image tool!

Image Properties Translator By Alphabet

(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.
/**
 * Processes an image to create a "paint by alphabet" style canvas.
 * It quantizes the image colors to a limited palette, creates outlines between the
 * resulting color regions, and places letters from a specified alphabet in each region.
 * Each letter corresponds to a color in the generated palette. A legend showing the
 * color-to-letter mapping is drawn at the bottom of the canvas.
 *
 * @param {Image} originalImg The original image object to process.
 * @param {number} [numColors=16] The target number of colors for the final palette. The actual number may be lower if the image has fewer colors.
 * @param {string} [alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ'] The string of characters to use for labeling color regions.
 * @param {number} [fontSize=10] The font size in pixels for the letter labels on the canvas.
 * @param {number} [showOutlines=1] A flag to control drawing outlines. Use 1 to show outlines, 0 to hide them.
 * @param {string} [outlineColor='black'] The CSS color string for the outlines and text labels.
 * @param {number} [outlineWidth=1] The width in pixels of the outlines between color regions.
 * @returns {HTMLCanvasElement} A new canvas element containing the "paint by alphabet" representation of the image.
 */
function processImage(originalImg, numColors = 16, alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', fontSize = 10, showOutlines = 1, outlineColor = 'black', outlineWidth = 1) {
    // Helper function to calculate squared Euclidean distance between two RGB colors
    const colorDistanceSq = (c1, c2) => {
        const dr = c1[0] - c2[0];
        const dg = c1[1] - c2[1];
        const db = c1[2] - c2[2];
        return dr * dr + dg * dg + db * db;
    };

    // Helper to find the index of the closest centroid for a given color
    const findClosestCentroid = (color, centroids) => {
        let minDistanceSq = Infinity;
        let bestIndex = 0;
        for (let i = 0; i < centroids.length; i++) {
            const distanceSq = colorDistanceSq(color, centroids[i]);
            if (distanceSq < minDistanceSq) {
                minDistanceSq = distanceSq;
                bestIndex = i;
            }
        }
        return bestIndex;
    };

    // --- 1. Canvas Setup and Pixel Data Extraction ---
    const width = originalImg.naturalWidth;
    const height = originalImg.naturalHeight;

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = width;
    tempCanvas.height = height;
    const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
    tempCtx.drawImage(originalImg, 0, 0, width, height);

    let imageData;
    try {
        imageData = tempCtx.getImageData(0, 0, width, height);
    } catch (e) {
        // Handle security errors if the image is cross-origin
         const errorCanvas = document.createElement('canvas');
         errorCanvas.width = width || 300;
         errorCanvas.height = height || 150;
         const errorCtx = errorCanvas.getContext('2d');
         errorCtx.fillStyle = '#f0f0f0';
         errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
         errorCtx.fillStyle = 'red';
         errorCtx.textAlign = 'center';
         errorCtx.textBaseline = 'middle';
         errorCtx.font = '14px sans-serif';
         errorCtx.fillText('Could not process image due to cross-origin restrictions.', errorCanvas.width / 2, errorCanvas.height / 2);
         return errorCanvas;
    }
    const pixels = imageData.data;

    // Create a representative sample of colors from the image
    const colorSamples = [];
    for (let i = 0; i < pixels.length; i += 4) {
        // Sample only non-transparent pixels
        if (pixels[i + 3] > 128) {
            colorSamples.push([pixels[i], pixels[i + 1], pixels[i + 2]]);
        }
    }
    
    if (colorSamples.length === 0) {
         const emptyCanvas = document.createElement('canvas');
         emptyCanvas.width = width || 100;
         emptyCanvas.height = height || 100;
         const emptyCtx = emptyCanvas.getContext('2d');
         emptyCtx.fillStyle = 'white';
         emptyCtx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
         emptyCtx.fillStyle = 'black';
         emptyCtx.textAlign = 'center';
         emptyCtx.textBaseline = 'middle';
         emptyCtx.fillText('Image is empty or transparent.', emptyCanvas.width / 2, emptyCanvas.height / 2);
         return emptyCanvas;
    }

    // --- 2. Color Quantization using K-Means Clustering ---
    const k = Math.min(numColors, [...new Set(colorSamples.map(c => c.join(',')))].length);
    if (k === 0) return processImage(new Image()); // Recurse with empty image to get the error message

    // Initialize centroids by picking unique random colors from the samples
    let centroids = [];
    const usedColors = new Set();
    while (centroids.length < k && usedColors.size < colorSamples.length) {
        const index = Math.floor(Math.random() * colorSamples.length);
        const colorKey = colorSamples[index].join(',');
        if (!usedColors.has(colorKey)) {
            centroids.push(colorSamples[index]);
            usedColors.add(colorKey);
        }
    }

    const maxIterations = 20;
    for (let iter = 0; iter < maxIterations; iter++) {
        const clusters = Array.from({ length: k }, () => []);
        for (const color of colorSamples) {
            const clusterIndex = findClosestCentroid(color, centroids);
            clusters[clusterIndex].push(color);
        }

        let hasChanged = false;
        const newCentroids = [];
        for (let i = 0; i < k; i++) {
            if (clusters[i].length === 0) {
                newCentroids.push(centroids[i]);
                continue;
            }
            const sum = clusters[i].reduce((acc, c) => [acc[0] + c[0], acc[1] + c[1], acc[2] + c[2]], [0, 0, 0]);
            const avgColor = [
                Math.round(sum[0] / clusters[i].length),
                Math.round(sum[1] / clusters[i].length),
                Math.round(sum[2] / clusters[i].length)
            ];
            newCentroids.push(avgColor);
            
            if (!centroids[i] || colorDistanceSq(avgColor, centroids[i]) > 0) {
                hasChanged = true;
            }
        }
        centroids = newCentroids;
        if (!hasChanged) break;
    }
    
    const palette = centroids;

    // --- 3. Create a map of the image where each pixel maps to a palette index ---
    const indexMap = new Int16Array(width * height);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;
            if (pixels[i + 3] < 128) {
                indexMap[y * width + x] = -1; // Special value for transparent pixels
            } else {
                const color = [pixels[i], pixels[i + 1], pixels[i + 2]];
                indexMap[y * width + x] = findClosestCentroid(color, palette);
            }
        }
    }
    
    // --- 4. Prepare the final output Canvas ---
    const legendItemSize = 20;
    const legendPadding = 10;
    const legendItemWidth = 70; 
    const itemsPerRow = Math.max(1, Math.floor((width - 2 * legendPadding) / legendItemWidth));
    const numRows = Math.ceil(palette.length / itemsPerRow);
    const legendHeight = numRows > 0 ? (numRows * (legendItemSize + legendPadding)) + legendPadding : 0;
    
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height + legendHeight;
    const ctx = outputCanvas.getContext('2d');
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
    
    // --- 5. Draw Outlines between different color regions ---
    if (showOutlines == 1 && parseFloat(outlineWidth) > 0) {
        ctx.beginPath();
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const currentIndex = indexMap[y * width + x];
                if (currentIndex === -1) continue;

                if (x < width - 1) {
                    const rightIndex = indexMap[y * width + x + 1];
                    if (currentIndex !== rightIndex && rightIndex !== -1) {
                        ctx.moveTo(x + 1, y);
                        ctx.lineTo(x + 1, y + 1);
                    }
                }
                if (y < height - 1) {
                    const downIndex = indexMap[(y+1) * width + x];
                    if (currentIndex !== downIndex && downIndex !== -1) {
                        ctx.moveTo(x, y + 1);
                        ctx.lineTo(x + 1, y + 1);
                    }
                }
            }
        }
        ctx.strokeStyle = outlineColor;
        ctx.lineWidth = parseFloat(outlineWidth);
        ctx.stroke();
    }

    // --- 6. Find contiguous color regions and draw letter labels ---
    const visited = new Uint8Array(width * height);
    ctx.font = `${fontSize}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = outlineColor;
    
    const minRegionSize = Math.pow(fontSize * 1.5, 2);

    for(let y = 0; y < height; y++) {
        for(let x = 0; x < width; x++) {
            const pos = y * width + x;
            if(visited[pos]) continue;
            
            const regionColorIndex = indexMap[pos];
            if(regionColorIndex === -1) continue;

            const stack = [[x, y]];
            visited[pos] = 1;
            let sumX = 0, sumY = 0, count = 0;

            while(stack.length > 0) {
                const [cx, cy] = stack.pop();
                sumX += cx;
                sumY += cy;
                count++;

                const neighbors = [[cx,cy-1], [cx,cy+1], [cx-1, cy], [cx+1, cy]];
                for(const [nx, ny] of neighbors) {
                    if(nx >= 0 && nx < width && ny >= 0 && ny < height) {
                        const neighborPos = ny * width + nx;
                        if(!visited[neighborPos] && indexMap[neighborPos] === regionColorIndex) {
                            visited[neighborPos] = 1;
                            stack.push([nx, ny]);
                        }
                    }
                }
            }

            if (count > minRegionSize) {
                const centerX = sumX / count;
                const centerY = sumY / count;
                const char = alphabet[regionColorIndex] || '?';
                ctx.fillText(char, centerX, centerY);
            }
        }
    }

    // --- 7. Draw the color palette legend at the bottom ---
    ctx.font = `${legendItemSize * 0.7}px sans-serif`;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    
    for (let i = 0; i < palette.length; i++) {
        const color = palette[i];
        if(!color) continue;
        const char = alphabet[i] || '?';
        const row = Math.floor(i / itemsPerRow);
        const col = i % itemsPerRow;
        const lx = legendPadding + col * legendItemWidth;
        const ly = height + legendPadding + row * (legendItemSize + legendPadding);

        ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
        ctx.fillRect(lx, ly, legendItemSize, legendItemSize);
        ctx.strokeStyle = '#999';
        ctx.lineWidth = 1;
        ctx.strokeRect(lx - 0.5, ly - 0.5, legendItemSize + 1, legendItemSize + 1);

        ctx.fillStyle = outlineColor;
        ctx.fillText(`: ${char}`, lx + legendItemSize + 5, ly + legendItemSize / 2);
    }
    
    return outputCanvas;
}

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 Properties Translator by Alphabet tool converts images into a unique ‘paint by alphabet’ style representation. It reduces the colors in the image to a defined palette, creates outlines around different color regions, and assigns letters from a specified alphabet to each region. Users can customize the number of colors, the alphabet used for labeling, font size, and outline appearance. This tool is ideal for creating engaging visual art, educational material, or unique graphic designs that blend imagery with alphabetical elements, making it suitable for artists, teachers, and designers.

Leave a Reply

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