Decoding Stardent AVS Bitmap Image (.avs, .x) files in C# and WPF
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.
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.