Click here to Skip to main content
15,885,546 members
Articles / Multimedia / GDI+

Steganography 12 - Regions with different data density

Rate me:
Please Sign up or sign in to vote.
4.81/5 (26 votes)
12 Jul 2006CPOL6 min read 113.8K   2.5K   61  
Define regions inside an image to keep smooth colours free from hidden data.
/* This class has been written by
 * Corinna John (Hannover, Germany)
 * cj@binary-universe.net
 * 
 * You may do with this code whatever you like,
 * except selling it or claiming any rights/ownership.
 * 
 * Please send me a little feedback about what you're
 * using this code for and what changes you'd like to
 * see in later versions. (And please excuse my bad english.)
 * 
 * WARNING: This is experimental code.
 * Some bugs and flaws have been left in there,
 * to keep the code readable to people who want
 * to understand the algorithm.
 * Please do not expect "Release Quality".
 * */

#region Using directives

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Collections;
using System.Text;
using System.IO;

#endregion

namespace SteganoRegion
{
    public class ImageUtility
    {
        /// <summary>stores the colors of a pixel</summary>
        public struct PixelData
        {
            public byte Blue;
            public byte Green;
            public byte Red;
        }

        //Color component to hide the next byte in
        //Can be 0-R, 1-G, 2-B and rotates with every hidden bit-group
        private int currentColorComponent = 0;

        private ImageInfo carrierFile;

        public ImageUtility(ImageInfo imageInfo)
        {
            this.carrierFile = imageInfo;
        }

        /// <summary>Converts an image to a 24-bit bitmap with default resolution</summary>
        /// <param name="original">Any image</param>
        /// <returns>Formatted image</returns>
        private Bitmap PaletteToRGB(Bitmap original)
        {
            Bitmap image = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb);
            Graphics graphics = Graphics.FromImage(image);
            graphics.DrawImage(original, 0, 0, original.Width, original.Height);
            graphics.Dispose();
            original.Dispose();
            return image;
        }

        /// <summary>Copy one or more bits from the message into a color value</summary>
        /// <param name="bitsPerUnit">Count of bits to copy</param>
        /// <param name="messageByte">Source byte</param>
        /// <param name="messageBitIndex">Index of the first copied bit</param>
        /// <param name="colorComponent">Destination byte</param>
        private void CopyBitsToColor(int bitsPerUnit, byte messageByte, ref int messageBitIndex, ref byte colorComponent)
        {
            for (int carrierBitIndex = 0; carrierBitIndex < bitsPerUnit; carrierBitIndex++)
            {
                SetBit(messageBitIndex, messageByte, carrierBitIndex, ref colorComponent);
                messageBitIndex++;
            }
        }

