Reading and writing farbfeld images using C#
Normally when I load textures in OpenGL, I have a PNG file which
I load into a System.Drawing.Bitmap
and from there I pull out
the bytes and pass to glTexImage2D
. It works, but seems a bit
silly having to create the bitmap in the first place. For this
reason, I was toying with the idea of creating a very simple
image format so I could just read the data directly without
requiring intermediate objects.
While mulling this idea over, I spotted an article on Hacker News describing a similar and simple image format named farbfeld. This format by suckless.org is described as "a lossless image format which is easy to parse, pipe and compress".
Not having much else to do on a Friday night, I decided I'd write a C# encoder and decoder for this format, along with a basic GUI app for viewing and converting farbfeld images.
The format
Bytes | Description |
---|---|
8 | "farbfeld" magic value |
4 | 32-Bit BE unsigned integer (width) |
4 | 32-Bit BE unsigned integer (height) |
[2222] | 4x16-Bit BE unsigned integers [RGBA] / pixel, row-aligned |
As you can see, it's about as simple as you can get, barring the big-endian encoding I suppose. The main thing we have to worry about is that farbeld stores RGBA values in the range 0-65535, whereas in .NET-land we tend to use 0-255.
Decoding an image
Decoding an image is fairly straight forward. The difficult part is turning those values into a .NET image in a fast manner.
public bool IsFarbfeldImage(Stream stream)
{
byte[] buffer;
buffer = new byte[8];
stream.Read(buffer, 0, buffer.Length);
return buffer[0] == 'f' && buffer[1] == 'a' && buffer[2] == 'r' && buffer[3] == 'b' && buffer[4] == 'f' && buffer[5] == 'e' && buffer[6] == 'l' && buffer[7] == 'd';
}
public Bitmap Decode(Stream stream)
{
int width;
int height;
int length;
ArgbColor[] pixels;
width = stream.ReadUInt32BigEndian();
height = stream.ReadUInt32BigEndian();
length = width * height;
pixels = this.ReadPixelData(stream, length);
return this.CreateBitmap(width, height, pixels);
}
private ArgbColor[] ReadPixelData(Stream stream, int length)
{
ArgbColor[] pixels;
pixels = new ArgbColor[length];
for (int i = 0; i < length; i++)
{
int r;
int g;
int b;
int a;
r = stream.ReadUInt16BigEndian() / 257;
g = stream.ReadUInt16BigEndian() / 257;
b = stream.ReadUInt16BigEndian() / 257;
a = stream.ReadUInt16BigEndian() / 257;
pixels[i] = new ArgbColor(a, r, g, b);
}
return pixels;
}
private Bitmap CreateBitmap(int width, int height, IList<ArgbColor> pixels)
{
Bitmap bitmap;
BitmapData bitmapData;
bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
unsafe
{
ArgbColor* pixelPtr;
pixelPtr = (ArgbColor*)bitmapData.Scan0;
for (int i = 0; i < width * height; i++)
{
*pixelPtr = pixels[i];
pixelPtr++;
}
}
bitmap.UnlockBits(bitmapData);
return bitmap;
}
Encoding an image
As with decoding, the difficult of encoding mainly lies in getting the pixel data quickly. In this implementation, only 32bit RGBA images are supported. I will update it at some point to support other colour depths (or at the very least add a hack to convert lesser depths to 32bpp).
public void Encode(Stream stream, Bitmap image)
{
int width;
int height;
ArgbColor[] pixels;
stream.WriteByte((byte)'f');
stream.WriteByte((byte)'a');
stream.WriteByte((byte)'r');
stream.WriteByte((byte)'b');
stream.WriteByte((byte)'f');
stream.WriteByte((byte)'e');
stream.WriteByte((byte)'l');
stream.WriteByte((byte)'d');
width = image.Width;
height = image.Height;
stream.WriteBigEndian(width);
stream.WriteBigEndian(height);
pixels = this.GetPixels(image);
foreach (ArgbColor pixel in pixels)
{
ushort r;
ushort g;
ushort b;
ushort a;
r = (ushort)(pixel.R * 257);
g = (ushort)(pixel.G * 257);
b = (ushort)(pixel.B * 257);
a = (ushort)(pixel.A * 257);
stream.WriteBigEndian(r);
stream.WriteBigEndian(g);
stream.WriteBigEndian(b);
stream.WriteBigEndian(a);
}
}
private ArgbColor[] GetPixels(Bitmap bitmap)
{
int width;
int height;
BitmapData bitmapData;
ArgbColor[] results;
width = bitmap.Width;
height = bitmap.Height;
results = new ArgbColor[width * height];
bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
unsafe
{
ArgbColor* pixel;
pixel = (ArgbColor*)bitmapData.Scan0;
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++)
{
results[row * width + col] = *pixel;
pixel++;
}
}
}
bitmap.UnlockBits(bitmapData);
return results;
}
Nothing complicated
As you can see, it's a remarkably simple format and very easy to process. However, it does mean that images tend to be large - in my testing a standard HD image was 16MB for example. Of course, as you'll probably be using this for some specific process you'll be able to handle compression yourself.
After further reflection, I decided I wouldn't be using this format as it wouldn't quite fit my OpenGL scenario, as OpenGL (or at least the bits I'm familiar with) expect an array of bytes, one per channel, unlike farbfeld which uses two (and the larger value range as mentioned at the start). But I took the source I wrote for farbfeld, refactored it to use single bytes (and little-endian encoding for the other values), and that way I could just do something like this
byte[] pixels;
int length;
width = stream.ReadUInt32LittleEndian();
height = stream.ReadUInt32LittleEndian();
length = width * height * 4;
pixels = new byte[length];
stream.Read(pixels, 0, length);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixels);
No System.Drawing.Bitmap
, decoder class or complicated
decoding required!
The full source
The source presented here is abridged, you can get the full version from the GitHub repository.
Update History
- 2016-01-19 - First published
- 2017-03-04 - Corrected divisor
- 2020-11-21 - Updated formatting
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
mseeber
#
Please note, that your conversion to and from 16bit RGBA is not completely correct. To span the full range of 2^16 values in the unisgned integer of farbfeld, you would need to multiply or divide by 257 instead of 256 since the maximum value in an 8 bit integer is 255 and NOT 256. If you look at the implementation at suckless.org for reference, this is done exactly that way.
Richard Moss
#
Thanks for the comment! I'll get the source and article content updated accordingly.
Regards;
Richard Moss