Fast, small PNG decoder and encoder in modern C (GNU C2y), built on a vendored and modified
libdeflate, plus an optional vendored zlib-ng
encoder backend for graphics. Single header API, no other dependencies,
runtime CPU dispatch on x86-64 (SSE2 baseline, AVX2 / BMI2 / AVX-512 where available; never -march=native).
Other architectures (or -DQPNG_NO_SIMD) build portable scalar kernels with identical output.
- Decode: all PNG color types / bit depths, Adam7, tRNS, 16-bit; outputs raw samples, "expanded"
(png crate
EXPANDsemantics), or image-rs layout (8-bit RGBA/RGB/LA/L, 16-bit native endian). Inflate and un-filtering are fused (rows are un-filtered while still in cache), literal pairs are decoded two at a time, Paeth/Avg use 2-row wavefronts and an AVX2 4-row Paeth kernel. - Encode: four presets (FASTEST/FAST/DEFAULT/BEST) plus explicit filter / deflate-level overrides, fused SIMD adaptive filter selection, a PNG-specialised single-pass deflate ("png-fast") and libdeflate levels 1-12, content probing (photo vs graphics) for DEFAULT and BEST.
- Safety: CRC checking (on by default), optional Adler-32 checking, dimension and output-size limits against decompression bombs, allocator hooks, fuzz targets, ASan/UBSan script.
- Output is bit-exact with the png crate / image-rs on PngSuite and a 40-file format corpus (648 checks).
Single core, fresh run on 2026-10-23 (bench/run.py, 120 non-interlaced files from the format corpus, 3 rounds, box load ~1.1–1.4). Top: decode MB/s
(qpng is 1.3–2.1× image-rs; 1.45× over all files). Middle: encode MB/s (log scale) for qpng presets vs the png crate's
matching levels (FASTEST/FAST/DEFAULT/BEST ↔ Fastest/Fast/Balanced/High, plus png with the zlib-rs backend). Bottom: output size
relative to the png crate at that level. Over all files qpng is 1.7× (FASTEST), 1.7× (FAST), 5.9× (DEFAULT) and 3.4× (BEST)
faster than the png crate (2.0× / 2.2× vs png (zlib-rs) Balanced / High), with 3–9% smaller files. "Screenshot" = the corpus' graphics set. Data: bench/results/scenarios/,
chart: python3 bench/scenario_chart.py.
Needs GCC 16 or Clang 23 (-std=gnu2y); the public header itself works from C99 and C++17.
Developer notes: docs/dev/modern-c.md.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # -DCMAKE_C_COMPILER=gcc-16 / clang
cmake --build build -j
ctest --test-dir build # api, conformance, roundtrip, metadata, stream, apng, symbols, link_test, fuzz smoke
cmake --install build --prefix /usr/local # installs qpng.h, libqpng, qpng.pc, CMake packageOptions: QPNG_WITH_ZLIBNG (default ON when third_party/zlib-ng exists; OFF builds a libdeflate-only
library with the qpng 1.0 encoder policy), QPNG_BUILD_SHARED (default OFF), QPNG_BUILD_TESTS, QPNG_BUILD_TOOLS,
QPNG_HIDE_SYMBOLS (default ON: the library exports only the qpng_* API, see below), QPNG_SANITIZE (ASan+UBSan), QPNG_LIBFUZZER (clang), QPNG_CORPUS_DIR (extra round-trip corpus).
Consumers: pkg-config --cflags --libs qpng or find_package(qpng) + qpng::qpng.
#include <qpng.h>
/* decode */
qpng_decode_options dopt;
qpng_decode_options_init(&dopt);
dopt.output = QPNG_OUTPUT_IMAGERS; /* or QPNG_OUTPUT_RAW / QPNG_OUTPUT_EXPAND */
dopt.max_output_bytes = 256u << 20; /* bomb limit (default 1 GiB) */
qpng_image img;
qpng_status s = qpng_decode(png, png_size, &dopt, &img);
if (s != QPNG_OK) { fprintf(stderr, "%s\n", qpng_status_string(s)); return 1; }
/* img.pixels: img.height rows of img.stride bytes; img.channels, img.bit_depth, img.color_type */
qpng_image_free(&img);
/* encode */
qpng_encode_options eopt;
qpng_encode_options_init(&eopt);
eopt.preset = QPNG_PRESET_DEFAULT; /* FASTEST, FAST, DEFAULT, BEST */
/* optional: eopt.filter = QPNG_FILTER_PAETH; eopt.deflate_level = 9 (1..12, or QPNG_DEFLATE_QPNG*) */
/* optional: eopt.backend = QPNG_BACKEND_ZLIBNG (or LIBDEFLATE; default AUTO); with ZLIBNG, deflate_level is a
* zlib-ng level 1..9 and eopt.zlib_strategy a QPNG_ZSTRATEGY_*; qpng_backend_available() tells what is built in */
/* palette images: eopt.palette = rgb_triplets; eopt.palette_entries = n; optional eopt.trns / trns_bytes */
uint8_t *out; size_t out_size;
s = qpng_encode(pixels, width, height, 0 /* stride, 0 = packed */, QPNG_COLOR_RGBA, 8, &eopt, &out, &out_size);
/* ... */
qpng_buffer_free(out, NULL); /* same allocator as eopt.allocator */qpng_read_info() parses IHDR without decoding. Custom allocation: fill a qpng_allocator {alloc, free, user}
and pass it in either options struct; decoded images remember their allocator.
Encoding accepts samples in PNG byte order (16-bit big-endian, sub-byte depths packed MSB first) - exactly
what QPNG_OUTPUT_RAW decoding returns.
qpng_metadata meta; /* tEXt/zTXt/iTXt, iCCP, sRGB, gAMA, cHRM, cICP, mDCV, cLLI, */
if (qpng_read_metadata(png, png_size, NULL, &meta) == QPNG_OK) { /* pHYs, tIME, bKGD, sBIT, eXIf, unknown chunks */
for (size_t i = 0; i < meta.text_count; i++) printf("%s: %s\n", meta.texts[i].keyword, meta.texts[i].text);
if (meta.iccp) use_profile(meta.iccp, meta.iccp_size); /* decompressed */
eopt.metadata = &meta; /* write everything back (unknown chunks at their place) */
qpng_encode(pixels, w, h, 0, QPNG_COLOR_RGB, 8, &eopt, &out, &out_size);
qpng_metadata_free(&meta);
}
/* writing from scratch: */
qpng_text t = { QPNG_TEXT_ITXT, "Description", "gr\xc3\xbc\xc3\x9f" "e", 0, "de", "Beschreibung", 1 };
qpng_metadata m = {0}; m.texts = &t; m.text_count = 1; m.has_phys = 1; m.phys_x = m.phys_y = 2835; m.phys_unit = 1;
eopt.metadata = &m;Limits (qpng_metadata_options): 16 MiB per decompressed zTXt/iTXt/iCCP, 64 MiB in total, 4096 text + unknown
chunks; malformed / duplicate / bad-CRC / over-limit ancillary chunks are skipped and counted in meta.skipped.
Values are reported as stored: no color management is applied (gAMA, cHRM, iCCP, sRGB, cICP, mDCV, cLLI, sBIT
and bKGD are informational).
/* decode: push bytes as they arrive; rows are delivered through a callback */
static int on_row(void *user, uint32_t y, const uint8_t *row, size_t n) { consume(row, n); return 0; }
qpng_decoder *dec; qpng_decoder_new(&dopt, on_row, user, &dec); /* row_cb NULL: whole image at finish */
while ((n = recv(sock, buf, sizeof buf, 0)) > 0) {
s = qpng_decoder_push(dec, buf, n); /* QPNG_MORE_DATA, QPNG_OK after IEND, or an error */
if (qpng_decoder_info(dec, &info) == QPNG_OK) { /* header available as soon as IHDR arrived */ }
if (s != QPNG_MORE_DATA) break;
}
if (s == QPNG_OK) s = qpng_decoder_finish(dec, NULL /* or &img in image mode */);
qpng_decoder_free(dec);
/* or pull: qpng_decode_stream(read_fn, file, &dopt, on_row, user, NULL); */
/* encode: rows in, bytes out through a write callback, bounded memory */
static int wr(void *f, const uint8_t *p, size_t n) { return fwrite(p, 1, n, f) != n; }
qpng_encoder *enc; qpng_encoder_new(w, h, QPNG_COLOR_RGBA, 8, &eopt, wr, file, &enc);
for (y = 0; y < h; y += k) qpng_encoder_push_rows(enc, rows + y * stride, stride, k);
qpng_encoder_finish(enc); qpng_encoder_free(enc);Trade-offs: libdeflate (used by the one-shot decoder) cannot inflate incrementally. Row mode on a non-interlaced
image with zlib-ng built in is truly incremental (zlib-ng raw inflate into a one-row buffer, O(row) memory).
Image mode, interlaced images and libdeflate-only builds buffer the compressed input and decode it with the
one-shot path once IEND arrived (memory: file + image; image mode is then as fast as qpng_decode). The encoder
compresses FASTEST/FAST with libdeflate's streamed png-ultra (as one-shot) and DEFAULT/BEST with zlib-ng L6/L9
(png-ultra without zlib-ng); it filters row by row (no whole-image probing), so output can be a bit larger than
qpng_encode's. Measurements in docs/archive/NOTES.md (Phase 5).
qpng_apng *a;
if (qpng_apng_open(png, size, NULL, &a) == QPNG_OK) { /* png must stay valid until qpng_apng_free */
qpng_apng_info ai; qpng_apng_get_info(a, &ai); /* num_frames, num_plays, canvas size */
const uint8_t *canvas; qpng_frame_info fi;
while (qpng_apng_next_frame(a, &canvas, &fi) == QPNG_OK) /* composed RGBA8 canvas, width * 4 stride */
show(canvas, fi.delay_num, fi.delay_den ? fi.delay_den : 100);
qpng_image raw; qpng_apng_raw_frame(a, 1, NULL, &raw, &fi); /* one frame's own pixels, any output layout */
qpng_image_free(&raw);
qpng_apng_free(a);
}
/* encode: frames of their own size + offsets, dispose / blend ops, delays */
qpng_apng_frame fr[2] = { { px0, 0, { W, H, 0, 0, 1, 10, QPNG_DISPOSE_NONE, QPNG_BLEND_SOURCE } },
{ px1, 0, { 16, 16, 8, 8, 1, 10, QPNG_DISPOSE_PREVIOUS, QPNG_BLEND_OVER } } };
qpng_encode_apng(W, H, QPNG_COLOR_RGBA, 8, fr, 2, 0 /* loop forever */, NULL, 0, &eopt, &out, &out_size);Semantics follow the APNG spec / browsers (canvas starts transparent black, first-frame DISPOSE_PREVIOUS acts as
BACKGROUND, 8-bit source-over blending). Invalid animation structure (sequence numbers, frame count, regions, ...)
gives QPNG_ERR_APNG; qpng_decode ignores APNG chunks and returns the default image. Files without acTL open as a
one-frame still image. Tested against Philip Taylor's APNG suite; raw frames and fcTL fields are identical to the
png crate's for every valid suite file.
libqpng.a / libqpng.so export only the functions declared QPNG_API in qpng.h (export list generated
from the header): the shared library uses a linker version script, the static archive is one partially linked
object (ld -r) whose other globals are localized with objcopy. The vendored libdeflate and zlib-ng (including
zlib-ng internals such as deflate_medium, crc32_*, functable) therefore never clash with a system zlib,
zlib-ng or libdeflate; ctest symbols (nm check) and link_test (links against stock static zlib-ng in both
flavours and libdeflate) enforce it. -DQPNG_HIDE_SYMBOLS=OFF (or non-ELF platforms) skips the localization.
Source map and data-flow diagram: docs/dev/architecture.md.
Modern-C rules and scars: docs/dev/modern-c.md.
Engineering progress log (archived): docs/archive/NOTES.md.
| preset | what it does | compared against |
|---|---|---|
| FASTEST | Up filter + png-ultra (single pass, runs only, sampled per-block Huffman codes), streamed in L2-sized row chunks | png Compression::Fastest (Up + fdeflate UltraFast) |
| FAST | adaptive filter (Up after strongly compressible chunks) + png-ultra, streamed | png Fast (Adaptive + UltraFast) |
| DEFAULT | row-sample probe (1/6 of rows): photos -> adaptive + png-fast (palette: also Paeth; 16-bit: also libdeflate L6); graphics -> Up-biased adaptive vs None chosen on the sample, then zlib-ng level 6 (without zlib-ng: libdeflate L4-L8 with tuned search and minimum match 3) | png Balanced (Adaptive + flate2 L6, miniz_oxide or zlib-rs) |
| BEST | same probe; photos: libdeflate L8; graphics: zlib-ng level 9, chain 1024 (without zlib-ng: libdeflate L9, or bounded near-optimal L10 for alpha / 16-bit) | png High (Adaptive + flate2 L9) |
qpng_encode_options.backend:
| backend | what runs |
|---|---|
QPNG_BACKEND_AUTO (default) |
per preset and content (measured, see docs/archive/NOTES.md "Phase 4"): libdeflate png-ultra for FASTEST/FAST, png-fast / libdeflate L6-L8 for photos, zlib-ng L6 (DEFAULT) / L9 chain 1024 (BEST) for graphics. Without zlib-ng: same as LIBDEFLATE |
QPNG_BACKEND_LIBDEFLATE |
libdeflate only: exactly the qpng 1.0 output |
QPNG_BACKEND_ZLIBNG |
every stream through zlib-ng (FASTEST L1 = deflate_quick, FAST L2, DEFAULT L6, BEST L9; filter probing unchanged); deflate_level 1..9 and zlib_strategy (FILTERED, HUFFMAN_ONLY, RLE) select explicitly. QPNG_ERR_UNSUPPORTED when not built in |
Why two backends: libdeflate's PNG-specific single-pass compressors (png-ultra, png-fast) are unbeatable for speed and for photos, but on synthetic content (UI, text, alpha graphics, 16-bit) zlib's lazy parser and zlib-ng's deflate_medium find long far matches that libdeflate's L6-L9 miss; libdeflate needed its slow near-optimal parser to match zlib-9 sizes there. Decoding always uses the patched libdeflate (faster than zlib-ng inflate, see Results).
The adaptive filter is the libpng "minimum sum of absolute signed residuals" heuristic over all five filters (the png crate's Adaptive omits None).
See docs/dev/architecture.md. Decoder: fused inflate → unfilter,
wavefront Paeth/Avg. Encoder: fused SIMD adaptive filter, png-fast / libdeflate / zlib-ng backends.
Details: docs/dev/testing.md.
ctest: API (allocator leak accounting, limits, CRC/signature/truncation errors, argument validation, padded strides), conformance (PngSuite + formats corpus, 3 output modes, FNV-64 of every output vs png crate / image-rs references intests/data/refs.txt; regenerate withtests/gen_refs.py), round trips (every preset, every filter, several deflate levels), fuzz smoke runs.scripts/sanitize.sh [ITERS] [SECONDS]: ASan+UBSan build + ctest (SIMD and portable scalar builds), mutation fuzzing; libFuzzer sessions when clang is installed. Every encoder stream in the tests is also inflated by stock zlib when it is available (libdeflate alone accepts some invalid streams).- Fuzzing with AFL++:
afl-fuzz -i tests/data/pngsuite -o out -- build/fuzz_decode.
The format corpus is generated from six source images checked in at
bench/corpus/src/ (Kodak kodim01/03/05, imagecompression.info cathedral,
plus a synthetic screenshot and RGBA graphic). Hashes: bench/corpus/src/SHA256SUMS.
# needs: pip install -r bench/requirements.txt (Pillow 13.0.0, numpy 2.5.4)
python3 bench/gen_corpus.py # writes bench/corpus/formats/ (160 PNGs)
cmake --build build -j
(cd tools/rsref && cargo build --release && CARGO_TARGET_DIR=target-zlibrs cargo build --release --features zlibrs)
python3 bench/run.py encode --rounds 3 --reps 2 --cpu 3
python3 bench/run.py decode --rounds 3 --reps 3 --cpu 3
# optional: 2026-10-10 backend-forced table (#ld = libdeflate, #zng = zlib-ng)
python3 bench/run.py encode --rounds 3 --reps 2 --cpu 3 \
--configs qpng-default-ld,qpng-default-zng,qpng-best-ld,qpng-best-zng,qpng-fastest-zng,qpng-fast-zng
# optional, needs extra filtered-IDAT inputs in bench/corpus/deflate/ (or set QPNG_RAW / QPNG_STREAMS):
python3 bench/run.py deflate --rounds 3 --reps 5run.py refuses to start when the 1-minute load average exceeds --max-load, pins to one core
(--cpu, default 3), rotates configuration order per round and reports per-file medians aggregated per group
(MB/s of decoded image bytes; ratio = raw / encoded). Encode without --configs runs the ten published
presets (png-fastest / png-fast / png-balanced / pngz-balanced / png-high / pngz-high /
qpng-fastest / qpng-fast / qpng-default / qpng-best). The #ld / #zng backend-forced names
and exploration presets (qpng-lN, qpng-uN, qpng-nN, …) stay available via --configs.
qpng-fastest-ld and qpng-fast-ld exist too; they match AUTO for those presets (same path as libdeflate)
and are omitted from the published backend table. Default corpus: bench/corpus/formats
(override with QPNG_CORPUS): kodim01/03/05, cathedral, screenshot and graphics, each in 20 color
type / depth variants, plus Adam7 copies of kodim03 and screenshot. Encode/decode numbers in this
README skip the interlaced copies (120 files). tools/rsref pins the png crate
(b2f10c6), image-rs (075e65d6) and fdeflate (a713d02) by git revision. Results of the last
run are in bench/results/.
Results (2026-10-10, CST, Intel AVX-512 box shared with other agents, load 0.7-1.5, core 3, medians)
qpng 1.1 (gcc 12 -O2, zlib-ng backend built in) vs png crate git b2f10c6 / image-rs 0.25.9 / fdeflate.
png-* = png crate with flate2's default miniz_oxide backend, pngz-* = same with the zlib-rs feature.
Cells: MB/s of decoded image bytes @ compression ratio. Raw CSVs / tables: bench/results/phase4/ (encode_matrix,
decode, inflate; backend sweeps). Phase 3 (libdeflate only) results: bench/results/phase3_*.md.
| group | png-fastest | qpng-fastest | png-fast | qpng-fast |
|---|---|---|---|---|
| graphics/16bit | 1912 @ 8.309 | 1905 @ 11.835 | 1181 @ 10.968 | 1887 @ 11.903 |
| graphics/g8 | 2146 @ 9.729 | 2938 @ 13.007 | 1329 @ 11.795 | 2586 @ 13.004 |
| graphics/ga8 | 895 @ 5.499 | 1278 @ 8.060 | 672 @ 6.810 | 1241 @ 8.173 |
| graphics/p8 | 1896 @ 9.648 | 2324 @ 12.772 | 1106 @ 11.574 | 2117 @ 12.772 |
| graphics/rgb8 | 2384 @ 11.001 | 3744 @ 14.087 | 1454 @ 13.721 | 3395 @ 14.134 |
| graphics/rgba8 | 1096 @ 5.858 | 1149 @ 8.390 | 835 @ 7.741 | 1094 @ 8.468 |
| graphics/sub8 | 1664 @ 7.702 | 1754 @ 10.287 | 974 @ 8.790 | 1275 @ 10.418 |
| photo/16bit | 362 @ 1.312 | 547 @ 1.435 | 299 @ 1.348 | 563 @ 1.470 |
| photo/g8 | 531 @ 1.726 | 836 @ 1.808 | 422 @ 1.861 | 664 @ 1.951 |
| photo/ga8 | 554 @ 2.341 | 605 @ 2.522 | 443 @ 2.483 | 581 @ 2.669 |
| photo/p8 | 425 @ 1.750 | 468 @ 1.978 | 344 @ 1.755 | 532 @ 1.968 |
| photo/rgb8 | 486 @ 1.675 | 773 @ 1.755 | 397 @ 1.796 | 670 @ 1.874 |
| photo/rgba8 | 497 @ 1.936 | 754 @ 1.985 | 420 @ 2.063 | 633 @ 2.113 |
| photo/sub8 | 385 @ 1.509 | 524 @ 2.158 | 308 @ 1.538 | 512 @ 2.158 |
| ALL | 461 @ 1.703 | 670 @ 1.875 | 375 @ 1.778 | 659 @ 1.936 |
| group | png-balanced | pngz-balanced | qpng-default | png-high | pngz-high | qpng-best |
|---|---|---|---|---|---|---|
| graphics/16bit | 191 @ 39.708 | 317 @ 37.603 | 426 @ 40.674 | 67 @ 40.573 | 41 @ 42.345 | 96 @ 45.950 |
| graphics/g8 | 194 @ 32.794 | 296 @ 33.606 | 431 @ 37.847 | 75 @ 32.715 | 48 @ 35.154 | 109 @ 39.817 |
| graphics/ga8 | 80 @ 18.187 | 127 @ 18.661 | 155 @ 19.257 | 26 @ 19.145 | 15 @ 20.689 | 31 @ 20.553 |
| graphics/p8 | 157 @ 26.299 | 230 @ 27.102 | 316 @ 31.178 | 62 @ 26.269 | 41 @ 28.386 | 87 @ 33.758 |
| graphics/rgb8 | 167 @ 34.483 | 437 @ 32.704 | 589 @ 36.298 | 43 @ 34.573 | 46 @ 36.173 | 139 @ 42.242 |
| graphics/rgba8 | 123 @ 24.980 | 162 @ 24.841 | 216 @ 25.930 | 39 @ 25.895 | 19 @ 27.964 | 43 @ 28.031 |
| graphics/sub8 | 112 @ 28.214 | 145 @ 28.987 | 273 @ 34.192 | 49 @ 29.524 | 29 @ 32.881 | 84 @ 37.836 |
| photo/16bit | 12 @ 1.565 | 34 @ 1.584 | 46 @ 1.586 | 6 @ 1.574 | 11 @ 1.569 | 24 @ 1.592 |
| photo/g8 | 12 @ 1.776 | 42 @ 1.811 | 350 @ 1.966 | 9 @ 1.778 | 12 @ 1.766 | 30 @ 1.966 |
| photo/ga8 | 9 @ 2.615 | 24 @ 2.642 | 331 @ 2.724 | 3 @ 2.661 | 7 @ 2.677 | 10 @ 2.727 |
| photo/p8 | 18 @ 1.936 | 41 @ 1.961 | 113 @ 2.037 | 8 @ 1.942 | 11 @ 1.942 | 22 @ 2.055 |
| photo/rgb8 | 13 @ 1.770 | 44 @ 1.796 | 489 @ 1.888 | 10 @ 1.773 | 14 @ 1.769 | 31 @ 1.913 |
| photo/rgba8 | 10 @ 2.012 | 38 @ 2.024 | 359 @ 2.142 | 6 @ 2.023 | 9 @ 2.019 | 21 @ 2.178 |
| photo/sub8 | 11 @ 1.974 | 34 @ 1.996 | 213 @ 2.148 | 4 @ 1.991 | 6 @ 1.996 | 18 @ 2.163 |
| ALL | 14 @ 2.022 | 41 @ 2.045 | 79 @ 2.086 | 8 @ 2.032 | 12 @ 2.029 | 26 @ 2.101 |
Encode with the backend forced (#ld = QPNG_BACKEND_LIBDEFLATE = qpng 1.0, #zng = QPNG_BACKEND_ZLIBNG)
| group | qpng-default-ld | qpng-default-zng | qpng-best-ld | qpng-best-zng | qpng-fastest-zng | qpng-fast-zng |
|---|---|---|---|---|---|---|
| graphics/16bit | 359 @ 39.272 | 422 @ 40.674 | 16 @ 45.479 | 52 @ 46.764 | 1575 @ 21.807 | 941 @ 31.189 |
| graphics/g8 | 335 @ 34.830 | 444 @ 37.847 | 109 @ 37.438 | 75 @ 40.225 | 1679 @ 20.433 | 855 @ 29.368 |
| graphics/ga8 | 142 @ 18.748 | 147 @ 19.257 | 8 @ 21.817 | 16 @ 21.360 | 937 @ 9.901 | 567 @ 14.159 |
| graphics/p8 | 325 @ 29.531 | 312 @ 31.178 | 87 @ 33.045 | 65 @ 33.998 | 1464 @ 17.227 | 748 @ 23.100 |
| graphics/rgb8 | 449 @ 35.737 | 578 @ 36.298 | 165 @ 39.789 | 78 @ 42.838 | 2004 @ 22.508 | 1105 @ 31.253 |
| graphics/rgba8 | 191 @ 25.586 | 212 @ 25.930 | 11 @ 28.025 | 21 @ 29.080 | 1194 @ 13.772 | 700 @ 18.820 |
| graphics/sub8 | 292 @ 29.289 | 268 @ 34.192 | 71 @ 36.038 | 52 @ 37.988 | 1409 @ 16.604 | 630 @ 22.954 |
| photo/16bit | 45 @ 1.586 | 36 @ 1.589 | 23 @ 1.592 | 12 @ 1.574 | 123 @ 1.174 | 80 @ 1.562 |
| photo/g8 | 526 @ 1.966 | 47 @ 1.812 | 30 @ 1.966 | 13 @ 1.767 | 123 @ 1.196 | 80 @ 1.786 |
| photo/ga8 | 316 @ 2.724 | 25 @ 2.644 | 10 @ 2.727 | 8 @ 2.679 | 187 @ 1.592 | 121 @ 2.449 |
| photo/p8 | 103 @ 2.037 | 36 @ 2.004 | 22 @ 2.055 | 12 @ 1.987 | 149 @ 1.481 | 100 @ 1.918 |
| photo/rgb8 | 461 @ 1.888 | 48 @ 1.798 | 31 @ 1.913 | 15 @ 1.770 | 128 @ 1.195 | 79 @ 1.772 |
| photo/rgba8 | 341 @ 2.142 | 39 @ 2.026 | 20 @ 2.178 | 10 @ 2.021 | 128 @ 1.327 | 94 @ 1.958 |
| photo/sub8 | 215 @ 2.148 | 37 @ 2.046 | 18 @ 2.163 | 7 @ 2.044 | 159 @ 1.422 | 98 @ 1.988 |
| ALL | 78 @ 2.085 | 44 @ 2.055 | 21 @ 2.101 | 13 @ 2.038 | 153 @ 1.460 | 99 @ 2.006 |
AUTO equals #ld for FASTEST/FAST and photos (same code path; differences are noise) and #zng for DEFAULT
graphics; BEST graphics uses zlib-ng L9 with chain 1024 (between the two BEST columns in speed).
Decoder unchanged in 1.1; re-measured as a no-regression check (phase 3 ALL: qpng-imagers 389 / imagers 261, now 359 / 252 under higher load, same ratio within noise; every group still ahead).
| group | qpng-imagers | qpng-raw | imagers | png-raw |
|---|---|---|---|---|
| graphics/16bit | 1403 | 2519 | 972 | 1546 |
| graphics/g8 | 831 | 1151 | 291 | 1000 |
| graphics/ga8 | 961 | 948 | 618 | 603 |
| graphics/p8 | 1824 | 5248 | 1047 | 2867 |
| graphics/rgb8 | 1658 | 2371 | 693 | 1541 |
| graphics/rgba8 | 1877 | 1834 | 1065 | 1083 |
| graphics/sub8 | 560 | 5108 | 286 | 2680 |
| photo/16bit | 302 | 346 | 233 | 265 |
| photo/g8 | 270 | 300 | 126 | 193 |
| photo/ga8 | 419 | 433 | 251 | 253 |
| photo/p8 | 413 | 548 | 318 | 398 |
| photo/rgb8 | 310 | 341 | 190 | 225 |
| photo/rgba8 | 351 | 402 | 261 | 263 |
| photo/sub8 | 283 | 553 | 170 | 380 |
| ALL | 359 | 423 | 252 | 304 |
Inflate (MB/s) incl. zlib-ng: libdeflate stays the decoder (patched pair decoding 570 vs zlib-ng 363 MB/s).
| group | libdeflate | qpng-pairs | zlib-ng | fdeflate |
|---|---|---|---|---|
| stream from fdeflate-1 | 383 | 664 | 367 | 517 |
| stream from fdeflate-ultrafast | 391 | 640 | 379 | 515 |
| stream from libdeflate-qpng | 387 | 643 | 378 | 573 |
| stream from orig | 389 | 553 | 362 | 459 |
| stream from zng6 | 383 | 426 | 334 | 331 |
| ALL | 387 | 570 | 363 | 462 |
| group | fdeflate-ultrafast | fdeflate-rle | fdeflate-1 | fdeflate-6 | qpng-rle | qpng-fast | qpng-6 |
|---|---|---|---|---|---|---|---|
| graphics | 1041 @ 1.805 | 544 @ 2.721 | 281 @ 2.947 | 74 @ 3.561 | 796 @ 2.894 | 618 @ 3.125 | 87 @ 3.638 |
| photo | 460 @ 1.549 | 399 @ 1.665 | 399 @ 1.665 | 34 @ 1.640 | 672 @ 1.678 | 599 @ 1.678 | 55 @ 1.637 |
| ALL | 518 @ 1.594 | 422 @ 1.805 | 368 @ 1.824 | 38 @ 1.839 | 693 @ 1.832 | 603 @ 1.849 | 59 @ 1.840 |
| group | fdeflate | libdeflate | qpng-pairs |
|---|---|---|---|
| stream from fdeflate-1 | 474 | 350 | 618 |
| stream from fdeflate-ultrafast | 470 | 357 | 528 |
| stream from libdeflate-qpng | 501 | 370 | 558 |
| stream from orig | 403 | 361 | 477 |
| stream from zng6 | 299 | 362 | 391 |
| ALL | 415 | 360 | 502 |
- png High with zlib-rs, graphics ga8: BEST 31 MB/s @ 20.55 vs 15 @ 20.69 (0.7% larger, 2x faster). zlib-ng chain 1536 reaches 20.83 but then loses speed to miniz High (26 vs 26-28 MB/s).
- png Fastest, graphics 16-bit: speed tie (1905 vs 1912 MB/s) at 42% better ratio.
- Everything else: each qpng preset is faster and smaller than its png counterpart with either flate2 backend in every group (phase-3 losses on DEFAULT rgb8/16-bit and BEST alpha/16-bit are closed by zlib-ng).
- Raw deflate on graphics: fdeflate UltraFast is still faster as a raw compressor (table above: 1041 vs 796 MB/s).
MIT; see LICENSE. Bundled third-party code keeps its own license: third_party/libdeflate (MIT) and third_party/zlib-ng (zlib license).
The images in bench/corpus/src/ are not covered by that MIT license:
kodim01.png,kodim03.png,kodim05.png: Kodak True Color suite (copyright Eastman Kodak Company), included as widely used research/test images.cathedral.png: from the imagecompression.info RGB 8-bit test set; please credit that source.screenshot.pngandgraphics.png: synthetic images created for this project.
