Welcome to CSharp Labs

Adding the SGI Image Codec for the Bitmap Class

Wednesday, June 26, 2013

The .NET Framework supports reading and writing several different image types through the Bitmap class. Unfortunately, if you need to support a new type, the Image class has internal constructors and the Bitmap class is sealed. To add the SGI image type (specification), I created the IBitmapEncoder and IBitmapDecoder interfaces to read and write bitmaps through extension methods and the SGIEncoder and SGIDecoder classes read and write SGI images using the BitmapPixels class.

How it Works

Producing a framework to read and write bitmaps is a straight-forward process using BinaryReader and BinaryWriter extension methods:

        /// <summary>
        /// Reads and decodes a bitmap.
        /// </summary>
        /// <param name="reader">A reader to read from.</param>
        /// <param name="decoder">A bitmap decoder to decode bytes.</param>
        /// <returns>A bitmap read from the underlying stream.</returns>
        public static Bitmap ReadBitmap(this BinaryReader reader, IBitmapDecoder decoder)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            if (decoder == null)
                throw new ArgumentNullException("decoder");

            return decoder.Decode(reader);
        }

        /// <summary>
        /// Encodes and writes a bitmap.
        /// </summary>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="bitmap">A bitmap to write.</param>
        /// <param name="encoder">The encoder to encode the bitmap with.</param>
        public static void Write(this BinaryWriter writer, Bitmap bitmap, IBitmapEncoder encoder)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");

            if (bitmap == null)
                throw new ArgumentNullException("bitmap");

            if (encoder == null)
                throw new ArgumentNullException("encoder");

            encoder.Encode(bitmap, writer);
        }

The IBitmapDecoder interface provides a single Decode method intended to read data from a stream to construct a Bitmap:

    /// <summary>
    /// Provides a way to decode a bitmap from a stream.
    /// </summary>
    public interface IBitmapDecoder
    {
        /// <summary>
        /// Decodes an SGI bitmap from the specified reader.
        /// </summary>
        /// <param name="reader">The reader to read binary bitmap data from.</param>
        /// <returns>A Bitmap read from the underlying stream.</returns>
        Bitmap Decode(BinaryReader reader);
    }

Contrarily, the IBitmapEncoder interface provides a single Encode method intended to write bitmap data to a stream:

    /// <summary>
    /// Provides a way to encode a bitmap to a stream.
    /// </summary>
    public interface IBitmapEncoder
    {
        /// <summary>
        /// Encodes the bitmap and writes to the specified writer.
        /// </summary>
        /// <param name="bitmap">The bitmap to encode and write to the underlying stream.</param>
        /// <param name="writer">A writer to write to.</param>
        void Encode(Bitmap bitmap, BinaryWriter writer);
    }
}

These interfaces are implemented in the SGI image codec to encode and decode SGI images with verbatim and run-length encoding in a variety of color schemes. This implementation contains copious amounts of unsafe code and must be compiled with the /unsafe compiler option. (How to use unsafe code.)

