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://
Or download a ZIP of all the files: https://
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 asint
,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 classBufferedRgbaImage
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 likeRgbaImage
(raw pixel arrays). - No
null
s -
All function arguments, return values, and object fields must not be
null
. Users of this library must not pass innull
values, and in turn, the library will not returnnull
values. The optionality of a value is instead conveyed byjava.util.Optional
. The library might usenull
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 internalbyte[]
directly to avoid the cost of making defensive copies, but this means immutability cannot be enforced.XngFile
objects should be treated as immutable, but theirList<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
,BufferedRgbaImage
,BufferedGrayImage
. 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
orprivate
. 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
andPngFile
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
takes a.decode() PngImage
object containing all the chunks in memory, and yields aBufferedRgbaImage
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
-
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
-
Writes a true-color PNG depicting a rainbow ring that is transparent inside and outside the circle.
- Grayscale bit depths
-
Writes 1,2,4,8,16-bit grayscale PNGs showing all the values of its respective bit depth.
- Paletted rectangles
-
Generates a random palette and random set of colored rectangles, then writes an indexed-color PNG.
- Paletted checkerboard
-
Writes an indexed-color PNG showing a checkerboard with 2 colors and 128 levels of transparency.
- Manually crafted tiny image
-
Creates an IDAT chunk from a raw sequence of bytes representing row filters and channel sample values, then writes a PNG file.
- Animated Mandelbrot
-
Calculates and writes a grayscale Animated PNG that zooms into the Mandelbrot set fractal.
- Read and print chunks
-
Reads the PNG file and prints some details about each chunk.
- Decode-encode image and copy chunks
-
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
, andPngChunk
’s subclassesAlmost 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 methodsChunk field values are stored in hash tables inside a
PngImage
object; the keys are strings and the values are dynamically typedIn v1, chunk-type classes are package-private and looked up through reflection
Some classes replicate JDK classes, such as v1’s
CRCInputStream
being likejava.util.zip.CheckedInputStream
, and v2’sIntegers
being likejava.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
sectionsMany 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