Reading and writing farbfeld images using C#

image opengl farbfeld 2 Comments

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.

A simple program for viewing and converting farbfeld images.
A simple program 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.