Normal images are rendered as a grid of pixels where each pixel has 24bits, 8 for the red channel, 8 for the green, and 8 for the blue (RGB). This allows 256 possible values for each color channel for a total of 256**3 ~ 16.7 million colors.
However, the human eye cannot perceive this many colors. In fact, the human eye can't notice a difference if the 2 most insignifcant bits of each channel were replaced with random noise! We can abuse this fact and replace the 2 most insignificant bits of each channel with actual data we want to transmit with the image.
The example here uses the HTML5 canvas tag to do operations on image bitmap data. It stores one character of the message inside 1.33 pixels. Images are saved as png so that compression does not mess with the data.
function textToImageData(text, imgd) {
text = "@!$" + text + String.fromCharCode(0);
var imageidx = 0;
for (var i = 0; i < text.length; i++) {
var charCode = text.charCodeAt(i);
imgd.data[imageidx] = (imgd.data[imageidx] & (~3)) | (charCode & 3);
imageidx++;
if (imageidx & 3 == 3) {
imageidx++;
}
imgd.data[imageidx] = (imgd.data[imageidx] & (~3)) | ((charCode >> 2) & 3);
imageidx++;
if (imageidx & 3 == 3) {
imageidx++;
}
imgd.data[imageidx] = (imgd.data[imageidx] & (~3)) | ((charCode >> 4) & 3);
imageidx++;
if (imageidx & 3 == 3) {
imageidx++;
}
imgd.data[imageidx] = (imgd.data[imageidx] & (~3)) | ((charCode >> 6) & 3);
imageidx++;
if (imageidx & 3 == 3) {
imageidx++;
}
}
return imgd;
}
function textFromImage(imgd) {
var text = "";
for (var i = 0; i < imgd.width*imgd.height*4;) {
var charCode = 0;
charCode |= imgd.data[i] & 3;
i++;
if (i & 3 == 3) {
i++;
}
charCode |= (imgd.data[i] & 3) << 2;
i++;
if (i & 3 == 3) {
i++;
}
charCode |= (imgd.data[i] & 3) << 4;
i++;
if (i & 3 == 3) {
i++;
}
charCode |= (imgd.data[i] & 3) << 6;
i++;
if (i & 3 == 3) {
i++;
}
if (charCode == 0) {
break;
}
text += String.fromCharCode(charCode);
}
if (text.substring(0,3) != "@!$") {
return "";
}
return text.substring(3);
}