Please bookmark this page to avoid losing your image tool!

Image Hidden Message Filter Effect 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.
function processImage(originalImg, message = "Hidden Message Inside!") {

    // Helper function to draw error messages on a canvas
    function _drawErrorOnCanvas(canvasToDrawOn, width, height, errorMessage) {
        // Ensure canvas has valid dimensions
        if (!canvasToDrawOn) {
            canvasToDrawOn = document.createElement('canvas');
        }
        // Use provided width/height, or defaults if they are invalid
        canvasToDrawOn.width = width > 0 ? width : 200;
        canvasToDrawOn.height = height > 0 ? height : 100;
        
        const ctx = canvasToDrawOn.getContext('2d');
        ctx.clearRect(0, 0, canvasToDrawOn.width, canvasToDrawOn.height); // Clear previous content
        ctx.fillStyle = 'lightgray';
        ctx.fillRect(0, 0, canvasToDrawOn.width, canvasToDrawOn.height);
        ctx.fillStyle = 'red';
        ctx.font = '16px Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';

        const words = errorMessage.split(' ');
        let line = '';
        const lines = [];
        const maxWidth = canvasToDrawOn.width * 0.9; // Max width of text line

        for (let n = 0; n < words.length; n++) {
            const testLine = line + words[n] + ' ';
            const metrics = ctx.measureText(testLine); // Measure text width
            if (metrics.width > maxWidth && n > 0) {
                lines.push(line.trim());
                line = words[n] + ' ';
            } else {
                line = testLine;
            }
        }
        lines.push(line.trim()); // Add the last line

        const lineHeight = 20; // Approximate line height
        const totalTextHeight = lines.length * lineHeight;
        
        // Calculate starting Y position to center the block of text vertically.
        // startY will be the y-coordinate for the middle of the first line.
        let startY = (canvasToDrawOn.height - totalTextHeight) / 2 + lineHeight / 2;

        for (let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], canvasToDrawOn.width / 2, startY + (i * lineHeight));
        }
        return canvasToDrawOn;
    }

    // Validate the input image object
    if (!(originalImg instanceof HTMLImageElement) || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
        // Create a new canvas for the error message if originalImg is problematic
        let errorCanvas = document.createElement('canvas');
        return _drawErrorOnCanvas(errorCanvas, 200, 100, 'Error: Invalid or unloaded image provided. Ensure the image is fully loaded.');
    }
    
    const canvas = document.createElement('canvas');
    canvas.width = originalImg.naturalWidth; // Use actual image dimensions
    canvas.height = originalImg.naturalHeight;
    
    // The willReadFrequently hint can optimize getImageData calls.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    } catch (e) {
        console.error("Error getting ImageData:", e);
        let errorMsg = 'Error: Could not process image pixels.';
        if (e.name === 'SecurityError') {
            errorMsg = 'Error: Cross-origin image security policy prevents pixel access. The image might be from another website or a local file accessed without proper permissions.';
        }
        // Cannot draw on the original canvas if it's tainted by cross-origin data.
        // Create a new, clean canvas for the error message.
        let errorCanvas = document.createElement('canvas');
        return _drawErrorOnCanvas(errorCanvas, canvas.width, canvas.height, errorMsg);
    }

    const data = imageData.data; // Pixel data: R,G,B,A, R,G,B,A, ...

    const encoder = new TextEncoder(); // Standard API to convert string to UTF-8 bytes
    const messageBytes = encoder.encode(message);
    
    const BITS_FOR_LENGTH_HEADER = 32; // Using 32 bits (4 bytes) to store the length of the message.
                                       // This length is the number of *bytes* in the UTF-8 encoded message.
    
    // Calculate total available bits using LSB of R,G,B channels
    const totalAvailableDataBits = canvas.width * canvas.height * 3;

    if (totalAvailableDataBits < BITS_FOR_LENGTH_HEADER) {
        console.error("Image is too small to store even the message length header.");
        // Draw error on the main canvas (it's usable since getImageData succeeded)
        return _drawErrorOnCanvas(canvas, canvas.width, canvas.height, "Image is too small to hide message (cannot fit length header).");
    }

    // Maximum number of message bytes that can be stored after the length header
    const maxMessagePayloadBytes = Math.floor((totalAvailableDataBits - BITS_FOR_LENGTH_HEADER) / 8);
    
    let actualMessageBytesToEncode;
    if (maxMessagePayloadBytes <= 0) { 
        actualMessageBytesToEncode = new Uint8Array(0); // No space for payload, encode empty message.
        if (messageBytes.length > 0) {
            console.warn(`Image has no space for message payload (only ${maxMessagePayloadBytes} bytes available after header). Encoding an empty message.`);
        }
    } else if (messageBytes.length > maxMessagePayloadBytes) {
        console.warn(`Message is too long. Original length: ${messageBytes.length} bytes. Maximum allowed: ${maxMessagePayloadBytes} bytes. Message will be truncated.`);
        actualMessageBytesToEncode = messageBytes.slice(0, maxMessagePayloadBytes);
    } else {
        actualMessageBytesToEncode = messageBytes;
    }
    
    const actualMessageLengthInBytes = actualMessageBytesToEncode.length;

    let dataArrIndex = 0; // Current index in the imageData.data array, points to an R, G, or B component.

    // Helper to modify the Least Significant Bit (LSB) of a color component
    const setLSB = (value, bit) => (bit === 1 ? (value | 1) : (value & 0xFE));

    // 1. Encode message length (number of BYTES in the message) into LSBs
    for (let i = 0; i < BITS_FOR_LENGTH_HEADER; i++) {
        if (dataArrIndex >= data.length) { 
            console.error("Critical Error: Ran out of image data while writing length header. This should have been caught by capacity checks.");
            return _drawErrorOnCanvas(canvas, canvas.width, canvas.height, "Internal error: Ran out of space for message header. Image might be too small."); 
        }
        
        // Get the i-th bit of the length (MSB first)
        const bit = (actualMessageLengthInBytes >> (BITS_FOR_LENGTH_HEADER - 1 - i)) & 1; 
        data[dataArrIndex] = setLSB(data[dataArrIndex], bit);
        
        dataArrIndex++; // Move to next color component (e.g., R to G)
        // imageData.data is [R,G,B,A, R,G,B,A, ...]. Alpha channels (at indices 3, 7, 11, ...) are skipped.
        // If dataArrIndex was pointing to B (e.g. index 2), after increment it points to A (index 3).
        // (dataArrIndex + 1) % 4 === 0 means dataArrIndex is currently an Alpha channel index. This is slightly off.
        // Correct logic: if current dataArrIndex is an R or G, next is G or B. If current is B, next is A. After modifying B, dataArrIndex points to A. Skip A.
        if ((dataArrIndex + 1) % 4 === 0) { // If dataArrIndex is now an Alpha channel's predecessor (B)
                                          // after this write dataArrIndex points to current Alpha
            dataArrIndex++; // Skip Alpha channel by incrementing index again.
        }
    }

    // 2. Encode message content (bytes) into LSBs
    for (let byteIdx = 0; byteIdx < actualMessageBytesToEncode.length; byteIdx++) {
        const currentByte = actualMessageBytesToEncode[byteIdx];
        for (let bitIdx = 0; bitIdx < 8; bitIdx++) { // Iterate through 8 bits of the current byte
            if (dataArrIndex >= data.length) {
                console.error("Critical Error: Ran out of image data while writing message body. Message partially encoded.");
                ctx.putImageData(imageData, 0, 0); // Save whatever was encoded
                return _drawErrorOnCanvas(canvas, canvas.width, canvas.height, "Internal error: Ran out of space for full message body. Message partially hidden.");
            }

            const bit = (currentByte >> (7 - bitIdx)) & 1; // Get bit from byte (MSB first)
            data[dataArrIndex] = setLSB(data[dataArrIndex], bit);

            dataArrIndex++; 
            if ((dataArrIndex + 1) % 4 === 0) { 
                dataArrIndex++; 
            }
        }
    }

    // Write the modified pixel data back to the canvas
    ctx.putImageData(imageData, 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 Hidden Message Filter Effect Tool allows users to embed hidden messages within images by manipulating the pixel data of the original image. This tool can be utilized for various purposes, such as creating unique digital art, embedding messages in photos for personal use, or for educational purposes to teach about steganography. Users can input a message, and the tool will modify the least significant bits of the image’s pixel data to conceal the message, thereby creating a new image that appears visually unchanged. This feature can be particularly useful for sending secure communications or for creating puzzles and games.

Leave a Reply

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