using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Text; //using Microsoft.Xna.Framework; //using Microsoft.Xna.Framework.Graphics; //using XnaColor = Microsoft.Xna.Framework.Graphics.Color; //using XnaRectangle = Microsoft.Xna.Framework.Rectangle; using SysColor = System.Drawing.Color; using SysRectangle = System.Drawing.Rectangle; namespace ComicCompressor { /// /// This class provides a managed C# abstraction for Portable Bit Map (.pbm), Portable Grey Map (.pgm) and /// Portable Pixel Map (.ppm)images. The class is able to handle P1 (ASCII .pbm), P2 (ASCII .pgm) P3 (ASCII .ppm), /// P5 (binary .pgm), and P6 (binary .ppm) files and streams. P4 (binary .pbm) is not supported. The class can /// process normal images, which have widths that are multiples of 4 pixels wide, and can also handle "off-size" /// images using a less efficient algorithm. It presents .pbm and .pgm images using the 8bppIndexed pixel format, /// and presents .ppm images using the 24bppRGB pixel format. It can convert these images into a visually pleasing /// greyscale bitmap image (in a less efficient 24bppRGB pixel format), which is accessible using the GreyMap /// property. /// /// To use this class: /// 1. Copy the PixelMap.cs file into your own project. /// 2. Change the namespace of the class (above this summary) from "PixelMap" to the namespace of your project. /// 3. Compile. /// /// This class is used by the PixelMapViewer application. /// public class PixelMap { private PixelMapHeader header; /// /// The header portion of the PixelMap. /// public PixelMapHeader Header { get { return header; } set { header = value; } } private byte[] imageData; /// /// The data portion of the PixelMap. /// public byte[] ImageData { get { return imageData; } set { imageData = value; } } private PixelFormat pixelFormat; /// /// The pixel format used by the BitMap generated by this PixelMap. /// public PixelFormat PixelFormat { get { return pixelFormat; } } private int bytesPerPixel; /// /// The number of bytes per pixel. /// public int BytesPerPixel { get { return bytesPerPixel; } } private int stride; /// /// The stride of the scan across the image. Typically this is width * bytesPerPixel, and is a multiple of 4. /// public int Stride { get { return stride; } set { stride = value; } } private Bitmap bitmap; /// /// The Bitmap created from the PixelMap. /// public Bitmap BitMap { get { return bitmap; } } /// /// A 24bpp Grey map (really only 8bpp of information, since R,G,B bytes are identical) created from the PixelMap. /// public Bitmap GreyMap { get { return CreateGreyMap(); } } /// /// File-based constructor. /// /// The name of the .pbm, .pbm, or .ppm file. public PixelMap(string filename) { if (File.Exists(filename)) { FileStream stream = new FileStream(filename, FileMode.Open); this.FromStream(stream); stream.Close(); } else { throw new FileNotFoundException("The file " + filename + " does not exist", filename); } } /// /// Stream-based constructor. Typically, the stream will be a FileStream, but it may also be a MemoryStream /// or other object derived from the Stream class. /// /// A stream containing the header and data portions of the .pbm, .pgm, or .ppm image. public PixelMap(Stream stream) { this.FromStream(stream); } private void FromStream(Stream stream) { int index; this.header = new PixelMapHeader(); int headerItemCount = 0; stream.Position = 0; BinaryReader binReader = new BinaryReader(stream, Encoding.ASCII); try { //1. Read the Header. while (headerItemCount < 4) { char nextChar = (char)binReader.PeekChar(); if (nextChar == '#') // comment { while (binReader.ReadChar() != '\n') ; // ignore the rest of the line. } else if (Char.IsWhiteSpace(nextChar)) // whitespace { binReader.ReadChar(); // ignore whitespace } else { switch (headerItemCount) { case 0: // next item is Magic Number // Read the first 2 characters and determine the type of pixelmap. char[] chars = binReader.ReadChars(2); this.header.MagicNumber = chars[0].ToString() + chars[1].ToString(); headerItemCount++; break; case 1: // next item is the width. this.header.Width = ReadValue(binReader); headerItemCount++; break; case 2: // next item is the height. this.header.Height = ReadValue(binReader); headerItemCount++; break; case 3: // next item is the depth. if (this.header.MagicNumber == "P1" | this.header.MagicNumber == "P4") { // no depth value for PBM type. headerItemCount++; } else { this.header.Depth = ReadValue(binReader); headerItemCount++; } break; default: throw new Exception("Error parsing the file header."); } } } // 2. Read the image data. // 2.1 Size the imageData array to hold the image bytes. switch (this.header.MagicNumber) { case "P1": // 1 byte per pixel this.pixelFormat = PixelFormat.Format8bppIndexed; this.bytesPerPixel = 1; break; case "P2": // 1 byte per pixel this.pixelFormat = PixelFormat.Format8bppIndexed; this.bytesPerPixel = 1; break; case "P3": // 3 bytes per pixel this.pixelFormat = PixelFormat.Format24bppRgb; this.bytesPerPixel = 3; break; case "P4": throw new Exception("Binary .pbm (Magic Number P4) is not supported at this time."); case "P5": // 1 byte per pixel this.pixelFormat = PixelFormat.Format8bppIndexed; this.bytesPerPixel = 1; break; case "P6": // 3 bytes per pixel this.pixelFormat = PixelFormat.Format24bppRgb; this.bytesPerPixel = 3; break; default: throw new Exception("Unknown Magic Number: " + this.header.MagicNumber); } this.imageData = new byte[this.header.Width * this.header.Height * this.bytesPerPixel]; this.stride = this.header.Width * this.bytesPerPixel; //while ((this.stride % 4) != 0) //{ // this.stride++; //} if (this.header.MagicNumber == "P1" | this.header.MagicNumber == "P2" | this.header.MagicNumber == "P3") // ASCII Encoding { int charsLeft = (int)(binReader.BaseStream.Length - binReader.BaseStream.Position); char[] charData = binReader.ReadChars(charsLeft); // read all the data into an array in one go, for efficiency. string valueString = string.Empty; index = 0; for (int i = 0; i < charData.Length; i++) { if (Char.IsWhiteSpace(charData[i])) // value is ignored if empty, or converted to byte and added to array otherwise. { if (valueString != string.Empty) { this.imageData[index] = (byte)int.Parse(valueString); valueString = string.Empty; index++; } } else // add the character to the end of the valueString. { valueString += charData[i]; } } } else // binary encoding. { int bytesLeft = (int)(binReader.BaseStream.Length - binReader.BaseStream.Position); this.imageData = binReader.ReadBytes(bytesLeft); } // 3. We need to change the byte order of this.imageData from BGR to RGB. ReorderBGRtoRGB(); // 4. Create the BitMap if (this.stride % 4 == 0) { this.bitmap = CreateBitMap(); } else { this.bitmap = CreateBitmapOffSize(); } // 5. Rotate the BitMap by 180 degrees so it is in the same orientation as the original image. this.bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); } // If the end of the stream is reached before reading all of the expected values raise an exception. catch (EndOfStreamException e) { Console.WriteLine(e.Message); throw new Exception("Error reading the stream! ", e); } catch (Exception ex) { Console.WriteLine(ex.Message); throw new Exception("Error reading the stream! ", ex); } finally { binReader.Close(); } } /// /// As it stands, the native byte order in .pbm, .pgm, and .ppm images is BGR. We need to reverse the order /// into RGB as this is what is needed for 24bppRGB bitmap pixel format on Windows. /// private void ReorderBGRtoRGB() { byte[] tempData = new byte[this.imageData.Length]; for (int i = 0; i < this.imageData.Length; i++) { tempData[i] = this.imageData[this.imageData.Length - 1 - i]; } this.imageData = tempData; } private int ReadValue(BinaryReader binReader) { string value = string.Empty; while (!Char.IsWhiteSpace((char)binReader.PeekChar())) { value += binReader.ReadChar().ToString(); } binReader.ReadByte(); // get rid of the whitespace. return int.Parse(value); } private Bitmap CreateBitMap() { IntPtr pImageData = Marshal.AllocHGlobal(this.imageData.Length); Marshal.Copy(this.imageData, 0, pImageData, this.imageData.Length); Bitmap bitmap = new Bitmap(this.header.Width, this.header.Height, this.stride, this.pixelFormat, pImageData); return bitmap; } private Bitmap CreateGreyMap() { byte[] greyData = new byte[this.header.Width * this.header.Height * 3]; int stride = this.header.Width * 3; //while (stride % 4 != 0) //{ // stride++; //} if (stride % 4 != 0) { return CreateGreyMapOffSize(greyData); } byte grey; switch (this.pixelFormat) { case PixelFormat.Format24bppRgb: int red, green, blue; for (int i = 0; i < this.imageData.Length; i = i + 3) { red = (int)this.imageData[i]; green = (int)this.imageData[i + 1]; blue = (int)this.imageData[i + 2]; grey = (byte)((red + green + blue) / 3); greyData[i] = grey; greyData[i + 1] = grey; greyData[i + 2] = grey; } break; case PixelFormat.Format8bppIndexed: int index = 0; for (int i = 0; i < this.imageData.Length; i++) { index = 3 * i; grey = this.imageData[i]; if (this.header.MagicNumber == "P1") { if ((int)grey == 1) { grey = (byte)255; } } greyData[index] = grey; greyData[index + 1] = grey; greyData[index + 2] = grey; } break; default: throw new Exception("Unsupported Pixel Format: " + this.pixelFormat.ToString()); } IntPtr pGreyData = Marshal.AllocHGlobal(greyData.Length); Marshal.Copy(greyData, 0, pGreyData, greyData.Length); Bitmap bitmap = new Bitmap(this.header.Width, this.header.Height, stride, PixelFormat.Format24bppRgb, pGreyData); bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); return bitmap; } /// /// This method is able to handle the process of creating Bitmaps that are not sized in widths that are multiples /// of 4 pixels (which is needed for the rapid drawing using the Marshal class.) Unfortunately, this is a very slow /// method, since it uses SetPixel(). /// /// private Bitmap CreateBitmapOffSize() { Bitmap bitmap = new Bitmap(this.header.Width, this.header.Height, PixelFormat.Format24bppRgb); SysColor sysColor = new SysColor(); int red, green, blue, grey; int index; for (int x = 0; x < this.header.Width; x++) { for (int y = 0; y < this.header.Height; y++) { index = x + y * this.header.Width; switch (this.header.MagicNumber) { case "P1": grey = (int)this.imageData[index]; if (grey == 1) { grey = 255; } sysColor = SysColor.FromArgb(grey, grey, grey); break; case "P2": grey = (int)this.imageData[index]; sysColor = SysColor.FromArgb(grey, grey, grey); break; case "P3": index = 3 * index; blue = (int)this.imageData[index]; green = (int)this.imageData[index + 1]; red = (int)this.imageData[index + 2]; sysColor = SysColor.FromArgb(red, green, blue); break; case "P4": break; case "P5": grey = (int)this.imageData[index]; sysColor = SysColor.FromArgb(grey, grey, grey); break; case "P6": index = 3 * index; blue = (int)this.imageData[index]; green = (int)this.imageData[index + 1]; red = (int)this.imageData[index + 2]; sysColor = SysColor.FromArgb(red, green, blue); break; default: break; } bitmap.SetPixel(x, y, sysColor); } } return bitmap; } private Bitmap CreateGreyMapOffSize(byte[] greyData) { Bitmap bitmap = new Bitmap(this.header.Width, this.header.Height, PixelFormat.Format24bppRgb); SysColor sysColor = new SysColor(); int index = 0; int x = 0; int y = -1; switch (this.pixelFormat) { case PixelFormat.Format24bppRgb: int red, green, blue, grey; for (int i = 0; i < this.imageData.Length; i = i + 3) { index = i / 3; // the current pixel if (index%this.header.Width == 0) y++; // the current row = y; x = index - y * this.header.Width; // the current column = x; red = (int)this.imageData[i]; green = (int)this.imageData[i + 1]; blue = (int)this.imageData[i + 2]; grey = (byte)((red + green + blue) / 3); sysColor = SysColor.FromArgb(grey, grey, grey); bitmap.SetPixel(x, y, sysColor); } break; case PixelFormat.Format8bppIndexed: for (int i = 0; i < this.imageData.Length; i++) { index = i; // the current pixel if (index % this.header.Width == 0) y++; // the current row = y; x = index - y * this.header.Width; // the current column = x; grey = this.imageData[i]; if (this.header.MagicNumber == "P1") { if ((int)grey == 1) { grey = (byte)255; } } sysColor = SysColor.FromArgb(grey, grey, grey); bitmap.SetPixel(x, y, sysColor); } break; default: throw new Exception("Unsupported Pixel Format: " + this.pixelFormat.ToString()); } bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); return bitmap; } /// /// This struct contains the objects that are found in the header of .pbm, .pgm, and .ppm files. /// [Serializable] public struct PixelMapHeader { private string magicNumber; /// /// The "Magic Number" that identifies the type of Pixelmap. P1 = PBM (ASCII); P2 = PGM (ASCII); P3 = PPM (ASCII); P4 is not used; /// P5 = PGM (Binary); P6 = PPM (Binary). /// public string MagicNumber { get { return magicNumber; } set { magicNumber = value; } } private int width; /// /// The width of the image. /// public int Width { get { return width; } set { width = value; } } private int height; /// /// The height of the image. /// public int Height { get { return height; } set { height = value; } } private int depth; /// /// The depth (maximum color value in each channel) of the image. This allows the format to represent /// more than a single byte (0-255) for each color channel. /// public int Depth { get { return depth; } set { depth = value; } } } } }