Handcrafted bitmaps

From Electriki
Jump to navigationJump to search


NOTE: this page is copied here because this is (sort of) electronics-related - the original page is here.

Hand-crafted bitmaps

This page illustrates how to generate simple bitmaps using nothing more than your brain and your favourite text-editor.

Background

For another project, I wanted to generate (animated) grid-/matrix-like bitmaps programmatically.

Although there are numerous libraries to do exactly that, I looked around for bitmap-formats that I can type "from memory" using a text-editor (i.e. ASCII) or generate using "printf"-like statements, and that I can understand using a text-editor (i.e. roughly see what the image looks like).

Being ASCII and simple will practically guarantee language-independence.

In addition, it would be nice if such bitmaps could be displayed in a web-browser, but not strictly necessary (as long as ImageMagick's "convert" could handle them).

(It turns out that indeed only the more complex graphics-formats are supported by common browsers. Oh well.)

First candidate: X PixMap

X PixMap or "XPM" (see specification) is a bitmap-format to be used with the X Window System we all love and hate.

There seem to be 3 versions: XPM, XPM2 and XPM3. It seems XPM and XPM3 are only meant to be used from C-code, being in the form of a struct-literal.

I really didn't want the C-literal stuff, so XPM2 remained, and seemed promising and simple enough to use.

However, "convert" seems to only accept the C-style versions, so this was a dead end. That's a shame, because IMHO this is a real pretty format.

A simple XPM3 bitmap can look like this:

static char *x[] = {
    "8 8 2 1",
    ". c #ffffff",
    "* c #ff0000",
    "..****..",
    ".*....*.",
    "*.*..*.*",
    "*......*",
    "**....**",
    "*.****.*",
    ".*....*.",
    "..****..",
};

(The "8 8 2 1" means "width and height is 8 pixels, there are 2 colours, and 1 character per pixel".)

I don't like the quotes, the trailing comma, the brackets and the mandatory C-style comment, because this might cause problems when using it with arbitrary languages.

Scaled and converted to something GPicView can display using...

    convert -scale 1000% xpm.xpm xpm.png

...it looks like this:

xpm_bitmap

Next up: Netpbm (PBM, PGM, PPM)

"Netpbm" is a project comprising several graphics formats and utilities to manipulate them.

It defines 3 file-formats under umbrella-term "PNM" (Portable aNy Map) format:

  • PBM (Portable BitMap - monochrome bitmap, 1bpp)
  • PGM (Portable GreyMap - greyscale bitmap)
  • PPM (Portable PixMap - colour bitmap)

Furthermore, it also defines a PAM (Portable Arbitrary Map) format, which seemed to offer features I didn't need, so I didn't look at it.

None of PBM, PGM and PPM supports an alpha-channel for (partial) transparency.

PBM (monochrome)

Although not as intuitive as XPM for monochrome bitmaps (I really like the way in which you can define characters to make up a bitmap yourself), this is a very simple format.

An example is given below:

    P1
    10 10
    1 1 1 1 1 1 1 1 1 1
    1 0 0 1 0 0 1 1 1 1
    1 0 0 1 0 0 1 0 1 1
    1 1 1 1 1 1 1 1 1 1
    1 0 0 1 1 1 1 0 0 1
    1 0 0 1 0 1 1 0 0 1
    1 1 1 1 1 1 1 1 1 1
    1 1 1 1 0 0 1 1 1 1
    1 0 1 1 0 0 1 0 1 1
    1 1 1 1 1 1 1 1 1 1

("P1" is a mandatory header to distinguish it from other Netpbm formats. "10 10" are the width and height of the image.)

Scaled and converted to .png, it looks like this:

pbm_bitmap

NOTE: if you compare this to the PGM bitmap shown next, you'll notice that the highest value ("1" in this case) means "darkest" in PBM, but "brightest" in PGM (and PPM).

PGM (greyscale)

PGM is probably the format I'll use the most (if ever), because it offers a nice compromise between readability and features. (Each pixel can still be represented by a single character, yet it offers greyscales in a nice and flexible way.)

A PGM bitmap is written as follows:

    P2
    10 10
    2
    0 0 0 0 0 0 0 0 0 0
    0 2 2 0 2 2 0 1 1 0
    0 2 2 0 2 2 0 1 1 0
    0 0 0 0 0 0 0 0 0 0
    0 2 2 0 1 1 0 2 2 0
    0 2 2 0 1 1 0 2 2 0
    0 0 0 0 0 0 0 0 0 0
    0 1 1 0 2 2 0 1 1 0
    0 1 1 0 2 2 0 1 1 0
    0 0 0 0 0 0 0 0 0 0

(Again, a mandatory "P2" header identifies it as a PGM file. The "10 10" once again specifies the width and height of the image. The following "2" defines the maximum greyscale-index. That is, "2" in the following bitmap-data corresponds to "white", and "0" to "black".)

After scaling and converting:

pbm_bitmap

This image may look a bit boring, because only a single greyscale-value was used (other than white or black).

PPM (colour)

A PPM-bitmap is consistent with the previous 2 formats, but uses R,G,B-tuples for each pixel:

    P3
    3 3
    1
    0 0 0   0 0 1   0 1 0
    0 1 1   1 0 0   1 0 1
    1 1 0   1 1 1   1 1 1

("P3" is again an identifying header; "3 3" again defines the width and height of the image. The following "1" defines the highest R/G/B-index, that is, the brightest R/G/B-tint. Therefore, this picture has a pallette of 8 colours, including white ("111") and black ("000").)

After scaling and converting:

ppm_bitmap

(The 2 lower-right pixels are both white.)

This is still a very simply format, but unfortunately it makes understanding the bitmap when looking at it in a text-editor a bit difficult, if not impossible. But excellent for programmatic generation.

Scripted bitmaps: animations

While playing around, I made a couple of animated GIF images, using ImageMagick's "convert".

(Note that animations have nothing to do with Netpbm itself - this section is just here to show off :-)

Example: random colours using PPM-bitmap

The following shell-script creates a number of 3x3-pixel PPM-images, and then glues them together into an animation.

    #!/usr/bin/env bash
    
    
    
    ANIMATION=random_rgb.gif
    
    N=3   # Width and height of image.
    
    NUM_PHOTOS=10
    
    
    
    LINE_VALUE_LIMIT=$[ 2 ** ( 3 * $N ) ]
    
    photos=""
    
    for i in $( seq $NUM_PHOTOS ); do
    
        photo=$i.ppm
    
        echo P3    >  $photo   # 'PPM' identifier
        echo $N $N >> $photo   # width and height of image
        echo 1     >> $photo   # max R-/G-/B-value
    
        for y in $( seq $N ); do
    
            # Get bitwise representation of a random number for use as R,G,B tuples.
            #
            # The 'LINE_VALUE_LIMIT' is added, and later removed, to zero-pad the binary number
            # (thereby guaranteeing the desired number of '1'- or '0'-values in each image-line).
    
            bin=$( ( echo obase=2; echo $[ ( $RANDOM % $LINE_VALUE_LIMIT ) + $LINE_VALUE_LIMIT ] ) | bc )
            bits=$( echo $bin | sed 's/\(.\)/\1 /g' | sed 's/. //' )
    
            echo $bits >> $photo
    
        done
    
        photos="$photos $photo"
    
    done
    
    
    
    convert -scale 3000% -delay 50 -loop 0 $photos $ANIMATION

(Mental note: the "-delay" option to "convert" is in ticks, where the default ticks-per-second is 100. Therefore, the animation advances twice each second.)

Result:

randomrgbanimation

Example: flying over a checkerboard using PBM-bitmap

Again using a shell-script, showing how easy it is to generate an image:

    #!/usr/bin/env bash
    
    
    
    ANIMATION=checkerboard.gif
    
    N=20   # Width and height of image.
    
    
    
    # Get pixel-value at given coordinate in 'infinite checkerboard' bitmap.
    
    function pixel_at {
    
        local x=$1
        local y=$2
    
        local x_pattern=$[ ( ( 2 * $x ) / $N ) % 2 ]
        local y_pattern=$[ ( ( 2 * $y ) / $N ) % 2 ]
    
        echo $[ ( $x_pattern + $y_pattern ) % 2 ]
    }
    
    
    
    # Fly over a bitmap.
    
    offset=0
    
    photos=""
    
    for i in $( seq $N ); do
    
        photo=$i.pbm
    
        echo P1    >  $photo   # 'PBM' indentifier
        echo $N $N >> $photo   # width and height of image
    
        for y in $( seq $N ); do
    
            for x in $( seq $N ); do
    
                echo -n $( pixel_at  $[ $x + $offset ]  $[ $y + ( $offset * 2 ) ] ) >> $photo
    
            done
    
            echo >> $photo
    
        done
    
        photos="$photos $photo"
    
        offset=$[ $offset + 1 ]
    
    done
    
    
    
    convert -scale 500% -delay 5 -loop 0 $photos $ANIMATION

Final result looks like this:

checkerboard_animation

Example: sticky balls using PBM-bitmap

I always liked this effect - let's try some C-code:

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    
    
    
    #define SIZE       80
    #define CEN_GRAV   21
    #define STICKYNESS  5
    #define MARGIN     14
    
    
    
    typedef struct { int step; int val; } POS;
    
    typedef struct { POS x; POS y; } BALL;
    
    static BALL balls[] = {
        { { 1, CEN_GRAV }, { 1, CEN_GRAV } },
        { { 1, CEN_GRAV }, { 2, CEN_GRAV } },
        { { 2, CEN_GRAV }, { 3, CEN_GRAV } },
        { { 3, CEN_GRAV }, { 4, CEN_GRAV } },
    
        { { 0, SIZE / 3 }, { 0, SIZE / 3 } },
    };
    
    #define NUM_BALLS   ( sizeof( balls ) / sizeof( *balls ) )
    
    
    
    static void adv_pos( POS *p ) { p->val += p->step; if ( p->val < MARGIN ||  ( SIZE - p->val - 1 ) < MARGIN ) p->step *= -1; }
    
    static void adv_ball( BALL *b ) { adv_pos( &b->x ); adv_pos( &b->y ); }
    
    static void advance( void ) { int i = NUM_BALLS; while ( i-- ) adv_ball( &balls[ i ] ); }
    
    static int hyp( int a, int b ) { return ( int )sqrt( a * a  +  b * b ); }
    
    static int dist( int x0, int y0, int x1, int y1 ) { return hyp( abs( x0 - x1 ), abs( y0 - y1 ) ); }
    
    static int max( int a, int b ) { return  a > b  ?  a  :  b; }
    
    static int grav_from( int x, int y, const BALL *b ) { return max( 0,  CEN_GRAV - dist( x, y, b->x.val, b->y.val ) ); }
    
    static int grav_at( int x, int y ) { int i = NUM_BALLS, s = 0; while ( i-- ) s += grav_from( x, y, &balls[ i ] ); return s; }
    
    static int pixel( int x, int y ) { return  !!( grav_at( x, y ) > STICKYNESS ); }
    
    static void wr_header( FILE *f ) { fprintf( f, "P1\n%d %d\n", SIZE, SIZE ); }
    
    static void wr_row( FILE *f, int y ) { int x = SIZE; while ( x-- ) fprintf( f, "%d ", pixel( x, y ) ); fprintf( f, "\n" ); }
    
    static void wr_data( FILE *f ) { int y = SIZE; while ( y-- ) wr_row( f, y ); }
    
    static void wr_contents( FILE *f ) { wr_header( f ); wr_data( f ); }
    
    static const char *fname( int nr ) { static char a[ 100 ]; sprintf( a, "%d.pbm", nr ); return a; }
    
    static void write_photo( int nr ) { FILE *f = fopen( fname( nr ), "w" ); wr_contents( f ); fclose( f ); }
    
    static void die( const char *msg ) { printf( "FATAL: %s\n", msg ); exit( 1 ); }
    
    static int parse_arg( int argc, char **argv ) { if ( argc < 2 ) die( "need #photos as argument" ); return atoi( argv[ 1 ] ); }
    
    int main( int argc, char **argv ) { int i, N = parse_arg( argc, argv ); for ( i = 0; i < N; i++ ) { write_photo( i ); advance(); } }

Animated result (108 frames):

balls_animation

That's all!