Project Nayuki


PNG library

Introduction

This is my modern Java library for decoding and encoding PNG image files. All chunk types and color modes are supported. Example usage:

// Encoding
var img = new BufferedRgbaImage(...);
img.setPixel(...);
PngImage png = ImageEncoder.encode(img);
png.beforeIdats.add(new Gama(1 / 2.2));
png.afterIdats.add(new Text("Author", "Myself"));
png.write(new File("output.png"));

// Decoding
PngImage png = PngImage.read(new File("input.png"));
for (Chunk chunk : png.beforeIdats)
	print(chunk);
var img = (GrayImage)ImageDecoder.decode(png);
for (int y = 0; y < img.getHeight(); y++) {
	for (int x = 0; x < img.getWidth(); x++) {
		draw(img.getPixel(x, y));
	}
}

What makes this library modern? It:

  • was started in the year with all the benefits of hindsight (with respect to understanding the PNG format, evaluating existing PNG libraries, and utilizing programming languages effectively);

  • presents an easy-to-use, type-safe, misuse-resistant API;

  • emphasizes implementation correctness and security over sprawling features and fast code;

  • uses recent Java-language features to make the code more concise and readable;

  • consumes more CPU and memory to simplify the logic and improve reliability.

This work builds upon my PNG file chunk inspector from two years earlier, where I needed to understand every field of every chunk type and detect as many data errors as possible.

Library features

  • Decode RGB, grayscale, and paletted images, without or without alpha channel, of all bit depths, with all filter types, with or without interlacing

  • Encode RGB, grayscale, and paletted images, without or without alpha channel, of all bit depths, with filter type 0, with or without interlacing

  • Up-convert images with bit depths that are not 1/2/4/8/16 (e.g. RGBA 5.6.5.4 to 8.8.8.8)

  • Parse, represent, interpret, and serialize all the known chunk types for the PNG standard, extension, and APNG

  • Handle huge chunks up to the standard’s size limit (231 − 1 bytes)

  • Store and convey all unknown chunk types

  • Treat MNG and JNG files as entirely composed of custom chunks

  • Concise and relatively safe API where most objects are immutable

  • Compact, modular, auditable implementation at ~5400 lines of code

  • Strictly check out-of-range values, checksum mismatches, reading/writing more/less than the expected data length, arithmetic overflow, length overflow

  • Have an extensive test suite for image encode-decode round trip and every chunk type

Future wish list:

  • Encode images with interlacing

  • Port to other programming languages

Outside of scope:

  • Drawing, filtering, resampling, color space conversion, and other image effects

  • Lossy color reduction, palette quantization, and dithering

  • Minimizing data size by manipulating row filters and DEFLATE compression

  • Streaming chunks, rows, and pixels instead of buffering everything in memory

Source code

Browse the project’s source code at GitHub: https://github.com/nayuki/PNG-library

Or download a ZIP of all the files: https://github.com/nayuki/PNG-library/archive/refs/heads/master.zip

The code is open source under the MIT License. See the readme file for details.

Code overview

XngFile class

This low-level class reads and writes PNG/MNG/JNG files, handles chunk boundaries and checksums, and optionally parses known PNG chunk types. Most users don’t need to use this.

PngImage class

This mid-level class is like XngFile but only works with PNG files and imposes some constraints on chunk ordering and expected chunks. This does not deal with raw pixel data.

Chunk and subtypes

These represent chunks in memory. Reading bytes can produce Chunk objects, and these objects can be written to bytes. The raw bytes that comprise chunk fields are interpreted as int, String, enum, etc. to the maximum extent possible.

Random-access image types

The interface RgbaImage represents an image where any pixel can be retrieved or computed quickly. The class Buffered­RgbaImage is backed by an array so that you can get or set any pixel. There are analogous types for grayscale images.

ImageDecoder, ImageEncoder

These translate between PngImage objects (with chunks and compressed bytes) and types like RgbaImage (raw pixel arrays).

No nulls

All function arguments, return values, and object fields must not be null. Users of this library must not pass in null values, and in turn, the library will not return null values. The optionality of a value is instead conveyed by java.util.Optional. The library might use null internally within functions, but does not expose these values to user code.

