- encodes any file into a picture
- decodes this file from the same picture
How does it work?
So easy... you know a bitmap file is nothing more than a succession of pixels, and a pixel is made of 3 bytes Red Green Blue, right? (let's ignore the file header, the reverse order of rows, etc etc... that's not important here)The big picture is to hide one byte of the message per pixel. Red is coded on 8 bits, right? Let's use the 2 least significant bits (LSB) to encode 2 bits of this message byte. Green is coded on 8 bits too, so let's use the 3 LSB to encode 3 bits of the message byte. Same thing for Blue, let's use the 3 LSB to encode 3 bits of the message byte. You got it: 2 bits + 3 bits + 3 bits = 1 byte. This is how we will hide each byte from the message in one pixel.
The good news is that we always modified the LSB of the pixel components... when you will look at the modified picture (containing the message), you will not be able to detect the color changes from the original one.
Now, have a closer look at the two interesting parts in this program: encoding of data, decoding of data.
Encoding
The following method inserts a message (a memory Stream, it can be anything) into the original picture. Of course, we have to ensure the picture file is big enough to contain the message.Note the the first 4 pixels will be used to encode the size of the message. We will need it to decode the message.
The source code is a bit messy, because it contains a homemade BMP writer. But the data cloaking itself is quite straightforward, provided you are familiar with bitmasks.
private void ProcessBmp(string InputFilename, System.IO.MemoryStream Message, string OutputFilename) { using (System.IO.FileStream fs = new System.IO.FileStream(InputFilename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read, 1024 * 1024)) { using (System.Drawing.Bitmap b = new System.Drawing.Bitmap(fs)) { if (b.Width * b.Height < Message.Length + 4) throw new Exception(string.Format("BMP file is too small, it must be at least {0} bytes to encode this message", (Message.Length + 4) * 3)); using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(new System.IO.FileStream(OutputFilename, System.IO.FileMode.Create, System.IO.FileAccess.Write))) { int ComplementToAdd = (4 - ((b.Width * 3) % 4)) % 4; byte[] BlackPixel = new byte[3] { 0, 0, 0 }; bw.Write('B'); bw.Write('M'); int x, y; bw.Write((uint)(54 + b.Width * b.Height * 3 + b.Height * ComplementToAdd)); bw.Write((uint)0); bw.Write((uint)54); bw.Write((uint)40); bw.Write((uint)b.Width); bw.Write((uint)b.Height); bw.Write((uint)24 * 0x10000 + 1); // planes & bitcount bw.Write((uint)0); // compression bw.Write((uint)b.Width * b.Height * 3 + ComplementToAdd * b.Height); // size of image bw.Write((uint)0); // XPPM bw.Write((uint)0); // YPPM bw.Write((uint)0); // color used int Idx = 0; for (y = b.Height - 1; y >= 0; y--) { for (x = 0; x < b.Width; x++) { Color c = b.GetPixel(x, y); byte[] RGB = new byte[3] { c.B , c.G, c.R }; byte ByteToEncode; switch (Idx) { // The 4 first bytes wil be used to encode the size of the message case 0: ByteToEncode = (byte)(Message.Length & 0x000000FF); break; case 1: ByteToEncode = (byte)((Message.Length & 0x0000FF00) >> 8); break; case 2: ByteToEncode = (byte)((Message.Length & 0x00FF0000) >> 16); break; case 3: ByteToEncode = (byte)((Message.Length & 0xFF000000) >> 32); break; default: ByteToEncode = Idx - 4 < Message.Length ? (byte)Message.ReadByte() : (byte)0; break; } byte EncodeB = (byte)((ByteToEncode & (32 + 64 + 128)) >> 5); // Find the biggest 3 bits in the byte to encode byte EncodeG = (byte)((ByteToEncode & (4 + 8 + 16)) >> 2); // Find the middle 3 bits in the byte to encode byte EncodeR = (byte)(ByteToEncode & (1 + 2)); // Find the smallest 2 bits in the byte to encode RGB[0] = (byte)((RGB[0] & 0xF8) + EncodeB); // In Blue, 3 bits are used to encode (0xF8 = 1111 1000) RGB[1] = (byte)((RGB[1] & 0xF8) + EncodeG); // In Green, 3 bits are used to encode (0xF8 = 1111 1000) RGB[2] = (byte)((RGB[2] & 0xFC) + EncodeR); // In Red, 2 bits are used to encode (0xFC = 1111 1100) bw.Write(RGB, 0, 3); Idx++; } // Rows in bmp must be a multiple of 4, so let's fill with black pixels if necessary if (ComplementToAdd != 0) bw.Write(BlackPixel, 0, ComplementToAdd); } } } } }
Decoding
This one is much easier than the encoding source code. So straightforward that is does not need any comment...private System.IO.MemoryStream UnprocessBmp(string InputFilename) { System.IO.MemoryStream Result = new System.IO.MemoryStream(); using (System.IO.FileStream fs = new System.IO.FileStream(InputFilename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read, 1024 * 1024)) { using (System.Drawing.Bitmap b = new System.Drawing.Bitmap(fs)) { int x, y, MessageSize = 0, Idx = 0; for (y = b.Height - 1; y >= 0; y--) { for (x = 0; x < b.Width; x++) { Color c = b.GetPixel(x, y); byte EncodedB = (byte)(c.B & 0x07); // Find the last 3 bits of the blue component byte EncodedG = (byte)(c.G & 0x07); // Find the last 3 bits of the green component byte EncodedR = (byte)(c.R & 0x03); // Find the last 2 bits of the red component byte EncodedByte = (byte)((EncodedB << 5) + (EncodedG << 2) + EncodedR); // combine the 3 + 3 + 2 bits to make a byte switch (Idx) { // The 4 first bytes wil be used to encode the size of the message case 0: MessageSize = EncodedByte; break; case 1: MessageSize = MessageSize + (EncodedByte << 8); break; case 2: MessageSize = MessageSize + (EncodedByte << 16); break; case 3: MessageSize = MessageSize + (EncodedByte << 32); break; default: if (Idx - 4 < MessageSize) Result.WriteByte(EncodedByte); break; } Idx++; } } } } Result.Seek(0, System.IO.SeekOrigin.Begin); return Result; }
You can download the solution here. Please, pay special attention to my high-level skills in GUI design... \m/
No comments:
Post a Comment