The SGIDecoder class is responsible for decoding SGI images from a stream into a .NET Bitmap object. Take a look at the Decode method used to read binary data and construct the Bitmap:

        /// <summary>
        /// Decodes an SGI bitmap from the specified reader.
        /// </summary>
        /// <param name="reader">The reader to read binary bitmap data from.</param>
        /// <returns>A Bitmap read from the underlying stream.</returns>
        public Bitmap Decode(BinaryReader reader)
        {
            uint start = (uint)reader.BaseStream.Position; //store position to allow agnostic bitmap positions

            short MAGIC = reader.ReadBigEndianInt16(); //read magic number

            if (MAGIC != MAGIC_NUMBER) //validate magic number
                throw new InvalidFileType(MAGIC_NUMBER.ToString(), MAGIC.ToString());

            SGIEncoding STORAGE = (SGIEncoding)reader.ReadByte();

            byte BPC = reader.ReadByte(); //BCP pixel precision

            //validate pixel precision:
            switch (BPC)
            {
                case 1:
                    break;
                case 2:
                    throw new NotImplementedException(string.Format(NONIMPLEMENTED_BCP_EXCEPTION, BPC));
                default:
                    throw new InvalidFileDataException(string.Format(UNSUPPORTED_BCP_EXCEPTION, BPC));
            }

            ushort DIMENSION = reader.ReadBigEndianUInt16(); //Number of dimensions
            ushort XSIZE = reader.ReadBigEndianUInt16(); //width
            ushort YSIZE = reader.ReadBigEndianUInt16(); //height
            ushort ZSIZE = reader.ReadBigEndianUInt16(); //channels
            uint PIXMIN = reader.ReadBigEndianUInt32(); //Minimum pixel value 

            if (PIXMIN != 0)
                throw new NotImplementedException(string.Format(UNSUPPORTED_PIXMIN_EXCEPTION, PIXMIN));

            uint PIXMAX = reader.ReadBigEndianUInt32(); //Maximum pixel value

            if (PIXMAX != 255)
                throw new NotImplementedException(string.Format(UNSUPPORTED_PIXMAX_EXCEPTION, PIXMAX));

            int DUMMY = reader.ReadBigEndianInt32(); //Ignored
            byte[] IMAGENAME = reader.ReadBytes(80); //Image name
            uint COLORMAP = reader.ReadBigEndianUInt32(); //Colormap ID

            //validate COLORMAP:
            switch (COLORMAP)
            {
                case 0: //only implemented NORMAL color maps currently
                    break;
                case 1: //DITHERED 
                case 2: //SCREEN 
                case 3: //COLORMAP
                    throw new NotImplementedException(string.Format(NONIMPLEMENTED_COLORMAP_EXCEPTION, COLORMAP));
                default:
                    throw new InvalidFileDataException(string.Format(UNSUPPORTED_COLORMAP_EXCEPTION, COLORMAP));
            }

            byte[] DUMMYB = reader.ReadBytes(404); //Ignored

            SGIFormat format;

            //validates and determines the format:
            switch (DIMENSION)
            {
                case 1: //single channel and single scanline
                case 2: //single channel and multiple scanlines
                    format = SGIFormat.BW;
                    break;
                case 3: //multiple channels and multiple scanlines
                    switch (ZSIZE)
                    {
                        case 2: //BW+A
                            format = SGIFormat.BWA;
                            break;
                        case 3: //RGB
                        case 4: //RGBA
                            format = (ZSIZE == 4 ? SGIFormat.RGBA : SGIFormat.RGB);
                            break;
                        default:
                            throw new NotSupportedException(string.Format(UNSUPPORTED_ZSIZE_EXCEPTION, ZSIZE));
                    }
                    break;
                default:
                    throw new NotSupportedException(string.Format(UNSUPPORTED_DIMENSION_EXCEPTION, DIMENSION));
            }

            Bitmap bitmap = null;

            try
            {
                //initialize the bitmap:
                switch (format)
                {
                    case SGIFormat.BWA:
                    case SGIFormat.RGBA:
                        bitmap = new Bitmap(XSIZE, YSIZE, PixelFormat.Format32bppArgb);
                        break;
                    default:
                        bitmap = new Bitmap(XSIZE, YSIZE, PixelFormat.Format32bppRgb);
                        break;
                }

                //initialize the BitmapPixels class to do fast pixel processing
                using (BitmapPixels pixels = new BitmapPixels(bitmap))
                {
                    //lock the bitmap:
                    pixels.Lock();

                    unsafe
                    {
                        switch (STORAGE)
                        {
                            case SGIEncoding.RLE: //decodes run length encoded bitmap

                                int scanlines = YSIZE * ZSIZE; //number of expected scanlines

                                //scanline stream start positions:
                                uint[] starts = new uint[scanlines];

                                //read and offset each start position by the start position:
                                for (int i = 0; i < scanlines; i++)
                                    starts[i] = reader.ReadBigEndianUInt32() + start;

                                //read the scanline length (unused currently), just skip bytes:
                                //for (int i = 0; i < scanlines; i++)
                                //reader.ReadBigEndianUInt32();
                                reader.BaseStream.Seek(scanlines * 4, SeekOrigin.Current);

                                for (int y = 0; y < YSIZE; y++) //read each row
                                {
                                    //SGI images are stored from bottom to top while Bitmap pixels are stored top to bottom.
                                    //this offsets the pointer to the correct row:
                                    byte* buffer_p = (byte*)(pixels.Pointer + ((YSIZE - 1) - y) * XSIZE);

                                    for (int z = 0; z < ZSIZE; z++)
                                    {
                                        //offset the pointer from BGRA to RGBA:
                                        byte* buffer_ps = OffsetChannelPointer(format, z, buffer_p);

                                        //reads run length encoded channel scanline:
                                        ReadRLEScanline(reader.BaseStream, starts[y + YSIZE * z], buffer_ps);
                                    }

                                    //BW images need to have the color component copied:
                                    switch (format)
                                    {
                                        case SGIFormat.BW:
                                        case SGIFormat.BWA:

                                            RGBAColor* buffer_ps = (RGBAColor*)buffer_p;

                                            for (int x = 0; x < XSIZE; x++)
                                                CopyColorComponents(ref buffer_ps);

                                            break;
                                    }
                                }
                                break;
                            case SGIEncoding.Verbatim: //reads bitmap pixels

                                for (int z = 0; z < ZSIZE; z++)
                                {
                                    for (int y = 0; y < YSIZE; y++)
                                    {
                                        //SGI images are stored from bottom to top while Bitmap pixels are stored top to bottom.
                                        //this offsets the pointer to the correct row:
                                        byte* buffer_p = (byte*)(pixels.Pointer + ((YSIZE - 1) - y) * XSIZE);

                                        //offset the pointer from BGRA to RGBA:
                                        byte* buffer_ps = OffsetChannelPointer(format, z, buffer_p);

                                        //reads channel scanline verbatim:
                                        for (int x = 0; x < XSIZE; x++)
                                        {
                                            *buffer_ps = reader.ReadByte();
                                            buffer_ps += 4;
                                        }
                                    }
                                }

                                //BW images need to have the color component copied:
                                switch (format)
                                {
                                    case SGIFormat.BW:
                                    case SGIFormat.BWA:

                                        RGBAColor* buffer_ps = pixels.Pointer;

                                        for (int i = 0; i < YSIZE * XSIZE; i++)
                                            CopyColorComponents(ref buffer_ps);

                                        break;
                                }

                                break;
                            default:
                                throw new NotSupportedException(string.Format(UNSUPPORTED_STORAGE_EXCEPTION, STORAGE));
                        }
                    }
                }

                return bitmap;
            }
            catch
            {
                if (bitmap != null)
                    bitmap.Dispose();

                throw;
            }
        }