Immutability

Objects of any chunk type included in this library must be treated as immutable. All their fields are private. Due to record, each field implicitly generates a getter method with the same name. There are no setter methods. If a chunk type is composed entirely of immutable fields (e.g. int, String), then it is truly immutable. Otherwise, a chunk type might choose to return its internal byte[] directly to avoid the cost of making defensive copies, but this means immutability cannot be enforced.

XngFile objects should be treated as immutable, but their List<Chunk> and the chunks themselves might not be able to enforce immutability.

For all immutable types, all data values are checked strictly at the time of object construction.

These objects are mutable: PngImage, Buffered­RgbaImage, Buffered­GrayImage. The validity of input data is checked at idiosyncratic occasions.

Access control

Every class, interface, enumeration, record, method, and field is marked with the proper access modifier such as public or private. This is the standard practice in Java programming, and is hardly special if it wasn’t for other libraries making mistakes in this aspect.

Lossless chunks

All the included chunk types support lossless round-tripping, where reading bytes into chunk objects and writing them out will produce exactly the same bytes. This means, for example, that any compressed data must be stored in memory because decompressing and recompressing can produce different results. Furthermore, XngFile and PngFile preserve the order of chunks without forcing a canonical representation.

Assuming abundant memory

For the sake of reducing conceptual complexity and improving reliability, this library makes design trade-offs that increase memory usage. This is possible because memory is much cheaper now than when the PNG format was first released, but correctness and security vulnerabilities became bigger concerns.

The included in-memory image formats all use 16 bits per channel, even when handling images with lower bit depths like 8. This increases generality and decreases special cases at the cost of using more memory.

There is no support for streaming chunks or pixels; most operations are one-shot. For example, ImageDecoder.decode() takes a PngImage object containing all the chunks in memory, and yields a Buffered­RgbaImage object containing all the pixels in memory. The lack of streaming dramatically simplifies the API, reduces the implementation logic and error checks, and minimizes the chances of errors in both the library code and user code.

Default concurrency

The codebase essentially doesn’t deal with concurrency. There is no global mutable state. Static functions are reentrant, so they can be called from multiple threads simultaneously. Functions and methods are structured around call-and-return without unbounded waits (except for I/O). The code has no considerations for situations where two or more threads use mutable objects. There is no locking, inter-thread communication, waiting for actions from other threads, etc. Sharing mutable objects safely requires the user’s code to have proper locking or transfers. The library may choose in the future to implement fork-join for intensive calculations, but these private threads have no visible effect to the user.

Examples

Red-green gradient

RedGreenGradient.java

Writes an 8-bit true-color PNG showing all 65536 possible combinations of red and green values, with the blue channel set to zero.

Rainbow ring

RainbowRing.java

Writes a true-color PNG depicting a rainbow ring that is transparent inside and outside the circle.

Grayscale bit depths

GrayscaleDepths.java

Writes 1,2,4,8,16-bit grayscale PNGs showing all the values of its respective bit depth.

Paletted rectangles

PalettedRectangles.java

Generates a random palette and random set of colored rectangles, then writes an indexed-color PNG.

Paletted checkerboard

PalettedCheckerboard.java

Writes an indexed-color PNG showing a checkerboard with 2 colors and 128 levels of transparency.

Manually crafted tiny image

ManualTiny.java

Creates an IDAT chunk from a raw sequence of bytes representing row filters and channel sample values, then writes a PNG file.

Animated Mandelbrot

AnimatedMandelbrot.java

Calculates and writes a grayscale Animated PNG that zooms into the Mandelbrot set fractal.

Read and print chunks

ReadPrintChunks.java

Reads the PNG file and prints some details about each chunk.

Decode-encode image and copy chunks

DecodeEncodeCopy.java

Usage: DecodeEncodeCopy Input.png Output.png

Reads the input PNG, decompresses and decodes the image data to a buffered image object in memory, encodes and compresses it, copies all chunks except {IHDR, sBIT, PLTE, IDAT, IEND}, and writes the output PNG. The compressed representation of the image might change between the input file and output file, but the decoded image and all other chunks will be the same.

