Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

PJA

Lossless recompression of JPEG and JPEG-LS files, the successor of packJPG: FreePascal, with the model's core in C and some x86-64 assembly; no libraries. Also a C library, a 7-Zip plugin and a zpaq-std plugin

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

PJA

packJPG Algorithm: lossless recompression of JPEG and JPEG-LS files, in FreePascal, with the coefficient model's core in C and some x86-64 assembly; no libraries. The successor of packJPG: a new codec and format of its own (it does not read .pjg files), smaller and verified before anything is written.

Component Status
CI [![CI][ciBadge]][ciLink]
Latest release [![Release][relBadge]][relLink]
bash
pja a photo.jpg          # -> photo.pja, about 27% smaller
pja x photo.pja          # -> photo.jpg, the same file byte for byte

The original comes back byte for byte, and nothing is written under an output's name unless it verified: compressing decompresses the result and compares it with the input before writing, and extracting checks the size and the BLAKE3 hash stored in the .pja.

The formats are stable since 0.9.0: a .pja, a 7-Zip archive with PJA or a zpaq-std block written today opens in every later release (doc/STABILITY.md).

PJA belongs to a family of recompressors that share that rule: the original comes back byte for byte, and nothing is written unless it verified.

Sibling Formats Successor of
PMPA MPEG audio (MP1, MP2, MP3) packMP3, packMP2
PRA raster images (PNM, BMP, TIFF, PSD, HDR, ...) packPNM
PPA the PNG family (PNG, APNG, MNG, JNG) packPNG
PAIA animated images (GIF, WebP, FLI/FLC, ANI, ...) (new)
PJA JPEG and JPEG-LS packJPG

Against other compressors

One file at a time: packJPG 5.0g and JPEG XL's lossless transcoding of a JPEG (cjxl/djxl 0.11.2), which both give the JPEG back byte for byte too, against pja 0.6.2. Measured on 05/11/2026 ([how][wComp]): each set's whole output against its input, every file decompressed and compared with the original; a file a tool does not take, or that does not come back the same, counts at its own size.

set files size packJPG 5.0g JPEG XL Lepton brunsli PJA 0.7.0
photos (Huffman JPEG) 151 78.4 MB 76.62% 79.80% 79.42% (takes 136) 77.98% 72.60%
corpus of JPEGs of every kind 78 33.0 MB 77.23% (takes 65) 86.03% (takes 56) 85.11% (takes 55) 75.36% (takes 64) 70.39%
MPO (stereo pairs) 6 41.0 MB 86.54% takes none 87.65% 87.71% 73.38%
arithmetic-coded JPEG 244 6.7 MB 78.52% takes none takes none takes none 74.59%
generated JPEGs 640 18.3 MB 77.48% 85.35% (takes 528) 83.95% (takes 511) 79.69% (takes 604) 73.72%
12-bit JPEG, small 10 83 KB takes none takes none takes none takes none 88.14%
12-bit JPEG from camera RAWs 12 27.7 MB takes none takes none takes none takes none 84.80%
lossless JPEG 36 390 KB takes none takes none takes none takes none 96.04%

And how fast, on the 151 photos (two Xeon E5-2697A v4, 64 threads, with other work running). "8 at once": pja -th 8; the others, 8 processes of one file each, best of three runs for one thread. PJA, packJPG and Lepton verify what they write; cjxl and cbrunsli do not. Lepton (Dropbox, commit f34c7f4, -singlethread -allowprogressive) and brunsli (Google, commit c18b962) were built from their repositories.

packJPG 5.0g JPEG XL Lepton brunsli PJA 0.7.0
compress, 1 thread 72.2 s 27.0 s 40.8 s 13.3 s 66.2 s
decompress, 1 thread 37.3 s 10.9 s 15.3 s 9.0 s 32.5 s
compress, 8 at once 10.3 s 4.2 s 5.8 s 1.9 s 9.4 s
decompress, 8 at once 5.3 s 1.5 s 2.2 s 1.4 s 4.7 s

