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