Brunsli: Practical JPEG repacker (now part of JPEG XL)
github.com
github.com
But seeing the 22% improvement figure reminded me that the typical JPEG file on the internet is rather unoptimized even on write-once-read-many services like imgur or i.reddit.com which transform files (stripping meta data etc) and do not preserve the original files. Just using the regular vanilla libjpeg encoder you can usually save 5%-10% just by lossless recoding of the coeffs and the better-yet-more-computionally-intense mozjpeg coder can even get you a bit further than that.
Then again, the imgur single image view page (desktop) I just randomly opened by randomly clicking an item on their homepage transfers 2.9MiB of data with ads blocked (3.9MiB deflated), 385KiB of which was the actual image, and that image can be lossless recoded by mozjpeg to 361KiB (24KiB difference, a 6.2% reduction), so the 24KiB (0.8%) reduction out of 2.9MiB of cruft hardly matters to them I suppose and may be cheaper in bandwidth and storage cost to them than the compute cost (and humans writing and maintaining the code).
Using brunsli, that same 385KiB imgur file went down to 307KiB so roughly a 20% reduction, but still only 2.6% reduction of that massive 2.9MiB the imgur site transferred in total.
4MiB to 2.5MiB is out of range for brunsli, more like 3.2MiB if you're lucky.
I also tried with a 54 MiB JPEG[1] just now. The brunsli coding is 49MiB so not even a 10% reduction on this particular file. And it took a wallclock time of 16 seconds on my last gen Intel. Decoding it back to JPEG took 11 seconds on the same box.
[1] A picture of a wedding cake, taken with a NIKON D810, one of the largest JPEGs I had available. I was "exported" by Lightroom 9 it seems from a NEF/RAW source, and is full of meta data too, around 100KiB of it.
What about JPEG XL? That's the primary usefulness of brunsli IMO, it's built into JPEG XL as one of the ways it achieves much better compression (comparable to, or possibly better than, AVIF).
3.8 MiB (q=70)
13 MiB (q=90) ("visually lossless (side by side). Default for generic input.")
19 MiB (q=95) ("visually lossless (flicker-test).")
40 MiB (q=100) ("mathematically lossless. Default for already-lossy input (JPEG/GIF)")
I'm guessing q=100 is essentially (mostly, minus rounding errors) lossless non-reversible brunsli JPEG-XL.To compare, webp (cwebp -q x, also operating on a q=0...100 scale)
9.9 MiB (q=70)
18 MiB (q=90)
23 MiB (q=95)
28 MiB (q=100)
65 MiB (z=6, medium lossless preset)
I didn't do exact timings but JPEG XL was noticeably faster, then again they use multithreaded (using 4 threads here) AVX2 code, which cwebp does not (used the debian provided package for webp), so I wouldn't pay too much attention to this result.Keep in mind JPEG XL isn't finished, and that neither are their defaults and recommendations.
Also, your lossless encoding was only 40 MiB instead of 49 MiB for pure Brunsli, so presumably JPEG XL is capable of some stronger lossless compression.
I could be wrong, I only skimmed the docs and code; when I was quite tired.
[1] JXL has a "bg" mode too that is reversible to the original JPEGs, but that's currently not exposed by the cjpegxl tool. But I saw in in their benchmark tool, and there it produced essentially the same size files than plain brunsli tool did, probably because it's essentially just the brunsli tool.
This lead to a conversation with my friend about how it was a recording, and I never needed to hear this cut of the message. You could do it over and over until you got it right and I would be none the wiser. But somehow when we record things we feel like it’s “out there” and we can’t take it back.
Or, we make the reverse mistake and do things “live”, resulting in tremendous amounts of resources being spent to redo work that could have been one and done, or really only changes infrequently. In the analog world, or with software.
In the middle on the software side are tools in the vein of continuous improvement. There’s that service that will file PRs to fix you dependencies. There should be linters and test fuzzers that do the same in the easy cases.
We have tools to scan the assets we already have and try to precompress them better. New ones like this one arrive from time to time. But doing them prior to publication introduces friction and people push back. And once it ships to our servers we erroneously believe it’s too late to change them and I don’t know why.
Are we stuck in the old headspace of shrinkwrapped software that you can’t change without enormous difficulty? Or is something else going on?
This is unfair argument from my point of view. When we deployed a 30 % improvement for fonts (woff to woff2), people argued that fonts is just a small part of websites. When we deployed a 30 % improvement for PNG-like images in WebP lossless, people argued that they are only a small part of the traffic. When we deployed a 15-30 % improvement for CSS, JS and HTML with Brotli people argued that those are only a small part of the web. When we deploy improvements to video, people argue that 'yes it is a lot of data, but it is buffered so people are not waiting and they care less than for other types of data'.
Let's review each technology within its scope.
_If_ users, on average, look at lots of images, that divisor could be large.
Does this reduce the size of JPEG files, maintaining JPEG format?
Or is this about repacking JPEG files into JPEG XL files (while reducing size), while maintaining the ability to losslessly revert to JPEG?
The page never explicitly states what format the "repacking" is into, and it has so many references to JPEG XL that it no longer seems obvious that it's into just JPEG?
The README does a poor job explaining this.
It is repacking/recoding jpegs in a lossless and reversible manner, so that clients supporting brunsli can be served directly with the optimized version (their apache and nginx modules seem to serve files with a image/x-j mime), and clients without support can be served with the original jpeg file (or served with a brunsli file and decoded with a wasm-brunsli->native jpeg decoder pipeline if wasm is an option), while you only have to keep the brunsli (or original jpeg) file around.
Since JPEG XL isn't finished yet, there still might be minor differences in the end that make the current brunsli format incompatible to the JPEG XL format, so I wouldn't mass-convert your files just yet.
Also, 20% off JPEG1 sizes is better than it may sound; you only save about 30% more by switching to any of the new codecs (JXL's VarDCT or the video-codec-based ones) that apply a ton of new tools. Given JPEG1 was published 1992, that just confirms to me Tim Terriberry's quip that it was "alien technology from the future."
Working against JPEG XL: it's four modern image formats in one (VarDCT, Brunsli, FUIF, and a separate lossless codec) so a ton of work to independently implement. Also, video codecs already have ecosystem support, and have or will probably get hardware support first.
Further out: "JPEG restoration" is something I mostly see experimental work about but could also take the old format a bit further. The idea is to use priors about what real images should look like to make an educated guess at the information that was quantized away, so you get less of the characteristic blocking and ringing from overcompresed images.
(For example, look at Knusperli, JXL's "edge-preserving filter", or AV1's CDEF. The "quantization constraint" supported by the first two of those is, to me, what makes it "restoration" and not just another loop filter: it can always return pixels that could have compressed to the exact DCT coefficients in the file.)
JPEG XL decoder no longer performs 'quantization constraint', it is a classic loop filter, just with heuristics and control fields to maintain detail much better than loop filters in video codecs.
If you start with sharp high quality originals that don't have yuv420 in them, you usually will see 65 % savings with JPEG XL, i.e. more than 30 % on top of brunsli. You can think of VarDCT-mode as guetzli(-35 %) + brunsli(-22 %) + format specific changes(-30 %) like variable sized dcts, better colorspace, adaptive quantization and loop filtering.
I was mostly going by the committee draft on the arXiv (https://arxiv.org/abs/1908.03565 ), which still mentioned the quantization constraint (J.3) and a "mathematically lossless" mode (annex O) distinct from modular. "Four formats" was just imprecise wording on my part.
The material already public about JPEG XL's design rationale, testing, and so on has been really fun to follow as an outsider. I selfishly hope there's more of it as the work on it and rollout continue. Even with the standard itself public, a lot can be mysterious to those not immersed in this stuff.
https://github.com/dropbox/lepton
The goals and results appear similar. Is the primary difference that brunsli will likely actually be part of a standard (JPEG XL)?
[1] Decoding HEIC in Windows (10) requires you to have installed a compatible HEVC decoder. Which is 99 cents (and the hassle of setting up a store accounts and payment processing with MS) or an alternative free one which will use the HEVC codec that is shipped with hardware such as newer Intels (QSV) or GPUs. Thank you patent mess!
[2] HEIF the container format can contain JPEG data, but in practice does not or only as a supplementary image (previews, pre-renders, thumbnails, etc)
There is a thread on Doom9 [1] with similar results though.
[1] https://forum.doom9.org/showpost.php?p=1894341&postcount=167
To compare the size of a Brunsli/Lepton encoded JPEG file with an HEIF image, you'd need to define some sort of quality equivalence between the two, which gets complicated fast.
I got <22% with Brunsli.
Brunsli and, IIUC, Lepton, make use of these correlations.
[^] the average color of blocks is not compressed strictly independently, but the space used on those is small compared to all the rest of the information about a block
Disclaimer: I've worked on Brunsli.
Present to a user two images, one an image compressed by image compressor X, and one compressed by the same image compressor with a single bit of output flipped.
In an ideal image compression scenario, the decompressed images would not be the same, but a user could not tell which was the 'correct' image, since both would look equally realistic.
Imagine that a compression scheme used the first bit to indicate if the encoded image is an image of a cat or not. Changing that bit would then have very obvious and significant implications on the encoded image.
If that example seems too unrealistic, imagine a modification of a compression scheme that, before decoding, xors every non-first bit with the first bit. Then flipping the first bit in the modified scheme is equivalent to flipping a lot of bits in the unmodified scheme, but they are equivalently good at encoding images.
Edit: To put it short, the important property is that equally-long encoded images are "equally plausible": it's not important how many bits differ between them, and it doesn't matter if they are similar to each other.
So you flip the cat bit and get an image of a helicopter, and they still can't tell which one is 'correct'.
[^] the property should hold not only for single-bit changes, but all length-preserving changes -- it's perfectly fine for all single bitflips to e.g. result in invalid codestreams.
Do you know how Brunsli & Lepton fare when it comes to parallelizability?
JPEG's decoding is poorly parallelizable: the entropy decoding is necessarily serial; only inverse FFTs can be parallelized.
Sharing the data about boundaries need not hamper parallelizability in its most simple meaning: imagine a format where we first encode some data for each boundary, and then we encode all the blocks that can only be decoded when provided the data for all its four boundaries.
However, what often matters is the total number of non-cacheable memory roundtrips that each pixel/block of the image has to take part in: a large cost during decoding is memory access time. If we assume that a single row of blocks across the whole image is larger than the cache, then any approaches similar to the one I described in the previous paragraph add one roundtrip.
Another consideration is that a single block is often too small to be a unit of parallelization -- parallelizing entropy decoding usually has additional costs in filesize (e.g. for indices), and any parallelization has startup costs for each task.
The end-result is that a reasonably useful approach to parallelization is to split the image into "large blocks" that are large enough to be units of parallelization on their own, and encode _those_ independently.
In JPEG XL, there's both the original sequential Brunsli, and a tiled version of it (using groups of 256x256 pixels) which can be encoded/decoded in parallel. If you have a 4:4:4 JPEG (no chroma subsampling), you can also instead of Brunsli, use the VarDCT mode of JPEG XL, where all the block sizes happen to be 8x8. Compression is similar to that of Brunsli, and it's even slightly faster (but it doesn't allow you to reconstruct the JPEG _file_ exactly, only the _image_ itself is lossless in that case).
The interesting part is you can therefore convert between these formats thousands of times without visual regressions. You could (in a few years) only store the jpeg xr file and your webserver may transcode it to jpeg for legacy browsers.
You mean JPEG XL?
JPEG XR is a completely different thing [1]
Decoding with lepton wants .23s and 8MB of RAM while brunsli used .13s and 42MB of RAM.
Not sure if it is the best recipe - the ones I use are usually written in German.
jpegtran -copy none -optimize -progressive -outfile "$image" "$image"
I have a wrapper script around this to allow bulk optimization as well as calculating stats.Time to switch I guess.
Mozjpeg does try different progressive scan scripts – different scripts lead to different compression behavior. That helps a bit, though I don't like it when it separates the Cb and Cr planes, becauses that leads to "flash of weird color" when decoding progressively. See also: https://cloudinary.com/blog/progressive_jpegs_and_green_mart...
JPEG -> [something else] compression (like Brunsli or Lepton) can compress better though, since they can use (better) predictors, entropy coding, context modeling, etc. But they do require either a new decoder (saving both bandwidth and storage), or an on-the-fly conversion back to JPEG (saving only storage; bandwidth wouldn't change).
"Nginx transcoding module"
What I know: In Photoshop, when I save an image as JPEG, I can decide the "quality" (Low, Medium, High, etc). The lower it is, the smaller the file size but the image will have (more) artifacts. The resulting image can then be opened in any image viewer including browsers.
Also, I was told to save the "master" copy in a lossless format (e.g. TIFF or PNG) because JPEG is a lossy format (like MP3 to WAV).
So how does a "repacker" come to play?
It's a lossless file compression application that's specialized on compressing a specific file format, so it can beat generic compression tools like gzip.
1. Discrete Cosine Transform, discarding insignificant information.
2. Compression of bitstream.
Step 1 is lossy in practice because it throws away things that seem insignificant. The "quality" control determines what counts as insignificant. Step 2 is lossless, and just tries to make the data coming out of Step 1 take up as little space as possible.
A repacker redoes Step 2 better: it makes the file smaller without reducing the quality, by changing how the data is compressed, not changing which parts are kept.
I thought the lossless part of JPEG XL was done by FUIF, am I misunderstanding something?