Writing Images: Portable Anymap
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
Graymap
P2
3 3
255
0 100 200
50 150 250
75 175 255
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
Full sourcecode
You can find the full sourcecode for C and C++ on GitHub.