On this page

# steg.js

    import * as util from './utils.js'

    // /** @namespace */
    /** @module */

    /**
     * @param {Image|Imageable} img img: image, canvas, context2d, url, data url
     * @returns A Canvas.context object
     */
    async function toContext(img) {
        const type = util.typeOf(img)
        switch (type) {
            // image url or html element
            case 'string': // Note: drop thru to image
                img = await util.imagePromise(img)
            case 'htmlimageelement':
                return util.imageToCtx(img)

            // canvas
            case 'htmlcanvaselement':
            case 'offscreencanvas':
                return img.getContext('2d')

            // image 2D context
            case 'canvasrenderingcontext2d':
                return img

            default:
                throw Error('toContext: bad img type: ' + type)
        }
    }
    // msg can be string or Uint8Array

    /**
     * @param {*} msg A string, charCode (number), Uint8Array or Uint8ClampedArray
     * @returns A Uint8Array or Uint8ClampedArray
     */
    function toUint8Array(msg) {
        const type = util.typeOf(msg)
        switch (type) {
            case 'number':
                msg = String.fromCharCode(msg) // drop thru to 'string'
            case 'string':
                return new TextEncoder().encode(msg)

            case 'uint8array':
            case 'uint8clampedarray':
                return msg

            default:
                throw Error('toUint8Array: bad msg type: ' + type)
        }
    }
    // Convert a char to an array of three RGB low order pixel values
    /**
     *
     * @param {} char An 8 bit char
     * @returns Array of 3 parts of char using the bits tempate (3, 2, 3 bits)
     */
    function charToBits(char) {
        // return [char >> 5, char >> 3 & 0b11111100, char & 0b11111000]
        return [
            char >> bits[0].shift,
            (char >> bits[1].shift) & bits[1].msgMask,
            char & bits[2].msgMask,
        ]
    }
    const bits = [
        { shift: 5, msgMask: 0b00000111, dataMask: 0b11111000 }, // bits: 3
        { shift: 3, msgMask: 0b00000011, dataMask: 0b11111100 }, // bits: 2
        { shift: 0, msgMask: 0b00000111, dataMask: 0b11111000 }, // bits: 3
    ]
    function checkSize(msg, width, height) {
        const imgSize = width * height // 1 px = 1 byte
        if (imgSize < msg.length)
            throw Error(`encode: image size < msg.length: ${imgSize} ${msg.length}`)
    }
    // Return the character length of the message within the image's imgData
    export function stegMsgSize(imgData) {
        for (let i = 3; i < imgData.length; i = i + 4) {
            if (imgData[i] === 254) return (i - 3) / 4
        }
        throw Error(
            `decode: no message terminator in image data, length = ${imgData.length}`
        )
    }

    // ============== encode/decode ==============

    // img can be png/jpeg image, url, dataurl, context2d, canvas
    // msg is a string or a Uint8 array
    export async function encode(img, msg) {
        const ctx = await toContext(img)
        const { width, height } = ctx.canvas
        checkSize(msg, width, height)

        const msgArray = toUint8Array(msg)
        console.log('msg buffer', msgArray)

        const imageData = ctx.getImageData(0, 0, width, height)
        const data = imageData.data
        console.log('imgageData.data', data)

        let ix
        msgArray.forEach((char, i) => {
            const [ch0, ch1, ch2] = charToBits(char)
            ix = i * 4
            data[ix] = (data[ix++] & bits[0].dataMask) + ch0
            data[ix] = (data[ix++] & bits[1].dataMask) + ch1
            data[ix] = (data[ix++] & bits[2].dataMask) + ch2
            data[ix] = 255
        })
        data[ix + 4] = 254 // use alpha to terminate message

        console.log('encoded imgageData.data', data)
        ctx.putImageData(imageData, 0, 0)

        console.log('msg length', msg.length)
        console.log('encode: embedded msg size', stegMsgSize(data))

        return ctx
    }

    // img can be png/jpeg image, url, dataurl, context2d, canvas
    // returnU8: return a UintTypedArray; default return string
    export async function decode(img, returnU8 = false) {
        const ctx = await toContext(img)
        const { width, height } = ctx.canvas
        const data = ctx.getImageData(0, 0, width, height).data
        const msgSize = stegMsgSize(data)
        console.log('decode: embedded msg size', msgSize)

        const msgArray = new Uint8Array(msgSize)
        msgArray.forEach((char, i) => {
            let ix = i * 4
            const ch0 = (bits[0].msgMask & data[ix++]) << bits[0].shift
            const ch1 = (bits[1].msgMask & data[ix++]) << bits[1].shift
            const ch2 = (bits[2].msgMask & data[ix++]) << bits[2].shift
            msgArray[i] = ch0 + ch1 + ch2
        })
        console.log('decode msgArray', msgArray)

        if (returnU8) return msgArray
        return new TextDecoder().decode(msgArray)
    }

Type anything to view search result