A small, self-contained image compressor that shows how a JPEG-like codec actually works, end to end. It takes a 24-bit BMP and produces a much smaller .sjpg file, and it can decode that file back into a BMP.
The goal is to show how compression algorithms work - every stage is plain, readable C++ with no third-party dependencies.
RGB -> YCbCr -> 4:2:0 chroma subsampling -> NxN DCT -> quantisation
-> zig-zag scan -> differential DC + run-length AC -> Huffman bitstream
- Colour transform: RGB is converted to YCbCr, and every plane is level-shifted so it is centred on zero, which is what the DCT and the quantisation tables expect.
- Chroma subsampling: Cb and Cr are halved in both directions (4:2:0), because the eye is far less sensitive to colour detail than to brightness detail. Can be turned off with
--no-subsampling. - DCT: each plane is split into
NxNblocks and transformed with a separable orthonormal DCT-II. Blocks that hang off the right or bottom edge are filled by replicating the edge samples. - Quantisation: the baseline JPEG (Annex K) tables, scaled to the requested quality and block size. This is the only lossy step.
- Entropy coding: coefficients are read out in zig-zag order so they run from low to high frequency. DC values are stored as differences between neighbouring blocks; AC values are run-length coded as
(zero run, magnitude)pairs and then Huffman coded. Luma and chroma get their own code tables, which are stored in the file so the decoder can rebuild them exactly.
- Clone the repository:
git clone https://github.com/jholaj/SimpleJPEGCompress.git
cd SimpleJPEGCompress- Build:
make # or: g++ -std=c++17 -O2 -Iinclude -o compressor src/*.cpp- Create a test image and run it:
mkdir -p samples results
python3 tools/make_sample.py samples/sample.bmp
./compressor samples/sample.bmp results/sample.sjpg -q 75 --preview results/preview.bmp
./compressor -d results/sample.sjpg results/decoded.bmp./compressor [options] <input.bmp> <output.sjpg> compress
./compressor -d <input.sjpg> <output.bmp> decompress
| Option | Description |
|---|---|
-q, --quality <1-100> |
Quality. Lower means a smaller file and more visible artefacts (default: 75). |
-b, --block-size <2-32> |
DCT block size (default: 8, as in JPEG). |
--no-subsampling |
Keep chroma at full resolution (4:4:4). Larger file, better colour edges. |
--preview <file.bmp> |
Also decode the result and write it as a BMP, reporting the PSNR. |
-d, --decompress |
Decode a .sjpg back into a BMP. |
-h, --help |
Show help. |
Input must be an uncompressed 24-bit or 32-bit BMP (top-down and bottom-up row order are both accepted). Output BMPs are always written as 24-bit.
Compressing the 640x400 sample above (750 KiB as a BMP):
| Quality | Output | Ratio | PSNR |
|---|---|---|---|
| 1 | 1.9 KiB | 393:1 | 23.3 dB |
| 25 | 5.6 KiB | 135:1 | 31.5 dB |
| 50 | 9.4 KiB | 80:1 | 33.3 dB |
| 75 | 18.5 KiB | 41:1 | 34.5 dB |
| 90 | 42.6 KiB | 18:1 | 35.5 dB |
| 100 | 141.7 KiB | 5.3:1 | 37.4 dB |
The same generator at 1920x1080 (python3 tools/make_sample.py samples/hd.bmp 1920 1080,
5.9 MiB as a BMP) compresses to 139 KiB at -q 75 in about 0.1 s.
make test # zig-zag, DCT/IDCT, quantisation tables, Huffman, codec round trip
make sanitize # the same tests under AddressSanitizer and UndefinedBehaviorSanitizer- Image formats: only BMP is read and written. The compressed
.sjpgcontainer is specific to this project and is not a real JPEG file, so ordinary image viewers cannot open it — use-dto convert back to BMP. - Block sizes other than 8: supported, but the quantisation table is resampled from the 8x8 baseline table, so
-b 8gives the best quality per byte. - Single-threaded: fast enough for large images in practice, but the DCT is a straightforward matrix multiplication rather than a fast DCT.