Run-length encoded scanlines are read and decompressed in the following manner:

        /// <summary>
        /// Reads a scanline from the stream into a pointer.
        /// </summary>
        /// <param name="stream">The stream to read from.</param>
        /// <param name="start">The stream start position.</param>
        /// <param name="buffer">A pointer to copy data to.</param>
        private static unsafe void ReadRLEScanline(Stream stream, uint start, byte* buffer)
        {
            //set the stream position:
            stream.Position = start;

            int x = 0;
            int pixel;
            int count;

            for (; ; )
            {
                pixel = stream.ReadByte(); //read the first value
                count = pixel & 127; //gets the number of pixels to read or copy

                if (count == 0)
                    break;

                if ((pixel & 0x80) > 0) //determines if pixels should be read
                {
                    while (count > 0) //read count pixels
                    {
                        *buffer = (byte)stream.ReadByte(); //set pixel to buffer

                        count--; //decrement count
                        x++; //increment horizontal position
                        buffer += 4; //get next pixel
                    }
                }
                else //otherwise, pixels should be copied
                {
                    pixel = stream.ReadByte(); //read first pixel

                    while (count > 0) //copy count times
                    {
                        *buffer = (byte)pixel; //set pixel

                        count--; //decrement count
                        x++; //increment horizontal position
                        buffer += 4; //get next pixel
                    }
                }
            }
        }

