5
\$\begingroup\$

Normally, you would change the LSB of the RGB channels in a pixel, they would represent the bits you are storing in the image. My method differs in that I represent my bits as either odd or even numbers stored inside the image channels ARGB. So an odd number represents a 1 and an even number represents a 0.

I also can't find anything online that mentions doing this the way I decided to. Am I the "first" to do it this way? What other ways are there for doing this?

using System;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

namespace Steganography
{
    public partial class Steganography : Form
    {
        OpenFileDialog ofd = new OpenFileDialog();
        SaveFileDialog sfd = new SaveFileDialog();

        const string Filter = "Image Files (*.png) | *.png";
        const string StartDir = @"./Images";

        string _binImg = "";

        public Steganography()
        {
            InitializeComponent();

            ofd.Filter = Filter;
            ofd.InitialDirectory = StartDir;

            sfd.Filter = Filter;
            sfd.InitialDirectory = StartDir;
        }

        #region #### Open Images ####
        private void btnOpenBaseImage_Click(object sender, EventArgs e)
        {
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                pbBaseImage.Image = new Bitmap(ofd.FileName);
            }
        }

        private void btnOpenHideImage_Click(object sender, EventArgs e)
        {
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                pbHideImage.Image = new Bitmap(ofd.FileName);

                _binImg = ByteArrayToBinary(File.ReadAllBytes(ofd.FileName));
            }
        }
        #endregion

        #region #### Hide Image ####
        /// <summary>
        /// Hide an image inside of another image
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnHideImage_Click(object sender, EventArgs e)
        {
            // We only want to run the code below if we have images to work with.
            if (pbBaseImage == null)
            {
                return;
            }

            if (pbHideImage.Image == null)
            {
                return;
            }

            // Counter used to keep track of what bit we are at
            int dataWriteCtr = 0;
            // The data to be written
            char[] data = _binImg.ToCharArray();

            // Length of the data
            int msgLenWriteCtr = 0;

            // Convert length of the data into binary
            char[] dataLen = Convert.ToString(data.Length, 2).PadLeft(24, '0').ToCharArray();

            // We only write the length of the data in the last 6 pixels, max message is 16777215 bits.
            if (data.Length > 16777215) // Last 6 pixels
                return;

            // Image to have data written too.
            var img = (Bitmap)pbBaseImage.Image;

            // If the data is too big to be stored inside the image, return.
            // We -6 because the last 6 pixels store the data length
            if ((data.Length / 4) > (img.Width * img.Height) - 6)
                return;

            // Used in changing pixels in the image
            var newPixel = new Pixel();

            // Loop over every pixel but the last 2
            for (int x = 0; x < img.Width; x++)
            {
                for (int y = 0; y < img.Height; y++)
                {
                    var currPixel = img.GetPixel(x, y);
                    newPixel.A = currPixel.A;
                    newPixel.R = currPixel.R;
                    newPixel.G = currPixel.G;
                    newPixel.B = currPixel.B;

                    if (ProcessingImageLastSixPixels(img, x, y))
                    {
                        newPixel.A = SetPixelChannel(currPixel.A, dataLen, ref msgLenWriteCtr);
                        newPixel.R = SetPixelChannel(currPixel.R, dataLen, ref msgLenWriteCtr);
                        newPixel.G = SetPixelChannel(currPixel.G, dataLen, ref msgLenWriteCtr);
                        newPixel.B = SetPixelChannel(currPixel.B, dataLen, ref msgLenWriteCtr);
                    }
                    else if (dataWriteCtr < data.Length)
                    {
                        newPixel.A = SetPixelChannel(currPixel.A, data, ref dataWriteCtr);
                        newPixel.R = SetPixelChannel(currPixel.R, data, ref dataWriteCtr);
                        newPixel.G = SetPixelChannel(currPixel.G, data, ref dataWriteCtr);
                        newPixel.B = SetPixelChannel(currPixel.B, data, ref dataWriteCtr);
                    }
                    /*else // Uncomment if you want to show what pixels are being modified
                    {
                        newPixel.A = 255;
                        newPixel.R = 255;
                        newPixel.G = 255;
                        newPixel.B = 255;
                    }*/

                    img.SetPixel(x, y, Color.FromArgb(newPixel.A, newPixel.R, newPixel.G, newPixel.B));
                }
            }

            // Save the file
            if (sfd.ShowDialog() == DialogResult.OK)
            {
                img.Save(sfd.FileName);
            }
        }

        /// <summary>
        /// Are the X and Y cood's the last 6 pixels in any given image?
        /// </summary>
        /// <param name="img"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private static bool ProcessingImageLastSixPixels(Bitmap img, int x, int y)
        {
            // Images are 0-x indexed so to get to the last 6 pixels we take 
            // 6 + 1 from the width and 1 + 1 from the height to get to those pixels.
            return x > (img.Width - 7) && y > (img.Height - 2);
        }

        /// <summary>
        /// Store one bit in a channel of a pixel
        /// There is 4 channels, Alpha, Red, Green, Blue.
        /// </summary>
        /// <param name="currPixelChannel"></param>
        /// <param name="data"></param>
        /// <param name="msgWriteCtr"></param>
        /// <returns></returns>
        private static int SetPixelChannel(byte currPixelChannel, char[] data, ref int msgWriteCtr)
        {
            int newPixelChannel;

            // If the current pixel's channel value is odd
            // then we want to check the msg for the current bit
            if (currPixelChannel % 2 == 1)
            {
                // If the bit we want to write is 1
                if (data[msgWriteCtr++] == '1')
                {
                    // save the Alpha value for later
                    newPixelChannel = currPixelChannel;
                }
                else // its 0
                {
                    // change the Alpha value by 1 and save for later
                    newPixelChannel = currPixelChannel - 1;
                }
            }
            else // its even
            {
                // if the bit we want to write is 1
                if (data[msgWriteCtr++] == '1')
                {
                    // change the Alpha value by 1 and save for later
                    newPixelChannel = currPixelChannel + 1;
                }
                else // its 0
                {
                    // save the Alpha value for later
                    newPixelChannel = currPixelChannel;
                }
            }

            return newPixelChannel;
        }
        #endregion

        #region #### Recover Image ####
        /// <summary>
        /// Recover data hidden inside an image
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRecoverImage_Click(object sender, EventArgs e)
        {
            // Only run the code below if we have an image to work with.
            if (pbBaseImage.Image == null)
                return;

            var img = (Bitmap)pbBaseImage.Image;
            var bitStream = "";

            // Get the length of the data
            for (int x = img.Width - 6; x < img.Width; x++)
            {
                int y = img.Height - 1;
                var currPixel = img.GetPixel(x, y);

                bitStream = GetNybbleFromPixelChannels(currPixel, bitStream);
            }

            int dataLen = Convert.ToInt32(bitStream, 2);
            int dataCtr = 0;
            bitStream = "";

            // Get the data stored inside the image
            for (int x = 0; x < img.Width; x++)
            {
                for (int y = 0; y < img.Height; y++)
                {
                    var currPixel = img.GetPixel(x, y);

                    bitStream = GetNybbleFromPixelChannels(currPixel, bitStream);

                    dataCtr++;

                    if (dataCtr > ((dataLen / 4) - 1))
                    {
                        x = img.Width;
                        y = img.Height;
                    }
                }
            }

            var hiddenImage = BinaryToByteArray(bitStream);

            if (sfd.ShowDialog() == DialogResult.OK)
            {
                File.WriteAllBytes(sfd.FileName, hiddenImage);
            }
        }

        /// <summary>
        /// Retrieve each bit previously stored in each pixel channel
        /// There should be 4 bits (A nybble) retrieved
        /// </summary>
        /// <param name="currPixel">The pixel containing the hidden bits</param>
        /// <param name="bitStream">The variable used to store the retrieved bits</param>
        /// <returns>Returns the bits retrieved</returns>
        private static string GetNybbleFromPixelChannels(Color currPixel, string bitStream)
        {
            // Alpha
            if (currPixel.A % 2 == 1)
            {
                bitStream += 1;
            }
            else
            {
                bitStream += 0;
            }

            // Red
            if (currPixel.R % 2 == 1)
            {
                bitStream += 1;
            }
            else
            {
                bitStream += 0;
            }

            // Green
            if (currPixel.G % 2 == 1)
            {
                bitStream += 1;
            }
            else
            {
                bitStream += 0;
            }

            // Blue
            if (currPixel.B % 2 == 1)
            {
                bitStream += 1;
            }
            else
            {
                bitStream += 0;
            }

            return bitStream;
        }
        #endregion

        #region #### Binary conversions ####
        /// <summary>
        /// Convert binary to a Byte array
        /// </summary>
        /// <param name="data">A string containing binary</param>
        /// <returns>Returns a byte array</returns>
        public static byte[] BinaryToByteArray(string data)
        {
            var bytes = new byte[data.Length / 8];
            int idx = 0;

            for (int i = 0; i < data.Length; i += 8)
            {
                bytes[idx++] = Convert.ToByte(data.Substring(i, 8), 2);
            }

            return bytes;
        }

        /// <summary>
        /// Convert a byte array to binary
        /// </summary>
        /// <param name="data">An array of bytes</param>
        /// <returns>Returns a string containing binary</returns>
        public static string ByteArrayToBinary(byte[] data)
        {
            var buf = new StringBuilder();

            foreach (var b in data)
            {
                var binaryStr = Convert.ToString(b, 2);
                var padStr = binaryStr.PadLeft(8, '0');
                buf.Append(padStr);
            }

            return buf.ToString();
        }
        #endregion
    }
}

