PNG library
Introduction
This is my modern Java library for decoding and encoding PNG image files. All chunk types and most 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 and grayscale images, without or without alpha channel, of all bit depths, with all filter types, with or without interlacing
Encode RGB and grayscale images, without or without alpha channel, of all bit depths, with filter type 0, 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) Represent, interpret, and parse all the known standard PNG chunk types
Store and convey all unknown chunk types
Treat MNG and JNG files as entirely composed of custom chunks
Compact, modular, auditable implementation at ~4100 lines of code
Concise and relatively safe API where most objects are immutable
Strictly check out-of-range values, checksum mismatches, reading/
writing more/less than the expected data length, arithmetic overflow
Future wish list:
Represent, encode, and decode paletted (indexed-color) images
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.
- 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.
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++