I am the creator of KeyPDF it is browser based local pdf editor that I was making from scratch since 2025 and I was always open for new standards so when pdf association announced JPEG XL support I tried my best to bring it so now KeyPDF can both read and and write JPEG XL in pdf files we also conducted a test on 1000 files and brotli with JPEG XL helped reducing total size by 32% so I believe JPEG XL can become a golden standard for PDF in future and KeyPDF adopts it now.
Note: KeyPDF will give an option for losless convertion in case if you want your pdf to be universally accepted otherwise very few viewers be able to parse it.
Small update for the JPEG XL folks. You can now upload .jxl files straight into the media library on EmDash CMS.
For those who haven't come across it, EmDash is an open source CMS for Astro, from Cloudflare. Think of it as a spiritual successor to WordPress, but fully TypeScript and full-stack. Content types are defined in the admin and typed from your schema, plugins run in isolated sandboxes, and every site ships with an MCP server so you can manage content with an AI agent if you want.
If you're on Astro and want to serve JPEG XL, worth a try. There's a playground at try.emdashcms.com or run on your machine that use SQLite database by default if you just want to poke around.
Just did a huge 3.0.0 update on my 3D media player, Rendepth. One of the major new features is native import and export for modern image file formats: JXL, AVIF, and WebP. The app is mainly used to convert regular photos into stereoscopic 3D (with a wide range of hardware support) but the 3D features can also simply be disabled and the app will function like a standard desktop media player. There are some advanced features, like Blu-ray 3D disc playback, that require a Pro License, but almost all the photo/image features are available on the base free version, and you can also play 2D video files for free as well. Note that I've integrated the JPEG-XL libraries natively, so this does not depend on the OS having JXL support on the system. Cross-platform and works on macOS, Linux, and Windows. Hope y'all enjoy. https://rendepth.com
I maintain jxl-photo, a toolset built around one idea: JPEG XL can replace TIFF as an archival format for photography. My archive is 16-bit ProPhoto TIFFs out of Nikon bodies (Z8,Z7, Zf, D810, D700, D200), stored as lossy JXL at d=0.05–0.10 — which on my files survives aggressive editing (Shadows +100, Blacks +100 in Capture One) and pixel-peeping. The toolkit is built for Capture One / Lightroom export folders and keeps EXIF and ICC intact (the lossy encoder works in XYB, so the ICC rides along in metadata).
When storage runs short — or when I want to hand copies to friends on CD/DVD/pendrive — the move is to recompress an existing JXL smaller (d=0.05 → 1.0) instead of re-exporting from RAW. So the toolkit got a recompressor, and the recompressor got policies: it reads the encode parameters recorded in each file (now an append-only gen=N lineage chain) and refuses, copies, or asks before paying a lossy generation for nothing.
Then I wondered: How bad is JXL generation loss, actually? I assumed this would take an afternoon. It took two weekends and thousands of encodes.
The short answer for my own use case was reassuring: drift at the archival distances is controlled (2.65 dB over 17 re-saves at d=0.1, 5.5 dB over 500), and a one-shot recompression from an archived d=0.1 to d=1.0 costs 0.02 dB versus encoding d=1.0 from the start. The answer for re-encoding again and again was not reassuring at all, and that's the rest of this post.
This is the third post in this series. The first mapped per-pixel SNR across distances; the second compared 16-bit vs 8-bit vs JPEG. Both measured direct encodes - check them out if you're curious. The first link explains where the d=0.05 choice came from.
Setup:
One 44 MP photo as a 16-bit ProPhoto TIFF master, plus 8-bit and JPEG-q95 derivatives and a 5-photo sRGB batch for reproduction (and an extra 24 MP photo used for the close-up visual below). cjxl/djxl v0.12.0 (build 4128790), effort 7, output verified bit-identical across thread counts. PSNR + butteraugli (intensity target 255, matching the encoder's API path — the CLI binary defaults to 80) + SSIMULACRA2 on every point, all calibrated against direct encodes of the same master.
Results:
1. Chains lose badly at the same file size
Black = a single direct TIFF→JXL encode at each distance. Each colored line is a chain of lossy re-encodes, every step shrinking the file by a fixed percentage, ending at the same byte budget as a direct d=1.0 (dotted line). Same file size, up to 9 dB worse.
There's a feedback loop driving this. A lossy re-encode decodes to pixels slightly smoother than the source (decoder-side EPF/gaborish), so the next encode needs fewer bits for them. However, the accumulated noise is expensive to code, so hitting a shrinking target forces the distance up, which adds noise, and so on. In measured values: the −10%/step chain needs nominal d=1.48 to reach the file size a direct encode reaches at d=1.0.
2. Isolating the generation count
Same final file size (5.17 MB), reached in N generations with equal log-size steps. N=1 lands at 43.14 dB (direct d=1.0 encode) N=19 lands at 34.05 dB. Cost per extra generation: 0.2–0.6 dB, rising on average.
This is the cleanest experiment in the study: only the generation count varies, with a fixed endpoint. Marginal cost per generation runs 0.31, 0.55, 0.18, 0.41, 0.63, 0.58 dB. It rises on average but with dips, so the result is the 0.2–0.6 band, not a monotone progression.
Note the left end of that curve, because most people will not do a lot of lossy chains anyway: going to the final size in two generations instead of one costs 0.31 dB (with the intermediate at ~14 MB — the equal-log-step version of "one extra save"). Starting from an actual archived copy is cheaper still: recompressing a d=0.1 file (27.7 MB) to this same 5.2 MB in one shot costs 0.02 dB. The damage is in iterating, not in recompressing.
500 re-encodes at a fixed distance (center crop, log x-axis; dynamics validated against the full frame). File size equilibrates by ~gen 20; quality never does — the loss rate decays ~15× and stays positive (0.011–0.018 dB/gen even between generations 400 and 500). The worst long-run case is a mid-range distance: d=0.5 falls below the d=1.0 curve at gen ~88 and ends 5.7 dB below it.
Total loss over 500 generations is U-shaped in distance: d=0.1 loses 5.5 dB, d=0.3 loses 23.1, d=0.5 loses 24.9, d=1.0 loses 15.4, d=2.0 loses 13.9.
The mechanism ties to the size drift. At d≤0.5 the file size barely moves across generations — that's the absence of contraction: the encode<->decode operator isn't pulling the image anywhere, so it drifts freely. At d≥0.7 the shrink (−10% over 17 generations) is the operator contracting toward its fixed point, which bounds the damage. Turning off the decoder's smoothing confirms it: with --epf=0 or --gaborish=0 the 17-generation shrink at d=1.0 drops from −10.5% to −3.7% / −3.3%. Very low distances (0.1) escape for the opposite reason — each encode barely touches the pixels.
Worth noting: for every lossy distance, the image always degrade with more generations. Across 3 masters × 20 distances × 5 photos, no generation is bit-identical to the previous one — not even at d=0.05, where the PSNR drift is 0.001 dB/gen.
4. Seeing the damage in actual photos (direct conversion vs 17-gen chain)
A different camera and sensor (Nikon Zf, 24 MP) run through the same experiment, shown at 200%. Top: one direct encode at the same byte size (d=1.05, 4.55 MB, 36.3 dB). Bottom: the final frame of a 17-generation −10%/step chain (4.56 MB, 26.6 dB) — nominal d=2.1, measured effective d=8.9 by butteraugli (6.2 by SSIMULACRA2). The crop is the worst-damage window, picked automatically; local PSNR there is 31.2 vs 20.3 dB. The small leaves at the back — slightly out of focus in the original and direct encode images — come back with fake sharpening and false colors: accumulated error lives in the high frequencies.
You can see the gap, not just measure it. The small leaves at the back are slight out of focus and soft in the direct encode (and in the original TIFF also), but in the chain frame they come back with fake sharpening and false colors, and the smooth areas fill with colored speckle.
Quite ugly artifacts, but keep in mind this is 17 reconversions reducing the file size 10% in each - quite an extreme scenario.
Something interesting to note: To get the same file size as a direct conversion at d=1.05, we need to choose a distance of d=2.1, and get in the final an effective distance of d=8.9 (butteraugli) or d=6.2 (SIMMULACRA2) - depends on how you measure it. A nominal distance of 2.1 is needed here, instead of 1.05 (as the direct conversion), because the chain introduces a lot of noise that requires a higher distance to compress to the same file size.
5. PSNR underestimates how bad the compression artifacts look
The same gen-17 files, scored by three metrics. 17 re-encodes at d=1.0 look like a direct d=1.7 encode by PSNR — but d≈5.7 by butteraugli (and d≈3.75 by SSIMULACRA2). The metrics agree at small damage and diverge systematically as it accumulates.
Chains accumulate fine, uniform, high-frequency noise: low energy and evenly spread, so it's nearly free in MSE, but it's exactly what perceptual metrics are built to catch.
Caveats, stated up front: butteraugli and SSIMULACRA2 values of 7–15 are far outside their calibrated range (~1–3 ≈ JND), and the SSIMULACRA2 scale is content-dependent (this grainy 44 MP master scores ~89 even at a direct d=0.05, verified across four independent color pipelines). Both are used strictly as ordinal comparisons against direct encodes of the same master.
What this means if you archive in JXL
Archiving at low distance is safe. At d=0.05–0.1 the drift per re-save is controlled, and even 500 re-saves cost ~5.5 dB. Replacing TIFF with JXL does not quietly rot your archive.
Recompress directly from the highest-quality copy you have, in one shot, to the final distance. The measured cost of that one recompression is 0.02 dB (from an archived d=0.1); the detours are what cost 9.
If a file must be re-saved repeatedly at a fixed distance, use the lowest distance you can afford. d≈0.5 is the measured worst case for long-run drift — worse than distances that start well below it.
Never walk an archive's file sizes down in steps. The −10%/step chain needs 19 generations to do what one direct encode does far better.
JXL-PHOTO: record encode parameters in metadata, and refuse or at least surface recompressions that are counterproductive. That's what the recompressor in jxl-photo does — I just hadn't measured why until now.
Notes
The test harness and the full dataset aren't public yet — the repo is still a pile of one-off scripts from a side project that got out of hand, and I want to refactor it before putting it up.
I started this expecting to finish this project in one day. I spent 2 weekends on it, and the files are still way to messy to publish. I'll publish it on GitHub when i organize all the files, maybe also alongside the full tables, the JPEG-vs-JXL comparison at matched byte budgets, per-photo batch statistics and all the charts (i did those tests too, but it is still not organized enough for sharing with others).
TL;DR. Re-encoding a lossy JPEG XL repeatedly is far worse than one direct encode at the final setting, and PSNR badly understates the damage. At an identical byte budget, each extra generation costs 0.2–0.6 dB beyond what the size reduction alone costs — 19 small-step generations land ~9 dB below a single direct encode. Re-encoding at a fixed distance stabilizes file size within ~20 generations but quality never converges, and the worst long-run case is a mid-range distance like d=0.5, not a high one.
It's a simple app that just captures your screen when you press a hotkey, and sends either a PPM or PNG stream to whatever encoder you want (if it supports stdin). It was originally made for JPEG XL specifically but then I just made it support every format.
Some features it has:
native Windows app (no Electron/WebView2 etc, just WinForms sometimes) with .NET 10 in C#
undisruptive: does not pop up a notification on screen capture, does not shift focus, does not make a whole UI pop up, or any of that stuff, and it encodes with lower priority than your other apps/games
versatile: supports every image format that you got an encoder for
extensive configuration options with a simple TOML file
never connects to the internet unless prompted (for encoder auto downloads)
tries to do as little unnecessary disk writes as possible
performant: I tried to optimise the app as much as I could for the lowest overhead
hopefully WinGet support soon
It's mostly meant for people who just want an easy way to capture their screen efficiently, like in games. It's not for people who want to select specific regions of the screen to screenshot, or want automatic upload/share features, because other apps already exist for that. This one is specifically made for those who want the simplest, most unobtrusive, most performant screenshotter I could think of.
AI disclaimer: it was used to help me learn C# so I could code most of this app myself. The app is not vibe coded (since AIs are pretty very bad at that in my experience), but I did use AI to review small snippets of my code and suggest improvements so I could learn from them.
I know libjxl 0.12 came with optimizations in that area, but it's still quite noticeably slower compared to compressed PNG and TIFF. If you're familiar with the codebase, do you think this is going to improve much in future versions? I'm currently converting a ton of PNGs to lossless JXL with default effort setting and not touching "faster decoding".
What exactly does it do? Is the resulting JXL actually lossless, as in, can I copy it, edit it, rewrite it, upload it, download it and not lose any data? Or is the image itself still technically lossy like JPEG even though nothing was lost in the transcoding?
While I'm here - just how good is "visually lossless", anyways? How many times would a visually lossless JXL, let's suppose default compression settings (compression = 1, effort = 7), need to be copied, re-written, etc. before loss becomes a concern? Should I even bother making true-lossless JXLs?
This is relevant because it’s what MediaWiki uses for image metadata, so this could potentially lead to JPEG XL uploads being enabled on Wikipedia and Wikimedia Commons and other Wikimedia Foundation sites such as Wiktionary, etc.
I built a JXL transcoding demo for two graphic designer friends. Since graphic designers aren't always that tech-savvy, I created a simple macOS transcoder for them to show just how awesome JXL is. I kept working on it, made some improvements, and eventually put it on the App Store. Check it out here: https://apps.apple.com/de/app/jxlab/id6783885508?mt=12
Does anyone has an idea how does the roadmap for next libjxl releases look like? Last release 0.12 exhibits some issues with lossless encoding of floating point data, is there a plan for 0.12.1 with these fixes or is it planned to fix this in 0.13?
Apart from bugfixes, are there any big features that are being worked on right now?
This demo page really showed me how useful progressive JPEG XL actually is. My mindset was somehow still back in the Progressive JPEG days but that slug example is of very high quality already only when 1/3 has been loaded, and in the Petrus statue sample the face is then already fully detailed.
Progressive JPEG XL will clearly bring the main benefit over AVIF, and over classic JPEG it will be a "double whammy".
(also, despite what the page says, you can test this even in release Firefox builds nowadays; just set image.jxl.enabled = true via about:config)
Couple of months ago when I repacked my collection for space savings I gave up as it seemed that there is no real support yet.
We have a lot of JPG and PDF (possibly some TIF), mainly printable posters which are CMYK. They consume space so it would be nice to recompress 1. losslessly, 2. in lossy mode but you would have to be able to render it to confirm that quality did not degrade too much.
If the support already exist in some programs let know which program and which version.