PJA is the smallest on every set and the only one that takes every file. On photos it is 5.5% smaller than packJPG and 9 to 15% faster; 7.4% smaller than brunsli, the fastest (3.4 to 5 times PJA's speed); 9.4% smaller than Lepton (1.6 to 2.1 times as fast, and it leaves 15 of the 151 photos as they are); 9.9% smaller than JPEG XL (2.2 to 3.1 times as fast). None of the four takes 12-bit or lossless JPEG, and only packJPG takes arithmetic-coded ones.

Safe by design

PJA's first rule is that the original comes back byte for byte, or nothing is written. Measured against the other JPEG recompressors on the same 20 photos and the same damage ([how][wSafe]):

PJA packJPG Lepton brunsli JPEG XL
4,000 single-bit flips in the compressed file: a wrong JPEG given back without an error 0 4 3,712 195 361
stores a hash of the original yes (BLAKE3) no no no no
checks its output before writing it, by default yes no yes no no
fuzzed inputs taken that do not come back 0 199 0 0 17
disk full: a truncated file under the output's name no (an old file is kept) no yes (and exit 0) yes yes
writes through a symbolic link at the output's name no yes yes yes yes
killed while writing: a partial file under the output's name no yes yes yes yes
written promise that its files keep opening yes (STABILITY) no archived no ISO standard

It is design, not luck: a stored hash and size, a decode-and-compare before writing, a temporary that takes the output's name only when it is complete, and a clean refusal of anything it does not understand. What it does not do: repair a damaged .pja (keep backups, or use an archiver with recovery records) or authenticate it (the hash catches accidents, not tampering).

Against archivers

One archive per set, 8 threads, on 05/11/2026; every file extracted and compared, and every one came back. Times are to write the archive / to extract it.

  • 7-Zip + PJA: 7-Zip 25.01 with PJA 0.7.0's plugin, 7z a -mmt=8 -m0=PJA -m1=LZMA2 (below).
  • zpaq-std + PJA: zpaq-std 2.3.0 with the PJA 0.6.4 plugin it ships, zpaq-std a set.zpaq FILES -sa -t8 (stream version 7, the command's model); it keeps the files a codec takes in one block, and its z codecs put the PJA streams of a block's files through an LZ77 that reaches back into the files before.
  • Aqueous: Aqueous (aqr, commit 30f9a4e), aqr -c -T 8 -l high.
  • For reference, 7-Zip (7z a -mmt=8, LZMA2) and zpaq-std (-t8, its default method) without PJA.
set 7-Zip + PJA zpaq-std + PJA Aqueous 7-Zip zpaq-std
photos (151) 72.57%, 9.6 / 4.7 s 72.88%, 16.0 / 5.4 s 80.46%, 2.8 / 0.7 s 97.04%, 6.8 / 1.0 s 97.84%, 0.6 / 0.4 s
corpus (78) 66.86%, 8.0 / 3.5 s 68.41%, 16.3 / 3.0 s 71.98%, 1.5 / 0.3 s 82.24%, 2.8 / 0.4 s 85.28%, 0.4 / 0.3 s
MPO (6) 73.39%, 4.6 / 2.2 s 73.76%, 13.9 / 4.0 s 78.94%, 1.3 / 0.4 s 99.57%, 3.7 / 0.9 s 100.04%, 0.3 / 0.2 s
arithmetic-coded (244) 17.25%, 1.7 / 0.8 s 18.14%, 5.0 / 0.9 s 85.73%, 0.2 / 0.0 s 74.49%, 0.5 / 0.1 s 75.43%, 0.1 / 0.1 s
generated (640) 16.40%, 2.8 / 1.4 s 16.96%, 5.8 / 1.6 s 33.98%, 0.7 / 0.3 s 62.85%, 1.2 / 0.4 s 67.22%, 0.4 / 0.3 s
12-bit, small (10) 58.28%, 0.2 / 0.2 s 95.81%, 0.3 / 0.0 s 92.63%, 0.0 / 0.0 s 90.38%, 0.0 / 0.0 s 95.55%, 0.0 / 0.0 s
12-bit from RAWs (12) 84.80%, 3.7 / 2.1 s 84.99%, 11.7 / 3.1 s 100.00%, 0.2 / 0.0 s 100.00%, 2.4 / 0.1 s 100.07%, 0.3 / 0.2 s
lossless JPEG (36) 27.37%, 0.2 / 0.2 s 25.98%, 0.4 / 0.2 s 96.63%, 0.0 / 0.0 s 95.64%, 0.0 / 0.0 s 97.82%, 0.0 / 0.0 s

7-Zip + PJA is smaller than Aqueous on every set, and the smallest of all on every set but the lossless one; zpaq-std + PJA is smaller than Aqueous on all but the small 12-bit one. That set is 10 small files (8 made with cjpeg, 2 from the sample corpus), 83 KB in all: each zpaq-std block carries pjax7's ZPAQL decoder (35 KB), more than PJA saves on so few bytes, so zpaq-std rightly stores them its usual way (and two are arithmetic-coded 12-bit, which no plugin codec takes). 12-bit files of real size pay the decoder back: on 12 made from camera RAWs (NEF and DNG from the sample corpus, developed to 16 bits, about 2 to 5 megapixels, cjpeg 3.0.4 -precision 12 -quality 90, baseline, optimized and progressive), 350 KB to 4 MB each, zpaq-std + PJA is 0.19 points behind 7-Zip + PJA, and nothing else takes them. On the arithmetic-coded and generated sets, whose files are variants of a few images, both keep each image about once: 16 to 18% of the input, where Aqueous writes 34 to 86%. zpaq-std + PJA writes stream version 7, as the command does, and since 2.3.0 keeps a codec's files in one block: it is 0.3 to 1.6 points behind 7-Zip + PJA, 1.7 to 3 times slower to write and up to 1.8 times slower to extract; what it spends buys an archive any zpaq can extract without the plugin, through the ZPAQL decoder each block carries. Aqueous is 3 to 25 times faster than either to write.

In 7-Zip and 7-Zip-zstd

libpja (in each release) is a C library and, in the same file, a codec for 7-Zip and 7-Zip-zstd: put pja.dll (or pja.so) in the Codecs folder next to 7-Zip (the x64 build for 64-bit 7-Zip, the x86 one for 32-bit), and

bash
7z a -m0=PJA -m1=LZMA2 photos.7z photos/      # 7-Zip-zstd: -m1=zstd

In 7-Zip's window (Add to archive, 7z format), the same goes in Parameters: 0=PJA 1=LZMA2 (7-Zip-zstd: 0=PJA 1=zstd). 0=PJA alone also works, but then nothing follows PJA in the chain and every byte that is not a JPEG (other files, EXIF) goes into the archive as it is. Extracting needs nothing but the plugin.

PJA goes first in the chain as a filter: it recompresses the JPEG and JPEG-LS files it finds in the solid block and passes every other byte on to LZMA2 or zstd, which match what PJA leaves (EXIF, other files); its own data goes into the archive by a second stream, with what repeats between files taken out (one image saved several times, progressive or not, with or without restarts, gives the same coefficients). Extracting needs the plugin too (doc/LIBRARY.md); its sizes and times are in the table above.

Since 0.5.0 the filter also takes the JPEGs that FlashPix, Picture It's and PhotoDraw's .mix and TIFF keep as tiles without their tables: 34 such files of the sample corpus in one archive, 71.92% with 0.4.0 (no better than plain LZMA2), 59.55% now (doc/LIBRARY.md). Since 0.5.2 it gives the standard tables to those whose container keeps none (Konica's Q-Photo files: 27% smaller in an archive of their own), and the command and the filter repair a scan that ends early, a download cut short included (a photo cut at 99%: 85.9% as bytes, 61.1% now).

It handles baseline, extended and progressive JPEG (Huffman or arithmetic, 8 and 12 bits), the JPEGs inside a JPEG (thumbnails, MPO, Ultra HDR), lossless JPEG, and JPEG-LS, along with the oddities real encoders leave. Anything else goes through as bytes, and pja a says why ([details][wHandles]).

Install

Download the command for your platform from the [latest release][relLink]: Linux (x86-64, x86, arm64; glibc 2.34 or later: Debian 12, Ubuntu 22.04, RHEL 9 and newer), macOS (Apple silicon) or Windows (x64, and x86 from XP on). Each build is tested by CI on its own platform, and a .pja made on one opens on any other ([installing][wInstall], [platforms][wPlat]).

bash
pja a *.jpg --output-dir=packed   # compress many
pja x packed/*.pja -o restored/   # and back
pja list packed/*.pja             # sizes and hashes

The command's manual is doc/CLI.md.

More

The [wiki][wiki] has the rest: [benchmarks][wComp], the [zpaq-std plugin][wPlugin] (PJA as codecs for zpaq archives), [the library and 7-Zip plugin][wLib], [building and testing][wBuild], [how it works][wHow], the [source layout][wLayout], [FreePascal pitfalls][wFpc] and the [release history][wRel]. The codec's full notes are in doc/CODEC.md, the file format in doc/FORMAT.md, what does not change in doc/STABILITY.md.

Building

FreePascal 3.2 and a C compiler (gcc or clang):

bash
cd pas
./build_c.sh build/c                               # the C part
fpc -O3 -dPJA_FAST -Fobuild/c pjacli.pas -opja     # the command
cd .. && ./run_tests.sh                            # every test

License

MIT, see LICENSE. The arithmetic-coded JPEG support is based in part on the work of the Independent JPEG Group; see NOTICE for that and other credits.

[ciBadge]: https://github.com/YadeWira/PJA/actions/workflows/ci.yml/badge.svg "Continuous Integration" [ciLink]: https://github.com/YadeWira/PJA/actions/workflows/ci.yml [relBadge]: https://img.shields.io/github/v/release/YadeWira/PJA "Latest release" [relLink]: https://github.com/YadeWira/PJA/releases/latest [wiki]: https://github.com/YadeWira/PJA/wiki [wComp]: https://github.com/YadeWira/PJA/wiki/Benchmarks [wSafe]: https://github.com/YadeWira/PJA/wiki/Safety-and-robustness [wHandles]: https://github.com/YadeWira/PJA/wiki/What-PJA-handles [wInstall]: https://github.com/YadeWira/PJA/wiki/Installing [wPlat]: https://github.com/YadeWira/PJA/wiki/Platforms [wLib]: https://github.com/YadeWira/PJA/wiki/Library-and-7-Zip [wPlugin]: https://github.com/YadeWira/PJA/wiki/The-zpaq-std-plugin [wBuild]: https://github.com/YadeWira/PJA/wiki/Building [wHow]: https://github.com/YadeWira/PJA/wiki/How-it-works [wLayout]: https://github.com/YadeWira/PJA/wiki/Source-layout [wFpc]: https://github.com/YadeWira/PJA/wiki/FreePascal-pitfalls [wRel]: https://github.com/YadeWira/PJA/wiki/Releases