Please bookmark this page to avoid losing your image tool!

Image Comic Book Filter Applicator

(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.
function processImage(originalImg, posterizationLevels = 5, edgeThreshold = 50, lineColor = "black") {
    // 1. Setup Canvas
    const canvas = document.createElement('canvas');
    let ctx;
    try {
        // Use { willReadFrequently: true } for potential performance optimization if supported by the browser.
        ctx = canvas.getContext('2d', { willReadFrequently: true });
    } catch (e) {
        // Fallback for environments or older browsers that don't support the options object.
        ctx = canvas.getContext('2d');
    }

    // If canvas context itself could not be retrieved (highly unlikely for '2d')
    if (!ctx) {
        console.error("Could not get 2D context from canvas.");
        // Return an empty canvas, sized appropriately if possible.
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = originalImg.width > 0 ? originalImg.width : 1;
        emptyCanvas.height = originalImg.height > 0 ? originalImg.height : 1;
        return emptyCanvas;
    }
    
    canvas.width = originalImg.width;
    canvas.height = originalImg.height;

    // Handle cases where originalImg might not have loaded or has zero dimensions
    if (canvas.width === 0 || canvas.height === 0) {
        // Return the empty (but correctly sized if originalImg had 0,0) canvas.
        return canvas;
    }

    // Draw the original image onto the canvas
    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    let imageData;
    try {
        // Get pixel data from the canvas
        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    } catch (e) {
        // This can happen due to a tainted canvas (e.g., cross-origin image loaded without CORS headers)
        console.error("Could not getImageData from canvas. Ensure the image is CORS-enabled if from another domain.", e);
        // Fallback: return the canvas with the original image drawn, as processing is not possible.
        return canvas;
    }
    
    const data = imageData.data; // Original pixel data (Uint8ClampedArray view)
    const width = canvas.width;
    const height = canvas.height;

    // Helper function to parse a CSS color string (e.g., "red", "#FF0000", "rgb(255,0,0)") to an [R, G, B] array.
    function parseColorToRGB(colorStr) {
        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = 1;
        tempCanvas.height = 1;
        const tempCtx = tempCanvas.getContext('2d');
        
        if (!tempCtx) return [0, 0, 0]; // Should not happen for a 1x1 canvas

        tempCtx.fillStyle = colorStr; // Assign color string (browser parses it)
        tempCtx.fillRect(0, 0, 1, 1); // Fill a single pixel
        
        // Read the RGB value of the filled pixel
        const colorPixelData = tempCtx.getImageData(0, 0, 1, 1).data;
        // Browsers handle invalid color strings by often defaulting to 'rgba(0,0,0,0)' or 'black'.
        // We want the RGB part. Alpha of lines is forced to 255 later.
        return [colorPixelData[0], colorPixelData[1], colorPixelData[2]];
    }
    const [R_line, G_line, B_line] = parseColorToRGB(lineColor);

    // 2. Create a separate buffer for posterized image data
    const posterizedPixelData = new Uint8ClampedArray(data.length);
    // Copy original data to posterized buffer. This preserves alpha and provides a working copy.
    for (let i = 0; i < data.length; i++) {
        posterizedPixelData[i] = data[i];
    }
    
    // 3. Apply Color Posterization
    // Ensure posterizationLevels is a positive integer, at least 2 for meaningful quantization.
    const numLevels = Math.max(2, Math.floor(posterizationLevels));
    const step = 255 / (numLevels - 1); // Calculate the size of each color quantization step

    for (let i = 0; i < posterizedPixelData.length; i += 4) {
        posterizedPixelData[i]   = Math.round(Math.round(posterizedPixelData[i] / step) * step);   // Red channel
        posterizedPixelData[i+1] = Math.round(Math.round(posterizedPixelData[i+1] / step) * step); // Green channel
        posterizedPixelData[i+2] = Math.round(Math.round(posterizedPixelData[i+2] / step) * step); // Blue channel
        // Alpha channel (posterizedPixelData[i+3]) is preserved from the original image copy
    }

    // 4. Convert original image to Grayscale (for edge detection)
    // Using original data (pre-posterization) for grayscale can sometimes yield better edge definition.
    const grayscaleValues = new Uint8ClampedArray(width * height); // 1 byte per pixel for grayscale intensity
    for (let i = 0, j = 0; i < data.length; i += 4, j++) { // i is index for data (RGBA), j is for grayscaleValues
        const r = data[i];
        const g = data[i + 1];
        const b = data[i + 2];
        // Standard luminance calculation
        grayscaleValues[j] = (0.299 * r + 0.587 * g + 0.114 * b);
    }

    // 5. Edge Detection using Sobel Operator
    const sobelMagnitudes = new Float32Array(width * height); // Store gradient magnitudes
    const Gx_kernel = [ // Sobel kernel for X-direction gradient
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const Gy_kernel = [ // Sobel kernel for Y-direction gradient
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ];

    // Iterate over image pixels, skipping 1px border where kernel cannot be fully applied
    for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
            let Gx_sum = 0; // Sum for X-gradient
            let Gy_sum = 0; // Sum for Y-gradient
            for (let ky = -1; ky <= 1; ky++) { // Kernel Y-offset
                for (let kx = -1; kx <= 1; kx++) { // Kernel X-offset
                    // Calculate index for the 1D grayscaleValues array from 2D coordinates
                    const grayscaleIndex = (y + ky) * width + (x + kx);
                    const pixelIntensity = grayscaleValues[grayscaleIndex];
                    
                    Gx_sum += pixelIntensity * Gx_kernel[ky + 1][kx + 1];
                    Gy_sum += pixelIntensity * Gy_kernel[ky + 1][kx + 1];
                }
            }
            // Calculate gradient magnitude: sqrt(Gx^2 + Gy^2)
            const magnitude = Math.sqrt(Gx_sum * Gx_sum + Gy_sum * Gy_sum);
            sobelMagnitudes[y * width + x] = magnitude;
        }
    }
    
    // 6. Threshold Edges to create a binary edge map
    const edgeMap = new Uint8ClampedArray(width * height); // 0 for no edge, 1 for edge
    // edgeThreshold parameter determines sensitivity. Higher values mean fewer edges.
    // Max theoretical Sobel magnitude can be >255 (e.g., for 8-bit monochrome, ~1442).
    // User might need to adjust edgeThreshold based on image characteristics.
    const effectiveEdgeThreshold = Math.max(0, edgeThreshold);

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const idx = y * width + x;
            // Pixels on the 1px border will have a magnitude of 0 (as Sobel wasn't computed there).
            // They will be classified as 'no edge' if effectiveEdgeThreshold > 0.
            if (sobelMagnitudes[idx] > effectiveEdgeThreshold) {
                edgeMap[idx] = 1; // Mark as edge
            } else {
                edgeMap[idx] = 0; // Mark as no edge
            }
        }
    }
    
    // 7. Combine Posterized Image with Detected Edges
    // Create new ImageData for the final output
    const finalImageData = ctx.createImageData(width, height);
    const finalPixelData = finalImageData.data;

    for (let i = 0; i < edgeMap.length; i++) { // i is the pixel index (0 to width*height-1)
        const R_idx = i * 4; // Base index for RGBA data in finalPixelData and posterizedPixelData
        
        if (edgeMap[i] === 1) { // If current pixel is detected as an edge
            finalPixelData[R_idx]     = R_line;   // Set line color (Red component)
            finalPixelData[R_idx + 1] = G_line;   // Set line color (Green component)
            finalPixelData[R_idx + 2] = B_line;   // Set line color (Blue component)
            finalPixelData[R_idx + 3] = 255;      // Lines are fully opaque
        } else { // Not an edge, use the posterized color from the earlier step
            finalPixelData[R_idx]     = posterizedPixelData[R_idx];
            finalPixelData[R_idx + 1] = posterizedPixelData[R_idx + 1];
            finalPixelData[R_idx + 2] = posterizedPixelData[R_idx + 2];
            finalPixelData[R_idx + 3] = posterizedPixelData[R_idx + 3]; // Preserve alpha from posterized (original) data
        }
    }

    // 8. Put the processed image data back onto the canvas
    ctx.putImageData(finalImageData, 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 Comic Book Filter Applicator allows users to apply a comic book-style filter to their images. This tool processes images by posterizing colors, detecting edges, and overlaying these edges to create a unique, artistic effect reminiscent of comic book art. It’s ideal for users looking to transform their photos into illustrations, create visually engaging graphics for social media, or enhance artwork for creative projects. With adjustable parameters like posterization levels and edge thresholds, users can customize the output to achieve their desired comic-style appearance.

Leave a Reply

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