Generating images in JavaScript

Introduction

This is a small JavaScript library that transforms an array of pixels (a bitmap) into a PNG image encapsulated in a data: URL. This is intended for small graphics: I made it to create graphics of stars in a space-themed browser-based game. The technique is quite old, and somewhat obsoleted by the <canvas> element, but still interesting if only because of the number of data transformations involved.

Files

Library:

document bitmap.js

repository Source code on GitHub

Live demo:

document bitmap_demo.html

Bitmap

The library provides a Bitmap class with a simple interface:

var bmp = new Bitmap(w, h) create bitmap of specified size
bmp.pixel[x][y] = [r, g, b, a] set pixel colour, components in the range [0, 1]
bmp.subsample(n) scale down by integer factor n
var url = bmp.dataURL() generate URL which can be used as <img> src

Gamma-encoding

In the Bitmap class colour values are represented linearly by floats in the range [0, 1]. Linear encoding makes physically-correct compositing simpler (double the value means double the brightness) but it is not intuitive, because ordinarily we deal with gamma-encoded values. For example, the medium grey colour #808080 actually has only about 21% of the brightness of the colour white.

In the PNG image colour values are represented by one of 256 discrete values. To give more sample space to darker colours, we apply a power function with an exponent smaller than 1, which is the encoder gamma value. Any gamma value can be chosen and specified in the file. We use the default value of 1/2.2 that decoders usually assume if a value is not specified.

Pixel data

Pixel data is stored in the PNG image as a sequence of rows like this:

0RGBARGBARGBARGBARGBA
0RGBARGBARGBARGBARGBA
0RGBARGBARGBARGBARGBA

The 0 at the front of each row specifies the filter method for that row, in this case – no filtering.

This data is then zlib-compressed using the deflate algorithm. The deflate algorithm is specified in RFC 1951. We don't actually want to compress anything, so we just put it in a block with no compression. The format is:

Deflated data
1 byte bit 0 final block flag 1 = final
bits 1–2 block type 0 = not compressed
2 bytes data length little-endian
2 bytes ~(data length) little-endian
* uncompressed data

This deflated data goes in a zlib container, which is specified in RFC 1950. This includes a smaller header and a checksum. The format is:

zlib-compressed data
1 byte bits 0–3 compression method 8 = deflate
bits 4–7 window size 7 = 32 KB
1 byte bits 0–4 check bits for first 2 bytes 1
bit 5 preset dictionary 0 = don't use
bits 6–7 compression level 0 = least compression
* deflated data
4 bytes Adler-32 checksum of uncompressed data

The Adler-32 checksum is also specified in RFC 1950, which also contains a sample implementation.

PNG file

The file format is described in the PNG specification. It consists of a signature followed by a series of chunks. The simplest PNG image is represented by three chunks: a header (IHDR), the image data (IDAT) and an end-of-file marker (IEND), so that's what we write.

PNG file
8 bytes signature "\211PNG\r\n\32\n"
25 bytes IHDR chunk
* IDAT chunk
12 bytes IEND chunk

Each chunk is structured like this:

Chunk
4 bytes data length big-endian
4 bytes chunk name e.g. "IDHR"
* chunk data
4 bytes CRC over chunk name and data

The CRC uses the 32-bit IEEE 802.3 polynomial. The PNG standard contains a sample implementation.

The IHDR chunk contains information about the image format:

IHDR data
4 bytes width big-endian
4 bytes height big-endian
1 byte bit depth 8, 16
1 byte colour type 2 = RGB, 6 = RGBA
1 byte compression method 0 = default
1 byte filter method 0 = default
1 byte interlace method 0 = none

The IDAT chunk contains zlib-compressed pixel data.

The IEND chunk contains no data.

data: URL

The data: scheme is specified in RFC 2397. We use Base64 encoding, so the URL contains:

data: URL
scheme data:
MIME type image/png
encoding extension ;base64
separator ,
Base64-encoded file data

The current standard for Base64 is RFC 4648.

Putting it all together

The final image URL is a data-URL-encapsulated Base64-encoded PNG file that contains CRC-verified zlib-encapsulated deflate-compressed Adler32-verified filtered rows of gamma-encoded pixel values.