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:
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.
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:
IHDRrecords width, height, eight-bit samples, and indexed-color type 3.PLTEstores three bytes per palette entry: red, green, and blue.tRNSstores one alpha byte for each corresponding palette entry.IDATcontains the zlib-wrapped scanlines.IENDterminates the image stream.
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:
- Only eight-bit indexed-color PNGs are produced; there is no grayscale or truecolor mode.
- The palette is limited to the configured depth and never exceeds 256 entries.
- DEFLATE blocks are stored without compression, so photographic or large images are inefficient.
- Dimensions, coordinates, channel values, and palette exhaustion are not validated.
- Pixel writes access the internal buffer directly; there are no clipping or drawing operations.
- Encoding is synchronous and holds the complete PNG in memory.
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
- [PNG]World Wide Web Consortium, Portable Network Graphics (PNG) Specification, Third Edition.
- [RFC1950]P. Deutsch and J-L. Gailly, RFC 1950: ZLIB Compressed Data Format Specification version 3.3, May 1996.
- [RFC1951]P. Deutsch, RFC 1951: DEFLATE Compressed Data Format Specification version 1.3, May 1996.
- [Canvas]Mozilla Developer Network, HTMLCanvasElement: toBlob() method.