Sony ARW6 (LLVC v3) reverse-engineered decoder A developer has reverse-engineered and released an open-source decoder for Sony's ARW6 lossy raw format, used by the ILCE‑7RM6 and ILCE‑7M5 cameras, with the initial implementation done using Claude (Fable 5 and Opus 4.8). The decoder successfully processes APS-C and full-frame images and outputs DNG files, though the author notes potential errors in the format description. This is a decoder for Sony's ARW6 lossy raw format, used by at least ILCE‑7RM6 and ILCE‑7M5 when shooting lossy compressed raw lossless compressed ARW is a different, LJPEG-based format , along with a format description as far as I could figure it out. It successfully decodes APS-C and full-frame images from rawdb, along with the ones I took with my 7RM6. Most of the research and the initial implementation was done with Claude Fable 5 and Opus 4.8 . Unfortunately, I don't remember much about image compression, and this format gave me lots of trouble to understand the canvas was a pain , so I can't promise I haven't missed or misexplained something. Any errors made here are mine to blame on nix develop python -m arw6 proto FILE.ARW OUT.dng The image is split into one or several tiles with specified offsets, which can be decoded separately; their layout is described in the bitstream section. The pixels from the Bayer grid are rearranged into we call these components : | component | what it is | |---|---| green lo | the low‑pass half of the green channel | green hi | the high‑pass half of the green channel | chroma r | red pixels as a residual against the neighbouring greens | chroma b | blue pixels as a residual against the neighbouring greens | The green channel is split once by a single horizontal 5/3 DWT into green lo and green hi . This split is a transform in its own right and is not the 2‑D wavelet described below. The halves are then coded very differently: green lo : processed like chromas; green hi : stored as‑is. A red or blue pixel is stored as a residue from the two greens of its own 2×2 Bayer cell: one beside it, one above/below. After the image is split by channels, they are the two adjacent columns left and right . R and B are stored as: chroma r = R − mean g1, g2 / 2 chroma b = B − mean g1, g2 / 2 Green is stored as signed integers, offset by 2048. When we restore, we clip all values to 12 bit. The Bayer mosaic is then assembled decode.assemble tile : R = clip g1 + g2 //2 + 2·chroma r , 0, 4095 B = clip g1 + g2 //2 + 2·chroma b , 0, 4095 The bit depth seems to be a codec constant, 12 bits, and the component values are compressed. A fixed curve maps each code back to a linear sensor value. The curve is logarithmic Cineon / S‑Log family with a linear toe below a knee K : V c = c for c ≤ K V c = K − A + A·exp c − K /A for c K with two design inputs — the log rate A ≈ 658.4 and the white level VMAX = 39003 reached at the top code. The body above already meets the toe with slope 1 at the knee; K then follows from V 4095 = VMAX . These two constants were fit against a code-value table generated by running samples through Adobe DNG Converter scripts/delin fit table.csv ; the fit is scripts/fit delin curve.py . Adobe's actual curve is still a bit different and I couldn't figure out how exactly it is constructed. Delinearisation is the last decoding step, applied to the assembled mosaic. While green hi is stored as-is, green lo and the two chromas are each put through a 3‑level 2‑D 5/3 DWT. A 2‑D wavelet level filters both axes into Low/High, producing four sub‑bands named by horizontal, vertical : LL — low both ways: the blurry core, split again and stored at the next level; HL — vertical edges, LH — horizontal edges, HH — diagonal detail. The three high‑frequency sub‑bands HL, LH, HH are the orientations stored at each detail level; the LL is fed to the next level. After the coarsest level you are left with a single small LL . Numbering the detail levels finest‑first: component = D1 finest + D2 + D3 coarsest + LL └── detail levels, 3 orientations each ──┘ └ 1 orient Every one of D1 , D2 , D3 , LL is stored on its own; they differ only in how many orientations they hold — each detail level three, LL one. We call these regions ; a region holds its level's data for all the components that go through the DWT. The final LL is additionally stored as horizontal differences; an inverse DPCM undoes it pixelops.predict horizontal . Decoding combines the levels coarsest→finest idwt.idwt . There is a possible parity flip, see below. The region-component rows are split into chunks which are encoded separately. Also, some of the detail levels are merged at flipped parity during the IDWT the flips below . Both are derived from the number of detail levels and the tile height. Because none of this is stored on disk, we need to derive it too to decode properly. All regions use a shared canvas: Each orientation uses a lattice — a strided subset of this canvas's rows. The chunks are sized so that the coarsest lattice has one row per chunk: with three detail levels the coarsest step is 2³, so chunks are 8 canvas rows tall. Lattices can be odd or even; we know which orientations use which. Lattices correspond to the detail levels after the DWT; each level halves the resolution, so its lattice is twice as sparse. A lattice can be reused for different data then exactly the same layout is used for it . The coordinate axis is shared among regions. Two parameters are set: chunk origin — where does the first chunk start on the coordinates; comp top — where does the first row start on the coordinates. chunk origin is set to the first row of the coarsest level's detail odd lattice — half the coarsest step. comp top is set such that given a known tile height, the first and the last chunks are symmetrical in amount of data they have. To be clear, these coordinates never appear in the bitstream; the canvas only tells the decoder how many rows of each region to decode from each chunk. For instance, given the diagram above, the first chunk contains everything at canvas rows 10 and 11: - 2 green hi rows; - for D1: 1 HL row canvas row 10 and 1 LH and HH row each row 11 , for the 3 components green lo , chroma r , chroma b ; - for D2: 1 LH and HH row each, again per component — canvas row 10 is also on D2's odd lattice rows 2, 6, 10, … ; - no D3 or LL rows. Chunks are encoded separately by regions and components. An orientation only owns the lattice points that fall inside the real image. So, for a given lattice we calculate: top / bottom — the first and one‑past‑last lattice‑row indices that are inside the image; row base — the first lattice row at/after the chunk origin: chunk 0's window starts there. For 3 orientations, the lattices used are: - HL: even lattices; - LH, HH: odd lattices. Finally, the flips . DWT splits the rows into high and low; the order may be reversed. During IDWT, we must merge in the same order. The level's rows alternate between its two lattices; the merge is flipped when the topmost row inside the image falls on the odd lattice rather than the even one. We calculate the flip bit per detail level. The horizontal passes never flip. The one exception is recombining the green halves decode.combine green , which is always flipped, putting green hi on the even columns. As to why this canvas exists and why Sony doesn't just encode every region and component by chunks starting from row 0, I have no idea. Maybe this is somehow tied to how the encoder works. The encoder quantises the wavelet coefficients; this is the only lossy step. Each chunk carries its own per‑orientation quantiser q . Decoding dequantises with a mid‑rise reconstruction of each magnitude to its bin centre, mag + 0.5 ·2^q ; magnitude 0 stays 0, q == 0 means "no quantisation", signs are preserved. An .ARW is a TIFF file containing a compression tag value for ARW6 32766 in one of its IFDs. It can also contain a JPEG preview in a separate IFD. The standard TIFF geometry tags are used: | tag | meaning | |---|---| ImageWidth / ImageLength 0x100 / 0x101 | dimensions | StripOffsets / StripByteCounts 0x111 / 0x117 | location + size of the compressed strip | BlackLevel 0x7310, Sony‑private | black level per Bayer channel, R G1 G2 B expected uniform | WhiteLevel 0xc61d | white saturation level | WB RGGBLevels 0x7313, Sony‑private | white-balance level per Bayer channel, R G1 G2 B | The raw image is stored as a single strip StripOffsets 0 / StripByteCounts 0 . The black and white levels are stored halved I'm not sure why . See also: container.read tiff The image strip in TIFF terminology is split into independent, rectangular tiles, not to be confused with the TIFF tiles. The strip begins with a little-endian header describing them: strip: u32 count number of tiles u32 unknown unknown count × TileRecord: u64 offset byte offset of this tile's payload within the strip u32 x, y tile's top-left corner in the Bayer mosaic u32 w, h tile's size, in mosaic pixels Tiles do not necessarily have the same size — e.g. a 10016‑wide image may split 5024 + 4992 . Full‑frame files that I've observed have 4 tiles; APS‑C crop files have 1. Each tile's offset marks the start of its self‑contained compressed blob. See also: container.parse tiles Each tile starts with this header. Some sizes here and below are stored in blocks : units of 16 bytes. Everything is bit‑packed MSB‑first. tile blob: 0x00 sub-header block 0x10 bytes : u32 magic ASCII "0000" 0x30303030 u32 unknown0 unknown increments across a frame's tiles u16 width tile width, in mosaic pixels u16 comp height component height = tile height / 2 u32 unknown1 unknown 0x3d006e00 in every sample 0x10 region-totals area 0x20 bytes = 2 blocks : 5 × u24 total block count of each coding region 0x30 5 × record block 0x10 bytes each , one per coding region: u8 component count number of components in this region count × u24 block count of each component's data in this region 0x80 component data begins here The number of regions is not in the file and is always five, in this order: | region | contents | components | |---|---|---| | 0 | LL | 3 | | 1 | D3 — coarsest detail | 3 | | 2 | D2 | 3 | | 3 | D1 — finest detail | 3 | | 4 | green hi | 1 | Within the shared regions the components are stored in the order green lo , chroma r , chroma b . The region totals and per‑component sizes are plain sizes; the byte offset of any one component's data is the sum of everything before it. A region's total may be larger than the sum of its components' sizes — the next region then starts past the padding. Each region‑component starts with a 16‑byte header followed by a chunk table, then the entropy‑coded chunk data: component: 0x00 header block 0x10 bytes : u16 table blocks chunk-table size in blocks header + table = table blocks+1 blocks u24 chunk blocks entropy-chunk data size in blocks u8 unknown0 unknown constant format byte; 0x40 in every sample u2 orient count orientation count high bits of its byte : 1 = LL/green hi, 3 = detail u6 unknown1 unknown rest of the orientation-count byte — 0 in every sample, not assumed u16 chunk count number of chunk-table entries u8 unknown2 unknown constant format byte; 0x10 in every sample u48 padding 0x10 chunk table table blocks blocks : chunk count × chunk entry: u16 length byte length of chunk i 0 ⇒ empty chunk orient count × u4 q per-orientation quantiser, applied to that chunk's coefficients table blocks+1 ·0x10 entropy chunks begin here Chunk i is the data for the i ‑th 8‑row cut of the canvas component rows — 16 mosaic rows , anchored at chunk origin . A chunk length of 0 marks a chunk that has none of this component's rows e.g. the topmost chunks of the coarse regions, which fall in the margin "air" above the image . Each chunk is an independent bitstream holding its rows of each orientation in turn, in the HL, LH, HH order also the order of the q values in the chunk table . Each row is a line of ceil width / 4 groups of four coefficients the last group's excess coefficients are discarded , read with an adaptive Golomb‑Rice code bitstream.py : The Rice parameter k is updated with a variable-length code skipped after the line's last group : 0 keeps the current k ; 10 followed by a unary number zeroes ending with one raises it by number + 1; 11 followed by a unary number lowers it by number + 1, the run capped so k never drops below zero at the cap the terminating one is omitted . Each line starts by reading an update code for the Rice parameter k , initially 0. While k = 0 , the line is coded as runs of all‑zero groups an Elias‑gamma‑style exponent + mantissa , each run followed — unless it ends the line — by an unary code that raises k by at least 1. Otherwise, each group reads four k ‑bit values, then an update code for k , then one sign bit per non‑zero value is stored. The values are then dequantised with the chunk's own quantiser values from the chunk table. | module | role | |---|---| decode.py | top‑level decoder: decode tile one tile → mosaic and decode full mosaic | container.py | byte structure: TIFF/strip/tile parsing, the tile sub‑header, per‑component headers | geometry.py | the canvas coordinate frame and every region's derived geometry + component count | bitstream.py | Golomb‑Rice entropy decoding of the chunks | idwt.py | the inverse 5/3 wavelet IDWT | pixelops.py | dequant, LL predictor, delinearisation curve | utils.py | MSB bit readers, integer helpers | dngwriter.py | wrap a decoded mosaic into a mosaiced DNG | convert.py | the CLI python -m arw6 proto |