I gave my local LLM Adobe's closed-source converter, and it rebuilt the entire format from the bytes up

I gave my local LLM Adobe's closed-source converter, and it rebuilt the entire format from the bytes up

Published Sep 1, 2026, 4:00 PM EDT I’m Adam Conway, an Irish technology fanatic with a BSc in Computer Science and I'm XDA’s Lead Technical Editor. My Bachelor’s thesis was conducted on the viability of benchmarking the non-functional elements of Android apps and smartphones such as performance, and I’ve been working in the tech industry in some way or another since 2017. In my spare time, you’ll probably find me playing Counter-Strike or VALORANT, and you can reach out to me at adam@xda-developers.com, on Twitter as @AdamConwayIE, on Instagram as AdamConwayIE, or u/AdamConwayIE on Reddit. Sign in to your XDA account Adobe's DNG Converter is essentially a black box program; it takes in raw photos and converts them to Adobe's open-standard Digital Negative format, commonly known as DNG. There's no source code for Adobe’s converter, so while you can see exactly what goes in and what comes out, what happens in between is hidden. Z.ai recently launched GLM-5.3-Flash, an open weights model that initially launched covertly as Ox Alpha. It's a Mixture of Experts model that has 320 billion total parameters with 18 billion active, and I'm running the NVFP4 version across a two-machine DGX Spark cluster, driven with the Pi agent harness. I decided to put it to the test, and I wanted to see how it treated an open ended reverse engineering task. While this is a massive model, efforts are underway to get it running on more modest machines. Within a little over two hours it had a working Python proof of concept, which was already impressive, and its files were smaller than Adobe's own DNGs, too. The next morning, I followed up by asking for a Rust version, and a few hours later it had reimplemented all of it in roughly 2,900 lines across 13 Rust source files, including a batch CLI, self-verification, and a drag-and-drop GUI. The processed DNGs pass Adobe's own validator, decode to pixel-identical images compared to both the source ARW and Adobe's DNG, and when all is done, come out 0.95% smaller than Adobe's output. What the model was actually given A few images and a closed-source program I gave very little to the model to actually work with; it had access to five ARW files from a Sony A7 V, Adobe's converter, and the tools the Pi harness provided. I didn't give it a decompiler, and there's obviously no Adobe source to read, either. Instead, it was weirdly persistent: it settled on converting each file with Adobe's converter, comparing the input and output at the byte level, and then working backwards from the differences. People love to refer to "autonomous" AI, but that's genuinely what this was. Here's what I actually contributed to the conversation: it wrote 941 responses and 917 tool calls, and all I did was offer to give it extra ARWs because it couldn't reach files outside its project scope, I suggested trying smaller tiles when bigger ones lost, told it to compile Adobe's dng_validate, asked for the Rust port and the GUI, and reported to it what I could see in Affinity. I didn't give it any of the engineering, like its lossless-JPEG layout, its Huffman coder, its TIFF serializer, the Sony tag offsets, the digest algorithm, or the Rust architecture. A lot of models a year ago, especially local models, would have written a plausible-looking arw2dng.py that produced an unusable file, but the way GLM-5.3-Flash went about this was honestly remarkable. These Sony ARWs are already lossless JPEGs What Adobe does is already documented The first real finding by the model was that there's no lossy step anywhere in the pipeline. These Sony A7 V ARWs already store their raw data as 140 concatenated ITU-T T.81 lossless JPEG segments, one per 512x512 CFA block, and each of those is a 256x256 frame with four components carrying the R, G, G, and B planes at 14-bit precision. Sony labels the strip Compression=6, or old-style JPEG, and the data underneath is standard lossless JPEG. Adobe decodes all of that back to a single 14-bit Bayer image, crops it to the ActiveArea, 7030x4684 out of the 7168x5120 the file actually carries, and re-encodes it as tiles. The re-encode is where the size comes from. Each output tile is a two-component lossless JPEG where component one is the even columns and component two is the odd columns, so the predictors see a neighbour of the same colour instead of alternating between red and green on every sample. The model found this by parsing SOF3 headers individually, testing candidate layouts and discarding them one by one (side-by-side halves, top and bottom halves, row splits, row interleaving), and matching decoded values against reference pixels in Adobe's output. Then it explained why the winner worked, after all of that testing. To be clear, all of this is already documented, so it was likely somewhere in training data for all I know. Adobe's DNG Specification says under Compression value 7 that "it is common for CFA images to be encoded with a different width, length or component count to allow the JPEG compression predictors to work across like colors." FFmpeg's mjpegdec has actually handled this case since 2019 as well, so the model had rediscovered this through hex dumps, and it wasn't a novel finding by any stretch. But the second test was interesting. The second decoder is what made the results interesting A guiding light, even when referring to the wrong codec Early on, the model wrote a standalone T.81 decoder called "lj92.py", and its only job was to check the encoder. On its own it doesn't prove a whole lot, because if you build your encoder and run the output through your own decoder, you can just end up verifying that you're consistently wrong. Instead, it validated the decoder against Adobe's tile bytes first, and only once it could read Adobe's output correctly did it then use its own decoder on its own tiles. It opted to build a separately implemented check, so that it could consistently validate its results as it went. Then it used LibRaw, followed by Adobe's DNG SDK, which the model built from source after I asked for it to check, and it reported "Validation complete" on all five files. There's something rather strange about the way the model did all this, too. Throughout the session, the model called the codec ITU-T T.87... which is wrong. T.87 is JPEG-LS; SOF3 and the seven predictors it was implementing belong to T.81. Even still, all of the mechanics it followed to build the encoder came from the bytes themselves, so it never built the wrong thing. By using lj92.py as a guiding light, it allowed Adobe's tiles to be used for calibration, and both LibRaw and Adobe's SDK made further independent checks possible. It had empirically reconstructed the subset of T.81 these files exercised while consistently calling it the wrong standard. Those layers managed to catch a real, pretty dangerous bug. The encoder had been extended to choose the best of the seven T.81 predictors per tile, and it was writing the T.81 selection values in the wrong order relative to the standard. The predictions it computed were correct, but the labels were wrong, and since lj92.py shared the same misconception, the encoder and decoder agreed with each other while creating a non-conformant file. It was a good demonstration of why two pieces of code written separately aren't necessarily independent evidence when the same reasoning produced both. The model eventually found the discrepancy by reading LibRaw's ljpeg_row source against ITU-T T.81 Table H.1. I was curious how other models would fare with this task, so I had Claude Opus 5 reviewing the code separately as a second pair of eyes, and when it analyzed the same seven predictors, it labelled them in the same wrong order. It also recommended the same approach that GLM-5.3-Flash had explored and subsequently written off after reading LibRaw's source, and stated that GLM was wrong for writing it off in the first place. I wasn't trying to compare both models, but it meant my supposedly independent reviewer had arrived at the same misconception and, unlike GLM, was too stubborn to move away from it. GLM broke out of it when it checked its understanding against the source, and Opus 5 didn't. A similar thing happened again later, when GLM was testing a different tile layout. One of the builds produced files that dng_validate rejected, whereas rawpy could read it just fine and so could the project's own decoder. Two of the three checkers said the file was fine, while Adobe's said it wasn't. Its first claim was that Adobe was wrong, which wasn't entirely unreasonable. It said that "the spec explicitly allows component-count mismatch for CFA lossless JPEG, so 4-comp is legal, and the SDK failure is on its side, or a subtle bug." The spec does allow it, but instead, it read the SDK's inner MCU loop, its first-row predictor path, and both of the places it throws a format error, decided that "the SDK’s decode logic matches our model", and restructured its own encoder so the component count became a single constant it could flip with everything else held identical. It then ran an experiment, only to find it had broken the file itself, with a missing Se byte in the SOS marker. It identified this, totally unprompted, and fixed it for every layout afterwards. The project's decoder and LibRaw both tolerated the malformed SOS marker, but Adobe's SDK checked its length and rejected it. Even though there were two checkers against one, the model still went looking for its own mistake before anyone else's. Pixel-identical files can still render completely wrong What pixels can't see Around the time the Rust port finished, I opened one of its DNGs in Affinity, and the entire thing was green. Every automated check was still passing, though, and the CFA data was bit-identical to the source ARW. dng_validate was happy with the file, and rawpy agreed that the output was good, too. The problem, it turned out, was AsShotNeutral. The Rust port was using a gray-world estimate rather than reading the camera's actual white balance, and raw pixel equality structurally can't see the difference. White balance is metadata applied during development rather than being baked into the raw CFA samples, so the model traced Sony's per-shot levels to raw tag 0x7313 (WB_RGGBLevels) and switched the neutral calculation to [G/R, 1, G/B]. Green gone. After that, it was still slightly too dark, and Affinity showed 5419K against Adobe’s 5489K on the same shot. That was BaselineExposure, which Adobe writes as 0.35 for this camera to tell any reader these files need a third of a stop of lift. Affinity honours it, but GLM's files didn't contain it, so our file rendered exactly 0.35EV darker every time. GLM's suggestion at that point was to leave the residual Kelvin gap alone, on the basis that matching Adobe's private Sony block byte for byte would be a lot of reverse-engineering effort for marginal gain. I disagreed, because Affinity was clearly calculating that number from something deterministic, and after all, you can't claim parity if the file is interpreted differently. GLM investigated, and it turns out that Adobe's DNGPrivateData has an "Adobe\0" header followed by framed blocks: MakN, a big-endian block length, an II byte-order marker, the big-endian offset the block sat at in the source file, then Sony's maker note, with an SR2 block carrying the SR2 root IFD the same way. Reconstructing that is pretty messy given that every offset inside Sony's structures is relative to where the block used to live in a file it's no longer in. The model eventually figured out how to rebuild it, and its DNGPrivateData now matches all 80,664 bytes of Adobe's output exactly. The last rendering difference was OpcodeList3, where Adobe keeps WarpRectilinear lens-correction opcodes, which the model decoded the coefficients of across the reference files and looked for the inputs. Sony's static distortion tables at 0x7841 and 0x7842 were byte-for-byte identical across all five files, two of the shots used the same lens at the same 150mm focal length, and Adobe still produced different coefficients for them. Those static tables, therefore, aren't sufficient to reproduce Adobe's output, and the files don't reveal exactly what else Adobe is doing. At this point, I thought the model was going to fabricate something. It had been working on this through several compactions, and I'm used to a lot of models fabricating details rather than actually working out the answers. Instead, it noted it in the documentation for the project as something to come back to in the future. The converter beats Adobe's numbers And it explored more interesting options, too The final encoder uses 160x160 tiles rather than Adobe's default of 256x256, and this was one optimization I prompted rather than something the model discovered by itself. I had noticed that bigger tiles produced larger files, so I suggested trying 128x128. That was smaller, and the model then swept several sizes from 64x64 through 256x256, eventually settling on 160x160 as the most efficient. Those five ARW files, taking up 208,789,504 bytes, came out as 170,389,988 byte DNG files. Compared to the 172,029,380 bytes from Adobe, it had managed to achieve a compression ratio that was 0.953% smaller. For five images, with multithreading, it took 4.12 seconds to complete, which was 19.30 seconds aggregate CPU time. The separate Claude review also flagged what looked like a problem here: it said Adobe's DNGs contained a 1024x683 JPEG preview that GLM had omitted, which would have made the size comparison unfair. I checked, and that wasn't the case. GLM's files did contain previews, and they were actually larger at 1616x1080 compared to Adobe’s 1024x683. Adobe wasn't actually the strongest encoder I could have measured against, though, and it was just a test to see what GLM would do when given a binary while being told to rebuild it. dnglab, the Rust converter built on rawler, contains --ljpeg92-predictor 1 and compresses the files to 172,776,166 bytes, slightly bigger than Adobe's. With --ljpeg92-predictor 7, though, it drops to 168,346,554, which is 2.1% under Adobe and 1.2% under this project, with every file decoding losslessly and matching Adobe's DNG over the shared cropped region. It manages that while carrying more data, not less, because it keeps the full 7168x5120 sensor frame instead of cropping, so it encodes 11.4% more samples and still wins. Its own default embeds the entire original ARW inside the DNG, which produces a file bigger than the input, so I turned that off. That gain comes from the frame layout, not the predictor. In a 2x2 Bayer pattern, the same colour repeats every two columns and every two rows, and the even/odd column split only exploits one of those axes: inside a component the left neighbour is the same colour, but the one above is a row up and therefore the wrong colour. rawler pairs image rows together before encoding so that the vertical neighbour is two rows up and the same colour again, which turns (Ra + Rb)/2 into the right answer for nine tiles in ten. In a one-file cost comparison, the row-paired layout was worth about 2.1%, while GLM's per-tile predictor search was worth another 0.63%. GLM had found another way to exploit the same pattern, too, identifying a four-component layout that does it a different way measured 1.8% better, but it threw it out. Its own note on why is particularly interesting: "verify uses our own decoder, which knows the intended layout," then, after testing, "LibRaw writes 4-component scans linearly into the tile. The SDK likely does the same. That's why Adobe uses 2 components, it's the interoperable convention." It stopped in the middle of building a file only its own decoder could read, and it chose interoperability over 1.8% without me prompting it. I rebuilt that layout to check, and it's definitely the right call: LibRaw can recover all of the sample values correctly but puts them in the wrong places, and Adobe's validator fails against it. GLM-5.3-Flash made the right call, but it measured the opportunity incorrectly: rawler's row-pairing achieves something similar with a component assignment that does survive validation. Even with those caveats, I'm incredibly impressed, and the results were honestly kind of shocking. With just five photos to learn from and a single binary from Adobe, I now have a Rust-based converter for my ARW files that beats Adobe’s own compression ratio and only loses by 1.2% to a project with years of development and many contributors behind it. The compression ratio isn't really the part that impressed me most, though. Over the course of the project, GLM built checks that agreed with each other and were still wrong, blamed Adobe and then discovered Adobe was right, found a more efficient layout and discarded it because other decoders couldn't interpret it correctly, and eventually reached a part of Adobe's output that it couldn't explain. Instead of making an answer up, it saved the limitation for later review. The program works, but the more interesting result is how often the model worked out which of its own results not to trust.

Original Source

Read the full article at Xda-developers →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.