Pixel class:

namespace Steganography
{
    class Pixel
    {
        public int A { get; set; }
        public int R { get; set; }
        public int G { get; set; }
        public int B { get; set; }
    }
}

The full project can be accessed here: GitHub

\$\endgroup\$
2
  • \$\begingroup\$ Why not use a System.Drawing.Color struct to hold the ARGB values? \$\endgroup\$ Commented Apr 17, 2015 at 13:35
  • 1
    \$\begingroup\$ I don't think there's anything different about your methodology, except that you're doing weird as Snowbody mentioned. Even numbers have a '0' LSb and Odd numbers have a '1' LSb, guaranteed. Even 2's complement signed numbers work that way. This could be why you didn't find anything; you actually found a lot, but it was hidden steganographically. =) \$\endgroup\$
    – AaronD
    Commented Apr 17, 2015 at 15:23

1 Answer 1

5
\$\begingroup\$
  • I notice that your functions are a mix of user-interface stuff and the actual functionality. When you're writing code, it's best to separate user interface code and business logic code. That way, if you ever change the user interface (say, switch from Windows Forms to WPF) you don't have to perform brain surgery to get your business logic code out of there.
  • Why are you converting the bytes to a binary string and then operating on individual '0' and '1' chars? It would make much more sense to work with the bits directly using bitwise operations. The whole ByteArrayToBinary() and BinaryToByteArray() are unnecessary.
  • Why did you create your own class Pixel? You don't ever use it; you're using System.Drawing.Color like you're supposed to?
  • GetNybbleFromPixelChannels() is inefficient, as it appends character-by-character onto a string. You should be using a StringBuilder for tasks like that (which you do in ByteArrayToBinary())
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the cc! Helped me a lot. With the Pixel class, I am actually using it, this is because you cant directly change the individual channel values of a pixel, you have to use something like Color.FromArgb() or set the whole pixel with new values. \$\endgroup\$
    – Flyphe
    Commented Apr 18, 2015 at 16:32

Not the answer you're looking for? Browse other questions tagged or ask your own question.