Skip to main content

manifold/
mnf6.rs

1// MNF6/MNF7 — formats v2/v3 (README §9 v0.7/v0.8/v1.1 payload).
2//
3// MNF6 = MNF5 tile records + per-image coding selections:
4//   header (28 bytes): "MNF6", u32 LE width, height, tile=16, tile_count,
5//     payload_len, u32 flags.
6//   flags bit0 = planar RGB (mode 0 literals + mode 1/2 residual payloads
7//     carry [R][G][B] planes instead of interleaved),
8//   flags bit1 = zstd stream coder instead of zlib,
9//   bits 2..31 reserved, MUST be 0.
10// Modes: 0 literal (flagged layout), 1 exact reference, 2 geometric
11// reference (D8, MNF5 semantics), 3 YCoCg-R bit-packed literal:
12//   [3][n/3 bytes Y plane][ceil(9(n/3)/8) bytes co9][same cg9]
13// co/cg are biased +256 and LSB-first packed; 9-bit planes make YCoCg-R
14// losslessly packable (the MNF5 finding). Reconstruction:
15//   tmp = y - (cg>>1); g = tmp + cg; b = tmp - (co>>1); r = tmp + co - (co>>1)
16// Encoder is rate-aware (v0.8): builds the tile stream under each of the 4
17// flag variants, each tile choosing its mode by that variant's proxy cost;
18// the smallest final stream is emitted with its flags.
19
20use std::io::Read;
21
22use crate::codec::{apply_transform, div_ceil, tile_sig, Atlas, Dims, RANK_CANDIDATES, TILE_SIZE};
23use crate::error::{Error, Result};
24use flate2::write::ZlibEncoder;
25use flate2::Compression;
26use std::io::Write;
27
28/// Header size: `MNF6`/`MNF7` magic + 6×`u32` LE.
29pub const HEADER6: usize = 28;
30/// Record bodies packed channel-planar.
31pub const FLAG_PLANAR: u32 = 1;
32/// zstd container (MNF7); MNF6 = zlib.
33pub const FLAG_ZSTD: u32 = 2;
34/// Bits above bit1 reserved (MNF6/7 masks frozen).
35pub const FLAGS_RESERVED: u32 = !0b11;
36
37/// Entropy coder choice for the record stream.
38pub enum Coder {
39    /// zlib level 9 container (MNF6 header magic).
40    Zlib,
41    /// zstd at the given level (MNF7 header magic).
42    Zstd(i32),
43}
44
45/// Compress the full record stream with the chosen coder.
46pub fn compress_stream(coder: &Coder, buf: &[u8]) -> Vec<u8> {
47    match coder {
48        Coder::Zlib => {
49            let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
50            e.write_all(buf).expect("zlib encode");
51            e.finish().expect("zlib finish")
52        }
53        Coder::Zstd(level) => {
54            let mut e = zstd::stream::Encoder::new(Vec::new(), *level).expect("zstd");
55            // corruption must reject: enable the xxh64 frame checksum (zlib has Adler32 always)
56            e.include_checksum(true).expect("zstd checksum flag");
57            e.write_all(buf).expect("zstd write");
58            e.finish().expect("zstd finish")
59        }
60    }
61}
62
63/// Local cost proxy used to rank candidates (not a global optimum).
64pub fn proxy_cost(coder: &Coder, buf: &[u8]) -> u64 {
65    match coder {
66        Coder::Zlib => compress_stream(coder, buf).len() as u64,
67        // probe at level 3 for speed; final stream uses level 19
68        Coder::Zstd(_) => compress_stream(&Coder::Zstd(3), buf).len() as u64,
69    }
70}
71
72// --- 9-bit plane packing (LSB-first) -----------------------------------------
73
74/// Pack 9-bit values LSB-first (YCoCg chroma).
75pub fn pack9(values: &[u16]) -> Vec<u8> {
76    let mut out = Vec::with_capacity((values.len() * 9).div_ceil(8) + 1);
77    let mut acc: u32 = 0;
78    let mut bits = 0u32;
79    for &v in values {
80        debug_assert!(v < 512, "9-bit overflow");
81        acc |= (v as u32) << bits;
82        bits += 9;
83        while bits >= 8 {
84            out.push((acc & 0xFF) as u8);
85            acc >>= 8;
86            bits -= 8;
87        }
88    }
89    if bits > 0 {
90        out.push((acc & 0xFF) as u8);
91    }
92    out
93}
94
95/// Inverse of `pack9` for `n` values.
96pub fn unpack9(bytes: &[u8], n: usize) -> Result<Vec<u16>> {
97    let need = (n * 9).div_ceil(8);
98    if bytes.len() < need {
99        return Err(Error::fmt_msg("9-bit plane truncated"));
100    }
101    let mut out = Vec::with_capacity(n);
102    let mut acc: u32 = 0;
103    let mut bits = 0u32;
104    let mut bi = 0usize;
105    for _ in 0..n {
106        while bits < 9 {
107            if bi >= bytes.len() {
108                return Err(Error::fmt_msg("9-bit plane truncated"));
109            }
110            acc |= (bytes[bi] as u32) << bits;
111            bi += 1;
112            bits += 8;
113        }
114        out.push((acc & 0x1FF) as u16);
115        acc >>= 9;
116        bits -= 9;
117    }
118    Ok(out)
119}
120
121// --- YCoCg-R -------------------------------------------------------------------
122
123/// Integer YCoCg forward (deterministic; `y` + 9-bit chroma).
124pub fn ycocg_forward(rgb: &[u8]) -> (Vec<u8>, Vec<u16>, Vec<u16>) {
125    let n = rgb.len() / 3;
126    let (mut y, mut co, mut cg) = (
127        Vec::with_capacity(n),
128        Vec::with_capacity(n),
129        Vec::with_capacity(n),
130    );
131    for px in rgb.chunks_exact(3) {
132        let (r, g, b) = (px[0] as i32, px[1] as i32, px[2] as i32);
133        let co_v = r - b;
134        let tmp = b + (co_v >> 1);
135        let cg_v = g - tmp;
136        let y_v = tmp + (cg_v >> 1);
137        debug_assert!((0..=255).contains(&y_v), "y {y_v}");
138        debug_assert!((-255..=255).contains(&co_v));
139        debug_assert!((-255..=255).contains(&cg_v));
140        y.push(y_v as u8);
141        co.push((co_v + 256) as u16);
142        cg.push((cg_v + 256) as u16);
143    }
144    (y, co, cg)
145}
146
147/// Integer YCoCg inverse (exact counterpart of `ycocg_forward`).
148pub fn ycocg_inverse(y: &[u8], co: &[u16], cg: &[u16]) -> Vec<u8> {
149    let n = y.len();
150    let mut out = vec![0u8; n * 3];
151    for i in 0..n {
152        let y_v = y[i] as i32;
153        let co_v = co[i] as i32 - 256;
154        let cg_v = cg[i] as i32 - 256;
155        let tmp = y_v - (cg_v >> 1);
156        let g = tmp + cg_v;
157        let b = tmp - (co_v >> 1);
158        let r = tmp + co_v - (co_v >> 1);
159        out[i * 3] = r.clamp(0, 255) as u8;
160        out[i * 3 + 1] = g.clamp(0, 255) as u8;
161        out[i * 3 + 2] = b.clamp(0, 255) as u8;
162    }
163    out
164}
165
166// --- layouts ----------------------------------------------------------------------
167
168/// Split interleaved RGB into three planes (planar packing).
169pub fn planar_pack(data: &[u8]) -> Vec<u8> {
170    let n = data.len() / 3;
171    let mut out = Vec::with_capacity(data.len());
172    for c in 0..3 {
173        for i in 0..n {
174            out.push(data[i * 3 + c]);
175        }
176    }
177    out
178}
179
180/// Reassemble three planes into interleaved RGB.
181pub fn planar_unpack(data: &[u8]) -> Vec<u8> {
182    let n = data.len() / 3;
183    let mut out = vec![0u8; data.len()];
184    for c in 0..3 {
185        for i in 0..n {
186            out[i * 3 + c] = data[c * n + i];
187        }
188    }
189    out
190}
191
192// --- MNF7: modular 8-bit color-domain lifting (ADR 2026-10-25) ---------------
193// NOT standard signed YCoCg-R (that is MNF6 mode 3 with 9-bit chroma planes).
194// Modular variant: every step is a u8 bijection, `>>1` is a logical shift on
195// the wrapped byte. Encoder evaluates reference residuals in BOTH domains
196// per candidate and the mode byte tells the decoder which to use:
197//   mode 4 = exact reference, residual in modular-color domain
198//   mode 5 = geometric reference, residual in modular-color domain
199// Decode: base_t = T(G_t(P_hat)); pix = T^-1(base_t + resid mod 256).
200// Atlas patches stay canonical RGB8 (ADR integration rule).
201
202/// Modular YCoCg color transform (MNF7 color modes 4/5).
203pub mod modcolor {
204    /// Modular YCoCg-like forward: (r,g,b) -> (y, co, cg), all mod 256.
205    pub fn forward(rgb: [u8; 3]) -> [u8; 3] {
206        let [r, g, b] = rgb;
207        let co = r.wrapping_sub(b);
208        let t = b.wrapping_add(co >> 1);
209        let cg = g.wrapping_sub(t);
210        let y = t.wrapping_add(cg >> 1);
211        [y, co, cg]
212    }
213
214    /// Exact inverse: reverse sequence of the forward steps.
215    pub fn inverse(ycocg: [u8; 3]) -> [u8; 3] {
216        let [y, co, cg] = ycocg;
217        let t = y.wrapping_sub(cg >> 1);
218        let g = cg.wrapping_add(t);
219        let b = t.wrapping_sub(co >> 1);
220        let r = b.wrapping_add(co);
221        [r, g, b]
222    }
223}
224
225/// Forward transform, one tile (bytes).
226pub fn modcolor_forward(rgb: &[u8]) -> Vec<u8> {
227    let mut out = vec![0u8; rgb.len()];
228    for (o, px) in out.chunks_exact_mut(3).zip(rgb.chunks_exact(3)) {
229        let t = modcolor::forward([px[0], px[1], px[2]]);
230        o[0] = t[0];
231        o[1] = t[1];
232        o[2] = t[2];
233    }
234    out
235}
236
237/// Forward transform, iterator-friendly (avoids re-collect).
238pub fn modcolor_forward_iter(rgb: &[u8]) -> Vec<u8> {
239    let mut out = vec![0u8; rgb.len()];
240    for (o, px) in out.chunks_exact_mut(3).zip(rgb.chunks_exact(3)) {
241        let t = modcolor::forward([px[0], px[1], px[2]]);
242        o[0] = t[0];
243        o[1] = t[1];
244        o[2] = t[2];
245    }
246    out
247}
248
249/// Inverse transform (exact).
250pub fn modcolor_inverse(ycocg: &[u8]) -> Vec<u8> {
251    let mut out = vec![0u8; ycocg.len()];
252    for (o, px) in out.chunks_exact_mut(3).zip(ycocg.chunks_exact(3)) {
253        let t = modcolor::inverse([px[0], px[1], px[2]]);
254        o[0] = t[0];
255        o[1] = t[1];
256        o[2] = t[2];
257    }
258    out
259}
260
261/// MNF6/7 encode statistics.
262#[derive(Debug, Clone, Default)]
263pub struct MnF6Stats {
264    /// Mode-0 literals.
265    pub literal: u32,
266    /// Mode-3 YCoCg literals.
267    pub ycocg_literal: u32,
268    /// Exact references (t=0).
269    pub ref_exact: u32,
270    /// Geometric references (t≠0).
271    pub ref_geom: u32,
272    /// Color-domain exact references.
273    pub ref_exact_color: u32,
274    /// Color-domain geometric references.
275    pub ref_geom_color: u32,
276    /// Geometric refs with nontrivial transform.
277    pub geom_nontrivial: u32,
278    /// Flags id measured best for this stream.
279    pub chosen_flags: u32,
280    /// Per-tile chosen mode, scan order.
281    pub modes: Vec<u8>,
282    /// Per-tile (source index, transform) when referenced.
283    pub refs: Vec<Option<(u32, u8)>>,
284}
285
286enum TileChoice {
287    Literal(Vec<u8>),
288    YCoCg(Vec<u8>),
289    Exact(u32, Vec<u8>),
290    Geom(u32, u8, Vec<u8>),
291    ExactColor(u32, Vec<u8>),    // MNF7 mode 4
292    GeomColor(u32, u8, Vec<u8>), // MNF7 mode 5
293}
294
295/// Which flags to try, in order; returns (flags_value, planar, coder_ref).
296fn flag_variants() -> Vec<(u32, bool, Coder)> {
297    vec![
298        (0, false, Coder::Zlib),
299        (FLAG_ZSTD, false, Coder::Zstd(19)),
300        (FLAG_PLANAR, true, Coder::Zlib),
301        (FLAG_PLANAR | FLAG_ZSTD, true, Coder::Zstd(19)),
302    ]
303}
304
305/// Encode RGB8 pixels as MNF7, choosing coding flags by measured stream size.
306/// Supersedes encode_mnf6 (same flags + modes 0-3; adds color-domain modes 4/5).
307pub fn encode_mnf7(pixels: &[u8], dims: Dims, use_geom: bool) -> (Vec<u8>, MnF6Stats) {
308    let tiles = crate::codec::split_tiles(pixels, dims);
309    // ponytail: only the best variant is kept — earlier drafts retained all
310    // four; the stats of the winner are the only ones callers read.
311    let mut best: Option<(Vec<u8>, u32, MnF6Stats)> = None;
312
313    for (flags, planar, coder) in flag_variants() {
314        let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
315        let mut stats = MnF6Stats::default();
316        let mut atlas = Atlas::new();
317
318        for tile in &tiles {
319            let sig = tile_sig(tile.width, tile.height, &tile.data);
320            let pack = |rgb: &[u8]| -> Vec<u8> {
321                if planar {
322                    planar_pack(rgb)
323                } else {
324                    rgb.to_vec()
325                }
326            };
327            let n = tile.nbytes();
328
329            let lit_body = pack(&tile.data);
330            let mut choice: TileChoice = TileChoice::Literal(lit_body.clone());
331            let mut best_cost = 1 + proxy_cost(&coder, &lit_body);
332
333            // YCoCg-R literal candidate
334            {
335                let (cy, cco, ccg) = ycocg_forward(&tile.data);
336                let mut m3 = Vec::with_capacity(n + 160);
337                m3.extend_from_slice(&cy);
338                m3.extend_from_slice(&pack9(&cco));
339                m3.extend_from_slice(&pack9(&ccg));
340                let m3_cost = 1 + proxy_cost(&coder, &m3);
341                if m3_cost < best_cost {
342                    best_cost = m3_cost;
343                    choice = TileChoice::YCoCg(m3);
344                }
345            }
346
347            // atlas candidates
348            let transforms: &[u8] = if use_geom && tile.width == tile.height {
349                &[0, 1, 2, 3, 4, 5, 6, 7]
350            } else {
351                &[0, 1, 2, 3]
352            };
353            let do_atlas = tiles.len() > 1;
354            if do_atlas {
355                for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
356                    let (_, _, ref_rgb) = &atlas.patches[prior as usize];
357                    for &t in transforms {
358                        let base = if t == 0 {
359                            ref_rgb.clone()
360                        } else {
361                            apply_transform(t, tile.width, tile.height, ref_rgb)
362                        };
363                        let mut r = vec![0u8; n];
364                        for i in 0..n {
365                            r[i] = tile.data[i].wrapping_sub(base[i]);
366                        }
367                        let body = pack(&r);
368                        let meta: u64 = if t == 0 { 5 } else { 6 };
369                        let cost = meta + proxy_cost(&coder, &body);
370                        if cost < best_cost {
371                            best_cost = cost;
372                            choice = if t == 0 {
373                                TileChoice::Exact(prior, body)
374                            } else {
375                                TileChoice::Geom(prior, t, body)
376                            };
377                        }
378                        // MNF7 color-domain candidate: residual of T(tile) - T(base)
379                        let mut r_c = vec![0u8; n];
380                        let tile_t = modcolor_forward(&tile.data);
381                        let base_t = modcolor_forward(&base);
382                        for i in 0..n {
383                            r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
384                        }
385                        let body_c = pack(&r_c);
386                        let cost_c = meta + 1 + proxy_cost(&coder, &body_c);
387                        if cost_c < best_cost {
388                            best_cost = cost_c;
389                            choice = if t == 0 {
390                                TileChoice::ExactColor(prior, body_c)
391                            } else {
392                                TileChoice::GeomColor(prior, t, body_c)
393                            };
394                        }
395                    }
396                }
397            }
398
399            match &choice {
400                TileChoice::Literal(_) => {
401                    stats.literal += 1;
402                    stats.modes.push(0);
403                    stats.refs.push(None);
404                    raw.push(0u8);
405                }
406                TileChoice::YCoCg(_) => {
407                    stats.ycocg_literal += 1;
408                    stats.modes.push(3);
409                    stats.refs.push(None);
410                    raw.push(3u8);
411                }
412                TileChoice::Exact(p, _) => {
413                    stats.ref_exact += 1;
414                    stats.modes.push(1);
415                    stats.refs.push(Some((*p, 0)));
416                    raw.push(1u8);
417                    raw.extend_from_slice(&p.to_le_bytes());
418                }
419                TileChoice::Geom(p, t, _) => {
420                    stats.ref_geom += 1;
421                    if *t != 0 {
422                        stats.geom_nontrivial += 1;
423                    }
424                    stats.modes.push(2);
425                    stats.refs.push(Some((*p, *t)));
426                    raw.push(2u8);
427                    raw.extend_from_slice(&p.to_le_bytes());
428                    raw.push(*t);
429                }
430                TileChoice::ExactColor(p, _) => {
431                    stats.ref_exact_color += 1;
432                    stats.modes.push(4);
433                    stats.refs.push(Some((*p, 0)));
434                    raw.push(4u8);
435                    raw.extend_from_slice(&p.to_le_bytes());
436                }
437                TileChoice::GeomColor(p, t, _) => {
438                    stats.ref_geom_color += 1;
439                    if *t != 0 {
440                        stats.geom_nontrivial += 1;
441                    }
442                    stats.modes.push(5);
443                    stats.refs.push(Some((*p, *t)));
444                    raw.push(5u8);
445                    raw.extend_from_slice(&p.to_le_bytes());
446                    raw.push(*t);
447                }
448            }
449            match &choice {
450                TileChoice::Literal(b) | TileChoice::YCoCg(b) => raw.extend_from_slice(b),
451                TileChoice::Exact(_, b)
452                | TileChoice::Geom(_, _, b)
453                | TileChoice::ExactColor(_, b)
454                | TileChoice::GeomColor(_, _, b) => raw.extend_from_slice(b),
455            }
456            atlas.insert(tile);
457        }
458
459        let payload = compress_stream(&coder, &raw);
460        let mut file = Vec::with_capacity(HEADER6 + payload.len());
461        file.extend_from_slice(b"MNF7");
462        file.extend_from_slice(&dims.width.to_le_bytes());
463        file.extend_from_slice(&dims.height.to_le_bytes());
464        file.extend_from_slice(&TILE_SIZE.to_le_bytes());
465        file.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
466        file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
467        file.extend_from_slice(&flags.to_le_bytes());
468        file.extend_from_slice(&payload);
469        let size = file.len();
470        if best.as_ref().is_none_or(|b| size < b.0.len()) {
471            stats.chosen_flags = flags;
472            best = Some((file, flags, stats));
473        }
474    }
475
476    let (file, flags, mut stats) = best.expect("at least one variant");
477    stats.chosen_flags = flags;
478    (file, stats)
479}
480
481// --- decoding --------------------------------------------------------------------
482
483/// Configurable ceilings for MNF6/MNF7/MNF8 decoding (mirror of
484/// codec::DecodeLimits; the same limit semantics at the header-validation
485/// points). Defaults = the format-derived maxima (identical to codec's).
486#[derive(Debug, Clone, Copy)]
487pub struct DecodeLimits {
488    /// Max total pixels (w*h).
489    pub max_pixels: u64,
490    /// Max tiles.
491    pub max_tiles: u32,
492    /// Max decompressed payload bytes.
493    pub max_decompressed: u64,
494}
495
496impl Default for DecodeLimits {
497    fn default() -> Self {
498        DecodeLimits {
499            max_pixels: 1 << 26,
500            max_tiles: u32::MAX,
501            max_decompressed: u64::MAX, // replaced by the checked per-file bound
502        }
503    }
504}
505
506/// Decode MNF6/7 with configurable ceilings (library API; decode_mnf7 is the
507/// defaults wrapper).
508pub fn decode_mnf7_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
509    decode_mnf7_inner(buf, lim)
510}
511
512/// Decode MNF6/7 (magic auto) with default ceilings.
513pub fn decode_mnf7(buf: &[u8]) -> Result<(Dims, Vec<u8>)> {
514    decode_mnf7_inner(buf, &DecodeLimits::default())
515}
516
517fn decode_mnf7_inner(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
518    if buf.len() < HEADER6 {
519        return Err(Error::fmt_msg("short"));
520    }
521    if buf[0..4] != *b"MNF7" {
522        return Err(Error::fmt_msg("magic"));
523    }
524    let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
525    let width = u32le(4);
526    let height = u32le(8);
527    if width == 0 || height == 0 {
528        return Err(Error::fmt_msg("dims"));
529    }
530    if width as u64 * height as u64 > lim.max_pixels {
531        return Err(Error::Limits {
532            which: "max_pixels",
533            limit: lim.max_pixels,
534            actual: width as u64 * height as u64,
535        });
536    }
537    if u32le(12) != TILE_SIZE {
538        return Err(Error::fmt_msg("tilesize"));
539    }
540    let cols = div_ceil(width, TILE_SIZE);
541    let rows = div_ceil(height, TILE_SIZE);
542    let tile_count = u32le(16);
543    if tile_count != cols * rows {
544        return Err(Error::fmt_msg("tilecount"));
545    }
546    if tile_count > lim.max_tiles {
547        return Err(Error::Limits {
548            which: "max_tiles",
549            limit: u64::from(lim.max_tiles),
550            actual: u64::from(tile_count),
551        });
552    }
553    let payload_len = u32le(20);
554    if payload_len > (1 << 31) - 1 {
555        return Err(Error::fmt_msg("payloadlen"));
556    }
557    if buf.len() != HEADER6 + payload_len as usize {
558        return Err(Error::fmt_msg("filelen"));
559    }
560    let flags = u32le(24);
561    if flags & FLAGS_RESERVED != 0 {
562        return Err(Error::fmt_msg("reserved-flags"));
563    }
564    let planar = flags & FLAG_PLANAR != 0;
565    let zlib_in = flags & FLAG_ZSTD == 0;
566
567    let zlib_data = &buf[HEADER6..];
568    // Max record size, over all modes:
569    //   literal/refs:      1 mode + 5 refmeta + 768 bytes
570    //   mode 3 (YCoCg-R):  1 mode + np bytes Y + 2 * ceil(9*np/8) bytes, np <= 256
571    //                      => 1 + 256 + 2 * 288 = 833 (larger than the 776
572    //                      implied by the other modes; previously undercounted,
573    //                      which rejected all-mode-3 files as bombs — audit fix)
574    let mode3_max: u64 = 1 + 256 + 2 * u64::from((256u32 * 9).div_ceil(8));
575    let per_tile_max = 1 + 5 + (TILE_SIZE * TILE_SIZE * 3) as u64;
576    let limit = tile_count as u64 * per_tile_max.max(mode3_max) + per_tile_max + mode3_max;
577    let raw: Vec<u8> = if zlib_in {
578        let mut d = flate2::read::ZlibDecoder::new(zlib_data).take(limit + 1);
579        let mut v = Vec::new();
580        d.read_to_end(&mut v)
581            .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
582        v
583    } else {
584        let d = zstd::stream::read::Decoder::new(zlib_data)
585            .map_err(|_| Error::Decompress("zstd init"))?;
586        // same window cap as MNF8: level 19 uses <= 8 MiB; a hostile frame
587        // must not claim the 128 MiB library default (review fix 4)
588        let mut d = d;
589        d.window_log_max(23)
590            .map_err(|_| Error::Decompress("zstd window param"))?;
591        let mut d = d.take(limit + 1);
592        let mut v = Vec::new();
593        d.read_to_end(&mut v)
594            .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
595        v
596    };
597    if raw.len() as u64 > limit {
598        return Err(Error::fmt_msg("bomb"));
599    }
600
601    let pixels = decode_record_walk(&raw, width, height, tile_count, planar, false)?;
602    Ok((Dims { width, height }, pixels))
603}
604
605/// Shared MNF7/MNF8 record walk: decodes tile records (modes 0-5) over an
606/// already-decompressed `raw` stream into a width*height RGB8 buffer
607/// (extracted from decode_mnf7 for MNF8 reuse; semantics frozen — see
608/// docs/format.md §3/§MNF7).
609pub fn decode_record_walk(
610    raw: &[u8],
611    width: u32,
612    height: u32,
613    tile_count: u32,
614    planar: bool,
615    med: bool,
616) -> Result<Vec<u8>> {
617    let cols = div_ceil(width, TILE_SIZE);
618    let mut pixels = vec![0u8; width as usize * height as usize * 3];
619    let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
620    let mut ip = 0usize;
621    for i in 0..tile_count {
622        let row = i / cols;
623        let col = i % cols;
624        let tw = TILE_SIZE.min(width - col * TILE_SIZE);
625        let th = TILE_SIZE.min(height - row * TILE_SIZE);
626        let n = (tw * th * 3) as usize;
627
628        if ip >= raw.len() {
629            return Err(Error::fmt_msg(format!("eof at tile {i}")));
630        }
631        let mode = raw[ip];
632        ip += 1;
633
634        let tile = match mode {
635            0 => {
636                if ip + n > raw.len() {
637                    return Err(Error::fmt_msg("body"));
638                }
639                let body = raw[ip..ip + n].to_vec();
640                ip += n;
641                let body = if planar { planar_unpack(&body) } else { body };
642                if med {
643                    // FLAG_PRED_MED (MNF8 bit2): body = (X - MED(X_causal)) mod
644                    // 256. Context discipline (the audit bug): IN-TILE causal
645                    // neighbors live in `recon` (only blitted into `pixels`
646                    // after the whole tile reconstructs); OUT-OF-TILE
647                    // neighbors (above tile-row or left tile-column) are
648                    // already decoded in `pixels` (scan order guarantees the
649                    // earlier tiles finished). Read each sample from the
650                    // buffer that actually holds it.
651                    let mut recon = vec![0u8; n];
652                    let gy0 = row as usize * TILE_SIZE as usize;
653                    let gx0 = col as usize * TILE_SIZE as usize;
654                    for y in 0..th as usize {
655                        for x in 0..tw as usize {
656                            for c in 0..3usize {
657                                let k = (y * tw as usize + x) * 3 + c;
658                                let gy = gy0 + y;
659                                let gx = gx0 + x;
660                                // tile-local coords for recon reads
661                                let tl = |ggy: usize, ggx: usize| -> u32 {
662                                    if ggy >= gy0 && ggx >= gx0 {
663                                        u32::from(
664                                            recon
665                                                [((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
666                                        )
667                                    } else {
668                                        u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
669                                    }
670                                };
671                                let pred: u32 = if gy == 0 && gx == 0 {
672                                    127
673                                } else if gy == 0 {
674                                    // top row: left neighbor (out-of-tile or in-tile)
675                                    if gx - 1 < gx0 {
676                                        u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
677                                    } else {
678                                        u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
679                                    }
680                                } else if gx == 0 {
681                                    // left col: top neighbor (out-of-tile above or in-tile)
682                                    if gy - 1 < gy0 {
683                                        u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
684                                    } else {
685                                        u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
686                                    }
687                                } else {
688                                    let a = tl(gy, gx - 1);
689                                    let b = tl(gy - 1, gx);
690                                    let cc = tl(gy - 1, gx - 1);
691                                    med_predict_pixel(a, b, cc)
692                                };
693                                recon[k] =
694                                    (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
695                            }
696                        }
697                    }
698                    recon
699                } else {
700                    body
701                }
702            }
703            3 => {
704                // Y plane is 1 byte per pixel (np = n/3); n counts RGB bytes
705                let np = n / 3;
706                let n9 = (np * 9).div_ceil(8);
707                if ip + np + 2 * n9 > raw.len() {
708                    return Err(Error::fmt_msg(format!("tile {i}: body")));
709                }
710                let y = raw[ip..ip + np].to_vec();
711                let co9 = &raw[ip + np..ip + np + n9];
712                let cg9 = &raw[ip + np + n9..ip + np + 2 * n9];
713                ip += np + 2 * n9;
714                let co = unpack9(co9, np)?;
715                let cg = unpack9(cg9, np)?;
716                ycocg_inverse(&y, &co, &cg)
717            }
718            1 | 2 | 4 | 5 => {
719                let meta = if mode == 2 || mode == 5 { 5 } else { 4 };
720                if ip + meta > raw.len() {
721                    return Err(Error::fmt_msg("refmeta"));
722                }
723                let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
724                let tr = if mode == 2 || mode == 5 {
725                    raw[ip + 4]
726                } else {
727                    0
728                };
729                ip += meta;
730                if idx >= i as usize {
731                    return Err(Error::fmt_msg("reforder"));
732                }
733                if tr >= 8 {
734                    return Err(Error::fmt_msg("tranform-range"));
735                }
736                let (rw, rh, ref_rgb) = &decoded[idx];
737                if *rw != tw || *rh != th {
738                    return Err(Error::fmt_msg("refshape"));
739                }
740                if tw != th && tr >= 4 {
741                    return Err(Error::fmt_msg("xform-shape"));
742                }
743                if ip + n > raw.len() {
744                    return Err(Error::fmt_msg("body"));
745                }
746                let body = raw[ip..ip + n].to_vec();
747                ip += n;
748                let resid = if planar { planar_unpack(&body) } else { body };
749                let base = if tr == 0 {
750                    ref_rgb.clone()
751                } else {
752                    apply_transform(tr, tw, th, ref_rgb)
753                };
754                match mode {
755                    1 | 2 => base
756                        .iter()
757                        .zip(resid.iter())
758                        .map(|(a, r)| a.wrapping_add(*r))
759                        .collect(),
760                    // MNF7 color-domain: recon_t = T(base) + resid; pix = T^-1(recon_t)
761                    4 | 5 => {
762                        let base_t = modcolor_forward(&base);
763                        let recon_t: Vec<u8> = base_t
764                            .iter()
765                            .zip(resid.iter())
766                            .map(|(a, r)| a.wrapping_add(*r))
767                            .collect();
768                        modcolor_inverse(&recon_t)
769                    }
770                    _ => unreachable!(),
771                }
772            }
773            _ => {
774                return Err(Error::fmt_msg(format!(
775                    "tile {i}: mode byte {mode:#04x} pos {ip}"
776                )))
777            }
778        };
779        decoded.push((tw, th, tile.clone()));
780
781        for y_ in 0..th as usize {
782            let py = row as usize * TILE_SIZE as usize + y_;
783            for x in 0..tw as usize {
784                let pxx = col as usize * TILE_SIZE as usize + x;
785                let d = (py * width as usize + pxx) * 3;
786                let s = (y_ * tw as usize + x) * 3;
787                pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
788            }
789        }
790    }
791    if ip != raw.len() {
792        return Err(Error::fmt_msg("extra"));
793    }
794    // note: Ok(Dims..) tuple-return belonged to decode_mnf7; this walker returns pixels
795    Ok(pixels)
796}
797
798/// Image-wide MED (JPEG-LS LOCO-I) predictor over RGB8: pixel (x,y) predicts
799/// from left/top/topleft true pixels (edge-sense: flat branches take the
800/// plane a+b-cc). First pixel = 127; top row = left; left col = above.
801/// Deterministic; encoder and decoder compute the identical progression
802/// from reconstructed causal neighbors.
803pub fn med_predict(rgb: &[u8], w: usize, h: usize) -> Vec<u8> {
804    assert_eq!(rgb.len(), w * h * 3, "rgb size");
805    let mut out = vec![0u8; rgb.len()];
806    for y in 0..h {
807        for x in 0..w {
808            for c in 0..3usize {
809                let i = (y * w + x) * 3 + c;
810                if x == 0 && y == 0 {
811                    out[i] = 127;
812                } else if y == 0 {
813                    out[i] = rgb[i - 3];
814                } else if x == 0 {
815                    out[i] = rgb[i - w * 3];
816                } else {
817                    let a = i32::from(rgb[i - 3]);
818                    let b = i32::from(rgb[i - w * 3]);
819                    let cc = i32::from(rgb[i - w * 3 - 3]);
820                    let plane = a + b - cc;
821                    let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
822                    let p = if flat {
823                        plane
824                    } else {
825                        let pa = (plane - a).abs();
826                        let pb = (plane - b).abs();
827                        let pcc = (plane - cc).abs();
828                        if pa <= pb && pa <= pcc {
829                            a
830                        } else if pb <= pcc {
831                            b
832                        } else {
833                            cc
834                        }
835                    };
836                    out[i] = p.clamp(0, 255) as u8;
837                }
838            }
839        }
840    }
841    out
842}
843
844#[inline]
845fn med_predict_pixel(a: u32, b: u32, cc: u32) -> u32 {
846    let plane = a as i64 + b as i64 - cc as i64;
847    let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
848    if flat {
849        // plane branch; clamp into byte range — NOTE the encoder's predictor
850        // values live in 0..=255 after its own clamp; identical formula both
851        // sides (med_predict clamps identically)
852        plane.clamp(0, 255) as u32
853    } else {
854        let pa = (plane - a as i64).abs();
855        let pb = (plane - b as i64).abs();
856        let pcc = (plane - cc as i64).abs();
857        if pa <= pb && pa <= pcc {
858            a
859        } else if pb <= pcc {
860            b
861        } else {
862            cc
863        }
864    }
865}
866
867#[cfg(test)]
868mod tests {
869    use super::*;
870
871    fn roundtrip_mnf7(px: &[u8], dims: Dims, use_geom: bool) {
872        let (buf, st) = encode_mnf7(px, dims, use_geom);
873        let (d, out) = decode_mnf7(&buf).expect("decode");
874        assert_eq!(d, dims);
875        assert_eq!(out, px, "flags {:#b}", st.chosen_flags);
876    }
877
878    #[test]
879    fn bit9_roundtrip() {
880        let vals: Vec<u16> = (0..300).map(|i| (i * 137) % 512).collect();
881        let packed = pack9(&vals);
882        assert_eq!(unpack9(&packed, vals.len()).expect("unpack"), vals);
883        // edge values
884        for v in [0u16, 1, 511, 256, 255] {
885            let p = pack9(&[v]);
886            assert_eq!(unpack9(&p, 1).expect("u")[0], v);
887        }
888    }
889
890    #[test]
891    fn ycocg_exhaustive_sample() {
892        for v in (0..=0xFFFFFF).step_by(41) {
893            let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
894            let (y, co, cg) = ycocg_forward(&px);
895            assert_eq!(ycocg_inverse(&y, &co, &cg), px, "{px:?}");
896            // packed form must roundtrip too
897            let py = pack9(&co);
898            let pg = pack9(&cg);
899            let co2 = unpack9(&py, 1).expect("co");
900            let cg2 = unpack9(&pg, 1).expect("cg");
901            assert_eq!(ycocg_inverse(&y, &co2, &cg2), px, "{px:?}");
902        }
903    }
904
905    #[test]
906    fn mnf6_roundtrip_all_classes() {
907        for (w, h, seed) in [
908            (16u32, 16u32, 1),
909            (48, 33, 2),
910            (64, 64, 3),
911            (31, 17, 4),
912            (1, 1, 5),
913        ] {
914            let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
915            let mut rnd = move || {
916                s ^= s << 13;
917                s ^= s >> 7;
918                s ^= s << 17;
919                s
920            };
921            let px: Vec<u8> = (0..w as usize * h as usize * 3)
922                .map(|_| (rnd() & 0xFF) as u8)
923                .collect();
924            roundtrip_mnf7(
925                &px,
926                Dims {
927                    width: w,
928                    height: h,
929                },
930                true,
931            );
932        }
933    }
934
935    #[test]
936    fn mnf7_modcolor_is_bijective_exhaustive() {
937        // ADR gate: proof of reversibility. Genuinely exhaustive over all
938        // 2^24 RGB8 triples (review fix: a stride sweep is sampling, not proof).
939        // Runs in ~seconds under `cargo test --release`; skipped in debug
940        // builds where a stride sweep + corners carry the same structural check.
941        if cfg!(debug_assertions) {
942            for v in (0..=0xFFFFFFu32).step_by(17) {
943                let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
944                assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
945            }
946            for r in [0u8, 1, 127, 128, 254, 255] {
947                for g in [0u8, 85, 170, 255] {
948                    for b in [0u8, 128, 255] {
949                        let px = [r, g, b];
950                        assert_eq!(modcolor::inverse(modcolor::forward(px)), px);
951                    }
952                }
953            }
954        } else {
955            let mut checked: u64 = 0;
956            for v in 0..=0xFFFFFFu32 {
957                let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
958                assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
959                checked += 1;
960            }
961            assert_eq!(checked, 16_777_216, "exhaustive sweep count");
962        }
963    }
964
965    #[test]
966    fn mnf7_color_domain_used_when_it_wins() {
967        // structured color-delta over a noisy base: tile B = T^-1(T(A) + co ramp)
968        // makes the RGB residual incompressible noise but the color-domain
969        // residual mostly zeros + a repeating ramp — mode 4 must win.
970        let mut s: u64 = 42;
971        let mut rnd = move || {
972            s ^= s << 13;
973            s ^= s >> 7;
974            s ^= s << 17;
975            s
976        };
977        let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
978
979        let ta_t = modcolor_forward(&a);
980        let mut tb_t = ta_t.clone();
981        let ramp = [0u8, 1, 2, 255]; // repeating pattern in the co channel only
982        for i in (1..tb_t.len()).step_by(3) {
983            tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
984        }
985        let b = modcolor_inverse(&tb_t);
986
987        let mut two = a.clone();
988        two.extend_from_slice(&b);
989        let d2 = Dims {
990            width: 16,
991            height: 32,
992        };
993        let (buf, st) = encode_mnf7(&two, d2, true);
994        let (_, out) = decode_mnf7(&buf).expect("decode");
995        assert_eq!(out, two);
996        let color_modes = st.ref_exact_color + st.ref_geom_color;
997        assert!(
998            color_modes > 0,
999            "expected color-domain references, got {}",
1000            color_modes
1001        );
1002    }
1003
1004    #[test]
1005    fn mnf6_rate_aware_picks_flags() {
1006        // gradient should prefer planar; the study showed planar+zlib gradient = 5,018 vs interl 219,782
1007        let (w, h) = (64u32, 64u32);
1008        let mut px = vec![0u8; (w * h * 3) as usize];
1009        for y in 0..h {
1010            for x in 0..w {
1011                let i = ((y * w + x) * 3) as usize;
1012                px[i] = (x * 4) as u8;
1013                px[i + 1] = (y * 4) as u8;
1014                px[i + 2] = 128;
1015            }
1016        }
1017        let (buf, st) = encode_mnf7(
1018            &px,
1019            Dims {
1020                width: w,
1021                height: h,
1022            },
1023            false,
1024        );
1025        assert_eq!(
1026            st.chosen_flags & FLAG_PLANAR,
1027            FLAG_PLANAR,
1028            "gradient wants planar"
1029        );
1030        let (_, out) = decode_mnf7(&buf).expect("decode");
1031        assert_eq!(out, px);
1032    }
1033
1034    #[test]
1035    fn mnf6_rejects() {
1036        assert!(decode_mnf7(&[]).is_err());
1037        assert!(decode_mnf7(&[0u8; 27]).is_err());
1038        assert!(decode_mnf7(b"MNF5\x01\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00").is_err(), "mnf5 magic");
1039        // reserved flag bits
1040        let mut f = vec![0u8; 28];
1041        f[0..4].copy_from_slice(b"MNF6");
1042        f[24] = 0b100; // bit2 reserved
1043        assert!(decode_mnf7(&f).is_err(), "reserved flag");
1044    }
1045
1046    #[test]
1047    fn mnf6_beats_mnf5_on_gradient() {
1048        // rate-aware selection must not lose to the shipped codec on smooth content
1049
1050        use crate::codec::{build_header, compress_payload, encode_tiles};
1051        let (w, h) = (96u32, 96u32);
1052        let mut px = vec![0u8; (w * h * 3) as usize];
1053        for y in 0..h {
1054            for x in 0..w {
1055                let i = ((y * w + x) * 3) as usize;
1056                px[i] = (x * 2) as u8;
1057                px[i + 1] = (y * 2) as u8;
1058                px[i + 2] = ((x + y) / 2) as u8;
1059            }
1060        }
1061        let d = Dims {
1062            width: w,
1063            height: h,
1064        };
1065        let tiles = crate::codec::split_tiles(&px, d);
1066        let raw = encode_tiles(&tiles, true, true);
1067        let z = compress_payload(&raw);
1068        let mut m5 = build_header(d, tiles.len() as u32, z.len() as u32);
1069        m5.extend_from_slice(&z);
1070        let (m6, _) = encode_mnf7(&px, d, true);
1071        assert!(decode_mnf7(&m6).is_ok());
1072        assert!(
1073            m6.len() <= m5.len(),
1074            "MNF6/7 ({}) should not lose to MNF5 ({}) on gradient",
1075            m6.len(),
1076            m5.len()
1077        );
1078    }
1079    #[test]
1080    fn d8_atlas_full_roundtrip() {
1081        // Review fix 4 (conformance gate): a 5-level staircase image forces
1082        // distinct D8 matches; every nonidentity transform must be EMITTED
1083        // (not silently fallen back to literal) and the complete file must
1084        // decode back to the original pixels. geom_nontrivial assertion keeps
1085        // the test from passing via literal-only encoding.
1086        let (w, h) = (64u32, 64u32);
1087        let mut px = vec![0u8; (w * h * 3) as usize];
1088        // 16 unique structures: quantized x-position blocks, so rotated
1089        // variants of earlier tiles produce real zero-residual matches only
1090        // through the corresponding D8 transform.
1091        for y in 0..h {
1092            for x in 0..w {
1093                let i = ((y * w + x) * 3) as usize;
1094                let band = ((x / 4) as u16 * 13) as u8;
1095                px[i] = band;
1096                px[i + 1] = ((y / 4) as u16 * 11) as u8;
1097                px[i + 2] = band ^ (((y / 4) as u16 * 29) as u8);
1098            }
1099        }
1100        // build an image where row-blocks 2..8 are D8 transforms of the first
1101        let mut stacked = px.clone();
1102        for t in [1u8, 2, 3, 4, 5, 6, 7] {
1103            let transformed = crate::codec::apply_transform(t, w, h, &px);
1104            stacked.extend_from_slice(&transformed);
1105        }
1106        let d = Dims {
1107            width: w,
1108            height: h * 8,
1109        };
1110        let (buf, st) = encode_mnf7(&stacked, d, true);
1111        // the encoder must have emitted geometric references with nonidentity
1112        // transforms; literals would make this test meaningless
1113        assert!(
1114            st.ref_geom > 0,
1115            "no geometric references emitted: literal={} exact={} geom={} nontrivial={}",
1116            st.literal,
1117            st.ref_exact,
1118            st.ref_geom,
1119            st.geom_nontrivial
1120        );
1121        assert!(
1122            st.geom_nontrivial > 0,
1123            "only identity transforms used — test cannot verify D8 matching"
1124        );
1125        let (dims, out) = decode_mnf7(&buf).expect("full-file decode");
1126        assert_eq!((dims.width, dims.height), (w, h * 8));
1127        assert_eq!(out, stacked, "MNF7 D8 atlas roundtrip");
1128        // MNF7 decodes exact; also cross-check the file through the standalone CLI path in differential tests
1129    }
1130}