Steganography and manipulating images with JIMP

An old project (Oct 2017) to hide images inside other images, with some protection from modification.

This is a *very* old project that I wrote a long time ago (over 3 years ago at the time of writing!).

It can take an image and hide another image in it, in a way that is undetectable to the eye. This means that if the image is used elsewhere, you can extract the hidden image, as a kind of watermark.

How it works

In this process, there is a medium image to hide a message image in. The program begins by simplifying the message image. The message image's pixels each contain an RGB value, saying how much red, green and blue light is in the image at that point. These values for red, green and blue go from 0 to 255.

 

However, this means that there are over 16 million possibilities for each pixel - too much information to store in one pixel. To combat this, we "simplify" colours. For each colour of each pixel, we round the value to 0 or 255 - whichever is closer. This means that each colour of each pixel can only be 0 or 255.

This means that there are now only \(2^3 = 8\) possibilities for each pixel - much smaller than 16 million!

Below is an example of this simplification, on a colour wheel.

Now, we can look at putting the message image back into the medium image.

To do this, we begin by taking the medium image and laying the message image on top of it. We then take each pixel of the medium image and change the least significant bit of each colour channel to match the message image. For example if the message image's red value is 255 in one pixel, this pixel in the medium image has 1 as its least significant bit for its red value.

This means that each pixel of the medium image will only be slightly altered, as the red, green and blue values can only be changed by at most 1 each. This isn't detectable by the human eye, but a program can easily identify these differences.

Using JIMP to implement this

JIMP stands for Javascript Image Manipulation Program. JIMP has many image processing features, but we're going to be using JIMP to scan the pixels of the medium and message images to hide a message image in a medium image and extract a message image from an image with a watermark.

We begin by reading the images out of their files.