        /// <summary>Hide a message in the carrier image</summary>
        /// <param name="messageStream">The secret message</param>
        /// <param name="keyStream">A stream of seeds for a pseudo-random number generator</param>
        public unsafe void Hide(Stream messageStream, Stream keyStream) {
            Bitmap image = (Bitmap)carrierFile.Image;

            //make sure that the image has RGB format
            image = PaletteToRGB(image);

            int pixelOffset = 0;
            int maxOffset = 0;
            int messageValue;
            byte key, messageByte, colorComponent;
            Random random;

            BitmapData bitmapData = image.LockBits(
                new Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

            //go to the first pixel
            PixelData* pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
            PixelData* pFirstPixel;

            //get the first pixel that belongs to a region
            int firstPixelInRegions = image.Width * image.Height;
            foreach (RegionInfo info in carrierFile.RegionInfo) {
                info.PixelIndices.Sort();
                if ((int)info.PixelIndices[0] < firstPixelInRegions) {
                    firstPixelInRegions = (int)info.PixelIndices[0];
                }
            }

            //hide firstPixelInRegions
            HideInt32(firstPixelInRegions, ref pPixel);

            //get map stream
            MemoryStream regionData = new MemoryStream();
            BinaryWriter regionDataWriter = new BinaryWriter(regionData);
            foreach (RegionInfo regionInfo in carrierFile.RegionInfo) {
				byte[] regionBytes = PointsToBytes(regionInfo.Points);
				regionDataWriter.Write((Int32)regionBytes.Length);
                regionDataWriter.Write((Int32)regionInfo.Capacity);
                regionDataWriter.Write(regionInfo.CountUsedBitsPerPixel);
                regionDataWriter.Write(regionBytes);
            }
            //go to the beginning of the stream
            regionDataWriter.Flush();
            regionData.Seek(0, SeekOrigin.Begin);

            //hide length of map stream
            HideInt32((Int32)regionData.Length, ref pPixel);

            //hide regions

            pFirstPixel = pPixel; //don't overwrite already written header

            int regionByte;
            while ((regionByte = regionData.ReadByte()) >= 0) {
                key = GetKey(keyStream);
                random = new Random(key);

                for (int regionBitIndex = 0; regionBitIndex < 8; ) {

                    pixelOffset += random.Next(1, (int)((firstPixelInRegions - 1 - pixelOffset) / ((regionData.Length - regionData.Position + 1) * 8)));
                    pPixel = pFirstPixel + pixelOffset;

                    //place [regionBit] in one bit of the colour component

                    //rotate color components
                    currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                    //get value of Red, Green or Blue
                    colorComponent = GetColorComponent(pPixel, currentColorComponent);

                    //put the bits into the color component and write it back into the bitmap
                    CopyBitsToColor(1, (byte)regionByte, ref regionBitIndex, ref colorComponent);
                    SetColorComponent(pPixel, currentColorComponent, colorComponent);
                }
            }

            // ----------------------------------------- Hide the Message

            //begin with the first pixel
            pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
            pFirstPixel = pPixel; //first pixel of the image

            foreach (RegionInfo regionInfo in carrierFile.RegionInfo) {

                //go to first pixel of this region
                pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
                pPixel += (int)regionInfo.PixelIndices[0];
                pixelOffset = 0;

                for (int n = 0; n < regionInfo.Capacity; n++) {

                    messageValue = messageStream.ReadByte();
                    if (messageValue < 0) { break; } //end of message
                    messageByte = (byte)messageValue;

                    key = GetKey(keyStream);
                    random = new Random(key);

                    for (int messageBitIndex = 0; messageBitIndex < 8; ) {

                        maxOffset = (int)Math.Floor(
                            ((decimal)(regionInfo.CountPixels - pixelOffset - 1) * regionInfo.CountUsedBitsPerPixel)
                            /
                            (decimal)((regionInfo.Capacity - n) * 8)
                            );

                        pixelOffset += random.Next(1, maxOffset);

                        pPixel = pFirstPixel + (int)regionInfo.PixelIndices[pixelOffset];

                        //place [messageBit] in one bit of the colour component

                        //rotate color components
                        currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                        //get value of Red, Green or Blue
                        colorComponent = GetColorComponent(pPixel, currentColorComponent);

                        //put the bits into the color component and write it back into the bitmap
                        CopyBitsToColor(regionInfo.CountUsedBitsPerPixel, messageByte, ref messageBitIndex, ref colorComponent);
                        SetColorComponent(pPixel, currentColorComponent, colorComponent);
                    }
                }
            }

            image.UnlockBits(bitmapData);

            SaveBitmap(image, carrierFile.DestinationFileName);
        }

        /// <summary>Hide an Int32 value in pPixel an the following pixels</summary>
        /// <param name="secretValue">The value to hide</param>
        /// <param name="pPixel">The first pixel to use</param>
        private unsafe void HideInt32(Int32 secretValue, ref PixelData* pPixel)
        {
            byte secretByte;

            for (int byteIndex = 0; byteIndex < 4; byteIndex++)
            {
                secretByte = (byte)(secretValue >> (8 * byteIndex));
                HideByte(secretByte, ref pPixel);
            }
        }

        /// <summary>Hide a byte in pPixel an the following pixels</summary>
        /// <param name="secretByte">The value to hide</param>
        /// <param name="pPixel">The first pixel to use</param>
        private unsafe void HideByte(byte secretByte, ref PixelData* pPixel)
        {
            byte colorComponent;

            for (int bitIndex = 0; bitIndex < 8; )
            {
                pPixel += 1;

                //rotate color components
                currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                //get value of Red, Green or Blue
                colorComponent = GetColorComponent(pPixel, currentColorComponent);

                CopyBitsToColor(1, secretByte, ref bitIndex, ref colorComponent);
                SetColorComponent(pPixel, currentColorComponent, colorComponent);
            }
        }

		/// <summary>Convert points (X;Y|X;Y|X;Y) to plain bytes (XYXYXY)</summary>
		private byte[] PointsToBytes(Point[] points)
		{
			MemoryStream stream = new MemoryStream();
			BinaryWriter writer = new BinaryWriter(stream);

			for (int pointsIndex = 0; pointsIndex < points.Length; pointsIndex++)
			{
				writer.Write(points[pointsIndex].X);
				writer.Write(points[pointsIndex].Y);
			}

			writer.Flush();
			byte[] result = stream.ToArray();
			return result;
		}

		/// <summary>Convert plain bytes (XYXYXY) to points (X;Y|X;Y|X;Y)</summary>
		private Point[] BytesToPoints(byte[] bytes)
		{
			Point[] result = new Point[bytes.Length / 8];

			MemoryStream stream = new MemoryStream(bytes);
			BinaryReader reader = new BinaryReader(stream);
			stream.Position = 0;

			int resultIndex = 0;
			while (stream.Position < stream.Length)
			{
				result[resultIndex].X = reader.ReadInt32();
				result[resultIndex].Y = reader.ReadInt32();
				resultIndex++;
			}

			return result;
		}

        /// <summary>Extract an Int32 value from pPixel and the following pixels</summary>
        /// <param name="pPixel">The first pixel to use</param>
        /// <returns>The extracted value</returns>
        private unsafe Int32 ExtractInt32(ref PixelData* pPixel)
        {
            int returnValue = 0;
            byte readByte;

            for (int byteIndex = 0; byteIndex < 4; byteIndex++)
            {
                readByte = ExtractByte(ref pPixel);
                returnValue += readByte << (byteIndex * 8);
            }
            return returnValue;
        }

        /// <summary>Extract a byte value from pPixel and the following pixels</summary>
        /// <param name="pPixel">The first pixel to use</param>
        /// <returns>The extracted value</returns>
        private unsafe byte ExtractByte(ref PixelData* pPixel) {
            byte colorComponent;
            byte readByte = 0;

            for (int bitIndex = 0; bitIndex < 8; bitIndex++)
            {
                pPixel += 1;
                //rotate color components
                currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                //get value of Red, Green or Blue
                colorComponent = GetColorComponent(pPixel, currentColorComponent);
                AddBit(bitIndex, ref readByte, 0, colorComponent);
            }

            return readByte;
        }

        /// <summary>Extract the header from an image</summary>
        /// <remarks>The header contains information about the regions which carry the message</remarks>
        /// <param name="keyStream">Key stream</param>
        /// <returns>The extracted regions with all meta data that is needed to extract the message</returns>
        public unsafe RegionInfo[] ExtractRegionData(Stream keyStream) {
            byte key, colorComponent;
            PixelData* pPixel;
            PixelData* pFirstPixel;
            Random random;
            int pixelOffset = 0;

            Bitmap image = (Bitmap)carrierFile.Image;

            BitmapData bitmapData = image.LockBits(
                new Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

            //go to the first pixel
            pPixel = (PixelData*)bitmapData.Scan0.ToPointer();

            //get firstPixelInRegions
            int firstPixelInRegions = ExtractInt32(ref pPixel);

            //get length of region information
            int regionDataLength = ExtractInt32(ref pPixel);

            //get region information

            pFirstPixel = pPixel;

            MemoryStream regionData = new MemoryStream();

            byte regionByte;
            while (regionDataLength > regionData.Length) {
                regionByte = 0;
                key = GetKey(keyStream);
                random = new Random(key);

                for (int regionBitIndex = 0; regionBitIndex < 8; regionBitIndex++) {
                    //move to the next pixel
                    pixelOffset += random.Next(1, (int)((firstPixelInRegions - 1 - pixelOffset) / ((regionDataLength - regionData.Length) * 8)));
                    pPixel = pFirstPixel + pixelOffset;

                    //rotate color components
                    currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                    //get value of Red, Green or Blue
                    colorComponent = GetColorComponent(pPixel, currentColorComponent);

                    //extract one bit and add it to [regionByte]
                    AddBit(regionBitIndex, ref regionByte, 0, colorComponent);
                }

                //write the extracted byte
                regionData.WriteByte(regionByte);
            }

            image.UnlockBits(bitmapData);

            //read regions from [regionData]

            ArrayList regions = new ArrayList();
            BinaryReader regionReader = new BinaryReader(regionData);

            regionReader.BaseStream.Seek(0, SeekOrigin.Begin);
            do {
				//If the program crashes here,
				//the image is damaged,
				//it contains no hidden data,
				//or you tried to use a wrong key.
				int regionLength = regionReader.ReadInt32();
				int regionCapacity = regionReader.ReadInt32();
				byte regionBitsPerPixel = regionReader.ReadByte();
				byte[] regionContent = regionReader.ReadBytes(regionLength);

				Point[] regionPoints = BytesToPoints(regionContent);
				GraphicsPath regionPath = new GraphicsPath();
				regionPath.AddPolygon(regionPoints);

				Region region = new Region(regionPath);
				regions.Add(new RegionInfo(region, regionCapacity, regionBitsPerPixel, image.Size));
            } while (regionData.Position < regionData.Length);

            return (RegionInfo[])regions.ToArray(typeof(RegionInfo));
        }

        /// <summary>Extract a message</summary>
        /// <param name="messageStream">Empty stream to receive the extracted message</param>
        /// <param name="keyStream">Key stream</param>
        public unsafe void Extract(Stream messageStream, Stream keyStream) {
            Bitmap image = (Bitmap)carrierFile.Image;

            BitmapData bitmapData = image.LockBits(
                new Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

            byte key;
            byte messageByte, colorComponent;
            PixelData* pPixel;
            PixelData* pFirstPixel = (PixelData*)bitmapData.Scan0.ToPointer();

            Random random;
            int maxOffset, pixelOffset = 0;

            foreach (RegionInfo regionInfo in carrierFile.RegionInfo) {

                //go to first pixel of this region
                pFirstPixel = (PixelData*)bitmapData.Scan0.ToPointer();
                pPixel = pFirstPixel + (int)regionInfo.PixelIndices[0];
                pixelOffset = 0;

                for (int n = 0; n < regionInfo.Capacity; n++) {

                    messageByte = 0;
                    key = GetKey(keyStream);
                    random = new Random(key);

                    for (int messageBitIndex = 0; messageBitIndex < 8; ) {
                        //move to the next pixel

                        maxOffset = (int)Math.Floor(
                            ((decimal)(regionInfo.CountPixels - pixelOffset - 1) * regionInfo.CountUsedBitsPerPixel)
                            /
                            (decimal)((regionInfo.Capacity - n) * 8)
                            );

                        pixelOffset += random.Next(1, maxOffset);

                        pPixel = pFirstPixel + (int)regionInfo.PixelIndices[pixelOffset];

                        //rotate color components
                        currentColorComponent = (currentColorComponent == 2) ? 0 : (currentColorComponent + 1);
                        //get value of Red, Green or Blue
                        colorComponent = GetColorComponent(pPixel, currentColorComponent);

                        for (int carrierBitIndex = 0; carrierBitIndex < regionInfo.CountUsedBitsPerPixel; carrierBitIndex++) {
                            AddBit(messageBitIndex, ref messageByte, carrierBitIndex, colorComponent);
                            messageBitIndex++;
                        }
                    }

                    //add the re-constructed byte to the message
                    messageStream.WriteByte(messageByte);
                }
            }

            image.UnlockBits(bitmapData);
        }

        /// <summary>Return one component of a color</summary>
        /// <param name="pPixel">Pointer to the pixel</param>
        /// <param name="colorComponent">The component to return (0-R, 1-G, 2-B)</param>
        /// <returns>The requested component</returns>
        private unsafe byte GetColorComponent(PixelData* pPixel, int colorComponent)
        {
            byte returnValue = 0;
            switch (colorComponent)
            {
                case 0:
                    returnValue = pPixel->Red;
                    break;
                case 1:
                    returnValue = pPixel->Green;
                    break;
                case 2:
                    returnValue = pPixel->Blue;
                    break;
            }
            return returnValue;
        }

        /// <summary>Changes one component of a color</summary>
        /// <param name="pPixel">Pointer to the pixel</param>
        /// <param name="colorComponent">The component to change (0-R, 1-G, 2-B)</param>
        /// <param name="newValue">New value of the component</param>
        private unsafe void SetColorComponent(PixelData* pPixel, int colorComponent, byte newValue)
        {
            switch (colorComponent)
            {
                case 0:
                    pPixel->Red = newValue;
                    break;
                case 1:
                    pPixel->Green = newValue;
                    break;
                case 2:
                    pPixel->Blue = newValue;
                    break;
            }
        }

        /// <summary>Copy a bit from [messageByte] into to lowest bit of [carrierByte]</summary>
        /// <param name="bitIndex">Position of the bit to copy</param>
        /// <param name="messageByte">a byte from the message stream</param>
        /// <param name="carrierBitIndex">Position of the bit in [carrierByte]</param>
        /// <param name="carrierByte">a byte from the carrier file</param>
        private void SetBit(int messageBitIndex, byte messageByte, int carrierBitIndex, ref byte carrierByte)
        {
            //get one bit of the current message byte...
            bool messageBit = ((messageByte & (1 << messageBitIndex)) > 0);
            //get one bit of the carrier byte
            bool carrierBit = ((carrierByte & (1 << carrierBitIndex)) > 0);

            //place [messageBit] in the corresponding bit of [carrierByte]
            if (messageBit && !carrierBit)
            {
                carrierByte += (byte)(1 << carrierBitIndex);
            }
            else if (!messageBit && carrierBit)
            {
                carrierByte -= (byte)(1 << carrierBitIndex);
            }
        }

        /// <summary>Copy the lowest bit from [carrierByte] into a specific bit of [messageByte]</summary>
        /// <param name="messageBitIndex">Position of the bit in [messageByte]</param>
        /// <param name="messageByte">a byte to write into the message stream</param>
        /// <param name="carrierBitIndex">Position of the bit in [carrierByte]</param>
        /// <param name="carrierByte">a byte from the carrier file</param>
        private void AddBit(int messageBitIndex, ref byte messageByte, int carrierBitIndex, byte carrierByte)
        {
            int carrierBit = ((carrierByte & (1 << carrierBitIndex)) > 0) ? 1 : 0;
            messageByte += (byte)(carrierBit << messageBitIndex);
        }

        /// <summary>
        /// Read the next byte of the key stream.
        /// Reset the stream if it is too short.
        /// </summary>
        /// <returns>The next key byte</returns>
        private byte GetKey(Stream keyStream)
        {
            int keyValue;
            if ((keyValue = keyStream.ReadByte()) < 0)
            {
                keyStream.Seek(0, SeekOrigin.Begin);
                keyValue = keyStream.ReadByte();
            }
            return (byte)keyValue;
        }

        /// <summary>Save an image to a file</summary>
        /// <param name="bitmap">The iamge to save</param>
        /// <param name="fileName">Path and name for the file</param>
        private static void SaveBitmap(Bitmap bitmap, String fileName)
        {
            String fileNameLower = fileName.ToLower();

            System.Drawing.Imaging.ImageFormat format = System.Drawing.Imaging.ImageFormat.Bmp;
            if ((fileNameLower.EndsWith("tif")) || (fileNameLower.EndsWith("tiff")))
            {
                format = System.Drawing.Imaging.ImageFormat.Tiff;
            }
            else if (fileNameLower.EndsWith("png"))
            {
                format = System.Drawing.Imaging.ImageFormat.Png;
            }

            bitmap.Save(fileName, format);
            bitmap.Dispose();
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
Corinna lives in Hanover/Germany and works as a C# developer.

Comments and Discussions