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;
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++)
{
pixelDataA[i] = r.ReadByte(); pixelDataR[i] = r.ReadByte(); pixelDataG[i] = r.ReadByte(); pixelDataB[i] = r.ReadByte(); }
}
}
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++)
{
byte* row = (byte*)bmd.Scan0 + (y * bmd.Stride);
for (int x = 0; x < (bmd.Width*4); x+=4, i++;)
{
row[x + 3] = pixelsA[i]; row[x + 2] = pixelsR[i]; row[x + 1] = pixelsG[i]; row[x] = pixelsB[i]; }
}
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.