raw Software

PNGlib.js is a small JavaScript encoder that constructs indexed-color PNG images without Canvas, SVG, a server, or a native image library. I originally wrote it in 2010 to understand the PNG container from the byte level upward. Modern browsers can export Canvas content directly, but the library remains a compact example of how pixels, palettes, chunks, checksums, DEFLATE blocks, and Base64 fit together.

PNGlib does not draw lines, shapes, or text. It maps RGBA colors to palette indices, lets you place those indices at pixel coordinates, and serializes the result as either a binary string or Base64. The complete implementation is available as a single script:

Download PNGlib.js

Creating an image

The constructor accepts width, height, and palette depth. Register the background color first because every newly allocated pixel contains palette index zero. Then register further colors and assign their one-byte palette indices to pixel positions:

<script src="/js/pnglib.js"></script>
<img id="png-output" alt="PNG generated by PNGlib.js">
<script>
const png = new PNGlib(160, 96, 16);
const background = png.color(246, 247, 242, 255);
const blue = png.color(0, 68, 204, 255);
const red = png.color(204, 0, 68, 255);

for (let x = 0; x < png.width; x++) {
  const y = Math.round(48 + Math.sin(x / 12) * 24);
  png.buffer[png.index(x, y)] = x < 80 ? blue : red;
}

document.querySelector('#png-output').src =
  'data:image/png;base64,' + png.getBase64();
</script>

The background variable is intentionally unused after registration: calling color() assigns it palette index zero, and the zero-filled image buffer already points every pixel at that entry. Registering a transparent color first, such as png.color(0, 0, 0, 0), creates a transparent background instead.

Two-color sine wave encoded in the browser by PNGlib.js
The image is generated by PNGlib.js when the page loads; it is not a stored image asset.

The indexed-color model

Each pixel stores one palette index rather than four separate red, green, blue, and alpha channels. color(red, green, blue, alpha) deduplicates an RGBA tuple, appends its RGB channels to the palette, writes alpha to the transparency table, and returns the index as a one-character binary string.

The file uses PNG color type 3 with a fixed bit depth of eight. An index is therefore exactly one byte, so the format supports a maximum of 256 colors even when the requested palette depth is smaller. Once the configured depth is exhausted, the historical API falls back to index zero. Callers should count or otherwise constrain colors before encoding instead of relying on that fallback.

index(x, y) translates image coordinates into the correct byte inside the IDAT payload. PNG stores scanlines from top to bottom. Every row begins with a filter byte; PNGlib writes filter method zero, meaning that the following palette indices require no reconstruction.

PNG structure

The binary output starts with the eight-byte PNG signature and then emits IHDR, PLTE, tRNS, IDAT, and IEND in that order:

Every chunk has the same envelope: a four-byte big-endian data length, a four-byte ASCII type, the chunk data, and a four-byte CRC-32 over type and data. The CRC detects corruption in the PNG structure; it is not part of the compressed pixel stream.

Zlib and DEFLATE

PNG requires the IDAT payload to use the zlib format. PNGlib writes a two-byte zlib header, one or more uncompressed DEFLATE blocks, and an Adler-32 checksum of the uncompressed scanlines. The stored blocks deliberately perform no compression. Their purpose is to produce a valid stream with very little encoder code, not to minimize file size.

A stored DEFLATE block carries at most 65,535 bytes and contains its length together with the one's complement of that length. PNGlib splits larger pixel buffers across blocks automatically. The final Adler-32 covers both the row-filter bytes and palette indices, while the enclosing IDAT chunk receives its own CRC-32.

Output forms

getDump() returns the complete PNG as a binary string in which every JavaScript character represents one byte. getBase64() encodes that string with the browser's native btoa() when available and otherwise uses the built-in fallback encoder. A data URL can then be assigned directly to an image:

const image = new Image();
image.src = 'data:image/png;base64,' + png.getBase64();
document.querySelector('.preview').append(image);

Binary strings and data URLs keep this 2010 API dependency-free, but they duplicate image data in memory. For large images, prefer byte arrays, blobs, and object URLs.

When to use PNGlib

PNGlib is useful when an environment has no Canvas implementation, when a tiny dependency-free indexed-color encoder is sufficient, or when the PNG and zlib formats are the subject of the exercise. Its deterministic structure also makes it convenient for inspecting chunks and checksums.

For normal browser graphics, Canvas is the practical default. It provides drawing primitives, text, transforms, compositing, and image import. Use toBlob() with URL.createObjectURL() for exported images; this avoids holding the complete encoded file in a data-URL string. PNGlib is an encoder and format study, not a general Canvas replacement.

Boundaries

The public API intentionally remains close to the original implementation. Applications need to account for these limits:

Those constraints are acceptable for small generated diagrams and experiments. Production code that accepts untrusted dimensions or colors should validate every input before constructing the image, and should use a maintained encoder when compact output, streaming, or broad color support matters.

References