The SGIEncoder class handles encoding SGI images from a Bitmap object into binary data. The Encode method is used to write bitmap pixels to a stream:

        /// <summary>
        /// Encodes the bitmap and writes to the specified writer.
        /// </summary>
        /// <param name="bitmap">The bitmap to encode and write to the underlying stream.</param>
        /// <param name="writer">A writer to write to.</param>
        public void Encode(Bitmap bitmap, BinaryWriter writer)
        {
            if (bitmap == null)
                throw new ArgumentNullException("bitmap");

            if (writer == null)
                throw new ArgumentNullException("writer");

            writer.WriteBigEndian(MAGIC_NUMBER); //write magic number

            switch (Encoding)
            {
                case SGIEncoding.Verbatim:
                case SGIEncoding.RLE:
                    writer.Write((byte)Encoding); //write encoding
                    break;
                default:
                    throw new NotSupportedException(string.Format(INVALID_ENCODING_EXCEPTION, Encoding));
            }

            writer.Write((byte)1); //bpc

            ushort XSIZE = (ushort)bitmap.Width;
            ushort YSIZE = (ushort)bitmap.Height;

            ushort ZSIZE;
            ushort DIMENSION;

            //determine number of channels and dimensions:
            switch (Format)
            {
                case SGIFormat.RGBA:
                    ZSIZE = 4;
                    DIMENSION = 3;
                    break;
                case SGIFormat.RGB:
                    ZSIZE = 3;
                    DIMENSION = 3;
                    break;
                case SGIFormat.BWA:
                    ZSIZE = 2;
                    DIMENSION = 3;
                    break;
                case SGIFormat.BW:
                    ZSIZE = 1;
                    DIMENSION = YSIZE == 1 ? (ushort)1 : (ushort)2;
                    break;
                default:
                    throw new NotSupportedException(string.Format(INVALID_FORMAT_EXCEPTION, Format));
            }

            writer.WriteBigEndian(DIMENSION); //dimension
            writer.WriteBigEndian(XSIZE); //width
            writer.WriteBigEndian(YSIZE); //height
            writer.WriteBigEndian(ZSIZE); //number of channels

            writer.WriteBigEndian((uint)0); //PIXMIN
            writer.WriteBigEndian((uint)255); //PIXMAX
            writer.Write(new byte[4]); //DUMMY
            writer.Write(new byte[80]); //IMAGENAME
            writer.WriteBigEndian((uint)0); //COLORMAP : normal mode
            writer.Write(new byte[404]); //DUMMY

            uint[] starts;
            uint[] lengths;
            long scanlineStreamPosition;

            if (Encoding == SGIEncoding.RLE)
            {
                int scanlines = YSIZE * ZSIZE;

                //initialize start and length arrays to hold scanline stream positions and lengths:
                starts = new uint[scanlines];
                lengths = new uint[scanlines];

                scanlineStreamPosition = writer.BaseStream.Position;

                //write place holders for each row in each channel:
                writer.Write(new byte[4 * scanlines * 2]); //4 bytes * number of scanlines for start and lengths
            }
            else
            {
                //initialize unused locals:
                starts = null;
                lengths = null;
                scanlineStreamPosition = 0;
            }

            using (BitmapPixels pixels = new BitmapPixels(bitmap))
            {
                pixels.Lock();

                unsafe
                {
                    for (int z = 0; z < ZSIZE; z++) //writes each channel
                    {
                        for (int y = 0; y < YSIZE; y++) //writes each channel scanline
                        {
                            //SGI images are stored from bottom to top while Bitmap pixels are stored top to bottom.
                            //this offsets the pointer to the correct row:
                            byte* buffer_p = (byte*)(pixels.Pointer + ((YSIZE - 1) - y) * XSIZE);
                            //offset the pointer from BGRA to RGBA:
                            byte* buffer_ps = OffsetChannelPointer(Format, z, buffer_p);

                            if (Encoding == SGIEncoding.RLE)
                                //store the stream position the scanline is wrote to:
                                starts[y + YSIZE * z] = (uint)writer.BaseStream.Position;

                            byte[] channel = new byte[XSIZE]; //create a temporary channel buffer

                            fixed (byte* channel_p = channel) //get a pointer to the channel buffer
                            {
                                byte* channel_ps = channel_p; //duplicate pointer

                                for (int x = 0; x < XSIZE; x++) //fills the buffer with bitmap color
                                {
                                    switch (Format) //different formats need to be handled
                                    {
                                        case SGIFormat.BW: //convert the image to BW and apply opacity
                                            *channel_ps = (byte)((*(buffer_ps + 2) * 0.2126 + *(buffer_ps + 1) * 0.7152 + *buffer_ps * 0.0722) * *(buffer_ps + 3) / 255D);
                                            break;
                                        case SGIFormat.BWA: //convert the image to BW

                                            if (z == 0) //if RGB:
                                                *channel_ps = (byte)(*(buffer_ps + 2) * 0.2126 + *(buffer_ps + 1) * 0.7152 + *buffer_ps * 0.0722);
                                            else //if A:
                                                *channel_ps = *buffer_ps;

                                            break;
                                        case SGIFormat.RGB: //apply opacity
                                            *channel_ps = (byte)(*buffer_ps * *(buffer_ps + z + 1) / 255D);
                                            break;
                                        case SGIFormat.RGBA:
                                            *channel_ps = *buffer_ps;
                                            break;
                                    }

                                    buffer_ps += 4; //gets next channel color
                                    channel_ps++; //gets next position in temporary channel buffer
                                }

                                switch (Encoding)
                                {
                                    case SGIEncoding.RLE:

                                        //run length encode the scanline:
                                        uint length;
                                        WriteRLEScanline(writer, XSIZE, channel_p, out length);
                                        lengths[y + YSIZE * z] = length; //store the length of the scanline

                                        break;
                                    case SGIEncoding.Verbatim:

                                        channel_ps = channel_p;

                                        //write out the scanline:
                                        for (int x = 0; x < XSIZE; x++)
                                        {
                                            writer.Write(*channel_ps);
                                            channel_ps++;
                                        }

                                        break;
                                }
                            }

                            if (Encoding == SGIEncoding.RLE)
                            {
                                long position = writer.BaseStream.Position; //store the current stream position

                                writer.BaseStream.Position = scanlineStreamPosition; //move to end of header

                                //write scanline start positions:
                                foreach (uint start in starts)
                                    writer.WriteBigEndian(start);

                                //write scanline data lengths:
                                foreach (uint length in lengths)
                                    writer.WriteBigEndian(length);

                                //restore stream position
                                writer.BaseStream.Position = position; //return to stored position
                            }
                        }
                    }
                }
            }
        }

