1use std::io::Read;
48
49use crate::codec::{apply_transform, div_ceil, split_tiles, tile_sig, Atlas, Dims, TILE_SIZE};
50use crate::error::{Error, Result};
51use crate::mnf6::{
52 compress_stream, decode_record_walk, med_predict, modcolor_forward_iter, pack9, planar_pack,
53 proxy_cost, ycocg_forward, Coder, MnF6Stats, FLAG_PLANAR,
54};
55use crate::tokens;
56
57pub const HEADER8: usize = 32;
59
60pub const FLAG_PRED_MED: u32 = 4; pub const FLAG_TOKENS: u32 = 8;
65pub const FLAG_PRED_PLANE: u32 = 16;
67pub const FLAG_PRED_LEFT: u32 = 32;
69pub const FLAGS_RESERVED8: u32 = !0b111111;
71
72pub const BACKEND_ZLIB: u32 = 0;
74pub const BACKEND_ZSTD19: u32 = 1;
76pub const BACKEND_RESERVED2: u32 = 2; pub const BACKEND_RESERVED3: u32 = 3; pub const ZSTD_WINDOW_LOG_MAX: u32 = 23;
85
86use crate::codec::RANK_CANDIDATES;
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Profile {
93 Balanced,
95 Maximum,
97}
98
99fn backend_candidates() -> Vec<(u32, Coder)> {
101 vec![
102 (BACKEND_ZSTD19, Coder::Zstd(19)),
103 (BACKEND_ZLIB, Coder::Zlib),
104 ]
105}
106
107fn backend_compress(backend: u32, buf: &[u8]) -> Result<Vec<u8>> {
108 match backend {
109 BACKEND_ZLIB => Ok(compress_stream(&Coder::Zlib, buf)),
110 BACKEND_ZSTD19 => Ok(compress_stream(&Coder::Zstd(19), buf)),
111 other => Err(Error::fmt_msg(format!(
112 "backend {other} not a compressed-output coder"
113 ))),
114 }
115}
116
117fn backend_decompress(backend: u32, buf: &[u8], limit: u64) -> Result<Vec<u8>> {
118 match backend {
119 BACKEND_ZLIB => {
120 let mut d = flate2::read::ZlibDecoder::new(buf).take(limit + 1);
124 let mut v = Vec::new();
125 d.read_to_end(&mut v)
126 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
127 if v.len() as u64 > limit {
128 return Err(Error::fmt_msg("bomb"));
129 }
130 if d.get_ref().total_in() as usize != buf.len() {
131 return Err(Error::fmt_msg("trailing bytes after zlib stream"));
132 }
133 Ok(v)
134 }
135 BACKEND_ZSTD19 => {
136 let cursor = std::io::Cursor::new(buf);
140 let mut d = zstd::stream::read::Decoder::new(cursor)
141 .map_err(|_| Error::Decompress("zstd init"))?;
142 d.window_log_max(ZSTD_WINDOW_LOG_MAX)
143 .map_err(|_| Error::Decompress("zstd window param"))?;
144 let mut v = Vec::new();
145 d.single_frame()
146 .take(limit + 1)
147 .read_to_end(&mut v)
148 .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
149 if v.len() as u64 > limit {
150 return Err(Error::fmt_msg("bomb"));
151 }
152 Ok(v)
159 }
160 other => Err(Error::fmt_msg(format!(
161 "backend {other} not a decompressable coder"
162 ))),
163 }
164}
165
166pub fn encode_mnf8(
180 pixels: &[u8],
181 dims: Dims,
182 use_geom: bool,
183 profile: Profile,
184) -> (Vec<u8>, MnF6Stats, u32) {
185 let candidates: Vec<(u32, Coder)> = match profile {
186 Profile::Balanced => vec![(BACKEND_ZSTD19, Coder::Zstd(19))],
187 Profile::Maximum => backend_candidates(),
188 };
189 let mut best: Option<(Vec<u8>, u32, MnF6Stats, u32)> = None;
190 let pred_arms = [
195 PredKind::None,
196 PredKind::Med,
197 PredKind::Plane,
198 PredKind::Left,
199 ];
200 for (backend, coder) in candidates {
201 for kind in pred_arms {
202 for planar in [false, true] {
203 let med = kind == PredKind::Med;
204 let flags = kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 };
205 let (file, st) = encode_stream_coder(
206 pixels, dims, true, use_geom, flags, planar, med, kind, &coder, backend,
207 )
208 .expect("encoding an in-memory image cannot fail");
209 let size = file.len();
210 if best.as_ref().is_none_or(|b| size < b.0.len()) {
211 best = Some((file, flags, st, backend));
212 }
213 }
214 }
215 let flags = FLAG_PRED_MED | FLAG_TOKENS;
219 let (file, st) = encode_tokens_coder(pixels, dims, backend, flags)
220 .expect("encoding an in-memory image cannot fail");
221 let size = file.len();
222 if best.as_ref().is_none_or(|b| size < b.0.len()) {
223 best = Some((file, flags, st, backend));
224 }
225 }
226 let (file, flags, mut st, backend) = best.expect("at least one candidate");
227 st.chosen_flags = flags;
228 (file, st, backend)
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum PredKind {
235 None,
237 Med,
239 Plane,
241 Left,
243}
244
245impl PredKind {
246 fn from_flags(flags: u32) -> PredKind {
247 if flags & FLAG_PRED_MED != 0 {
248 PredKind::Med
249 } else if flags & FLAG_PRED_PLANE != 0 {
250 PredKind::Plane
251 } else if flags & FLAG_PRED_LEFT != 0 {
252 PredKind::Left
253 } else {
254 PredKind::None
255 }
256 }
257
258 fn flag_bit(self) -> u32 {
259 match self {
260 PredKind::None => 0,
261 PredKind::Med => FLAG_PRED_MED,
262 PredKind::Plane => FLAG_PRED_PLANE,
263 PredKind::Left => FLAG_PRED_LEFT,
264 }
265 }
266
267 fn predict(self, a: u32, b: u32, cc: u32) -> u32 {
270 match self {
271 PredKind::None | PredKind::Med => crate::mnf6::med_predict_pixel(a, b, cc),
272 PredKind::Plane => {
273 (a as i64 + b as i64 - cc as i64).clamp(0, 255) as u32
276 }
277 PredKind::Left => a,
278 }
279 }
280}
281
282fn image_predict(pixels: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
289 let mut out = vec![0u8; pixels.len()];
290 for y in 0..h {
291 for x in 0..w {
292 for c in 0..3usize {
293 let i = (y * w + x) * 3 + c;
294 let pred: u32 = if x == 0 && y == 0 {
295 127
296 } else if y == 0 {
297 u32::from(pixels[i - 3])
298 } else if x == 0 {
299 u32::from(pixels[i - w * 3])
300 } else {
301 let a = u32::from(pixels[i - 3]);
302 let b = u32::from(pixels[i - w * 3]);
303 let cc = u32::from(pixels[i - w * 3 - 3]);
304 kind.predict(a, b, cc)
305 };
306 out[i] = pred as u8;
307 }
308 }
309 }
310 out
311}
312
313fn encode_tokens_coder(
317 pixels: &[u8],
318 dims: Dims,
319 backend: u32,
320 flags: u32,
321) -> Result<(Vec<u8>, MnF6Stats)> {
322 let med_pred = med_predict(pixels, dims.width as usize, dims.height as usize);
323 let res: Vec<u8> = pixels
324 .iter()
325 .zip(&med_pred)
326 .map(|(x, m)| x.wrapping_sub(*m))
327 .collect();
328 let (wire, ts) = tokens::encode_plane(&res, dims.width as usize, dims.height as usize)?;
329 let payload = backend_compress(backend, &wire)?;
330 let payload_len =
331 u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?;
332 let mut file = Vec::with_capacity(HEADER8 + payload.len());
333 file.extend_from_slice(b"MNF8");
334 file.extend_from_slice(&dims.width.to_le_bytes());
335 file.extend_from_slice(&dims.height.to_le_bytes());
336 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
337 let cols = div_ceil(dims.width, TILE_SIZE);
338 let rows = div_ceil(dims.height, TILE_SIZE);
339 let tile_count: u32 = cols
340 .checked_mul(rows)
341 .ok_or_else(|| Error::fmt_msg("too many tiles"))?;
342 file.extend_from_slice(&tile_count.to_le_bytes());
343 file.extend_from_slice(&payload_len.to_le_bytes());
344 file.extend_from_slice(&flags.to_le_bytes());
345 file.extend_from_slice(&backend.to_le_bytes());
346 file.extend_from_slice(&payload);
347 let st = MnF6Stats {
348 literal: u32::try_from(ts.literals).unwrap_or(u32::MAX),
349 ref_exact: u32::try_from(ts.matches).unwrap_or(u32::MAX),
350 ..MnF6Stats::default()
351 };
352 Ok((file, st))
353}
354
355#[allow(clippy::too_many_arguments)]
366fn encode_stream_coder(
367 pixels: &[u8],
368 dims: Dims,
369 use_atlas: bool,
370 use_geom: bool,
371 flags: u32,
372 planar: bool,
373 med: bool,
374 kind: PredKind,
375 coder: &Coder,
376 backend: u32,
377) -> Result<(Vec<u8>, MnF6Stats)> {
378 let pure_pred = kind == PredKind::Plane || kind == PredKind::Left;
381 let med_pred = match kind {
382 PredKind::Med => med_predict(pixels, dims.width as usize, dims.height as usize),
383 PredKind::Plane => image_predict(
384 pixels,
385 dims.width as usize,
386 dims.height as usize,
387 PredKind::Plane,
388 ),
389 PredKind::Left => image_predict(
390 pixels,
391 dims.width as usize,
392 dims.height as usize,
393 PredKind::Left,
394 ),
395 PredKind::None => Vec::new(),
396 };
397 let tiles = split_tiles(pixels, dims);
398 let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
399 let mut stats = MnF6Stats::default();
400 let mut atlas = Atlas::new();
401
402 for (idx, tile) in tiles.iter().enumerate() {
403 let n = tile.nbytes();
404 let pack = |rgb: &[u8]| -> Vec<u8> {
405 if planar {
406 planar_pack(rgb)
407 } else {
408 rgb.to_vec()
409 }
410 };
411
412 let lit_src: Vec<u8> = if med || pure_pred {
415 let (tw, th) = (tile.width as usize, tile.height as usize);
416 let cols_px = dims.width as usize; let tx = (idx % ((dims.width as usize).div_ceil(16))) * 16;
418 let ty = (idx / ((dims.width as usize).div_ceil(16))) * 16;
419 let mut src = Vec::with_capacity(n);
420 for y in 0..th {
421 let row_off = ((ty + y) * cols_px + tx) * 3;
422 src.extend_from_slice(&med_pred[row_off..row_off + tw * 3]);
423 }
424 tile.data
425 .iter()
426 .zip(&src)
427 .map(|(x, m)| x.wrapping_sub(*m))
428 .collect()
429 } else {
430 tile.data.clone()
431 };
432
433 let lit_body = pack(&lit_src);
434 let mut choice_body = lit_body.clone();
435 let mut choice_mode = 0u8;
436 let mut choice_ref: Option<(u32, u8)> = None;
437 let mut best_cost = 1 + proxy_cost(coder, &lit_body);
438
439 if !pure_pred {
442 let (cy, cco, ccg) = ycocg_forward(&tile.data);
443 let mut m3 = Vec::with_capacity(n + 160);
444 m3.extend_from_slice(&cy);
445 m3.extend_from_slice(&pack9(&cco));
446 m3.extend_from_slice(&pack9(&ccg));
447 let m3_cost = 1 + proxy_cost(coder, &m3);
448 if m3_cost < best_cost {
449 best_cost = m3_cost;
450 choice_body = m3;
451 choice_mode = 3;
452 }
453 }
454
455 let tile_t = modcolor_forward_iter(&tile.data);
458
459 if use_atlas && !pure_pred && tiles.len() > 1 {
462 let sig = tile_sig(tile.width, tile.height, &tile.data);
463 for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
464 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
465 let transforms: &[u8] = if !use_geom {
470 &[0]
471 } else if tile.width == tile.height {
472 &[0, 1, 2, 3, 4, 5, 6, 7]
473 } else {
474 &[0, 1, 2, 3]
475 };
476 for &t in transforms {
477 let base = if t == 0 {
478 ref_rgb.clone()
479 } else {
480 apply_transform(t, tile.width, tile.height, ref_rgb)
481 };
482 let mut r = vec![0u8; n];
483 for i in 0..n {
484 r[i] = tile.data[i].wrapping_sub(base[i]);
485 }
486 let body = pack(&r);
487 let meta: u64 = if t == 0 { 5 } else { 6 };
489 let cost = meta + proxy_cost(coder, &body);
490 if cost < best_cost {
491 best_cost = cost;
492 choice_body = body;
493 choice_mode = if t == 0 { 1 } else { 2 };
494 choice_ref = Some((prior, t));
495 }
496 let base_t = modcolor_forward_iter(&base);
503 let mut r_c = vec![0u8; n];
504 for i in 0..n {
505 r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
506 }
507 let body_c = pack(&r_c);
508 let cost_c = meta + proxy_cost(coder, &body_c);
509 if cost_c < best_cost {
510 best_cost = cost_c;
511 choice_body = body_c;
512 choice_mode = if t == 0 { 4 } else { 5 };
513 choice_ref = Some((prior, t));
514 }
515 }
516 }
517 }
518
519 stats.modes.push(choice_mode);
520 stats.refs.push(choice_ref);
521 match choice_mode {
522 0 => stats.literal += 1,
523 1 => stats.ref_exact += 1,
524 2 => {
525 stats.ref_geom += 1;
526 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
527 stats.geom_nontrivial += 1;
528 }
529 }
530 3 => stats.ycocg_literal += 1,
531 4 => stats.ref_exact_color += 1,
532 5 => {
533 stats.ref_geom_color += 1;
534 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
535 stats.geom_nontrivial += 1;
536 }
537 }
538 _ => unreachable!("mode byte constrained above"),
539 }
540 raw.push(choice_mode);
541 if let Some((p, t)) = choice_ref {
542 raw.extend_from_slice(&p.to_le_bytes());
543 if t != 0 {
544 raw.push(t);
545 }
546 }
547 raw.extend_from_slice(&choice_body);
548 atlas.insert(tile);
549 }
550
551 let payload = backend_compress(backend, &raw)?;
552 let payload_len =
553 u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?; let mut file = Vec::with_capacity(HEADER8 + payload.len());
555 file.extend_from_slice(b"MNF8");
556 file.extend_from_slice(&dims.width.to_le_bytes());
557 file.extend_from_slice(&dims.height.to_le_bytes());
558 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
559 let tile_count = u32::try_from(tiles.len()).map_err(|_| Error::fmt_msg("too many tiles"))?;
560 file.extend_from_slice(&tile_count.to_le_bytes());
561 file.extend_from_slice(&payload_len.to_le_bytes());
562 file.extend_from_slice(&flags.to_le_bytes());
563 file.extend_from_slice(&backend.to_le_bytes());
564 file.extend_from_slice(&payload);
565 Ok((file, stats))
566}
567
568pub fn decode_mnf8(buf: &[u8]) -> Result<(Dims, Vec<u8>, u32)> {
576 decode_mnf8_with_limits(buf, &crate::mnf6::DecodeLimits::default())
577}
578
579pub fn decode_mnf8_with_limits(
582 buf: &[u8],
583 lim: &crate::mnf6::DecodeLimits,
584) -> Result<(Dims, Vec<u8>, u32)> {
585 if buf.len() < HEADER8 {
586 return Err(Error::fmt_msg("short"));
587 }
588 if buf[0..4] != *b"MNF8" {
589 return Err(Error::fmt_msg("magic"));
590 }
591 let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
592 let width = u32le(4);
593 let height = u32le(8);
594 if width == 0 || height == 0 {
595 return Err(Error::fmt_msg("dims"));
596 }
597 if width as u64 * height as u64 > lim.max_pixels {
598 return Err(Error::Limits {
599 which: "max_pixels",
600 limit: lim.max_pixels,
601 actual: width as u64 * height as u64,
602 });
603 }
604 if u32le(12) != TILE_SIZE {
605 return Err(Error::fmt_msg("tilesize"));
606 }
607 let cols = div_ceil(width, TILE_SIZE);
608 let rows = div_ceil(height, TILE_SIZE);
609 let tile_count = u32le(16);
610 if tile_count != cols * rows {
611 return Err(Error::fmt_msg("tilecount"));
612 }
613 if tile_count > lim.max_tiles {
614 return Err(Error::Limits {
615 which: "max_tiles",
616 limit: u64::from(lim.max_tiles),
617 actual: u64::from(tile_count),
618 });
619 }
620 let payload_len = u32le(20);
621 if payload_len > (1 << 31) - 1 {
622 return Err(Error::fmt_msg("payloadlen"));
623 }
624 if buf.len() != HEADER8 + payload_len as usize {
625 return Err(Error::fmt_msg("filelen"));
626 }
627 let flags = u32le(24);
628 if flags & FLAGS_RESERVED8 != 0 {
631 return Err(Error::fmt_msg("reserved-flags"));
632 }
633 let token_wire = flags & FLAG_TOKENS != 0;
634 if token_wire && flags & FLAG_PLANAR != 0 {
637 return Err(Error::fmt_msg("tokens+planar are exclusive"));
638 }
639 let pred_count = u32::from(flags & FLAG_PRED_MED != 0)
641 + u32::from(flags & FLAG_PRED_PLANE != 0)
642 + u32::from(flags & FLAG_PRED_LEFT != 0);
643 if pred_count > 1 {
644 return Err(Error::fmt_msg("multiple predictors selected"));
645 }
646 if token_wire && flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0 {
649 return Err(Error::fmt_msg("tokens+plane/left predictor exclusive"));
650 }
651 let backend = u32le(28);
652 if !matches!(
653 backend,
654 BACKEND_ZLIB | BACKEND_ZSTD19 | BACKEND_RESERVED2 | BACKEND_RESERVED3
655 ) {
656 return Err(Error::fmt_msg(format!("unknown backend {backend}")));
657 }
658 if matches!(backend, BACKEND_RESERVED2 | BACKEND_RESERVED3) {
659 return Err(Error::fmt_msg(format!(
660 "reserved backend {backend} (not shipped)"
661 )));
662 }
663 let planar = flags & FLAG_PLANAR != 0;
664
665 let px_bytes_total: u64 = width as u64 * height as u64 * 3;
680 let limit = if token_wire {
681 (4 * width as u64)
682 .checked_mul(height as u64)
683 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
684 } else {
685 let mode3_max: u64 = 1
686 + (TILE_SIZE * TILE_SIZE) as u64
687 + 2 * ((TILE_SIZE * TILE_SIZE) as u64 * 9 + 7) / 8
688 + 2;
689 px_bytes_total
690 .checked_add(6 * tile_count as u64)
691 .and_then(|l| l.checked_add(2 * mode3_max + 2))
692 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
693 };
694
695 let payload = &buf[HEADER8..];
696 let raw = match backend {
697 BACKEND_ZLIB | BACKEND_ZSTD19 => backend_decompress(backend, payload, limit)?,
698 other => return Err(Error::fmt_msg(format!("unsupported backend {other}"))),
699 };
700
701 let pixels = if token_wire {
702 let res = tokens::decode_plane(&raw, width as usize, height as usize)?;
706 pred_residual_invert(&res, width as usize, height as usize, PredKind::Med)
707 } else {
708 let kind = PredKind::from_flags(flags);
709 match kind {
713 PredKind::None => decode_record_walk(&raw, width, height, tile_count, planar, false)?,
714 PredKind::Med => decode_record_walk(&raw, width, height, tile_count, planar, true)?,
715 other => decode_pred_walk(&raw, width, height, tile_count, planar, other)?,
716 }
717 };
718 Ok((Dims { width, height }, pixels, backend))
719}
720
721fn pred_residual_invert(res: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
726 let mut out = vec![0u8; w * h * 3];
727 for y in 0..h {
728 for x in 0..w {
729 for c in 0..3usize {
730 let i = (y * w + x) * 3 + c;
731 let pred: u32 = if x == 0 && y == 0 {
732 127
733 } else if y == 0 {
734 u32::from(out[i - 3])
735 } else if x == 0 {
736 u32::from(out[i - w * 3])
737 } else {
738 let a = u32::from(out[i - 3]);
739 let b = u32::from(out[i - w * 3]);
740 let cc = u32::from(out[i - w * 3 - 3]);
741 kind.predict(a, b, cc)
742 };
743 out[i] = (pred + u32::from(res[i])).wrapping_rem_euclid(256) as u8;
744 }
745 }
746 }
747 out
748}
749
750fn decode_pred_walk(
757 raw: &[u8],
758 width: u32,
759 height: u32,
760 tile_count: u32,
761 planar: bool,
762 kind: PredKind,
763) -> Result<Vec<u8>> {
764 let cols = div_ceil(width, TILE_SIZE);
773 let mut pixels = vec![0u8; width as usize * height as usize * 3];
774 let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
775 let mut ip = 0usize;
776 for i in 0..tile_count {
777 let row = i / cols;
778 let col = i % cols;
779 let tw = TILE_SIZE.min(width - col * TILE_SIZE);
780 let th = TILE_SIZE.min(height - row * TILE_SIZE);
781 let n = (tw * th * 3) as usize;
782 if ip >= raw.len() {
783 return Err(Error::fmt_msg(format!("eof at tile {i}")));
784 }
785 let mode = raw[ip];
786 ip += 1;
787 let tile = match mode {
788 0 => {
789 if ip + n > raw.len() {
790 return Err(Error::fmt_msg("body"));
791 }
792 let body = raw[ip..ip + n].to_vec();
793 ip += n;
794 let body = if planar {
795 crate::mnf6::planar_unpack(&body)
796 } else {
797 body
798 };
799 let mut recon = vec![0u8; n];
802 let gy0 = row as usize * TILE_SIZE as usize;
803 let gx0 = col as usize * TILE_SIZE as usize;
804 for y in 0..th as usize {
805 for x in 0..tw as usize {
806 for c in 0..3usize {
807 let k = (y * tw as usize + x) * 3 + c;
808 let gy = gy0 + y;
809 let gx = gx0 + x;
810 let tl = |ggy: usize, ggx: usize| -> u32 {
811 if ggy >= gy0 && ggx >= gx0 {
812 u32::from(
813 recon[((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
814 )
815 } else {
816 u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
817 }
818 };
819 let pred: u32 = if gy == 0 && gx == 0 {
820 127
821 } else if gy == 0 {
822 if gx - 1 < gx0 {
823 u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
824 } else {
825 u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
826 }
827 } else if gx == 0 {
828 if gy - 1 < gy0 {
829 u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
830 } else {
831 u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
832 }
833 } else {
834 let a = tl(gy, gx - 1);
835 let b = tl(gy - 1, gx);
836 let cc = tl(gy - 1, gx - 1);
837 kind.predict(a, b, cc)
838 };
839 recon[k] = (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
840 }
841 }
842 }
843 recon
844 }
845 _ => {
846 return Err(Error::fmt_msg(format!(
851 "tile {i}: mode {mode} under PRED_PLANE/LEFT — not yet admitted"
852 )));
853 }
854 };
855 decoded.push((tw, th, tile.clone()));
856 for y_ in 0..th as usize {
857 let py = row as usize * TILE_SIZE as usize + y_;
858 for x_ in 0..tw as usize {
859 let pxx = col as usize * TILE_SIZE as usize + x_;
860 let d = (py * width as usize + pxx) * 3;
861 let s = (y_ * tw as usize + x_) * 3;
862 pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
863 }
864 }
865 }
866 if ip != raw.len() {
867 return Err(Error::fmt_msg("extra"));
868 }
869 Ok(pixels)
870}
871
872#[cfg(test)]
873mod gate30 {
874 use super::*;
875 use crate::mnf6::Coder;
876 use std::path::Path;
877 fn load(name: &str) -> (Dims, Vec<u8>) {
878 let p = format!("{}/testdata/{}", env!("CARGO_MANIFEST_DIR"), name);
879 let img = image::open(Path::new(&p))
880 .unwrap_or_else(|e| panic!("{name}: {e}"))
881 .to_rgb8();
882 (
883 Dims {
884 width: img.width(),
885 height: img.height(),
886 },
887 img.into_raw(),
888 )
889 }
890 #[test]
893 #[ignore]
894 fn gate_all_arms_real_wire() {
895 let files = [
896 "gradient_320x240.png",
897 "noise_256x256.png",
898 "repeat_256x256.png",
899 "screenshot_640x400.png",
900 "solid_256x256.png",
901 "heldout/blob_224x160.png",
902 "heldout/blocks_192x128.png",
903 "heldout/document_200x280.png",
904 "heldout/plaid_192.png",
905 "heldout/poster_208x144.png",
906 "heldout-photos/autumn_160x120_s3100006.png",
907 "heldout-photos/dusk_160x120_s3100003.png",
908 "heldout-photos/dusk_224x160_s3100002.png",
909 "heldout-photos/forest_192x144_s3100001.png",
910 "heldout-photos/forest_224x160_s3100005.png",
911 "heldout-photos/forest_224x160_s3100008.png",
912 "heldout-photos/ocean_192x144_s3100007.png",
913 "heldout-photos/sand_192x144_s3100004.png",
914 "heldout-photos/slate_160x120_s3100009.png",
915 "heldout-photos/terrain_160x120_s3100000.png",
916 "photos/autumn_192x144_s900001.png",
917 "photos/dusk_160x120_s900003.png",
918 "photos/forest_224x160_s900002.png",
919 "photos/ocean_192x144_s900007.png",
920 "photos/sand_160x120_s900000.png",
921 "photos/slate_192x144_s900004.png",
922 "photos/slate_224x160_s900008.png",
923 "photos/stone_160x120_s900006.png",
924 "photos/terrain_160x120_s900009.png",
925 "photos/terrain_224x160_s900005.png",
926 ];
927 let mut old_tot = 0usize;
928 let mut new_tot = 0usize;
929 let mut fires = 0usize;
930 for name in files {
931 let (d, px) = load(name);
932 let mut old = usize::MAX;
934 for kind in [PredKind::None, PredKind::Med] {
935 for (backend, coder) in [
936 (BACKEND_ZSTD19, Coder::Zstd(19)),
937 (BACKEND_ZLIB, Coder::Zlib),
938 ] {
939 for planar in [false, true] {
940 let med = kind == PredKind::Med;
941 let (file, _) = encode_stream_coder(
942 &px,
943 d,
944 true,
945 true,
946 kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
947 planar,
948 med,
949 kind,
950 &coder,
951 backend,
952 )
953 .unwrap();
954 let _ = &file;
956 old = old.min(file.len());
957 }
958 }
959 }
960 for (backend, _coder) in [
961 (BACKEND_ZSTD19, Coder::Zstd(19)),
962 (BACKEND_ZLIB, Coder::Zlib),
963 ] {
964 let (tw, _) =
965 encode_tokens_coder(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS).unwrap();
966 old = old.min(tw.len());
967 }
968 let mut new_ = old;
970 let mut new_win = String::new();
971 for kind in [PredKind::Plane, PredKind::Left] {
972 for (backend, coder) in [
973 (BACKEND_ZSTD19, Coder::Zstd(19)),
974 (BACKEND_ZLIB, Coder::Zlib),
975 ] {
976 let (file, _) = encode_stream_coder(
977 &px,
978 d,
979 true,
980 true,
981 kind.flag_bit(),
982 false,
983 false,
984 kind,
985 &coder,
986 backend,
987 )
988 .unwrap();
989 let (dd, out, _) =
991 decode_mnf8(&file).unwrap_or_else(|e| panic!("{name} {kind:?}: {e}"));
992 if out != px {
993 let pos = out
994 .iter()
995 .zip(&px)
996 .position(|(a, b)| a != b)
997 .expect("differs");
998 panic!(
999 "{name} {kind:?} NOT exact: first diff byte {pos} (pixel y={}, x={}, c={}): dec={} enc={}",
1000 pos / 3 / d.width as usize, pos / 3 % d.width as usize, pos % 3,
1001 out[pos], px[pos]
1002 );
1003 }
1004 let _ = dd;
1005 if file.len() < new_ {
1006 new_ = file.len();
1007 new_win = format!("{kind:?}-{backend}");
1008 }
1009 }
1010 }
1011 if new_ < old {
1012 fires += 1;
1013 }
1014 old_tot += old;
1015 new_tot += new_;
1016 println!(
1017 "{name:44} old={old:>8} new={new_:>8} {:+6} {}",
1018 new_ as i64 - old as i64,
1019 new_win
1020 );
1021 }
1022 println!(
1023 "TOTAL old={old_tot} new={new_tot} delta={:+} fires={fires}",
1024 new_tot as i64 - old_tot as i64
1025 );
1026 assert!(new_tot < old_tot, "aggregate must strictly improve");
1027 }
1028
1029 #[test]
1030 fn branch_arms_roundtrip_and_pick_fires() {
1031 let path = format!(
1035 "{}/testdata/heldout/poster_208x144.png",
1036 env!("CARGO_MANIFEST_DIR")
1037 );
1038 let img = image::open(std::path::Path::new(&path)).unwrap().to_rgb8();
1039 let d = Dims {
1040 width: img.width(),
1041 height: img.height(),
1042 };
1043 let px = img.into_raw();
1044 let (buf, st, _) = encode_mnf8(&px, d, true, Profile::Balanced);
1045 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1046 assert_eq!(out, px, "picked arm decodes exactly");
1047 assert!(
1048 st.chosen_flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1049 "poster must pick a branch arm (picked {:#x})",
1050 st.chosen_flags
1051 );
1052 let (bufold, _, _) = {
1054 let mut best: Option<(Vec<u8>, u32)> = None;
1056 for (backend, coder) in [(BACKEND_ZSTD19, Coder::Zstd(19))] {
1057 for kind in [PredKind::None, PredKind::Med] {
1058 for planar in [false, true] {
1059 let (f, _) = encode_stream_coder(
1060 &px,
1061 d,
1062 true,
1063 true,
1064 kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
1065 planar,
1066 kind == PredKind::Med,
1067 kind,
1068 &coder,
1069 backend,
1070 )
1071 .unwrap();
1072 if best.as_ref().is_none_or(|b| f.len() < b.0.len()) {
1073 best = Some((f, kind.flag_bit()));
1074 }
1075 }
1076 }
1077 let (t, _) =
1078 encode_tokens_coder(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS).unwrap();
1079 if best.as_ref().is_none_or(|b| t.len() < b.0.len()) {
1080 best = Some((t, FLAG_TOKENS));
1081 }
1082 }
1083 let (f, fl) = best.unwrap();
1084 (f, fl, 0u32)
1085 };
1086 assert!(
1087 buf.len() <= bufold.len(),
1088 "new pick ({}) must not exceed the old pick ({})",
1089 buf.len(),
1090 bufold.len()
1091 );
1092 }
1093
1094 #[test]
1095 fn rejects_multiple_predictors() {
1096 let mut f = vec![0u8; HEADER8];
1098 f[0..4].copy_from_slice(b"MNF8");
1099 f[4..8].copy_from_slice(&16u32.to_le_bytes());
1100 f[8..12].copy_from_slice(&16u32.to_le_bytes());
1101 f[12..16].copy_from_slice(&16u32.to_le_bytes());
1102 f[16..20].copy_from_slice(&1u32.to_le_bytes());
1103 f[20..24].copy_from_slice(&0u32.to_le_bytes());
1104 f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_PRED_PLANE).to_le_bytes());
1105 f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1106 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1107 assert!(
1108 r.is_ok() && r.unwrap().is_err(),
1109 "two predictors must reject"
1110 );
1111 f[24..28].copy_from_slice(&(FLAG_TOKENS | FLAG_PRED_LEFT).to_le_bytes());
1112 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1113 assert!(r.is_ok() && r.unwrap().is_err(), "tokens+left must reject");
1114 }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use super::*;
1120
1121 fn roundtrip(px: &[u8], dims: Dims, profile: Profile) -> u32 {
1122 let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
1123 let (d, out, _) = decode_mnf8(&buf).expect("decode");
1124 assert_eq!(d, dims);
1125 assert_eq!(
1126 out, px,
1127 "profile {profile:?} backend {backend} flags {st:?}"
1128 );
1129 backend
1130 }
1131
1132 #[test]
1133 fn mnf8_balanced_roundtrip_all_classes() {
1134 for (w, h, seed) in [(16u32, 16u32, 1), (48, 33, 2), (31, 17, 4), (1, 1, 5)] {
1135 let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
1136 let mut rnd = move || {
1137 s ^= s << 13;
1138 s ^= s >> 7;
1139 s ^= s << 17;
1140 s
1141 };
1142 let px: Vec<u8> = (0..w as usize * h as usize * 3)
1143 .map(|_| (rnd() & 0xFF) as u8)
1144 .collect();
1145 roundtrip(
1146 &px,
1147 Dims {
1148 width: w,
1149 height: h,
1150 },
1151 Profile::Balanced,
1152 );
1153 }
1154 }
1155
1156 #[test]
1157 fn mnf8_maximum_never_loses_to_mnf7() {
1158 for name in [
1161 "screenshot_640x400.png",
1162 "solid_256x256.png",
1163 "gradient_320x240.png",
1164 ] {
1165 let path = format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"));
1166 let img = image::open(&path)
1167 .unwrap_or_else(|e| panic!("{name}: {e}"))
1168 .to_rgb8();
1169 let d = Dims {
1170 width: img.width(),
1171 height: img.height(),
1172 };
1173 let px = img.into_raw();
1174 let (buf8, _, backend) = encode_mnf8(&px, d, true, Profile::Maximum);
1175 let (buf7, _) = crate::mnf6::encode_mnf7(&px, d, true);
1176 assert!(
1179 buf8.len() <= buf7.len() + 4,
1180 "{name}: MNF8({backend}) {} > MNF7(+4) {}",
1181 buf8.len(),
1182 buf7.len()
1183 );
1184 let (_, out, _) = decode_mnf8(&buf8).expect("decode");
1185 assert_eq!(out, px, "{name}: exact");
1186 }
1187 }
1188
1189 #[test]
1190 fn mnf8_rejects() {
1191 let mut f = vec![0u8; 32];
1192 f[0..4].copy_from_slice(b"MNF8");
1193 f[4..8].copy_from_slice(&16u32.to_le_bytes());
1194 f[8..12].copy_from_slice(&16u32.to_le_bytes());
1195 f[12..16].copy_from_slice(&16u32.to_le_bytes());
1196 f[16..20].copy_from_slice(&1u32.to_le_bytes());
1197 f[28..32].copy_from_slice(&7u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "unknown backend rejected");
1200 f[28..32].copy_from_slice(&3u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved backend rejected");
1202 f[28..32].copy_from_slice(&2u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved brotli backend rejected");
1204 f[28..32].copy_from_slice(&2u32.to_le_bytes());
1206 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1207 assert!(r.is_ok() && r.unwrap().is_err(), "must error, not panic");
1208 f[28..32].copy_from_slice(&0u32.to_le_bytes());
1209 f[24] = 0b100; assert!(decode_mnf8(&f).is_err(), "reserved flag rejected");
1211 f[0] = b'X';
1213 f[24] = 0;
1214 assert!(decode_mnf8(&f).is_err(), "magic");
1215 }
1216
1217 #[test]
1218 fn mnf8_balanced_picks_zstd_not_hardcoded() {
1219 let (w, h) = (64u32, 64u32);
1223 let mut px = vec![0u8; (w * h * 3) as usize];
1224 for y in 0..h {
1225 for x in 0..w {
1226 let i = ((y * w + x) * 3) as usize;
1227 px[i] = (x * 4) as u8;
1228 px[i + 1] = (y * 4) as u8;
1229 px[i + 2] = 128;
1230 }
1231 }
1232 let (buf, st, backend) = encode_mnf8(
1233 &px,
1234 Dims {
1235 width: w,
1236 height: h,
1237 },
1238 false,
1239 Profile::Balanced,
1240 );
1241 assert_eq!(backend, BACKEND_ZSTD19);
1242 assert!(
1249 st.chosen_flags & !0b1111 == 0,
1250 "balanced wrote a legal flags id, got {:#x}",
1251 st.chosen_flags
1252 );
1253 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1254 assert_eq!(out, px);
1255 }
1256
1257 #[test]
1258 fn mnf8_no_geom_no_flips() {
1259 let (w, h) = (32u32, 32u32); let mut px = vec![0u8; (w * h * 3) as usize];
1264 for y in 0..h {
1265 for x in 0..w {
1266 let i = ((y * w + x) * 3) as usize;
1267 px[i] = (x * 7 % 256) as u8;
1268 px[i + 1] = (y * 3 % 256) as u8;
1269 px[i + 2] = 5;
1270 }
1271 }
1272 let tile_row = 16usize;
1274 let tile_col = 0usize;
1275 let src_off = (tile_row * w as usize + tile_col) * 3;
1276 let tile = px[src_off..src_off + 16 * 16 * 3].to_vec();
1277 let flipped = apply_transform(1, 16, 16, &tile);
1278 px[src_off..src_off + 16 * 16 * 3].copy_from_slice(&flipped);
1279 let stacked = px.clone();
1280 let d = Dims {
1281 width: w,
1282 height: h,
1283 };
1284 let (buf, st, _) = encode_mnf8(&stacked, d, false, Profile::Balanced);
1285 assert_eq!(st.ref_geom, 0, "no flips allowed with use_geom=false");
1287 assert_eq!(st.geom_nontrivial, 0);
1288 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1289 assert_eq!(out, stacked);
1290 }
1291
1292 #[test]
1293 fn mnf8_no_atlas_arm() {
1294 let d = Dims {
1297 width: 32,
1298 height: 32,
1299 };
1300 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1301 .map(|i| (i * 7 % 256) as u8)
1302 .collect();
1303 let (buf, st, _) = encode_mnf8_noclimb(&px, d);
1304 assert_eq!(
1305 st.ref_exact + st.ref_geom + st.ref_exact_color + st.ref_geom_color,
1306 0,
1307 "no-atlas arm must emit zero references"
1308 );
1309 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1310 assert_eq!(out, px);
1311 }
1312
1313 fn encode_mnf8_noclimb(px: &[u8], d: Dims) -> (Vec<u8>, MnF6Stats, u32) {
1315 let mut best: Option<(Vec<u8>, MnF6Stats, u32)> = None;
1317 for flags in [0u32, FLAG_PLANAR] {
1318 let planar = flags & FLAG_PLANAR != 0;
1319 let (file, st) = encode_stream_coder(
1320 px,
1321 d,
1322 false,
1323 false,
1324 flags,
1325 planar,
1326 false,
1327 PredKind::None,
1328 &Coder::Zstd(19),
1329 BACKEND_ZSTD19,
1330 )
1331 .expect("encode");
1332 if best.as_ref().is_none_or(|b| file.len() < b.0.len()) {
1333 best = Some((file, st, BACKEND_ZSTD19));
1334 }
1335 }
1336 let (f, s, b) = best.expect("variant");
1337 (f, s, b)
1338 }
1339
1340 #[test]
1341 fn mnf8_med_flag_roundtrip_photo_like() {
1342 let (w, h) = (64u32, 64u32);
1347 let mut px = vec![0u8; (w * h * 3) as usize];
1348 for y in 0..h {
1349 for x in 0..w {
1350 let i = ((y * w + x) * 3) as usize;
1351 px[i] = (x * 4) as u8;
1352 px[i + 1] = (y * 4) as u8;
1353 px[i + 2] = ((x + y) / 2) as u8;
1354 }
1355 }
1356 let d = Dims {
1357 width: w,
1358 height: h,
1359 };
1360 let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1361 assert!(
1366 st.chosen_flags & !0b1111 == 0,
1367 "chosen flags must be a legal MNF8 id, got {:#x}",
1368 st.chosen_flags
1369 );
1370 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1371 if out != px {
1372 let first = out
1373 .iter()
1374 .zip(&px)
1375 .position(|(a, b)| a != b)
1376 .expect("differs");
1377 let (b_y, rem) = (first / 3 / (w as usize), (first / 3) % (w as usize));
1378 panic!(
1379 "not exact: first diff at byte {first} (pixel ({b_y},{rem}), ch {}): out={:?} px={:?}",
1380 first % 3,
1381 &out[first.saturating_sub(4)..first + 4],
1382 &px[first.saturating_sub(4)..first + 4]
1383 );
1384 }
1385 assert_eq!(out, px, "exact reconstruction under the measured flags");
1386 }
1387
1388 #[test]
1389 fn mnf8_med_flag_picked_on_photo_like_smoothing() {
1390 let (w, h) = (64u32, 64u32);
1394 let mut px = vec![0u8; (w * h * 3) as usize];
1395 for y in 0..h {
1396 for x in 0..w {
1397 let i = ((y * w + x) * 3) as usize;
1398 px[i] = (x * 4 + y * 2) as u8;
1400 px[i + 1] = (y * 6) as u8;
1401 px[i + 2] = ((x + y) * 3) as u8;
1402 }
1403 }
1404 let d = Dims {
1405 width: w,
1406 height: h,
1407 };
1408 let (_, st, _) = encode_mnf8(
1409 &px,
1410 d,
1411 false, Profile::Balanced,
1413 );
1414 assert!(
1419 st.chosen_flags & (FLAG_PRED_MED | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1420 "smooth no-repeat bodies must select a predictor (picked {:#x})",
1421 st.chosen_flags
1422 );
1423 let (_, out, _) =
1424 decode_mnf8(&encode_mnf8(&px, d, false, Profile::Balanced).0).expect("decode");
1425 assert_eq!(out, px, "exact under the measured predictor pick");
1426 }
1427
1428 #[test]
1429 fn mnf8_forced_mode4_mode5_records() {
1430 let mut s: u64 = 42;
1435 let mut rnd = move || {
1436 s ^= s << 13;
1437 s ^= s >> 7;
1438 s ^= s << 17;
1439 s
1440 };
1441 let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
1442 let ta_t = crate::mnf6::modcolor_forward(&a);
1443 let mut tb_t = ta_t.clone();
1444 let ramp = [0u8, 1, 2, 255];
1445 for i in (1..tb_t.len()).step_by(3) {
1446 tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
1447 }
1448 let b = crate::mnf6::modcolor_inverse(&tb_t);
1449 let mut two = a.clone();
1450 two.extend_from_slice(&b);
1451 let d2 = Dims {
1452 width: 16,
1453 height: 32,
1454 };
1455 let (buf, st, _) = encode_mnf8(&two, d2, true, Profile::Balanced);
1456 assert!(
1457 st.ref_exact_color > 0,
1458 "expected mode 4 (or a color-geom below) selected, stats: exact={} geom={} ec={} gc={}",
1459 st.ref_exact,
1460 st.ref_geom,
1461 st.ref_exact_color,
1462 st.ref_geom_color
1463 );
1464 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1465 assert_eq!(out, two, "mode-4 record reconstructs exactly through MNF8");
1466
1467 let flipped = apply_transform(1, 16, 16, &a);
1469 let tb_t2 = crate::mnf6::modcolor_forward(&flipped);
1470 let mut tc_t = tb_t2;
1471 for i in (1..tc_t.len()).step_by(3) {
1472 tc_t[i] = tc_t[i].wrapping_add(ramp[(i / 3) % 4]);
1473 }
1474 let c = crate::mnf6::modcolor_inverse(&tc_t);
1475 let mut three = flipped.clone();
1476 three.extend_from_slice(&c);
1477 let d3 = Dims {
1478 width: 16,
1479 height: 32,
1480 };
1481 let (buf3, st3, _) = encode_mnf8(&three, d3, true, Profile::Balanced);
1482 assert!(
1483 st3.ref_exact_color > 0 || st3.ref_geom_color > 0,
1484 "expected a color-domain reference (exact or geom), stats: ec={} gc={}",
1485 st3.ref_exact_color,
1486 st3.ref_geom_color
1487 );
1488 let (_, out3, _) = decode_mnf8(&buf3).expect("decode");
1489 assert_eq!(out3, three);
1490 }
1491
1492 fn roundtrip_tokens(px: &[u8], dims: Dims, profile: Profile) -> (u32, u32) {
1495 let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
1496 let (d, out, _) = decode_mnf8(&buf).expect("decode");
1497 assert_eq!(d, dims);
1498 assert_eq!(
1499 out, px,
1500 "profile {profile:?} backend {backend} flags {:#x} literal {} matches {}",
1501 st.chosen_flags, st.literal, st.ref_exact
1502 );
1503 (st.literal, st.ref_exact)
1504 }
1505
1506 #[test]
1507 fn token_wire_unit_roundtrip_and_errors() {
1508 use crate::tokens::*;
1509 let (w, h) = (24usize, 16usize);
1511 let mut plane = vec![0u8; w * h * 3];
1512 for i in 0..w * h {
1513 plane[i * 3] = (i % 251) as u8;
1514 plane[i * 3 + 1] = ((i * 7) % 251) as u8;
1515 }
1516 for k in 300..384 {
1517 plane[k * 3..k * 3 + 3].copy_from_slice(&[9, 9, 9]);
1518 }
1519 let (wire, st) = encode_plane(&plane, w, h).expect("encode");
1520 assert!(st.matches >= 1, "planted structures must fire: {st:?}");
1521 let back = decode_plane(&wire, w, h).expect("decode");
1522 assert_eq!(back, plane);
1523 let mut bad = wire.clone();
1525 bad.push(2); assert!(decode_plane(&bad, w, h).is_err());
1527 assert!(decode_plane(&wire[..wire.len() - 1], w, h).is_err());
1528 let mut bad = wire.clone();
1529 bad.push(TAG_LITERAL);
1530 bad.extend_from_slice(&[1, 2, 3]);
1531 assert!(decode_plane(&bad, w, h).is_err()); }
1533
1534 #[test]
1535 fn mnf8_token_flag_picked_on_repeating_residual() {
1536 let (w, h) = (256u32, 256u32);
1541 let mut px = vec![0u8; (w * h * 3) as usize];
1542 for y in 0..h {
1543 for x in 0..w {
1544 let i = ((y * w + x) * 3) as usize;
1545 px[i] = (y * 3) as u8;
1547 px[i + 1] = (y / 4) as u8;
1548 px[i + 2] = 200;
1549 }
1550 }
1551 let d = Dims {
1552 width: w,
1553 height: h,
1554 };
1555 let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1556 assert!(
1561 st.chosen_flags & (FLAG_TOKENS | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1562 "repeating-residual class must pick a measured-winning arm (picked {:#x})",
1563 st.chosen_flags
1564 );
1565 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1566 assert_eq!(out, px, "wire reconstructs exactly under the pick");
1567 }
1568
1569 #[test]
1570 fn mnf8_token_flag_not_picked_on_noise() {
1571 let (w, h) = (64u32, 64u32);
1575 let mut s: u64 = 0x5EED;
1576 let mut rnd = move || {
1577 s ^= s << 13;
1578 s ^= s >> 7;
1579 s ^= s << 17;
1580 s
1581 };
1582 let px: Vec<u8> = (0..w as usize * h as usize * 3)
1583 .map(|_| (rnd() & 0xFF) as u8)
1584 .collect();
1585 let d = Dims {
1586 width: w,
1587 height: h,
1588 };
1589 let (_, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1590 assert_eq!(
1591 st.chosen_flags & FLAG_TOKENS,
1592 0,
1593 "noise class must NOT pick tokens (picked {:#x})",
1594 st.chosen_flags
1595 );
1596 roundtrip_tokens(&px, d, Profile::Balanced);
1597 }
1598
1599 #[test]
1600 fn mnf8_tokens_decode_rejects_malformed_wire() {
1601 let f = vec![0u8; HEADER8 + 7];
1605 let mut f = f;
1606 f[0..4].copy_from_slice(b"MNF8");
1607 f[4..8].copy_from_slice(&16u32.to_le_bytes());
1608 f[8..12].copy_from_slice(&16u32.to_le_bytes());
1609 f[12..16].copy_from_slice(&16u32.to_le_bytes());
1610 f[16..20].copy_from_slice(&1u32.to_le_bytes());
1611 f[20..24].copy_from_slice(&7u32.to_le_bytes());
1612 f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_TOKENS).to_le_bytes());
1613 f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1614 f[28..32].copy_from_slice(&BACKEND_ZLIB.to_le_bytes());
1616 let res: Vec<u8> = vec![0u8; 16 * 16 * 3];
1617 let (wire, _) = crate::tokens::encode_plane(&res, 16, 16).expect("wire");
1618 let mut wire_bad = wire.clone();
1620 wire_bad[0] = 7;
1621 let comp: Vec<u8>;
1622 {
1623 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1624 std::io::Write::write_all(&mut e, &wire_bad).unwrap();
1625 comp = e.finish().unwrap();
1626 }
1627 let mut file = Vec::with_capacity(HEADER8 + comp.len());
1628 file.extend_from_slice(&f[..HEADER8]);
1629 let pl = comp.len() as u32;
1630 file[20..24].copy_from_slice(&pl.to_le_bytes());
1631 file.extend_from_slice(&comp);
1632 let r = std::panic::catch_unwind(|| decode_mnf8(&file));
1633 assert!(
1634 r.is_ok() && r.unwrap().is_err(),
1635 "corrupt wire: Err not panic"
1636 );
1637 let comp2: Vec<u8>;
1639 {
1640 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1641 std::io::Write::write_all(&mut e, &wire).unwrap();
1642 comp2 = e.finish().unwrap();
1643 }
1644 let mut file2 = Vec::with_capacity(HEADER8 + comp2.len());
1645 file2.extend_from_slice(&f[..HEADER8]);
1646 file2[20..24].copy_from_slice(&(comp2.len() as u32).to_le_bytes());
1647 file2.extend_from_slice(&comp2);
1648 let (_, out, _) = decode_mnf8(&file2).expect("valid token wire decodes");
1649 assert_eq!(out, pred_residual_invert(&res, 16, 16, PredKind::Med));
1650 }
1651}