NHollmann

English | German

Hi, I'm Nicolas Hollmann and this is my personal site and blog.
Happy reading! 📖

Writing Images: Portable Anymap

August 29, 20196 min read
Tags:

Writing Images: Portable Anymap

Image Output

Most of the time, image formats are somewhat complex and require some sort of compression. But there are some exceptions and Portable Anymaps are such an exception. Portable anymaps are not a single file format but a group of formats for saving Bitmaps, Grayscale and RGB images.

There are 7 different formats:

Format Magic Number File Ending
ASCII Bitmap P1 .pbm
ASCII Graymap P2 .pgm
ASCII Pixmap P3 .ppm
Binary Bitmap P4 .pbm
Binary Graymap P5 .pgm
Binary Pixmap P6 .ppm

The seventh format is a generalization of the binary types and we just ignore it in this article.

ASCII

The ASCII format is really simple. Ineed, it is so simple that you can export images in this format from every language that can output text to a file or console. Yes, you could even use Brainfuck to export portable anymaps.

All portable anymaps begin with a ASCII header in the following format:

MAGIC_NUMBER
WIDTH HEIGHT
MAX_VALUE

MAGIC_NUMBER is one of the 7 format identifiers we already looked at. WIDTH and HEIGHT are simply the width and height of the image in pixels. We look at MAX_VALUE a little bit later.

The fields in the header are seperated by a whitespace so you could even write every field in its own line or write the whole header in one line.

The actual image data begins right after the header. They are encoded as human readable ASCII numbers seperated by whitespace. Bitmaps can just use the 0 and 1 character, graymaps and pixmaps can use numbers from 0 to MAX_VALUE. Graymaps use one number per pixel while pixmaps use 3 numbers per pixel, one for each channel. The channel order is red, green, blue.

While not required, it is common to spereate lines in the image with a newline and also use a padding for smaller numbers so they align in columns. This makes it easier to read by humans. Look at the examples on this page to better undestand what I mean.

Comments

Portable Anymaps can also include comments. Comments start with a # character and end with a carriage return or newline.

Depth

Now it is time to look at the MAX_VALUE header field. It describes what the highest possible value in an image may be. It must be greater than 0 and less than 65536. In a graymap, a pixel with a value of MAX_VALUE would be white. In a pixmap, if all three channels of a pixel are MAX_VALUE, it also would be white. The most common value for MAX_VALUE is 255, because this is the maximal value one byte can hold.

Because portable bitmaps can only have 0 and 1 as values, there is no MAX_VALUE field for them.

Binary

While ASCII is human readable and easy to output, it does take up a lot of space. Even for a single value we need 2 bytes (1 whitespace + 1 data byte) but it can easy be more. Saving the same file in the binary mode we can save a lot space but we sacrifice readability.

To output a portable anymap in binary you just output the header in ASCII but directly after the last whitespace of the header we output the image data. Make sure that you don’t output any whitespaces after the last one in the header. They get interpreted as image values.

There is a little catch when using the binary mode: If MAX_VALUE is less than 256 the values are saved in one byte per pixel in graymaps and one byte per RGB channel in pixmaps. If MAX_VALUE is equal or greater than 256 values are saved with two bytes using Big Endian.

Implementation ASCII

There is nothing really special about the implementation of a portable anymap writer. I’ve decided to show you a simple example using C and writing a RGB image to a file. You can also find a C++ sample in the full sourcecode. The code should be easy to read even for a beginner.

#include <stdio.h>

#define WIDTH 300
#define HEIGHT 200

int main()
{
    FILE *outfile = fopen("out.ppm", "wb");

    fprintf(outfile, "P3");

    fprintf(outfile, "\n%d %d\n255\n", WIDTH, HEIGHT);

    for (int y = 0; y < HEIGHT; y++)
    {
        for (int x = 0; x < WIDTH; x++)
        {
            int r = (x / (float)WIDTH) * 255;
            int g = (x / (float)WIDTH) * 255;
            int b = (y / (float)HEIGHT) * 255;

            fprintf(outfile, "%3d %3d %3d ", r, g, b);
        }
        // Output newlines so the result is a little bit more readable.
        fprintf(outfile, "\n");
    }

    fclose(outfile);
    return 0;
}

Implementation Binary

This implementation isn’t very different from the ASCII version. Notice that the header is still saved as ASCII characters.

#include <stdio.h>

#define WIDTH 300
#define HEIGHT 200

int main()
{
    FILE *outfile = fopen("out.ppm", "wb");

    // Now it is P6 instead of P3
    fprintf(outfile, "P6");

    fprintf(outfile, "\n%d %d\n255\n", WIDTH, HEIGHT);

    for (int y = 0; y < HEIGHT; y++)
    {
        for (int x = 0; x < WIDTH; x++)
        {
            int r = (x / (float)WIDTH) * 255;
            int g = (x / (float)WIDTH) * 255;
            int b = (y / (float)HEIGHT) * 255;

            unsigned char colors[3] = {r, g, b};
            fwrite(colors, 3, 1, outfile);
        }
    }

    fclose(outfile);
    return 0;
}

Examples

Here are some examples using ASCII anymaps. They are all written by hand.

Bitmap

P1
3 3
1 1 1
0 1 0
0 1 0

Bitmap sample

Graymap

P2
3 3
255
  0 100 200
 50 150 250
 75 175 255

Graymap sample

Pixmap

P3
3 3
255
0   0   0   100 100 100  255 255 255
255 0   0   0   255   0    0   0 255
255 255 0   0   255 255  255   0 255

Pixmap sample

Full sourcecode

You can find the full sourcecode for C and C++ on GitHub.

Format specifications

  • Portable Bitmap: PBM
  • Portable Graymap: PGM
  • Portable Pixmap: PPM


© 2021, Nicolas Hollmann