Modern Java features used

The conciseness and readability of this library are enabled by numerous language and library features from modern versions of Java. This library was started in and uses features from Java SE 8 (year ) to 18 (year ). Notable features used:

  • Java 8: Streams, Optional, Math.multiplyExact()
  • Java 10: var declaration
  • Java 14: switch expression
  • Java 16: record type, instanceof pattern matching
  • Java 18: Math.ceilDiv()

The impact of avoiding a feature would depend on the feature. Some are trivial, like declaring full types instead of var. Some require re-implementing a function in perhaps 10 lines. Some like switch and instanceof can result in doubling the amount of boilerplate code in that section. Most importantly, avoiding record types would greatly increase the amount of repetitive, unenjoyable-to-read code for all the chunk types. While syntactical sugar like switch can be compiled with a modern compiler down to an older bytecode format for binary distribution, record types require the support of the class java.lang.Record which would not be present in an older JDK.

The majority of Java code I write only requires Java SE 5 due to generics, and occasionally I use Java 8 features like lambdas and streams. As much as I would like to make this library only require Java 8, the new features accumulated since then are too compelling to relinquish. I acknowledge the existence of environments that fall behind modern Java, such as enterprise JDK deployments, old operating system distributions, and the Android SDK, but dropping this library down to Java 8 would make the code quality markedly worse.

Compared to competitors

PNGJ (Java) by Hernan J. González
  • Developed from year to , currently having about 12000 lines of code (at commit fd2a2ea75a51, dated )

  • Boasts features like row-by-row reading, low memory usage, and fast encoding/decoding

  • Many classes have mutable state in their fields, such as PngReader, DeflatedChunksSet, and PngChunk’s subclasses

  • Almost all classes are marked as public, even ones that appear to provide internal functionality that a user wouldn’t directly use

Sixlegs Java PNG Library (Java) by Chris Nokleberg
  • Supports only image decoding, not encoding

  • Was actively developed from year to

  • Has two incompatible major versions, with about 4000 lines for v1.3.0 (dated ), and about 3300 lines for v2.0 (dated )

  • Designed around PngImage as a god object that serves many roles, has mutable state, and has many methods

  • Chunk field values are stored in hash tables inside a PngImage object; the keys are strings and the values are dynamically typed

  • In v1, chunk-type classes are package-private and looked up through reflection

  • Some classes replicate JDK classes, such as v1’s CRCInputStream being like java.util.zip.CheckedInputStream, and v2’s Integers being like java.lang.Integer

JDeli (Java) by IDRsolutions
  • Not open source, and costs thousands of dollars per year for a commercial license

  • The Javadoc (at version 2022.12, dated ) suggests that it has very few PNG-specific features and options, and seemingly no chunk or metadata handling at all

  • Outside of PNG handling, the library supports ~10 image file formats and operations like scaling and blurring

libpng (C) by many individuals
  • The original, reference, and feature-complete PNG library implementation from the creators of the format, actively developed from to the present day

  • Currently has about 37000 lines of core library code (excluding copyright header comments, test programs, and CPU-specific optimizations) (at version 1.6.39, dated )

  • Even the human-oriented manual document is 5400 lines of 80-column plain text

  • Contains many, many functions; implements a bunch of math for CIE XYZ and decimal conversions; much of the code is a mystery to me

  • Has many features that can be included/excluded at compile time through #ifdef sections

  • Many severe security vulnerabilities were found and fixed over the decades, not unusual for any library written in C or C++

LodePNG (C) by Lode Vandevenne
  • Developed from year to the present day, currently having about 7800 lines of code (at commit 997936fd2b45, dated )

  • Library is C/C++ polyglot code, utilities and most example programs are in C++, and a few examples are in C

  • Supports PNG encoding and decoding, and even impressively includes a DEFLATE compressor and decompressor, thus not relying on the popular zlib

  • Parses all the chunks and saves the values in one big structure representing the whole PNG file; does not preserve the ordering of chunks

  • Seems to have a history of correctness bugs being found a few times a year

More info