Bitmap pixel data is run-length encoded by compressing and writing scanlines to a stream:

        /// <summary>
        /// Run length encodes and writes a scanline to a stream.
        /// </summary>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="XSIZE">The scanline width.</param>
        /// <param name="buffer_p">A pointer to the scanline pixels.</param>
        /// <param name="length">When this method returns, contains the number of bytes wrote to the stream.</param>
        private static unsafe void WriteRLEScanline(BinaryWriter writer, int XSIZE, byte* buffer_p, out uint length)
        {
            length = 0;

            int x = 0;
            byte pixel = 0;
            int count = 0;
            int searchX = 0;
            int missed = 0;
            int writeX = 0;

            for (; ; )
            {
                if (x == XSIZE)
                    break;

                pixel = *(buffer_p + x); //get pixel
                count = 1; //set number of pixels

                //search for any identical pixels:
                for (searchX = x + 1; searchX - x < 127 && searchX < XSIZE; searchX++)
                {
                    if (*(buffer_p + searchX) == pixel)
                        count += 1;
                    else
                        break;
                }

                missed = 1; //init missed to 1 indicating a single invalid pixel

                //if there are less than 3 identical pixels, might as well check for more to write without encoding:
                if (count < 3)
                {
                    //searches for additional pixels that are not identical:
                    for (searchX = x + 1; searchX - x < 127 && searchX + 1 < XSIZE; searchX += 2)
                    {
                        if (*(buffer_p + searchX) != *(buffer_p + searchX + 1))
                            missed++;
                        else
                            break;
                    }
                }

                if (missed > 1) //if we have different pixels
                {
                    writer.Write((byte)(128 + missed)); //write flag and missed count
                    length++; //increment byte length

                    //write out different pixels:
                    for (writeX = x; writeX < x + missed; writeX++)
                    {
                        writer.Write(*(buffer_p + writeX));
                        length++; //increment byte length
                    }

                    x += missed; //increment x cursor
                }
                else
                {
                    writer.Write((byte)count); //write number of identical pixels
                    length++; //increment byte length
                    writer.Write(pixel); //write pixel
                    length++; //increment byte length
                    x += count; //increment x cursor by the number of identical pixels
                }
            }

            writer.Write((byte)0); //output terminating flag
            length++; //increment byte length
        }
Using

Read or write SGI image files using the BinaryReader.ReadBitmap and BinaryWriter.Write extension methods in the following manner:

        /// <summary>
        /// Reads an bitmap encoded in the SGI image format from the specified file.
        /// </summary>
        /// <param name="path">A path to an SGI encoded file.</param>
        /// <returns>A Bitmap decoded from an SGI encoded file.</returns>
        private static Bitmap ReadSGIFile(string path)
        {
            using (Stream stream = File.Open(path: path, mode: FileMode.Open, access: FileAccess.Read, share: FileShare.None))
            using (BinaryReader reader = new BinaryReader(input: stream, encoding: System.Text.Encoding.UTF8, leaveOpen: true))
                return reader.ReadBitmap(decoder: new SGIDecoder());
        }

        /// <summary>
        /// Writes a bitmap to the specified file in the SGI image format.
        /// </summary>
        /// <param name="path">A path to write the SGI encoded file to.</param>
        /// <param name="bitmap">A Bitmap to encode and write to a stream.</param>
        private static void WriteSGIFile(string path, Bitmap bitmap)
        {
            using (Stream stream = File.Open(path: path, mode: FileMode.Create, access: FileAccess.Write, share: FileShare.None))
            using (BinaryWriter writer = new BinaryWriter(output: stream, encoding: System.Text.Encoding.UTF8, leaveOpen: true))
                writer.Write(bitmap: bitmap, encoder: new SGIEncoder(SGIFormat.RGBA, SGIEncoding.RLE));
        }

The SGIEncoder accepts several different color formats and allows saving binary data verbatim or compressed using run-length encoding.

Source Code

Download SGICodec, Extension Methods and Supporting Classes

Comments