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.

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported

Comments

Gravatar

mseeber

# Reply

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.

Gravatar

Richard Moss

# Reply

Thanks for the comment! I'll get the source and article content updated accordingly.

Regards;
Richard Moss