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/// Limits for MNF6/7/8 decoding: the canonical [`crate::codec::DecodeLimits`]
498/// (re-exported under this name to keep one type across all decoders — the
499/// capi + docs surface stays single-struct).
500pub use crate::codec::DecodeLimits;
501
502/// Decode MNF6/7 with configurable ceilings (library API; decode_mnf7 is the
503/// defaults wrapper).
504pub fn decode_mnf7_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
505    decode_mnf7_inner(buf, lim)
506}
507
508/// Decode MNF6/7 (magic auto) with default ceilings.
509pub fn decode_mnf7(buf: &[u8]) -> Result<(Dims, Vec<u8>)> {
510    decode_mnf7_inner(buf, &DecodeLimits::default())
511}
512
513fn decode_mnf7_inner(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
514    if buf.len() < HEADER6 {
515        return Err(Error::fmt_msg("short"));
516    }
517    if buf[0..4] != *b"MNF7" {
518        return Err(Error::fmt_msg("magic"));
519    }
520    let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
521    let width = u32le(4);
522    let height = u32le(8);
523    if width == 0 || height == 0 {
524        return Err(Error::fmt_msg("dims"));
525    }
526    if width as u64 * height as u64 > lim.max_pixels {
527        return Err(Error::Limits {
528            which: "max_pixels",
529            limit: lim.max_pixels,
530            actual: width as u64 * height as u64,
531        });
532    }
533    if u32le(12) != TILE_SIZE {
534        return Err(Error::fmt_msg("tilesize"));
535    }
536    let cols = div_ceil(width, TILE_SIZE);
537    let rows = div_ceil(height, TILE_SIZE);
538    let tile_count = u32le(16);
539    if tile_count != cols * rows {
540        return Err(Error::fmt_msg("tilecount"));
541    }
542    if tile_count > lim.max_tiles {
543        return Err(Error::Limits {
544            which: "max_tiles",
545            limit: u64::from(lim.max_tiles),
546            actual: u64::from(tile_count),
547        });
548    }
549    let payload_len = u32le(20);
550    if payload_len > (1 << 31) - 1 {
551        return Err(Error::fmt_msg("payloadlen"));
552    }
553    if buf.len() != HEADER6 + payload_len as usize {
554        return Err(Error::fmt_msg("filelen"));
555    }
556    let flags = u32le(24);
557    if flags & FLAGS_RESERVED != 0 {
558        return Err(Error::fmt_msg("reserved-flags"));
559    }
560    let planar = flags & FLAG_PLANAR != 0;
561    let zlib_in = flags & FLAG_ZSTD == 0;
562
563    let zlib_data = &buf[HEADER6..];
564    // Max record size, over all modes:
565    //   literal/refs:      1 mode + 5 refmeta + 768 bytes
566    //   mode 3 (YCoCg-R):  1 mode + np bytes Y + 2 * ceil(9*np/8) bytes, np <= 256
567    //                      => 1 + 256 + 2 * 288 = 833 (larger than the 776
568    //                      implied by the other modes; previously undercounted,
569    //                      which rejected all-mode-3 files as bombs — audit fix)
570    let mode3_max: u64 = 1 + 256 + 2 * u64::from((256u32 * 9).div_ceil(8));
571    let per_tile_max = 1 + 5 + (TILE_SIZE * TILE_SIZE * 3) as u64;
572    let limit = tile_count as u64 * per_tile_max.max(mode3_max) + per_tile_max + mode3_max;
573    let raw: Vec<u8> = if zlib_in {
574        let mut d = flate2::read::ZlibDecoder::new(zlib_data).take(limit + 1);
575        let mut v = Vec::new();
576        d.read_to_end(&mut v)
577            .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
578        v
579    } else {
580        let d = zstd::stream::read::Decoder::new(zlib_data)
581            .map_err(|_| Error::Decompress("zstd init"))?;
582        // same window cap as MNF8: level 19 uses <= 8 MiB; a hostile frame
583        // must not claim the 128 MiB library default (review fix 4)
584        let mut d = d;
585        d.window_log_max(23)
586            .map_err(|_| Error::Decompress("zstd window param"))?;
587        let mut d = d.take(limit + 1);
588        let mut v = Vec::new();
589        d.read_to_end(&mut v)
590            .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
591        v
592    };
593    if raw.len() as u64 > limit {
594        return Err(Error::fmt_msg("bomb"));
595    }
596
597    let pixels = decode_record_walk(&raw, width, height, tile_count, planar, false)?;
598    Ok((Dims { width, height }, pixels))
599}
600
601/// Shared MNF7/MNF8 record walk: decodes tile records (modes 0-5) over an
602/// already-decompressed `raw` stream into a width*height RGB8 buffer
603/// (extracted from decode_mnf7 for MNF8 reuse; semantics frozen — see
604/// docs/format.md §3/§MNF7).
605pub fn decode_record_walk(
606    raw: &[u8],
607    width: u32,
608    height: u32,
609    tile_count: u32,
610    planar: bool,
611    med: bool,
612) -> Result<Vec<u8>> {
613    let cols = div_ceil(width, TILE_SIZE);
614    let mut pixels = vec![0u8; width as usize * height as usize * 3];
615    let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
616    let mut ip = 0usize;
617    for i in 0..tile_count {
618        let row = i / cols;
619        let col = i % cols;
620        let tw = TILE_SIZE.min(width - col * TILE_SIZE);
621        let th = TILE_SIZE.min(height - row * TILE_SIZE);
622        let n = (tw * th * 3) as usize;
623
624        if ip >= raw.len() {
625            return Err(Error::fmt_msg(format!("eof at tile {i}")));
626        }
627        let mode = raw[ip];
628        ip += 1;
629
630        let tile = match mode {
631            0 => {
632                if ip + n > raw.len() {
633                    return Err(Error::fmt_msg("body"));
634                }
635                let body = raw[ip..ip + n].to_vec();
636                ip += n;
637                let body = if planar { planar_unpack(&body) } else { body };
638                if med {
639                    // FLAG_PRED_MED (MNF8 bit2): body = (X - MED(X_causal)) mod
640                    // 256. Context discipline (the audit bug): IN-TILE causal
641                    // neighbors live in `recon` (only blitted into `pixels`
642                    // after the whole tile reconstructs); OUT-OF-TILE
643                    // neighbors (above tile-row or left tile-column) are
644                    // already decoded in `pixels` (scan order guarantees the
645                    // earlier tiles finished). Read each sample from the
646                    // buffer that actually holds it.
647                    let mut recon = vec![0u8; n];
648                    let gy0 = row as usize * TILE_SIZE as usize;
649                    let gx0 = col as usize * TILE_SIZE as usize;
650                    for y in 0..th as usize {
651                        for x in 0..tw as usize {
652                            for c in 0..3usize {
653                                let k = (y * tw as usize + x) * 3 + c;
654                                let gy = gy0 + y;
655                                let gx = gx0 + x;
656                                // tile-local coords for recon reads
657                                let tl = |ggy: usize, ggx: usize| -> u32 {
658                                    if ggy >= gy0 && ggx >= gx0 {
659                                        u32::from(
660                                            recon
661                                                [((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
662                                        )
663                                    } else {
664                                        u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
665                                    }
666                                };
667                                let pred: u32 = if gy == 0 && gx == 0 {
668                                    127
669                                } else if gy == 0 {
670                                    // top row: left neighbor (out-of-tile or in-tile)
671                                    if gx - 1 < gx0 {
672                                        u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
673                                    } else {
674                                        u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
675                                    }
676                                } else if gx == 0 {
677                                    // left col: top neighbor (out-of-tile above or in-tile)
678                                    if gy - 1 < gy0 {
679                                        u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
680                                    } else {
681                                        u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
682                                    }
683                                } else {
684                                    let a = tl(gy, gx - 1);
685                                    let b = tl(gy - 1, gx);
686                                    let cc = tl(gy - 1, gx - 1);
687                                    med_predict_pixel(a, b, cc)
688                                };
689                                recon[k] =
690                                    (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
691                            }
692                        }
693                    }
694                    recon
695                } else {
696                    body
697                }
698            }
699            3 => {
700                // Y plane is 1 byte per pixel (np = n/3); n counts RGB bytes
701                let np = n / 3;
702                let n9 = (np * 9).div_ceil(8);
703                if ip + np + 2 * n9 > raw.len() {
704                    return Err(Error::fmt_msg(format!("tile {i}: body")));
705                }
706                let y = raw[ip..ip + np].to_vec();
707                let co9 = &raw[ip + np..ip + np + n9];
708                let cg9 = &raw[ip + np + n9..ip + np + 2 * n9];
709                ip += np + 2 * n9;
710                let co = unpack9(co9, np)?;
711                let cg = unpack9(cg9, np)?;
712                ycocg_inverse(&y, &co, &cg)
713            }
714            1 | 2 | 4 | 5 => {
715                let meta = if mode == 2 || mode == 5 { 5 } else { 4 };
716                if ip + meta > raw.len() {
717                    return Err(Error::fmt_msg("refmeta"));
718                }
719                let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
720                let tr = if mode == 2 || mode == 5 {
721                    raw[ip + 4]
722                } else {
723                    0
724                };
725                ip += meta;
726                if idx >= i as usize {
727                    return Err(Error::fmt_msg("reforder"));
728                }
729                if tr >= 8 {
730                    return Err(Error::fmt_msg("tranform-range"));
731                }
732                let (rw, rh, ref_rgb) = &decoded[idx];
733                if *rw != tw || *rh != th {
734                    return Err(Error::fmt_msg("refshape"));
735                }
736                if tw != th && tr >= 4 {
737                    return Err(Error::fmt_msg("xform-shape"));
738                }
739                if ip + n > raw.len() {
740                    return Err(Error::fmt_msg("body"));
741                }
742                let body = raw[ip..ip + n].to_vec();
743                ip += n;
744                let resid = if planar { planar_unpack(&body) } else { body };
745                let base = if tr == 0 {
746                    ref_rgb.clone()
747                } else {
748                    apply_transform(tr, tw, th, ref_rgb)
749                };
750                match mode {
751                    1 | 2 => base
752                        .iter()
753                        .zip(resid.iter())
754                        .map(|(a, r)| a.wrapping_add(*r))
755                        .collect(),
756                    // MNF7 color-domain: recon_t = T(base) + resid; pix = T^-1(recon_t)
757                    4 | 5 => {
758                        let base_t = modcolor_forward(&base);
759                        let recon_t: Vec<u8> = base_t
760                            .iter()
761                            .zip(resid.iter())
762                            .map(|(a, r)| a.wrapping_add(*r))
763                            .collect();
764                        modcolor_inverse(&recon_t)
765                    }
766                    _ => unreachable!(),
767                }
768            }
769            _ => {
770                return Err(Error::fmt_msg(format!(
771                    "tile {i}: mode byte {mode:#04x} pos {ip}"
772                )))
773            }
774        };
775        decoded.push((tw, th, tile.clone()));
776
777        for y_ in 0..th as usize {
778            let py = row as usize * TILE_SIZE as usize + y_;
779            for x in 0..tw as usize {
780                let pxx = col as usize * TILE_SIZE as usize + x;
781                let d = (py * width as usize + pxx) * 3;
782                let s = (y_ * tw as usize + x) * 3;
783                pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
784            }
785        }
786    }
787    if ip != raw.len() {
788        return Err(Error::fmt_msg("extra"));
789    }
790    // note: Ok(Dims..) tuple-return belonged to decode_mnf7; this walker returns pixels
791    Ok(pixels)
792}
793
794/// Image-wide MED (JPEG-LS LOCO-I) predictor over RGB8: pixel (x,y) predicts
795/// from left/top/topleft true pixels (edge-sense: flat branches take the
796/// plane a+b-cc). First pixel = 127; top row = left; left col = above.
797/// Deterministic; encoder and decoder compute the identical progression
798/// from reconstructed causal neighbors.
799pub fn med_predict(rgb: &[u8], w: usize, h: usize) -> Vec<u8> {
800    assert_eq!(rgb.len(), w * h * 3, "rgb size");
801    let mut out = vec![0u8; rgb.len()];
802    for y in 0..h {
803        for x in 0..w {
804            for c in 0..3usize {
805                let i = (y * w + x) * 3 + c;
806                if x == 0 && y == 0 {
807                    out[i] = 127;
808                } else if y == 0 {
809                    out[i] = rgb[i - 3];
810                } else if x == 0 {
811                    out[i] = rgb[i - w * 3];
812                } else {
813                    let a = i32::from(rgb[i - 3]);
814                    let b = i32::from(rgb[i - w * 3]);
815                    let cc = i32::from(rgb[i - w * 3 - 3]);
816                    let plane = a + b - cc;
817                    let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
818                    let p = if flat {
819                        plane
820                    } else {
821                        let pa = (plane - a).abs();
822                        let pb = (plane - b).abs();
823                        let pcc = (plane - cc).abs();
824                        if pa <= pb && pa <= pcc {
825                            a
826                        } else if pb <= pcc {
827                            b
828                        } else {
829                            cc
830                        }
831                    };
832                    out[i] = p.clamp(0, 255) as u8;
833                }
834            }
835        }
836    }
837    out
838}
839
840#[inline]
841pub(crate) fn med_predict_pixel(a: u32, b: u32, cc: u32) -> u32 {
842    let plane = a as i64 + b as i64 - cc as i64;
843    let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
844    if flat {
845        // plane branch; clamp into byte range — NOTE the encoder's predictor
846        // values live in 0..=255 after its own clamp; identical formula both
847        // sides (med_predict clamps identically)
848        plane.clamp(0, 255) as u32
849    } else {
850        let pa = (plane - a as i64).abs();
851        let pb = (plane - b as i64).abs();
852        let pcc = (plane - cc as i64).abs();
853        if pa <= pb && pa <= pcc {
854            a
855        } else if pb <= pcc {
856            b
857        } else {
858            cc
859        }
860    }
861}
862
863#[cfg(test)]
864mod tests {
865    use super::*;
866
867    fn roundtrip_mnf7(px: &[u8], dims: Dims, use_geom: bool) {
868        let (buf, st) = encode_mnf7(px, dims, use_geom);
869        let (d, out) = decode_mnf7(&buf).expect("decode");
870        assert_eq!(d, dims);
871        assert_eq!(out, px, "flags {:#b}", st.chosen_flags);
872    }
873
874    #[test]
875    fn bit9_roundtrip() {
876        let vals: Vec<u16> = (0..300).map(|i| (i * 137) % 512).collect();
877        let packed = pack9(&vals);
878        assert_eq!(unpack9(&packed, vals.len()).expect("unpack"), vals);
879        // edge values
880        for v in [0u16, 1, 511, 256, 255] {
881            let p = pack9(&[v]);
882            assert_eq!(unpack9(&p, 1).expect("u")[0], v);
883        }
884    }
885
886    #[test]
887    fn ycocg_exhaustive_sample() {
888        for v in (0..=0xFFFFFF).step_by(41) {
889            let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
890            let (y, co, cg) = ycocg_forward(&px);
891            assert_eq!(ycocg_inverse(&y, &co, &cg), px, "{px:?}");
892            // packed form must roundtrip too
893            let py = pack9(&co);
894            let pg = pack9(&cg);
895            let co2 = unpack9(&py, 1).expect("co");
896            let cg2 = unpack9(&pg, 1).expect("cg");
897            assert_eq!(ycocg_inverse(&y, &co2, &cg2), px, "{px:?}");
898        }
899    }
900
901    #[test]
902    fn mnf6_roundtrip_all_classes() {
903        for (w, h, seed) in [
904            (16u32, 16u32, 1),
905            (48, 33, 2),
906            (64, 64, 3),
907            (31, 17, 4),
908            (1, 1, 5),
909        ] {
910            let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
911            let mut rnd = move || {
912                s ^= s << 13;
913                s ^= s >> 7;
914                s ^= s << 17;
915                s
916            };
917            let px: Vec<u8> = (0..w as usize * h as usize * 3)
918                .map(|_| (rnd() & 0xFF) as u8)
919                .collect();
920            roundtrip_mnf7(
921                &px,
922                Dims {
923                    width: w,
924                    height: h,
925                },
926                true,
927            );
928        }
929    }
930
931    #[test]
932    fn mnf7_modcolor_is_bijective_exhaustive() {
933        // ADR gate: proof of reversibility. Genuinely exhaustive over all
934        // 2^24 RGB8 triples (review fix: a stride sweep is sampling, not proof).
935        // Runs in ~seconds under `cargo test --release`; skipped in debug
936        // builds where a stride sweep + corners carry the same structural check.
937        if cfg!(debug_assertions) {
938            for v in (0..=0xFFFFFFu32).step_by(17) {
939                let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
940                assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
941            }
942            for r in [0u8, 1, 127, 128, 254, 255] {
943                for g in [0u8, 85, 170, 255] {
944                    for b in [0u8, 128, 255] {
945                        let px = [r, g, b];
946                        assert_eq!(modcolor::inverse(modcolor::forward(px)), px);
947                    }
948                }
949            }
950        } else {
951            let mut checked: u64 = 0;
952            for v in 0..=0xFFFFFFu32 {
953                let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
954                assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
955                checked += 1;
956            }
957            assert_eq!(checked, 16_777_216, "exhaustive sweep count");
958        }
959    }
960
961    #[test]
962    fn mnf7_color_domain_used_when_it_wins() {
963        // structured color-delta over a noisy base: tile B = T^-1(T(A) + co ramp)
964        // makes the RGB residual incompressible noise but the color-domain
965        // residual mostly zeros + a repeating ramp — mode 4 must win.
966        let mut s: u64 = 42;
967        let mut rnd = move || {
968            s ^= s << 13;
969            s ^= s >> 7;
970            s ^= s << 17;
971            s
972        };
973        let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
974
975        let ta_t = modcolor_forward(&a);
976        let mut tb_t = ta_t.clone();
977        let ramp = [0u8, 1, 2, 255]; // repeating pattern in the co channel only
978        for i in (1..tb_t.len()).step_by(3) {
979            tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
980        }
981        let b = modcolor_inverse(&tb_t);
982
983        let mut two = a.clone();
984        two.extend_from_slice(&b);
985        let d2 = Dims {
986            width: 16,
987            height: 32,
988        };
989        let (buf, st) = encode_mnf7(&two, d2, true);
990        let (_, out) = decode_mnf7(&buf).expect("decode");
991        assert_eq!(out, two);
992        let color_modes = st.ref_exact_color + st.ref_geom_color;
993        assert!(
994            color_modes > 0,
995            "expected color-domain references, got {}",
996            color_modes
997        );
998    }
999
1000    #[test]
1001    fn mnf6_rate_aware_picks_flags() {
1002        // gradient should prefer planar; the study showed planar+zlib gradient = 5,018 vs interl 219,782
1003        let (w, h) = (64u32, 64u32);
1004        let mut px = vec![0u8; (w * h * 3) as usize];
1005        for y in 0..h {
1006            for x in 0..w {
1007                let i = ((y * w + x) * 3) as usize;
1008                px[i] = (x * 4) as u8;
1009                px[i + 1] = (y * 4) as u8;
1010                px[i + 2] = 128;
1011            }
1012        }
1013        let (buf, st) = encode_mnf7(
1014            &px,
1015            Dims {
1016                width: w,
1017                height: h,
1018            },
1019            false,
1020        );
1021        assert_eq!(
1022            st.chosen_flags & FLAG_PLANAR,
1023            FLAG_PLANAR,
1024            "gradient wants planar"
1025        );
1026        let (_, out) = decode_mnf7(&buf).expect("decode");
1027        assert_eq!(out, px);
1028    }
1029
1030    #[test]
1031    fn mnf6_rejects() {
1032        assert!(decode_mnf7(&[]).is_err());
1033        assert!(decode_mnf7(&[0u8; 27]).is_err());
1034        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");
1035        // reserved flag bits
1036        let mut f = vec![0u8; 28];
1037        f[0..4].copy_from_slice(b"MNF6");
1038        f[24] = 0b100; // bit2 reserved
1039        assert!(decode_mnf7(&f).is_err(), "reserved flag");
1040    }
1041
1042    #[test]
1043    fn mnf6_beats_mnf5_on_gradient() {
1044        // rate-aware selection must not lose to the shipped codec on smooth content
1045
1046        use crate::codec::{build_header, compress_payload, encode_tiles};
1047        let (w, h) = (96u32, 96u32);
1048        let mut px = vec![0u8; (w * h * 3) as usize];
1049        for y in 0..h {
1050            for x in 0..w {
1051                let i = ((y * w + x) * 3) as usize;
1052                px[i] = (x * 2) as u8;
1053                px[i + 1] = (y * 2) as u8;
1054                px[i + 2] = ((x + y) / 2) as u8;
1055            }
1056        }
1057        let d = Dims {
1058            width: w,
1059            height: h,
1060        };
1061        let tiles = crate::codec::split_tiles(&px, d);
1062        let raw = encode_tiles(&tiles, true, true);
1063        let z = compress_payload(&raw);
1064        let mut m5 = build_header(d, tiles.len() as u32, z.len() as u32);
1065        m5.extend_from_slice(&z);
1066        let (m6, _) = encode_mnf7(&px, d, true);
1067        assert!(decode_mnf7(&m6).is_ok());
1068        assert!(
1069            m6.len() <= m5.len(),
1070            "MNF6/7 ({}) should not lose to MNF5 ({}) on gradient",
1071            m6.len(),
1072            m5.len()
1073        );
1074    }
1075    #[test]
1076    fn d8_atlas_full_roundtrip() {
1077        // Review fix 4 (conformance gate): a 5-level staircase image forces
1078        // distinct D8 matches; every nonidentity transform must be EMITTED
1079        // (not silently fallen back to literal) and the complete file must
1080        // decode back to the original pixels. geom_nontrivial assertion keeps
1081        // the test from passing via literal-only encoding.
1082        let (w, h) = (64u32, 64u32);
1083        let mut px = vec![0u8; (w * h * 3) as usize];
1084        // 16 unique structures: quantized x-position blocks, so rotated
1085        // variants of earlier tiles produce real zero-residual matches only
1086        // through the corresponding D8 transform.
1087        for y in 0..h {
1088            for x in 0..w {
1089                let i = ((y * w + x) * 3) as usize;
1090                let band = ((x / 4) as u16 * 13) as u8;
1091                px[i] = band;
1092                px[i + 1] = ((y / 4) as u16 * 11) as u8;
1093                px[i + 2] = band ^ (((y / 4) as u16 * 29) as u8);
1094            }
1095        }
1096        // build an image where row-blocks 2..8 are D8 transforms of the first
1097        let mut stacked = px.clone();
1098        for t in [1u8, 2, 3, 4, 5, 6, 7] {
1099            let transformed = crate::codec::apply_transform(t, w, h, &px);
1100            stacked.extend_from_slice(&transformed);
1101        }
1102        let d = Dims {
1103            width: w,
1104            height: h * 8,
1105        };
1106        let (buf, st) = encode_mnf7(&stacked, d, true);
1107        // the encoder must have emitted geometric references with nonidentity
1108        // transforms; literals would make this test meaningless
1109        assert!(
1110            st.ref_geom > 0,
1111            "no geometric references emitted: literal={} exact={} geom={} nontrivial={}",
1112            st.literal,
1113            st.ref_exact,
1114            st.ref_geom,
1115            st.geom_nontrivial
1116        );
1117        assert!(
1118            st.geom_nontrivial > 0,
1119            "only identity transforms used — test cannot verify D8 matching"
1120        );
1121        let (dims, out) = decode_mnf7(&buf).expect("full-file decode");
1122        assert_eq!((dims.width, dims.height), (w, h * 8));
1123        assert_eq!(out, stacked, "MNF7 D8 atlas roundtrip");
1124        // MNF7 decodes exact; also cross-check the file through the standalone CLI path in differential tests
1125    }
1126}