JPEG XL for imhex (+ pattern language thoughts)
posted
JPEG XL is the final image format. Or, one of the final formats, that is. It’s not going to subsume raw camera formats or certain specific interchange formats, but that’s alright; it’s enough to encompass most images most computer touchers encounter. I’ve been following the format for at least a few years, and it’s been quite exciting to see its gradual adoption among desktop software. Between screenshots, photos, and archival scans, it’s the dominant form of images on my computer, and one day I hope this is the same for the web.
In tandem, I’ve been trying to focus more on doing things; it helps manage the tedium of life when I contribute to projects or pursue knowledge on my own. Kind of “de-consumptioning” myself a bit—motioning away from the sludge of it all. This leads to me occasionally perusing the things I use to see if there’s anything I can help out with, filing issues or maybe a PR should motivation be tickled enough.
So, I talk to jonny about JXL a bunch. He relays updates on the standard & libjxl from the developers, and we have a good time ruminating its progression. Typical nerd stuff. Around two weeks ago (as of writing), I was looking around the libjxl repo and found this bug that he filed—seemingly an error when a specific flag is switched. Recreating the bug & putting the files in the imhex hex editor to diff them led to an interesting observation: only one bit in the first several bytes changes between the broken image and the correct image.
Interest was piqued, as combining this with the hint someone else commented pointed towards some bug in header encoding or decoding. Searching ‘round the block to see if anyone had made a JXL pattern for imhex turned up with this pull request on the patterns repo. A noble effort for sure, but a couple things with it didn’t work for my case:
-
It only matches against the box file format standard.
- A “bare” codestream is an equally valid JXL file, so this pattern would fail on those kinds of images.
- Corollary: it doesn’t do any fine-grained codestream decoding, which is what the entire bug is about.
I ~could~ have trawled through the libjxl source code with the stacktrace from someone else, but alas, the JXL standard is quite lengthy, and the source code of libjxl is quite sprawled1. It’d be folly to run through the bits myself or print at every stage of decoding every time a header bug pops up.
Given it all, I thought it’d be much faster, more useful to everyone, and kind of just a fun idea to make my own pattern for JXL. I’ve never made an imhex pattern before (or for that matter, I’ve barely used imhex at all before), so it’d be a simultaneous journey of learning imhex’s DSL and internalizing the JXL standard.
the file!
Cutting to the chase: You can get the pattern at my imhex patterns repo. While I would have wished that I could parse every bit of the format, the entropy coding parts2 are just a bit too complex to realistically implement in imhex’s not-that-much-of-a-programming language. As such, there’s a few deficiencies:
- Codestreams that have an embedded ICC pattern or a permuted TOC will return early with a warning.
- No fine-grained parsing is done of the actual frame groups—they’re just generic byte spans.
Despite those unimplemented paths, it seems to be one of the most complex imhex patterns that exist:
$ wc -l jxl.hexpat
1073 jxl.hexpat
$ wc -l /usr/share/imhex/patterns/* | sort -nr | head
5594 /usr/share/imhex/patterns/dicom.hexpat
1312 /usr/share/imhex/patterns/pe.hexpat
1140 /usr/share/imhex/patterns/3ds.hexpat
1121 /usr/share/imhex/patterns/java_class.hexpat
934 /usr/share/imhex/patterns/stdfv4.hexpat
921 /usr/share/imhex/patterns/dotnet_binaryformatter.hexpat
835 /usr/share/imhex/patterns/elf.hexpat
833 /usr/share/imhex/patterns/max_v104.hexpat
726 /usr/share/imhex/patterns/selinux.hexpat
Subtract about 150 lines from mine due to comments. They’re quite dense lines as well! Most of the lines for the other high rankers are taken up by huge lookup tables or pattern matches, while JXL just has a ton of conditional branching, bitfields, & types inherent to it.
It’s quite snazzy to see it in action:

And yes, in the end, I did end up fixing the bug with the help of this pattern. It was quite easy to see what caused it when I could quickly visualize that it happens on a byte boundary!
sidenote: jxl is crazier than I thought
I knew that JXL was The Future Image Format for a while now, but actually delving into the standard has made me realize just how much is done for the various use cases it wants to exist in.
For example, the codestream performs extensive bit packing techniques to squeeze out mere bytes. Is it storing width and height as 4 bytes each? nah, use 1 bit to determine if it’s a “small” image or not, and if it is, it read 5 bits for the height, then the real height is that multiplied by 8. Then read 3 bits to determine the “ratio”, which is a lookup table of common ratios. If it’s not any of those common ratios, then the width is encoded separately. Otherwise, no more bits are needed.3 The separate encoding isn’t just 4 bytes or whatever either; it uses two bits to multiplex four different other integer widths so that smaller images just use less space overall.4
In isolation, these things only shave off like, what, a couple tens of bytes? if you consider the entirety of the codestream’s complexity. But if you’re dealing with hundreds of thousands of low-resolution, standardized images (like, say, for thumbnailing or previewing), those bytes saved add up real fast.
That’s the kind of stuff the codestream does. It’s quite neat.
the pattern language…
Since this was my first imhex pattern, I made sure to scour the entire documentation and all the different avenues I could approach writing it from. While writing, I mentally noted aspects of the language that poked some thorns in my side. First off, things that jumped out at me:
-
I do not enjoy that variables are syntactically scoped like a conventional language. I have to use global variables to simultaneously track state and assign “default” values to fields, since variables in, say, an
ifblock are scoped to saidifblock.- I think the greatest “simple” thing that would make the language much better is if variables in a pattern were scoped to the pattern itself. This alone would eliminate most of the global variable duplication in the JXL pattern.
- In addition, being able to assign “default” values to memory-backed variables that would serve as its value in case it never gets matched in the file.
-
For some reason, template types don’t work on bitfields. However, non-type template parameters do? Very confusing.
-
This means I had to create five separate types for JXL’s
U32parameterizations, since they’re bit-level types.
-
This means I had to create five separate types for JXL’s
- I don’t understand why untagged unions exist when conditional parsing is a thing. the documentation only gives the use case of inspecting data as multiple types, but that could also be accomplished by a change to the pattern data viewer—some kind of format casting built in or something.
In an overall sense, the language feels like if C++ was kludged into a binary file pattern matcher, with a few minor rust-isms taken up along the way, rather than built from the ground up as its own DSL for its own domain.
So, it got me thinking about the language, and my mind wandered off many times along the way. A few scattered ideas came out of it all—some loose thoughts on how I’d want a pattern language to operate and feel like:
-
No treating bytes as something absolutely fundamental to the pattern language; bits and bytes should exist in harmony rather than separated by different types.
- As a result, there’s only one type for “collections” of things, and you can control whether it’s padded by default or bit-packed by default, with a quick way to make exceptions for individual fields.
-
Patterns are declarative rather than mimicing how an imperative language would go through something. This would alleviate cognitive disconnect when combined with pattern-scoped variables.
-
Combine this with “flattening” the syntax to further hound the idea that only pattern-level scopes matter. Say, instead of
if (parse_x) { x : u8 }, it’sx : u8 if parse_x.
-
Combine this with “flattening” the syntax to further hound the idea that only pattern-level scopes matter. Say, instead of
-
Variables are immutable; patterns can’t affect the file. A variable’s value is determined only by its memory or, if its condition for existing isn’t met, some other method of assigning a default value (adding on to the if?
x : u8 if parse_x else 0? Propagating sum types of non-existent variables?). - It would be cool if there was some kind of external plugin system that allowed more complex tasks to be done in a less domain-specific language (like, say, entropy decoding).
The overarching idea is just that matching patterns against a file would fit better as a more declarative (“functional”?) paradigm than how imhex operates. Alas, a complete, exact syntax escapes me, since I haven’t thought about it too much. I didn’t find much more enjoyment on thinking about what could be something when implementing it would be quite a large undertaking for little benefit (I don’t reverse engineer formats often enough, and imhex is already quite dominant, so would anyone even use a different pattern language?). But it’s nice to passively think about.
So many ideas, so little time.
-
Jon Sneyers said on the JXL Discord about it: “libjxl has never been a particularly elegant or clean codebase, it was born by smashing together the pik and fuif code and then doing lots of experimental stuff on top of that. I think it was a bit too ambitious to try to be at the same time a reference implementation, cutting edge in terms of both speed and compression performance, and production ready. I mean, I think it kind of achieves all of that, but not in a way that is easy to maintain or contribute improvements to.” ↩︎
-
Annex C of ISO/IEC 18181-1:2024 ↩︎
-
Section D.2 of ISO/IEC 18181-1:2024 ↩︎
-
Section B.2.2 of ISO/IEC 18181-1:2024 ↩︎