OVERHEAD

Generate PNGs from a script

Scripting

Netpbm

Netpbm is a multi-platform set of graphics programs and libraries which define the commonly-named PNM formats, which are text or binary encoded files which describe how to construct an image on a per-pixel basis, each with a different type identified with a magic number that is a single-digit number prefixed with “P”.

Portable BitMap

  • Format name: PBM (.pbm).
  • Magic numbers: P1 (plaintext), and P4 (binary).
  • Colour range: Binary (0-1) colour range.

Portable GrayMap

  • Format name: PGM (.pgm).
  • Magic numbers: P2 (plaintext), and P5 (binary).
  • Colour range: Grayscale, 8-bit (0-255) or 16-bit (0-65535) colour range.

Portable PixMap

  • Format name: PPM (.ppm).
  • Magic numbers: P3 (plaintext), and P6 (binary).
  • Colour range: RGB, 8-bit (0-255) or 16-bit (0-65535) per RGB colour channel.

Constructing a PPM file

A plaintext example of a simple three-by-two PPM file.

P3
3 2
255
# Like shell scripts, you can insert comments with hashtags, such as this text.
255 0 0
0 255 0
0 0 255
255 0 0
0 255 0
0 0 255

Some PNM conversion tools expect the file to end with a newline after the colours are defined, otherwise, they’ll fail to parse it. Either add a blank line at the end, or add extra colour lines to be ignored (e.g. A one-by-one image will ignore all colours defined after the first one).

P3 defines this file as a plaintext PPM file, 3 2 defines the image as a three-by-two image where the first number is the horizontal dimension and the second is the vertical dimension, and 255 defines what value should be considered the max value (so for a 16-bit PPM file, you’d replace 255 with 65535). The max-value line must be excluded for PBM files.

As you can see, PPM files are very simple, and because of that, this makes plaintext PNM files perfect candidates for generation via shell scripts.

Converting a PPM file

After installing the imagemagick command, you can run convert /path/to/file.pnm /path/to/output.png, or choose any other output format that ImageMagick supports, to convert the PNM file into an actual image. It’s worth noting that the image generated is nearly always larger than it could be, so consider passing the output through an image optimisation tool (if you’re converting to PNG, maybe check out my previous post on the topic).

Since PPM is only an RGB format, not RGBA which has a transparency channel, and the only PNM formats which support transparency are all binary-only types, you can use a command like convert -transparent “#000” /path/to/file.ppm /path/to/output.png to add binary transparency to the output image.

Scripting advice

I can’t include a generic script for generating a PPM file, simply because there’s a near endless number of designs and patterns you could want to generate, however, here’s some advice I learnt from writing my own shell script for this:

The script I linked is what I use to generate the header image on the Blog page.

A POSIX shell example showing how to write an arbitrary number of lines all at once using the printf command.

#!/bin/sh

number="16"
printf "%${number}s" ' ' | sed "s/ /0 0 0\n/g" >> /path/to/file.pnm

Using the above printf command, you can write an arbitrary number of lines of 0 0 0 to a file, with the number of lines defined by the value of the number variable. This is much faster than individually appending lines one-by-one in a while-loop.

A POSIX shell example showing the fastest way to randomly write groups of three lines to a file.

#!/bin/sh

multi_exec() {
	multi_exec_index="$1"
	while [ "$multi_exec_index" -gt 0 ]; do
		# Spawns a new process to execute argument two's function.
		"$2" &
		multi_exec_index="$(( "$multi_exec_index" - 1 ))"
	done
}

three_col() {
	printf "255 0 0\n0 255 0\n0 0 255\n"
}

# Execute 'three_col' four times, and append the results to 'file.pnm'.
multi_exec 4 three_col >> /path/to/file.pnm

In shell scripts, the append operation (>>) is actually atomic (almost, read the note below), meaning it will prevent any other writes from happening to a file while it is actively writing to it. By exploiting this detail, you can create a queue of write operations by running multiple processes that all attempt to write to the same file, where each write operation will happen as soon as the previous one is complete.

Although write operations are atomic in the way that’s described above, there is a race condition where, if two or more writes happen at the exact same time, they won’t know of each other’s presence and so can’t block each other, meaning you’ll infrequently get something like RRGGBB instead of RGBRGB.

This is substantially faster than running something like three_col && three_col && three_col && three_col, because any variations in the runtime of the three_col function will put slower processes towards the back of the queue instead of holding up the execution of the next function.

Unfortunately, since each execution of the three_col function is independent from one another, the ordering between the colour groups will be random. Therefore, this is only useful in cases where only the ordering within each colour group matters, such as always having the colours be ordered red, green, then blue, but then having the tint of each colour be randomised on each execution.