exports.write = function (image_path_in, image_path_write, image_path_out, options) {
    var image_in = image_path_in
    var image_out = image_path_out
    var image_write = image_path_write

    var Jimp = require("jimp")
    Jimp.read(image_in, function (err, data) {
        if (err) {
            throw err
        }
        Jimp.read(image_write, function (err_write, data_write) {
            if (err_write) {
                throw err_write
            }
    })
}

Next, we're going to use the .scan function. This will travel over all of the pixels in the image bitmap.

The image bitmap is an array of integers from 0 to 255. This array contains the red, green, blue and alpha values in that order of all the pixels, "scanning" the pixels from left to right and top to bottom.

I begin by scanning the message image and creating a new image bitmap with the simplified colours.

var imagematrix = []
data_write.scan(0, 0, data_write.bitmap.width, data_write.bitmap.height, function (x, y, idx) {
    if (x == 0) {
        imagematrix.push([])
    }
    var r = Math.round(this.bitmap.data[idx + 0] / 255)
    var g = Math.round(this.bitmap.data[idx + 1] / 255)
    var b = Math.round(this.bitmap.data[idx + 2] / 255)
    imagematrix[y].push(((r * 4) + (g * 2) + (b * 1)))
})

The imagematrix variable contains the pixels of the simplified message image as 3 bit numbers in a 2D array, with the most significant bit being the red value and the least significant bit being the blue value.

The next part then reads this array to modify the least significant bits of the medium image, and then save it to a file.

data.scan(0, 0, data.bitmap.width, data.bitmap.height, function (x, y, idx) {
    var matrix_x = Math.floor(x) % data_write.bitmap.width
    var matrix_y = Math.floor(y) % data_write.bitmap.height

    var r = this.bitmap.data[idx + 0]
    var g = this.bitmap.data[idx + 1]
    var b = this.bitmap.data[idx + 2]
    r = bit_write(r, 0, bit_read(imagematrix[matrix_y][matrix_x], 2))
    g = bit_write(g, 0, bit_read(imagematrix[matrix_y][matrix_x], 1))
    b = bit_write(b, 0, bit_read(imagematrix[matrix_y][matrix_x], 0))

    this.bitmap.data[idx + 0] = r
    this.bitmap.data[idx + 1] = g
    this.bitmap.data[idx + 2] = b
})

data.write(image_out, callback)

This uses the following bit manipulation functions:

function bit_read(num, bit) {
    return ((num >> bit) % 2)
}

function bit_set(num, bit) {
    return num | 1 << bit;
}

function bit_clear(num, bit) {
    return num & ~(1 << bit);
}

function bit_write(num, idx, bit) {
    return bit == 0 ? bit_clear(num, idx) : bit_set(num, idx);
}

Finally, I have this function, which can read a message image from another image. This creates and saves a new image, reversing the process.

exports.read = function (image_in, image_out, options) {
    if (typeof options === 'undefined') { options = {} }
    if (typeof options.callback === 'undefined') { options.callback = function () { } }
    var Jimp = require("jimp")
    Jimp.read(image_in, function (err, data) {
        if (err) {
            throw err
        }
        var iOut = new Jimp(data.bitmap.width, data.bitmap.height, function (err, image) {
            if (err) {
                throw err; // :D
            }
            data.scan(0, 0, data.bitmap.width, data.bitmap.height, function (x, y, idx) {
                image.setPixelColor(((bit_read(this.bitmap.data[idx], 0) * 255) << 24) + ((bit_read(this.bitmap.data[idx + 1], 0) * 255) << 16) + ((bit_read(this.bitmap.data[idx + 2], 0) * 255) << 8) + 255, x, y)
            })
            image.write(image_out, options.callback())
        })
    })
}

Protecting against the image being squished

One feature that I wanted to add was squish protection. If an image with a hidden message in it is squished or squashed, I still want to be able to extract the original message. To do this, I would enlarge the message image before writing it into the medium image so that it will be less affected by squishes or squashes. To do this, I reduce the relative speed with which I move along the images while scanning. What this means is that if I move 4 pixels in the medium image, I will only move a total of one pixel in the message image while updating the least significant bits.

exports.write = function (image_path_in, image_path_write, image_path_out, options) {
    var image_in = image_path_in
    var image_out = image_path_out
    var image_write = image_path_write
    if (typeof options === 'undefined') { options = {} }
    if (typeof options.callback === 'undefined') { options.callback = function () { } }
    if (typeof options.squeeze_protection === 'undefined') { options.squeeze_protection = 1 }
    var squeeze_protection = options.squeeze_protection
    var callback = options.callback
    var Jimp = require("jimp")
    Jimp.read(image_in, function (err, data) {
        if (err) {
            throw err
        }
        Jimp.read(image_write, function (err_write, data_write) {
            //if (err_write) {
            //    throw err_write
            //}
            var imagematrix = []
            data_write.scan(0, 0, data_write.bitmap.width, data_write.bitmap.height, function (x, y, idx) {
                if (x == 0) {
                    imagematrix.push([])
                }
                var r = Math.round(this.bitmap.data[idx + 0] / 255)
                var g = Math.round(this.bitmap.data[idx + 1] / 255)
                var b = Math.round(this.bitmap.data[idx + 2] / 255)
                imagematrix[y].push(((r * 4) + (g * 2) + (b * 1)))
            })
            // image matrix contains the compressed, "basicifyied" image
            data.scan(0, 0, data.bitmap.width, data.bitmap.height, function (x, y, idx) {
                var matrix_x = Math.floor(x / squeeze_protection) % data_write.bitmap.width
                var matrix_y = Math.floor(y / squeeze_protection) % data_write.bitmap.height

                var r = this.bitmap.data[idx + 0]
                var g = this.bitmap.data[idx + 1]
                var b = this.bitmap.data[idx + 2]
                r = bit_write(r, 0, bit_read(imagematrix[matrix_y][matrix_x], 2))
                g = bit_write(g, 0, bit_read(imagematrix[matrix_y][matrix_x], 1))
                b = bit_write(b, 0, bit_read(imagematrix[matrix_y][matrix_x], 0))

                this.bitmap.data[idx + 0] = r
                this.bitmap.data[idx + 1] = g
                this.bitmap.data[idx + 2] = b
            })

            data.write(image_out, callback)
        })
    })
}

Use this for yourself!

I packaged all of this up into one npm package you can use yourself.

The full source code is on my github page.

An example

This image seems unsuspecting, but it actually contains the watermark on the right.

 

The image can even be stretched and still have a watermark extracted from it.