1use std::io::Read;
31
32use crate::codec::{apply_transform, div_ceil, split_tiles, tile_sig, Atlas, Dims, TILE_SIZE};
33use crate::error::{Error, Result};
34use crate::mnf6::{
35 compress_stream, decode_record_walk, med_predict, modcolor_forward_iter, pack9, planar_pack,
36 proxy_cost, ycocg_forward, Coder, MnF6Stats, FLAG_PLANAR,
37};
38
39pub const HEADER8: usize = 32;
41
42pub const FLAG_PRED_MED: u32 = 4; pub const FLAGS_RESERVED8: u32 = !0b111;
47
48pub const BACKEND_ZLIB: u32 = 0;
50pub const BACKEND_ZSTD19: u32 = 1;
52pub const BACKEND_RESERVED2: u32 = 2; pub const BACKEND_RESERVED3: u32 = 3; pub const ZSTD_WINDOW_LOG_MAX: u32 = 23;
61
62use crate::codec::RANK_CANDIDATES;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Profile {
69 Balanced,
71 Maximum,
73}
74
75fn backend_candidates() -> Vec<(u32, Coder)> {
77 vec![
78 (BACKEND_ZSTD19, Coder::Zstd(19)),
79 (BACKEND_ZLIB, Coder::Zlib),
80 ]
81}
82
83fn backend_compress(backend: u32, buf: &[u8]) -> Result<Vec<u8>> {
84 match backend {
85 BACKEND_ZLIB => Ok(compress_stream(&Coder::Zlib, buf)),
86 BACKEND_ZSTD19 => Ok(compress_stream(&Coder::Zstd(19), buf)),
87 other => Err(Error::fmt_msg(format!(
88 "backend {other} not a compressed-output coder"
89 ))),
90 }
91}
92
93fn backend_decompress(backend: u32, buf: &[u8], limit: u64) -> Result<Vec<u8>> {
94 match backend {
95 BACKEND_ZLIB => {
96 let mut d = flate2::read::ZlibDecoder::new(buf).take(limit + 1);
100 let mut v = Vec::new();
101 d.read_to_end(&mut v)
102 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
103 if v.len() as u64 > limit {
104 return Err(Error::fmt_msg("bomb"));
105 }
106 if d.get_ref().total_in() as usize != buf.len() {
107 return Err(Error::fmt_msg("trailing bytes after zlib stream"));
108 }
109 Ok(v)
110 }
111 BACKEND_ZSTD19 => {
112 let cursor = std::io::Cursor::new(buf);
116 let mut d = zstd::stream::read::Decoder::new(cursor)
117 .map_err(|_| Error::Decompress("zstd init"))?;
118 d.window_log_max(ZSTD_WINDOW_LOG_MAX)
119 .map_err(|_| Error::Decompress("zstd window param"))?;
120 let mut v = Vec::new();
121 d.single_frame()
122 .take(limit + 1)
123 .read_to_end(&mut v)
124 .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
125 if v.len() as u64 > limit {
126 return Err(Error::fmt_msg("bomb"));
127 }
128 Ok(v)
135 }
136 other => Err(Error::fmt_msg(format!(
137 "backend {other} not a decompressable coder"
138 ))),
139 }
140}
141
142pub fn encode_mnf8(
156 pixels: &[u8],
157 dims: Dims,
158 use_geom: bool,
159 profile: Profile,
160) -> (Vec<u8>, MnF6Stats, u32) {
161 let candidates: Vec<(u32, Coder)> = match profile {
162 Profile::Balanced => vec![(BACKEND_ZSTD19, Coder::Zstd(19))],
163 Profile::Maximum => backend_candidates(),
164 };
165 let mut best: Option<(Vec<u8>, u32, MnF6Stats, u32)> = None;
166 for (backend, coder) in candidates {
167 for flags in [
168 0u32,
169 FLAG_PLANAR,
170 FLAG_PRED_MED,
171 FLAG_PLANAR | FLAG_PRED_MED,
172 ] {
173 let planar = flags & FLAG_PLANAR != 0;
174 let med = flags & FLAG_PRED_MED != 0;
175 let (file, st) = encode_stream_coder(
176 pixels, dims, true, use_geom, flags, planar, med, &coder, backend,
177 )
178 .expect("encoding an in-memory image cannot fail");
179 let size = file.len();
180 if best.as_ref().is_none_or(|b| size < b.0.len()) {
181 best = Some((file, flags, st, backend));
182 }
183 }
184 }
185 let (file, flags, mut st, backend) = best.expect("at least one candidate");
186 st.chosen_flags = flags;
187 (file, st, backend)
188}
189
190#[allow(clippy::too_many_arguments)]
201fn encode_stream_coder(
202 pixels: &[u8],
203 dims: Dims,
204 use_atlas: bool,
205 use_geom: bool,
206 flags: u32,
207 planar: bool,
208 med: bool,
209 coder: &Coder,
210 backend: u32,
211) -> Result<(Vec<u8>, MnF6Stats)> {
212 let med_pred = if med {
213 med_predict(pixels, dims.width as usize, dims.height as usize)
214 } else {
215 Vec::new()
216 };
217 let tiles = split_tiles(pixels, dims);
218 let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
219 let mut stats = MnF6Stats::default();
220 let mut atlas = Atlas::new();
221
222 for (idx, tile) in tiles.iter().enumerate() {
223 let n = tile.nbytes();
224 let pack = |rgb: &[u8]| -> Vec<u8> {
225 if planar {
226 planar_pack(rgb)
227 } else {
228 rgb.to_vec()
229 }
230 };
231
232 let lit_src: Vec<u8> = if med {
235 let (tw, th) = (tile.width as usize, tile.height as usize);
236 let cols_px = dims.width as usize; let tx = (idx % ((dims.width as usize).div_ceil(16))) * 16;
238 let ty = (idx / ((dims.width as usize).div_ceil(16))) * 16;
239 let mut src = Vec::with_capacity(n);
240 for y in 0..th {
241 let row_off = ((ty + y) * cols_px + tx) * 3;
242 src.extend_from_slice(&med_pred[row_off..row_off + tw * 3]);
243 }
244 tile.data
245 .iter()
246 .zip(&src)
247 .map(|(x, m)| x.wrapping_sub(*m))
248 .collect()
249 } else {
250 tile.data.clone()
251 };
252
253 let lit_body = pack(&lit_src);
254 let mut choice_body = lit_body.clone();
255 let mut choice_mode = 0u8;
256 let mut choice_ref: Option<(u32, u8)> = None;
257 let mut best_cost = 1 + proxy_cost(coder, &lit_body);
258
259 {
261 let (cy, cco, ccg) = ycocg_forward(&tile.data);
262 let mut m3 = Vec::with_capacity(n + 160);
263 m3.extend_from_slice(&cy);
264 m3.extend_from_slice(&pack9(&cco));
265 m3.extend_from_slice(&pack9(&ccg));
266 let m3_cost = 1 + proxy_cost(coder, &m3);
267 if m3_cost < best_cost {
268 best_cost = m3_cost;
269 choice_body = m3;
270 choice_mode = 3;
271 }
272 }
273
274 let tile_t = modcolor_forward_iter(&tile.data);
277
278 if use_atlas && tiles.len() > 1 {
280 let sig = tile_sig(tile.width, tile.height, &tile.data);
281 for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
282 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
283 let transforms: &[u8] = if !use_geom {
288 &[0]
289 } else if tile.width == tile.height {
290 &[0, 1, 2, 3, 4, 5, 6, 7]
291 } else {
292 &[0, 1, 2, 3]
293 };
294 for &t in transforms {
295 let base = if t == 0 {
296 ref_rgb.clone()
297 } else {
298 apply_transform(t, tile.width, tile.height, ref_rgb)
299 };
300 let mut r = vec![0u8; n];
301 for i in 0..n {
302 r[i] = tile.data[i].wrapping_sub(base[i]);
303 }
304 let body = pack(&r);
305 let meta: u64 = if t == 0 { 5 } else { 6 };
307 let cost = meta + proxy_cost(coder, &body);
308 if cost < best_cost {
309 best_cost = cost;
310 choice_body = body;
311 choice_mode = if t == 0 { 1 } else { 2 };
312 choice_ref = Some((prior, t));
313 }
314 let base_t = modcolor_forward_iter(&base);
321 let mut r_c = vec![0u8; n];
322 for i in 0..n {
323 r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
324 }
325 let body_c = pack(&r_c);
326 let cost_c = meta + proxy_cost(coder, &body_c);
327 if cost_c < best_cost {
328 best_cost = cost_c;
329 choice_body = body_c;
330 choice_mode = if t == 0 { 4 } else { 5 };
331 choice_ref = Some((prior, t));
332 }
333 }
334 }
335 }
336
337 stats.modes.push(choice_mode);
338 stats.refs.push(choice_ref);
339 match choice_mode {
340 0 => stats.literal += 1,
341 1 => stats.ref_exact += 1,
342 2 => {
343 stats.ref_geom += 1;
344 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
345 stats.geom_nontrivial += 1;
346 }
347 }
348 3 => stats.ycocg_literal += 1,
349 4 => stats.ref_exact_color += 1,
350 5 => {
351 stats.ref_geom_color += 1;
352 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
353 stats.geom_nontrivial += 1;
354 }
355 }
356 _ => unreachable!("mode byte constrained above"),
357 }
358 raw.push(choice_mode);
359 if let Some((p, t)) = choice_ref {
360 raw.extend_from_slice(&p.to_le_bytes());
361 if t != 0 {
362 raw.push(t);
363 }
364 }
365 raw.extend_from_slice(&choice_body);
366 atlas.insert(tile);
367 }
368
369 let payload = backend_compress(backend, &raw)?;
370 let payload_len =
371 u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?; let mut file = Vec::with_capacity(HEADER8 + payload.len());
373 file.extend_from_slice(b"MNF8");
374 file.extend_from_slice(&dims.width.to_le_bytes());
375 file.extend_from_slice(&dims.height.to_le_bytes());
376 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
377 let tile_count = u32::try_from(tiles.len()).map_err(|_| Error::fmt_msg("too many tiles"))?;
378 file.extend_from_slice(&tile_count.to_le_bytes());
379 file.extend_from_slice(&payload_len.to_le_bytes());
380 file.extend_from_slice(&flags.to_le_bytes());
381 file.extend_from_slice(&backend.to_le_bytes());
382 file.extend_from_slice(&payload);
383 Ok((file, stats))
384}
385
386pub fn decode_mnf8(buf: &[u8]) -> Result<(Dims, Vec<u8>, u32)> {
394 decode_mnf8_with_limits(buf, &crate::mnf6::DecodeLimits::default())
395}
396
397pub fn decode_mnf8_with_limits(
400 buf: &[u8],
401 lim: &crate::mnf6::DecodeLimits,
402) -> Result<(Dims, Vec<u8>, u32)> {
403 if buf.len() < HEADER8 {
404 return Err(Error::fmt_msg("short"));
405 }
406 if buf[0..4] != *b"MNF8" {
407 return Err(Error::fmt_msg("magic"));
408 }
409 let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
410 let width = u32le(4);
411 let height = u32le(8);
412 if width == 0 || height == 0 {
413 return Err(Error::fmt_msg("dims"));
414 }
415 if width as u64 * height as u64 > lim.max_pixels {
416 return Err(Error::Limits {
417 which: "max_pixels",
418 limit: lim.max_pixels,
419 actual: width as u64 * height as u64,
420 });
421 }
422 if u32le(12) != TILE_SIZE {
423 return Err(Error::fmt_msg("tilesize"));
424 }
425 let cols = div_ceil(width, TILE_SIZE);
426 let rows = div_ceil(height, TILE_SIZE);
427 let tile_count = u32le(16);
428 if tile_count != cols * rows {
429 return Err(Error::fmt_msg("tilecount"));
430 }
431 if tile_count > lim.max_tiles {
432 return Err(Error::Limits {
433 which: "max_tiles",
434 limit: u64::from(lim.max_tiles),
435 actual: u64::from(tile_count),
436 });
437 }
438 let payload_len = u32le(20);
439 if payload_len > (1 << 31) - 1 {
440 return Err(Error::fmt_msg("payloadlen"));
441 }
442 if buf.len() != HEADER8 + payload_len as usize {
443 return Err(Error::fmt_msg("filelen"));
444 }
445 let flags = u32le(24);
446 if flags & FLAGS_RESERVED8 != 0 {
448 return Err(Error::fmt_msg("reserved-flags"));
449 }
450 let backend = u32le(28);
451 if !matches!(
452 backend,
453 BACKEND_ZLIB | BACKEND_ZSTD19 | BACKEND_RESERVED2 | BACKEND_RESERVED3
454 ) {
455 return Err(Error::fmt_msg(format!("unknown backend {backend}")));
456 }
457 if matches!(backend, BACKEND_RESERVED2 | BACKEND_RESERVED3) {
458 return Err(Error::fmt_msg(format!(
459 "reserved backend {backend} (not shipped)"
460 )));
461 }
462 let planar = flags & FLAG_PLANAR != 0;
463
464 let mode3_max: u64 =
473 1 + (TILE_SIZE * TILE_SIZE) as u64 + 2 * ((TILE_SIZE * TILE_SIZE) as u64 * 9 + 7) / 8 + 2;
474 let px_bytes_total: u64 = width as u64 * height as u64 * 3;
475 let limit = px_bytes_total
476 .checked_add(6 * tile_count as u64)
477 .and_then(|l| l.checked_add(2 * mode3_max + 2))
478 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?;
479
480 let payload = &buf[HEADER8..];
481 let raw = match backend {
482 BACKEND_ZLIB | BACKEND_ZSTD19 => backend_decompress(backend, payload, limit)?,
483 other => return Err(Error::fmt_msg(format!("unsupported backend {other}"))),
484 };
485
486 let pixels = decode_record_walk(
487 &raw,
488 width,
489 height,
490 tile_count,
491 planar,
492 flags & FLAG_PRED_MED != 0,
493 )?;
494 Ok((Dims { width, height }, pixels, backend))
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 fn roundtrip(px: &[u8], dims: Dims, profile: Profile) -> u32 {
502 let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
503 let (d, out, _) = decode_mnf8(&buf).expect("decode");
504 assert_eq!(d, dims);
505 assert_eq!(
506 out, px,
507 "profile {profile:?} backend {backend} flags {st:?}"
508 );
509 backend
510 }
511
512 #[test]
513 fn mnf8_balanced_roundtrip_all_classes() {
514 for (w, h, seed) in [(16u32, 16u32, 1), (48, 33, 2), (31, 17, 4), (1, 1, 5)] {
515 let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
516 let mut rnd = move || {
517 s ^= s << 13;
518 s ^= s >> 7;
519 s ^= s << 17;
520 s
521 };
522 let px: Vec<u8> = (0..w as usize * h as usize * 3)
523 .map(|_| (rnd() & 0xFF) as u8)
524 .collect();
525 roundtrip(
526 &px,
527 Dims {
528 width: w,
529 height: h,
530 },
531 Profile::Balanced,
532 );
533 }
534 }
535
536 #[test]
537 fn mnf8_maximum_never_loses_to_mnf7() {
538 for name in [
541 "screenshot_640x400.png",
542 "solid_256x256.png",
543 "gradient_320x240.png",
544 ] {
545 let path = format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"));
546 let img = image::open(&path)
547 .unwrap_or_else(|e| panic!("{name}: {e}"))
548 .to_rgb8();
549 let d = Dims {
550 width: img.width(),
551 height: img.height(),
552 };
553 let px = img.into_raw();
554 let (buf8, _, backend) = encode_mnf8(&px, d, true, Profile::Maximum);
555 let (buf7, _) = crate::mnf6::encode_mnf7(&px, d, true);
556 assert!(
559 buf8.len() <= buf7.len() + 4,
560 "{name}: MNF8({backend}) {} > MNF7(+4) {}",
561 buf8.len(),
562 buf7.len()
563 );
564 let (_, out, _) = decode_mnf8(&buf8).expect("decode");
565 assert_eq!(out, px, "{name}: exact");
566 }
567 }
568
569 #[test]
570 fn mnf8_rejects() {
571 let mut f = vec![0u8; 32];
572 f[0..4].copy_from_slice(b"MNF8");
573 f[4..8].copy_from_slice(&16u32.to_le_bytes());
574 f[8..12].copy_from_slice(&16u32.to_le_bytes());
575 f[12..16].copy_from_slice(&16u32.to_le_bytes());
576 f[16..20].copy_from_slice(&1u32.to_le_bytes());
577 f[28..32].copy_from_slice(&7u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "unknown backend rejected");
580 f[28..32].copy_from_slice(&3u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved backend rejected");
582 f[28..32].copy_from_slice(&2u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved brotli backend rejected");
584 f[28..32].copy_from_slice(&2u32.to_le_bytes());
586 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
587 assert!(r.is_ok() && r.unwrap().is_err(), "must error, not panic");
588 f[28..32].copy_from_slice(&0u32.to_le_bytes());
589 f[24] = 0b100; assert!(decode_mnf8(&f).is_err(), "reserved flag rejected");
591 f[0] = b'X';
593 f[24] = 0;
594 assert!(decode_mnf8(&f).is_err(), "magic");
595 }
596
597 #[test]
598 fn mnf8_balanced_picks_zstd_not_hardcoded() {
599 let (w, h) = (64u32, 64u32);
603 let mut px = vec![0u8; (w * h * 3) as usize];
604 for y in 0..h {
605 for x in 0..w {
606 let i = ((y * w + x) * 3) as usize;
607 px[i] = (x * 4) as u8;
608 px[i + 1] = (y * 4) as u8;
609 px[i + 2] = 128;
610 }
611 }
612 let (buf, st, backend) = encode_mnf8(
613 &px,
614 Dims {
615 width: w,
616 height: h,
617 },
618 false,
619 Profile::Balanced,
620 );
621 assert_eq!(backend, BACKEND_ZSTD19);
622 assert!(
629 st.chosen_flags & !0b111 == 0,
630 "balanced wrote a legal flags id, got {:#x}",
631 st.chosen_flags
632 );
633 let (_, out, _) = decode_mnf8(&buf).expect("decode");
634 assert_eq!(out, px);
635 }
636
637 #[test]
638 fn mnf8_no_geom_no_flips() {
639 let (w, h) = (32u32, 32u32); let mut px = vec![0u8; (w * h * 3) as usize];
644 for y in 0..h {
645 for x in 0..w {
646 let i = ((y * w + x) * 3) as usize;
647 px[i] = (x * 7 % 256) as u8;
648 px[i + 1] = (y * 3 % 256) as u8;
649 px[i + 2] = 5;
650 }
651 }
652 let tile_row = 16usize;
654 let tile_col = 0usize;
655 let src_off = (tile_row * w as usize + tile_col) * 3;
656 let tile = px[src_off..src_off + 16 * 16 * 3].to_vec();
657 let flipped = apply_transform(1, 16, 16, &tile);
658 px[src_off..src_off + 16 * 16 * 3].copy_from_slice(&flipped);
659 let stacked = px.clone();
660 let d = Dims {
661 width: w,
662 height: h,
663 };
664 let (buf, st, _) = encode_mnf8(&stacked, d, false, Profile::Balanced);
665 assert_eq!(st.ref_geom, 0, "no flips allowed with use_geom=false");
667 assert_eq!(st.geom_nontrivial, 0);
668 let (_, out, _) = decode_mnf8(&buf).expect("decode");
669 assert_eq!(out, stacked);
670 }
671
672 #[test]
673 fn mnf8_no_atlas_arm() {
674 let d = Dims {
677 width: 32,
678 height: 32,
679 };
680 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
681 .map(|i| (i * 7 % 256) as u8)
682 .collect();
683 let (buf, st, _) = encode_mnf8_noclimb(&px, d);
684 assert_eq!(
685 st.ref_exact + st.ref_geom + st.ref_exact_color + st.ref_geom_color,
686 0,
687 "no-atlas arm must emit zero references"
688 );
689 let (_, out, _) = decode_mnf8(&buf).expect("decode");
690 assert_eq!(out, px);
691 }
692
693 fn encode_mnf8_noclimb(px: &[u8], d: Dims) -> (Vec<u8>, MnF6Stats, u32) {
695 let mut best: Option<(Vec<u8>, MnF6Stats, u32)> = None;
697 for flags in [0u32, FLAG_PLANAR] {
698 let planar = flags & FLAG_PLANAR != 0;
699 let (file, st) = encode_stream_coder(
700 px,
701 d,
702 false,
703 false,
704 flags,
705 planar,
706 false,
707 &Coder::Zstd(19),
708 BACKEND_ZSTD19,
709 )
710 .expect("encode");
711 if best.as_ref().is_none_or(|b| file.len() < b.0.len()) {
712 best = Some((file, st, BACKEND_ZSTD19));
713 }
714 }
715 let (f, s, b) = best.expect("variant");
716 (f, s, b)
717 }
718
719 #[test]
720 fn mnf8_med_flag_roundtrip_photo_like() {
721 let (w, h) = (64u32, 64u32);
726 let mut px = vec![0u8; (w * h * 3) as usize];
727 for y in 0..h {
728 for x in 0..w {
729 let i = ((y * w + x) * 3) as usize;
730 px[i] = (x * 4) as u8;
731 px[i + 1] = (y * 4) as u8;
732 px[i + 2] = ((x + y) / 2) as u8;
733 }
734 }
735 let d = Dims {
736 width: w,
737 height: h,
738 };
739 let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
740 assert!(
745 st.chosen_flags & !0b111 == 0,
746 "chosen flags must be a legal MNF8 id, got {:#x}",
747 st.chosen_flags
748 );
749 let (_, out, _) = decode_mnf8(&buf).expect("decode");
750 if out != px {
751 let first = out
752 .iter()
753 .zip(&px)
754 .position(|(a, b)| a != b)
755 .expect("differs");
756 let (b_y, rem) = (first / 3 / (w as usize), (first / 3) % (w as usize));
757 panic!(
758 "not exact: first diff at byte {first} (pixel ({b_y},{rem}), ch {}): out={:?} px={:?}",
759 first % 3,
760 &out[first.saturating_sub(4)..first + 4],
761 &px[first.saturating_sub(4)..first + 4]
762 );
763 }
764 assert_eq!(out, px, "exact reconstruction under the measured flags");
765 }
766
767 #[test]
768 fn mnf8_med_flag_picked_on_photo_like_smoothing() {
769 let (w, h) = (64u32, 64u32);
773 let mut px = vec![0u8; (w * h * 3) as usize];
774 for y in 0..h {
775 for x in 0..w {
776 let i = ((y * w + x) * 3) as usize;
777 px[i] = (x * 4 + y * 2) as u8;
779 px[i + 1] = (y * 6) as u8;
780 px[i + 2] = ((x + y) * 3) as u8;
781 }
782 }
783 let d = Dims {
784 width: w,
785 height: h,
786 };
787 let (_, st, _) = encode_mnf8(
788 &px,
789 d,
790 false, Profile::Balanced,
792 );
793 assert_eq!(
794 st.chosen_flags & FLAG_PRED_MED,
795 FLAG_PRED_MED,
796 "smooth no-repeat bodies must select the MED predictor (picked {:#x})",
797 st.chosen_flags
798 );
799 }
800
801 #[test]
802 fn mnf8_forced_mode4_mode5_records() {
803 let mut s: u64 = 42;
808 let mut rnd = move || {
809 s ^= s << 13;
810 s ^= s >> 7;
811 s ^= s << 17;
812 s
813 };
814 let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
815 let ta_t = crate::mnf6::modcolor_forward(&a);
816 let mut tb_t = ta_t.clone();
817 let ramp = [0u8, 1, 2, 255];
818 for i in (1..tb_t.len()).step_by(3) {
819 tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
820 }
821 let b = crate::mnf6::modcolor_inverse(&tb_t);
822 let mut two = a.clone();
823 two.extend_from_slice(&b);
824 let d2 = Dims {
825 width: 16,
826 height: 32,
827 };
828 let (buf, st, _) = encode_mnf8(&two, d2, true, Profile::Balanced);
829 assert!(
830 st.ref_exact_color > 0,
831 "expected mode 4 (or a color-geom below) selected, stats: exact={} geom={} ec={} gc={}",
832 st.ref_exact,
833 st.ref_geom,
834 st.ref_exact_color,
835 st.ref_geom_color
836 );
837 let (_, out, _) = decode_mnf8(&buf).expect("decode");
838 assert_eq!(out, two, "mode-4 record reconstructs exactly through MNF8");
839
840 let flipped = apply_transform(1, 16, 16, &a);
842 let tb_t2 = crate::mnf6::modcolor_forward(&flipped);
843 let mut tc_t = tb_t2;
844 for i in (1..tc_t.len()).step_by(3) {
845 tc_t[i] = tc_t[i].wrapping_add(ramp[(i / 3) % 4]);
846 }
847 let c = crate::mnf6::modcolor_inverse(&tc_t);
848 let mut three = flipped.clone();
849 three.extend_from_slice(&c);
850 let d3 = Dims {
851 width: 16,
852 height: 32,
853 };
854 let (buf3, st3, _) = encode_mnf8(&three, d3, true, Profile::Balanced);
855 assert!(
856 st3.ref_exact_color > 0 || st3.ref_geom_color > 0,
857 "expected a color-domain reference (exact or geom), stats: ec={} gc={}",
858 st3.ref_exact_color,
859 st3.ref_geom_color
860 );
861 let (_, out3, _) = decode_mnf8(&buf3).expect("decode");
862 assert_eq!(out3, three);
863 }
864}