/* 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();
}
}
}