Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Decoding Stardent AVS Bitmap Image (.avs, .x) files in C# and WPF

0.00/5 (No votes)
19 Dec 2011 1  
An article aimed at intermediate coders on decoding Stardent Corp.'s AVS Bitmap Image format.

Introduction and Background

Since beginning programming a couple of years ago, I have always made it my intention to create an image viewer. Although I started out in C and C++, I have moved onto programming in C# and was frustrated by the lack of information on reading image formats in that language. After learning more about programming and working on a separate project to read vCards, I have come back to creating an image viewer and this time with far more success than my earlier attempts.

Here, I am showing you my own implementation of a Stardent Corp. AVS Bitmap Image viewer, written solely in C# without third-party libraries, and in what I hope is a simple, fairly easy-to-follow way.

main.png

Stardent was an American corporation back in the early 90s which released a product called Application Visualisation Software or AVS. I have no idea what this software did (the name sounds a bit like marketing-speak to me), but I did manage to find a brief description of the format at http://paulbourke.net/dataformats/avs_x/. It's basically a raw image format, with the only information about the data being the height and width and the actual data as 32-bit ARGB.

Using the Code

To begin, we need to find the dimensions. They are stored as 4-bytes each in Little-Endian order, so we need to read those in. To keep with good practice, I've set up a struct, AVS_X_Header:

struct AVS_X_Header
{
    public Int32 Width;
    public Int32 Height;
};

The next part is a function that simply reads this data into the program. The BinaryEndian class can be found in the source under tools\BinaryEndian.cs. It reverses the order of the bytes that I read in and is actually taken from this discussion at bytes.com: http://bytes.com/topic/c-sharp/answers/454822-binarywriter-reader-big-endian. It simply reverses the order of the bytes if you ask it to.

AVS_X_Header AVSxHeader(string fileName)
{
    FileStream file = new FileStream(
       fileName, 
       FileMode.Open, 
       FileAccess.Read);

    BinaryReader reader = new BinaryReader(file);
    tools.BinaryEndian r = new tools.BinaryEndian();

    AVS_X_Header Header = new AVS_X_Header();

    reader.BaseStream.Seek(0, SeekOrigin.Begin);

    Header.Width = r.combine(
       reader, 
       4, 
       tools.BinaryEndian.ByteOrder.LittleEndian);
    

    Header.Height = r.combine(
       reader, 
       4, 
       tools.BinaryEndian.ByteOrder.LittleEndian);

    return Header;
}

Next up: reading the data. There is no compression and the data is stored as ARGB in that order with one byte per channel. We simply create four byte arrays - one for each channel - and read in the data for each pixel.

void readData(
   string fname, 
   int width, 
   int height, 
   out byte[] pixelDataA, 
   out byte[] pixelDataR, 
   out byte[] pixelDataG, 
   out byte[] pixelDataB)
{
    FileStream file = new FileStream(fname, FileMode.Open, FileAccess.Read);

    using (BinaryReader r = new BinaryReader(file))
    {
        r.BaseStream.Position = 8; 
        //skip height and width (in32 = 4 bytes)
        
        //width * height is total number of pixels
        pixelDataA = new byte[width * height];
        pixelDataR = new byte[width * height];
        pixelDataG = new byte[width * height];
        pixelDataB = new byte[width * height];

        for (int i = 0; i < (height * width); i++)
        {

           //For each pixel, there are 4 bytes
           //Remember ReadByte() advances the position in the BaseStream
           //by a byte.

            pixelDataA[i] = r.ReadByte();//read a byte (a)
            pixelDataR[i] = r.ReadByte();//read another byte (r)
            pixelDataG[i] = r.ReadByte();//read yet another byte (g)              
            pixelDataB[i] = r.ReadByte();//read a final byte (b)
        }
    }
}

Our last step is to create a bitmap from it. Here I'm using an unsafe code block, you could always use SetPixel or come up with an IntPtr of pixel data if you don't like unsafe code. I quickly found that Microsoft's bitmaps don't do ARGB, despite having a format of Format32ARGB. Instead, the format is BGRA (if someone knows why these bitmaps work like this, please share).

System.Windows.Media.Imaging.BitmapImage bmpFromBinaryPBM(
   int width,
   int height, 
   byte[] pixelsA, 
   byte[] pixelsR, 
   byte[] pixelsG, 
   byte[] pixelsB)
{
    int stride = ((width * ((1 + 7) / 8)) + 4 -
                 ((width * ((1 + 7) / 8)) % 4));

    System.Drawing.Bitmap B = new System.Drawing.Bitmap(
       width, 
       height, 
       System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    unsafe
    {
        System.Drawing.Imaging.BitmapData bmd = B.LockBits(
           new System.Drawing.Rectangle(0, 0, B.Width, B.Height),  
           System.Drawing.Imaging.ImageLockMode.ReadWrite, 
           B.PixelFormat);

        int i = 0;

        for (int y = 0; y < bmd.Height; y++)
        {
            //'Y' is the number widths that we have crossed and row is a 
            //pointer to the place in memory where the data for a row is 
            //found. 'x' is an integer that is used to index the pointer
            //effectively being added to the address (pointed to by row).
            //The memory at this address is then written with our pixel
            //values. 

            byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);

            for (int x = 0; x < (bmd.Width*4); x+=4, i++;)
            {
                row[x + 3] = pixelsA[i];//Alpha (Transparency Channel)
                row[x + 2] = pixelsR[i];//Red Channel
                row[x + 1] = pixelsG[i];//Green channel
                row[x] = pixelsB[i];//Blue channel
            }

            //Remember that there are four bytes per pixel, so we set 
            //each of those bytes in one loop.
        }

        B.UnlockBits(bmd);
    }

    MemoryStream ms = new MemoryStream();
    B.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
    ms.Position = 0;
    System.Windows.Media.Imaging.BitmapImage bi = 
       new System.Windows.Media.Imaging.BitmapImage();
    bi.BeginInit();
    bi.StreamSource = ms;
    bi.EndInit();
    return bi;
}

Finally, it is all called from one main function in the class. This returns the BitmapImage from our bitmap class back to wherever it is being called from. Specify this as the source of a WPF Image element and you should find that the image displays in your application. If you're using Windows Forms, then all you need to do is cast the Bitmap we unlocked as an Image and specify that as the Image of a PictureBox.

public System.Windows.Media.Imaging.BitmapImage readAVS_X_File(string fname)
{
     AVS_X_Header AvsXH = AVSxHeader(fname);
     byte[] A, R, G, B;
     readData(fname, AvsXH.Width, AvsXH.Height, out A, out R, out G, out B);
     return bmpFromBinaryPBM(AvsXH.Width, AvsXH.Height, A, R, G, B);
}

Oh, and to call all of this:

avs_x avs_x = new avs_x();
image1.Source = avs_x.readAVS_X_File(openDialog.FileName);

Well, I hope this has been some help to a few people. If all goes well, I intend to put a few more articles on image decoding on here, so any constructive feedback is good. If there are any questions, don't hesitate to leave a comment.

Points of Interest

There weren't any particular points of interest or annoyance with this project, but I think it's nice to see an archaic format like this being worked with in C#.

History

  • 19 Dec. 2011 (16:08 GMT): First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here