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 FLAG_CHANNEL_DIFF: u32 = 64; pub const FLAG_REF_R: u32 = 128;
79pub const FLAGS_RESERVED8: u32 = !0b11111111;
81
82fn cd_reference(flags: u32) -> u8 {
84 ((flags >> 6) & 0b11) as u8
85}
86
87pub const BACKEND_ZLIB: u32 = 0;
89pub const BACKEND_ZSTD19: u32 = 1;
91pub const BACKEND_RESERVED2: u32 = 2; pub const BACKEND_RESERVED3: u32 = 3;
96pub const BACKEND_MIX: u32 = 4;
99
100pub const BACKEND_MIX5: u32 = 5;
105
106pub const ZSTD_WINDOW_LOG_MAX: u32 = 23;
110
111use crate::codec::RANK_CANDIDATES;
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Profile {
118 Balanced,
120 Maximum,
122}
123
124fn backend_candidates() -> Vec<(u32, Coder)> {
128 vec![
129 (BACKEND_MIX5, Coder::Zstd(19)),
130 (BACKEND_MIX, Coder::Zstd(19)),
131 (BACKEND_ZSTD19, Coder::Zstd(19)),
132 (BACKEND_ZLIB, Coder::Zlib),
133 ]
134}
135
136fn backend_compress(backend: u32, buf: &[u8]) -> Result<Vec<u8>> {
137 match backend {
138 BACKEND_ZLIB => Ok(compress_stream(&Coder::Zlib, buf)),
139 BACKEND_ZSTD19 => Ok(compress_stream(&Coder::Zstd(19), buf)),
140 BACKEND_MIX => Ok(crate::mix_coder::mix_encode(buf)),
141 BACKEND_MIX5 => Ok(crate::mix_coder::mix_encode_with(
142 &crate::mix_coder::MixParams::BACKEND5,
143 buf,
144 )),
145 other => Err(Error::fmt_msg(format!(
146 "backend {other} not a compressed-output coder"
147 ))),
148 }
149}
150
151fn backend_decompress(backend: u32, buf: &[u8], limit: u64) -> Result<Vec<u8>> {
152 match backend {
153 BACKEND_ZLIB => {
154 let mut d = flate2::read::ZlibDecoder::new(buf).take(limit + 1);
158 let mut v = Vec::new();
159 d.read_to_end(&mut v)
160 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
161 if v.len() as u64 > limit {
162 return Err(Error::fmt_msg("bomb"));
163 }
164 if d.get_ref().total_in() as usize != buf.len() {
165 return Err(Error::fmt_msg("trailing bytes after zlib stream"));
166 }
167 Ok(v)
168 }
169 BACKEND_ZSTD19 => {
170 let cursor = std::io::Cursor::new(buf);
174 let mut d = zstd::stream::read::Decoder::new(cursor)
175 .map_err(|_| Error::Decompress("zstd init"))?;
176 d.window_log_max(ZSTD_WINDOW_LOG_MAX)
177 .map_err(|_| Error::Decompress("zstd window param"))?;
178 let mut v = Vec::new();
179 d.single_frame()
180 .take(limit + 1)
181 .read_to_end(&mut v)
182 .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
183 if v.len() as u64 > limit {
184 return Err(Error::fmt_msg("bomb"));
185 }
186 Ok(v)
193 }
194 other => Err(Error::fmt_msg(format!(
195 "backend {other} not a decompressable coder"
196 ))),
197 }
198}
199
200pub fn encode_mnf8(
214 pixels: &[u8],
215 dims: Dims,
216 use_geom: bool,
217 profile: Profile,
218) -> (Vec<u8>, MnF6Stats, u32) {
219 let candidates: Vec<(u32, Coder)> = match profile {
220 Profile::Balanced => vec![(BACKEND_ZSTD19, Coder::Zstd(19))],
221 Profile::Maximum => backend_candidates(),
222 };
223 let mut best: Option<(Vec<u8>, u32, MnF6Stats, u32)> = None;
224 let pred_arms = [
229 PredKind::None,
230 PredKind::Med,
231 PredKind::Plane,
232 PredKind::Left,
233 ];
234 for (backend, coder) in candidates {
235 for kind in pred_arms {
236 for planar in [false, true] {
237 let med = kind == PredKind::Med;
238 for reference in 0u8..=3 {
242 let flags = kind.flag_bit()
243 | if planar { FLAG_PLANAR } else { 0 }
244 | (u32::from(reference) << 6);
245 let (file, st) = encode_stream_coder(
246 pixels, dims, true, use_geom, flags, planar, med, kind, &coder, backend,
247 reference,
248 )
249 .expect("encoding an in-memory image cannot fail");
250 let size = file.len();
251 if best.as_ref().is_none_or(|b| size < b.0.len()) {
252 best = Some((file, flags, st, backend));
253 }
254 }
255 }
256 }
257 let base = FLAG_PRED_MED | FLAG_TOKENS;
261 for cap in [16, tokens::MAX_CANDIDATES] {
262 for reference in 0u8..=3 {
263 let flags = base | (u32::from(reference) << 6);
264 let (file, st) =
265 encode_tokens_coder_cap(pixels, dims, backend, flags, cap, reference)
266 .expect("encoding an in-memory image cannot fail");
267 let size = file.len();
268 if best.as_ref().is_none_or(|b| size < b.0.len()) {
269 best = Some((file, flags, st, backend));
270 }
271 }
272 }
273 }
274 let (file, flags, mut st, backend) = best.expect("at least one candidate");
275 st.chosen_flags = flags;
276 (file, st, backend)
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub enum PredKind {
283 None,
285 Med,
287 Plane,
289 Left,
291}
292
293impl PredKind {
294 fn from_flags(flags: u32) -> PredKind {
295 if flags & FLAG_PRED_MED != 0 {
296 PredKind::Med
297 } else if flags & FLAG_PRED_PLANE != 0 {
298 PredKind::Plane
299 } else if flags & FLAG_PRED_LEFT != 0 {
300 PredKind::Left
301 } else {
302 PredKind::None
303 }
304 }
305
306 fn flag_bit(self) -> u32 {
307 match self {
308 PredKind::None => 0,
309 PredKind::Med => FLAG_PRED_MED,
310 PredKind::Plane => FLAG_PRED_PLANE,
311 PredKind::Left => FLAG_PRED_LEFT,
312 }
313 }
314
315 fn predict(self, a: u32, b: u32, cc: u32) -> u32 {
318 match self {
319 PredKind::None | PredKind::Med => crate::mnf6::med_predict_pixel(a, b, cc),
320 PredKind::Plane => {
321 (a as i64 + b as i64 - cc as i64).clamp(0, 255) as u32
324 }
325 PredKind::Left => a,
326 }
327 }
328}
329
330fn image_predict(pixels: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
337 let mut out = vec![0u8; pixels.len()];
338 for y in 0..h {
339 for x in 0..w {
340 for c in 0..3usize {
341 let i = (y * w + x) * 3 + c;
342 let pred: u32 = if x == 0 && y == 0 {
343 127
344 } else if y == 0 {
345 u32::from(pixels[i - 3])
346 } else if x == 0 {
347 u32::from(pixels[i - w * 3])
348 } else {
349 let a = u32::from(pixels[i - 3]);
350 let b = u32::from(pixels[i - w * 3]);
351 let cc = u32::from(pixels[i - w * 3 - 3]);
352 kind.predict(a, b, cc)
353 };
354 out[i] = pred as u8;
355 }
356 }
357 }
358 out
359}
360
361fn channel_diff_relabel(
368 stream: &[u8],
369 planar: bool,
370 tokens: bool,
371 w: u32,
372 h: u32,
373 reference: u8,
374) -> Vec<u8> {
375 if tokens {
376 let mut out = Vec::with_capacity(stream.len());
378 let mut pos = 0usize;
379 let npx = (w as usize) * (h as usize);
380 let mut ppos = 0usize;
381 while ppos < npx && pos < stream.len() {
382 let tag = stream[pos];
383 out.push(tag);
384 pos += 1;
385 match tag {
386 tokens::TAG_LITERAL => {
387 let px = &stream[pos..pos + 3];
388 let (refc, a, b) = ref_pick(px, reference);
389 out.push(refc);
390 out.push(a.wrapping_sub(refc));
391 out.push(b.wrapping_sub(refc));
392 pos += 3;
393 ppos += 1;
394 }
395 tokens::TAG_MATCH => {
396 if pos + 5 > stream.len() {
397 panic!("cd: token match truncated (pos {pos}/{})", stream.len());
398 }
399 let len = u32::from(stream[pos + 4]) as usize;
400 out.extend_from_slice(&stream[pos..pos + 5]);
401 pos += 5;
402 ppos += len;
403 }
404 tokens::TAG_NEAR => {
405 if pos + 3 > stream.len() {
406 panic!("cd: token near truncated (pos {pos}/{})", stream.len());
407 }
408 let len = u32::from(stream[pos + 2]) as usize;
409 out.extend_from_slice(&stream[pos..pos + 3]);
410 pos += 3;
411 ppos += len;
412 }
413 other => panic!(
414 "token tag {other} outside the recorded grammar (pos {pos}/{}, ppos {ppos}/{npx})",
415 stream.len()
416 ),
417 }
418 }
419 debug_assert_eq!(ppos, npx);
420 out
421 } else if planar {
422 let mut out = Vec::with_capacity(stream.len());
425 let mut pos = 0usize;
426 let cols = (w as usize).div_ceil(TILE_SIZE as usize);
427 let tile_count =
428 (w as usize).div_ceil(TILE_SIZE as usize) * (h as usize).div_ceil(TILE_SIZE as usize);
429 for i in 0..tile_count {
430 let (row, col) = (i / cols, i % cols);
431 let tw = (TILE_SIZE as usize).min(w as usize - col * TILE_SIZE as usize);
432 let th = (TILE_SIZE as usize).min(h as usize - row * TILE_SIZE as usize);
433 let nb = tw * th * 3;
434 let mode = stream[pos];
435 out.push(mode);
436 pos += 1;
437 if mode == 0 {
438 let body = &stream[pos..pos + nb];
439 let third = nb / 3;
440 let (pr, pg, pb) = (&body[..third], &body[third..2 * third], &body[2 * third..]);
441 match reference {
442 1 => {
443 out.extend_from_slice(pg);
444 for (r, g) in pr.iter().zip(pg) {
445 out.push(r.wrapping_sub(*g));
446 }
447 for (b, g) in pb.iter().zip(pg) {
448 out.push(b.wrapping_sub(*g));
449 }
450 }
451 2 => {
452 out.extend_from_slice(pr);
453 for (g, r) in pg.iter().zip(pr) {
454 out.push(g.wrapping_sub(*r));
455 }
456 for (b, r) in pb.iter().zip(pr) {
457 out.push(b.wrapping_sub(*r));
458 }
459 }
460 3 => {
461 out.extend_from_slice(pb);
462 for (r, b) in pr.iter().zip(pb) {
463 out.push(r.wrapping_sub(*b));
464 }
465 for (g, b) in pg.iter().zip(pb) {
466 out.push(g.wrapping_sub(*b));
467 }
468 }
469 _ => unreachable!("reference constrained"),
470 }
471 pos += nb;
472 } else if mode == 1 {
473 const META: usize = 4;
474 out.extend_from_slice(&stream[pos..pos + META + nb]);
475 pos += META + nb;
476 } else if mode == 2 {
477 const META: usize = 5;
478 out.extend_from_slice(&stream[pos..pos + META + nb]);
479 pos += META + nb;
480 } else if mode == 3 {
481 let np = nb / 3;
482 let n9 = (np * 9).div_ceil(8);
483 out.extend_from_slice(&stream[pos..pos + np + 2 * n9]);
484 pos += np + 2 * n9;
485 } else if mode == 4 || mode == 5 {
486 let meta = if mode == 5 { 5 } else { 4 };
487 out.extend_from_slice(&stream[pos..pos + meta + nb]);
488 pos += meta + nb;
489 } else {
490 panic!(
491 "mode {mode} outside the recorded grammar (tile {i} pos {pos}/{})",
492 stream.len()
493 );
494 }
495 }
496 debug_assert_eq!(pos, stream.len());
497 out
498 } else {
499 let mut out = Vec::with_capacity(stream.len());
502 let mut pos = 0usize;
503 let cols = (w as usize).div_ceil(TILE_SIZE as usize);
504 let tile_count =
505 (w as usize).div_ceil(TILE_SIZE as usize) * (h as usize).div_ceil(TILE_SIZE as usize);
506 for i in 0..tile_count {
507 let (row, col) = (i / cols, i % cols);
508 let tw = (TILE_SIZE as usize).min(w as usize - col * TILE_SIZE as usize);
509 let th = (TILE_SIZE as usize).min(h as usize - row * TILE_SIZE as usize);
510 let nb = tw * th * 3;
511 let mode = stream[pos];
512 out.push(mode);
513 pos += 1;
514 if mode == 0 {
515 for k in 0..nb / 3 {
516 let t = &stream[pos + k * 3..pos + k * 3 + 3];
517 let (refc, a, b) = ref_pick(t, reference);
518 out.push(refc);
519 out.push(a.wrapping_sub(refc));
520 out.push(b.wrapping_sub(refc));
521 }
522 pos += nb;
523 } else {
524 let extra = match mode {
527 1 | 4 => 4 + nb,
528 2 | 5 => 5 + nb,
529 3 => {
530 let np = nb / 3;
531 (np * 9).div_ceil(8) * 2 + np
532 }
533 other => panic!(
534 "mode {other} outside the recorded grammar (pos {pos}/{})",
535 stream.len()
536 ),
537 };
538 out.extend_from_slice(&stream[pos..pos + extra]);
539 pos += extra;
540 }
541 }
542 debug_assert_eq!(pos, stream.len());
543 out
544 }
545}
546
547fn ref_pick(px: &[u8], reference: u8) -> (u8, u8, u8) {
550 let (r, g, b) = (px[0], px[1], px[2]);
551 match reference {
552 1 => (g, r, b),
553 2 => (r, g, b),
554 3 => (b, r, g),
555 _ => (0, 0, 0),
556 }
557}
558
559fn unpermute_wire_to_planar(wire: &[u8], reference: u8) -> Vec<u8> {
562 let third = wire.len() / 3;
563 let (pref, pa, pb) = (&wire[..third], &wire[third..2 * third], &wire[2 * third..]);
564 match reference {
565 1 => {
567 let mut out: Vec<u8> = pa
568 .iter()
569 .zip(pref)
570 .map(|(&da, &g)| da.wrapping_add(g))
571 .collect();
572 out.extend_from_slice(pref);
573 out.extend(
574 pb.iter()
575 .zip(pref)
576 .map(|(&db, &g)| db.wrapping_add(g))
577 .collect::<Vec<u8>>(),
578 );
579 out
580 }
581 2 => {
583 let mut out = pref.to_vec();
584 out.extend(
585 pa.iter()
586 .zip(pref)
587 .map(|(&da, &rf)| da.wrapping_add(rf))
588 .collect::<Vec<u8>>(),
589 );
590 out.extend(
591 pb.iter()
592 .zip(pref)
593 .map(|(&db, &rf)| db.wrapping_add(rf))
594 .collect::<Vec<u8>>(),
595 );
596 out
597 }
598 3 => {
600 let mut out: Vec<u8> = pa
601 .iter()
602 .zip(pref)
603 .map(|(&da, &bf)| da.wrapping_add(bf))
604 .collect();
605 out.extend(
606 pb.iter()
607 .zip(pref)
608 .map(|(&db, &bf)| db.wrapping_add(bf))
609 .collect::<Vec<u8>>(),
610 );
611 out.extend_from_slice(pref);
612 out
613 }
614 _ => {
615 let mut out = pref.to_vec();
616 out.extend_from_slice(pa);
617 out.extend_from_slice(pb);
618 out
619 }
620 }
621}
622
623fn push_inverse_pixel(out: &mut Vec<u8>, refc: u8, da: u8, db: u8, reference: u8) {
625 match reference {
626 1 => {
627 out.push(da.wrapping_add(refc)); out.push(refc); out.push(db.wrapping_add(refc)); }
631 2 => {
632 out.push(refc); out.push(da.wrapping_add(refc)); out.push(db.wrapping_add(refc)); }
636 3 => {
637 out.push(da.wrapping_add(refc)); out.push(db.wrapping_add(refc)); out.push(refc); }
641 _ => out.extend_from_slice(&[0, 0, 0]),
642 }
643}
644
645fn channel_diff_unrelabel(
650 stream: &[u8],
651 planar: bool,
652 tokens: bool,
653 w: u32,
654 h: u32,
655 reference: u8,
656) -> Result<Vec<u8>> {
657 if tokens {
658 let mut out = Vec::with_capacity(stream.len());
659 let mut pos = 0usize;
660 let npx = (w as usize) * (h as usize);
661 let mut ppos = 0usize;
662 while ppos < npx {
663 if pos >= stream.len() {
664 return Err(Error::fmt_msg("cd: token wire truncated"));
665 }
666 let tag = stream[pos];
667 out.push(tag);
668 pos += 1;
669 match tag {
670 tokens::TAG_LITERAL => {
671 if pos + 3 > stream.len() {
672 return Err(Error::fmt_msg("cd: token literal truncated"));
673 }
674 push_inverse_pixel(
675 &mut out,
676 stream[pos],
677 stream[pos + 1],
678 stream[pos + 2],
679 reference,
680 );
681 pos += 3;
682 ppos += 1;
683 }
684 tokens::TAG_MATCH => {
685 if pos + 5 > stream.len() {
686 return Err(Error::fmt_msg("cd: token match truncated"));
687 }
688 let len = u32::from(stream[pos + 4]) as usize;
689 out.extend_from_slice(&stream[pos..pos + 5]);
690 pos += 5;
691 ppos += len;
692 }
693 tokens::TAG_NEAR => {
694 if pos + 3 > stream.len() {
695 return Err(Error::fmt_msg("cd: token near truncated"));
696 }
697 let len = u32::from(stream[pos + 2]) as usize;
698 out.extend_from_slice(&stream[pos..pos + 3]);
699 pos += 3;
700 ppos += len;
701 }
702 _ => return Err(Error::fmt_msg("token wire: unknown tag")),
703 }
704 }
705 if pos != stream.len() {
706 return Err(Error::fmt_msg("cd: token wire trailing bytes"));
707 }
708 if ppos != npx {
709 return Err(Error::fmt_msg("cd: token pixel accounting"));
710 }
711 Ok(out)
712 } else {
713 let cols = (w as usize).div_ceil(TILE_SIZE as usize);
714 let tile_count =
715 (w as usize).div_ceil(TILE_SIZE as usize) * (h as usize).div_ceil(TILE_SIZE as usize);
716 let mut out = Vec::with_capacity(stream.len());
717 let mut pos = 0usize;
718 for i in 0..tile_count {
719 let (row, col) = (i / cols, i % cols);
720 let tw = (TILE_SIZE as usize).min(w as usize - col * TILE_SIZE as usize);
721 let th = (TILE_SIZE as usize).min(h as usize - row * TILE_SIZE as usize);
722 let nb = tw * th * 3;
723 if pos >= stream.len() {
724 return Err(Error::fmt_msg("cd: record stream truncated"));
725 }
726 let mode = stream[pos];
727 out.push(mode);
728 pos += 1;
729 match mode {
730 0 => {
731 #[allow(clippy::collapsible_if)]
732 if pos + nb > stream.len() {
733 return Err(Error::fmt_msg("cd: mode-0 body truncated"));
734 }
735 if planar {
736 let wire_planes = &stream[pos..pos + nb];
737 out.extend_from_slice(&unpermute_wire_to_planar(wire_planes, reference));
738 } else {
739 for k in 0..nb / 3 {
740 push_inverse_pixel(
741 &mut out,
742 stream[pos + k * 3],
743 stream[pos + k * 3 + 1],
744 stream[pos + k * 3 + 2],
745 reference,
746 );
747 }
748 }
749 pos += nb;
750 }
751 1 | 4 => {
752 if pos + 4 + nb > stream.len() {
753 return Err(Error::fmt_msg("cd: mode truncated"));
754 }
755 out.extend_from_slice(&stream[pos..pos + 4 + nb]);
756 pos += 4 + nb;
757 }
758 2 | 5 => {
759 if pos + 5 + nb > stream.len() {
760 return Err(Error::fmt_msg("cd: mode truncated"));
761 }
762 out.extend_from_slice(&stream[pos..pos + 5 + nb]);
763 pos += 5 + nb;
764 }
765 3 => {
766 let np = nb / 3;
767 let n9 = (np * 9).div_ceil(8);
768 if pos + np + 2 * n9 > stream.len() {
769 return Err(Error::fmt_msg("cd: mode-3 truncated"));
770 }
771 out.extend_from_slice(&stream[pos..pos + np + 2 * n9]);
772 pos += np + 2 * n9;
773 }
774 _ => return Err(Error::fmt_msg(format!("cd: mode {mode}"))),
775 }
776 }
777 if pos != stream.len() {
778 return Err(Error::fmt_msg("cd: trailing stream bytes"));
779 }
780 Ok(out)
781 }
782}
783fn encode_tokens_coder_cap(
789 pixels: &[u8],
790 dims: Dims,
791 backend: u32,
792 flags: u32,
793 cap: usize,
794 reference: u8,
795) -> Result<(Vec<u8>, MnF6Stats)> {
796 let med_pred = med_predict(pixels, dims.width as usize, dims.height as usize);
797 let res: Vec<u8> = pixels
798 .iter()
799 .zip(&med_pred)
800 .map(|(x, m)| x.wrapping_sub(*m))
801 .collect();
802 let (wire, ts) =
803 tokens::encode_plane_cap(&res, dims.width as usize, dims.height as usize, cap)?;
804 let wire = if reference != 0 {
806 channel_diff_relabel(&wire, false, true, dims.width, dims.height, reference)
807 } else {
808 wire
809 };
810 let payload = backend_compress(backend, &wire)?;
811 let payload_len =
812 u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?;
813 let mut file = Vec::with_capacity(HEADER8 + payload.len());
814 file.extend_from_slice(b"MNF8");
815 file.extend_from_slice(&dims.width.to_le_bytes());
816 file.extend_from_slice(&dims.height.to_le_bytes());
817 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
818 let cols = div_ceil(dims.width, TILE_SIZE);
819 let rows = div_ceil(dims.height, TILE_SIZE);
820 let tile_count: u32 = cols
821 .checked_mul(rows)
822 .ok_or_else(|| Error::fmt_msg("too many tiles"))?;
823 file.extend_from_slice(&tile_count.to_le_bytes());
824 file.extend_from_slice(&payload_len.to_le_bytes());
825 file.extend_from_slice(&flags.to_le_bytes());
826 file.extend_from_slice(&backend.to_le_bytes());
827 file.extend_from_slice(&payload);
828 let st = MnF6Stats {
829 literal: u32::try_from(ts.literals).unwrap_or(u32::MAX),
830 ref_exact: u32::try_from(ts.matches).unwrap_or(u32::MAX),
831 ..MnF6Stats::default()
832 };
833 Ok((file, st))
834}
835
836#[allow(clippy::too_many_arguments)]
847fn encode_stream_coder(
848 pixels: &[u8],
849 dims: Dims,
850 use_atlas: bool,
851 use_geom: bool,
852 flags: u32,
853 planar: bool,
854 med: bool,
855 kind: PredKind,
856 coder: &Coder,
857 backend: u32,
858 reference: u8,
859) -> Result<(Vec<u8>, MnF6Stats)> {
860 let pure_pred = kind == PredKind::Plane || kind == PredKind::Left;
863 let med_pred = match kind {
864 PredKind::Med => med_predict(pixels, dims.width as usize, dims.height as usize),
865 PredKind::Plane => image_predict(
866 pixels,
867 dims.width as usize,
868 dims.height as usize,
869 PredKind::Plane,
870 ),
871 PredKind::Left => image_predict(
872 pixels,
873 dims.width as usize,
874 dims.height as usize,
875 PredKind::Left,
876 ),
877 PredKind::None => Vec::new(),
878 };
879 let tiles = split_tiles(pixels, dims);
880 let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
881 let mut stats = MnF6Stats::default();
882 let mut atlas = Atlas::new();
883
884 for (idx, tile) in tiles.iter().enumerate() {
885 let n = tile.nbytes();
886 let pack = |rgb: &[u8]| -> Vec<u8> {
887 if planar {
888 planar_pack(rgb)
889 } else {
890 rgb.to_vec()
891 }
892 };
893
894 let lit_src: Vec<u8> = if med || pure_pred {
897 let (tw, th) = (tile.width as usize, tile.height as usize);
898 let cols_px = dims.width as usize; let tx = (idx % ((dims.width as usize).div_ceil(16))) * 16;
900 let ty = (idx / ((dims.width as usize).div_ceil(16))) * 16;
901 let mut src = Vec::with_capacity(n);
902 for y in 0..th {
903 let row_off = ((ty + y) * cols_px + tx) * 3;
904 src.extend_from_slice(&med_pred[row_off..row_off + tw * 3]);
905 }
906 tile.data
907 .iter()
908 .zip(&src)
909 .map(|(x, m)| x.wrapping_sub(*m))
910 .collect()
911 } else {
912 tile.data.clone()
913 };
914
915 let lit_body = pack(&lit_src);
916 let mut choice_body = lit_body.clone();
917 let mut choice_mode = 0u8;
918 let mut choice_ref: Option<(u32, u8)> = None;
919 let mut best_cost = 1 + proxy_cost(coder, &lit_body);
920
921 if !pure_pred {
924 let (cy, cco, ccg) = ycocg_forward(&tile.data);
925 let mut m3 = Vec::with_capacity(n + 160);
926 m3.extend_from_slice(&cy);
927 m3.extend_from_slice(&pack9(&cco));
928 m3.extend_from_slice(&pack9(&ccg));
929 let m3_cost = 1 + proxy_cost(coder, &m3);
930 if m3_cost < best_cost {
931 best_cost = m3_cost;
932 choice_body = m3;
933 choice_mode = 3;
934 }
935 }
936
937 let tile_t = modcolor_forward_iter(&tile.data);
940
941 if use_atlas && !pure_pred && tiles.len() > 1 {
944 let sig = tile_sig(tile.width, tile.height, &tile.data);
945 for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
946 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
947 let transforms: &[u8] = if !use_geom {
952 &[0]
953 } else if tile.width == tile.height {
954 &[0, 1, 2, 3, 4, 5, 6, 7]
955 } else {
956 &[0, 1, 2, 3]
957 };
958 for &t in transforms {
959 let base = if t == 0 {
960 ref_rgb.clone()
961 } else {
962 apply_transform(t, tile.width, tile.height, ref_rgb)
963 };
964 let mut r = vec![0u8; n];
965 for i in 0..n {
966 r[i] = tile.data[i].wrapping_sub(base[i]);
967 }
968 let body = pack(&r);
969 let meta: u64 = if t == 0 { 5 } else { 6 };
971 let cost = meta + proxy_cost(coder, &body);
972 if cost < best_cost {
973 best_cost = cost;
974 choice_body = body;
975 choice_mode = if t == 0 { 1 } else { 2 };
976 choice_ref = Some((prior, t));
977 }
978 let base_t = modcolor_forward_iter(&base);
985 let mut r_c = vec![0u8; n];
986 for i in 0..n {
987 r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
988 }
989 let body_c = pack(&r_c);
990 let cost_c = meta + proxy_cost(coder, &body_c);
991 if cost_c < best_cost {
992 best_cost = cost_c;
993 choice_body = body_c;
994 choice_mode = if t == 0 { 4 } else { 5 };
995 choice_ref = Some((prior, t));
996 }
997 }
998 }
999 }
1000
1001 stats.modes.push(choice_mode);
1002 stats.refs.push(choice_ref);
1003 match choice_mode {
1004 0 => stats.literal += 1,
1005 1 => stats.ref_exact += 1,
1006 2 => {
1007 stats.ref_geom += 1;
1008 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
1009 stats.geom_nontrivial += 1;
1010 }
1011 }
1012 3 => stats.ycocg_literal += 1,
1013 4 => stats.ref_exact_color += 1,
1014 5 => {
1015 stats.ref_geom_color += 1;
1016 if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
1017 stats.geom_nontrivial += 1;
1018 }
1019 }
1020 _ => unreachable!("mode byte constrained above"),
1021 }
1022 raw.push(choice_mode);
1023 if let Some((p, t)) = choice_ref {
1024 raw.extend_from_slice(&p.to_le_bytes());
1025 if t != 0 {
1026 raw.push(t);
1027 }
1028 }
1029 raw.extend_from_slice(&choice_body);
1030 atlas.insert(tile);
1031 }
1032
1033 let raw = if reference != 0 {
1035 channel_diff_relabel(&raw, planar, false, dims.width, dims.height, reference)
1036 } else {
1037 raw
1038 };
1039 let payload = backend_compress(backend, &raw)?;
1040 let payload_len =
1041 u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?; let mut file = Vec::with_capacity(HEADER8 + payload.len());
1043 file.extend_from_slice(b"MNF8");
1044 file.extend_from_slice(&dims.width.to_le_bytes());
1045 file.extend_from_slice(&dims.height.to_le_bytes());
1046 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
1047 let tile_count = u32::try_from(tiles.len()).map_err(|_| Error::fmt_msg("too many tiles"))?;
1048 file.extend_from_slice(&tile_count.to_le_bytes());
1049 file.extend_from_slice(&payload_len.to_le_bytes());
1050 file.extend_from_slice(&flags.to_le_bytes());
1051 file.extend_from_slice(&backend.to_le_bytes());
1052 file.extend_from_slice(&payload);
1053 Ok((file, stats))
1054}
1055
1056pub fn decode_mnf8(buf: &[u8]) -> Result<(Dims, Vec<u8>, u32)> {
1064 decode_mnf8_with_limits(buf, &crate::mnf6::DecodeLimits::default())
1065}
1066
1067pub fn decode_mnf8_with_limits(
1070 buf: &[u8],
1071 lim: &crate::mnf6::DecodeLimits,
1072) -> Result<(Dims, Vec<u8>, u32)> {
1073 if buf.len() < HEADER8 {
1074 return Err(Error::fmt_msg("short"));
1075 }
1076 if buf[0..4] != *b"MNF8" {
1077 return Err(Error::fmt_msg("magic"));
1078 }
1079 let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
1080 let width = u32le(4);
1081 let height = u32le(8);
1082 if width == 0 || height == 0 {
1083 return Err(Error::fmt_msg("dims"));
1084 }
1085 if width as u64 * height as u64 > lim.max_pixels {
1086 return Err(Error::Limits {
1087 which: "max_pixels",
1088 limit: lim.max_pixels,
1089 actual: width as u64 * height as u64,
1090 });
1091 }
1092 if u32le(12) != TILE_SIZE {
1093 return Err(Error::fmt_msg("tilesize"));
1094 }
1095 let cols = div_ceil(width, TILE_SIZE);
1096 let rows = div_ceil(height, TILE_SIZE);
1097 let tile_count = u32le(16);
1098 if tile_count != cols * rows {
1099 return Err(Error::fmt_msg("tilecount"));
1100 }
1101 if tile_count > lim.max_tiles {
1102 return Err(Error::Limits {
1103 which: "max_tiles",
1104 limit: u64::from(lim.max_tiles),
1105 actual: u64::from(tile_count),
1106 });
1107 }
1108 let payload_len = u32le(20);
1109 if payload_len > (1 << 31) - 1 {
1110 return Err(Error::fmt_msg("payloadlen"));
1111 }
1112 if buf.len() != HEADER8 + payload_len as usize {
1113 return Err(Error::fmt_msg("filelen"));
1114 }
1115 let flags = u32le(24);
1116 if flags & FLAGS_RESERVED8 != 0 {
1119 return Err(Error::fmt_msg("reserved-flags"));
1120 }
1121 let token_wire = flags & FLAG_TOKENS != 0;
1122 if token_wire && flags & FLAG_PLANAR != 0 {
1125 return Err(Error::fmt_msg("tokens+planar are exclusive"));
1126 }
1127 let pred_count = u32::from(flags & FLAG_PRED_MED != 0)
1129 + u32::from(flags & FLAG_PRED_PLANE != 0)
1130 + u32::from(flags & FLAG_PRED_LEFT != 0);
1131 if pred_count > 1 {
1132 return Err(Error::fmt_msg("multiple predictors selected"));
1133 }
1134 if token_wire && flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0 {
1137 return Err(Error::fmt_msg("tokens+plane/left predictor exclusive"));
1138 }
1139 let backend = u32le(28);
1140 if !matches!(
1141 backend,
1142 BACKEND_ZLIB
1143 | BACKEND_ZSTD19
1144 | BACKEND_RESERVED2
1145 | BACKEND_RESERVED3
1146 | BACKEND_MIX
1147 | BACKEND_MIX5
1148 ) {
1149 return Err(Error::fmt_msg(format!("unknown backend {backend}")));
1150 }
1151 if matches!(backend, BACKEND_RESERVED2 | BACKEND_RESERVED3) {
1155 return Err(Error::fmt_msg(format!(
1156 "reserved backend {backend} (not shipped)"
1157 )));
1158 }
1159 let planar = flags & FLAG_PLANAR != 0;
1160
1161 let px_bytes_total: u64 = width as u64 * height as u64 * 3;
1176 let limit = if token_wire {
1177 (4 * width as u64)
1178 .checked_mul(height as u64)
1179 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
1180 } else {
1181 let mode3_max: u64 = 1
1182 + (TILE_SIZE * TILE_SIZE) as u64
1183 + 2 * ((TILE_SIZE * TILE_SIZE) as u64 * 9 + 7) / 8
1184 + 2;
1185 px_bytes_total
1186 .checked_add(6 * tile_count as u64)
1187 .and_then(|l| l.checked_add(2 * mode3_max + 2))
1188 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
1189 };
1190
1191 let payload = &buf[HEADER8..];
1192 let raw = match backend {
1193 BACKEND_ZLIB | BACKEND_ZSTD19 => backend_decompress(backend, payload, limit)?,
1194 BACKEND_MIX => crate::mix_coder::mix_decode(payload, limit.min(i64::MAX as u64) as usize)?,
1195 BACKEND_MIX5 => crate::mix_coder::mix_decode_with(
1196 &crate::mix_coder::MixParams::BACKEND5,
1197 payload,
1198 limit.min(i64::MAX as u64) as usize,
1199 )?,
1200 other => return Err(Error::fmt_msg(format!("unsupported backend {other}"))),
1201 };
1202
1203 let reference = cd_reference(flags);
1208 let raw = if reference != 0 {
1209 channel_diff_unrelabel(&raw, planar, token_wire, width, height, reference)?
1210 } else {
1211 raw
1212 };
1213 let pixels = if token_wire {
1214 let res = tokens::decode_plane(&raw, width as usize, height as usize)?;
1218 pred_residual_invert(&res, width as usize, height as usize, PredKind::Med)
1219 } else {
1220 let kind = PredKind::from_flags(flags);
1221 match kind {
1225 PredKind::None => decode_record_walk(&raw, width, height, tile_count, planar, false)?,
1226 PredKind::Med => decode_record_walk(&raw, width, height, tile_count, planar, true)?,
1227 other => decode_pred_walk(&raw, width, height, tile_count, planar, other)?,
1228 }
1229 };
1230 Ok((Dims { width, height }, pixels, backend))
1231}
1232
1233fn pred_residual_invert(res: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
1238 let mut out = vec![0u8; w * h * 3];
1239 for y in 0..h {
1240 for x in 0..w {
1241 for c in 0..3usize {
1242 let i = (y * w + x) * 3 + c;
1243 let pred: u32 = if x == 0 && y == 0 {
1244 127
1245 } else if y == 0 {
1246 u32::from(out[i - 3])
1247 } else if x == 0 {
1248 u32::from(out[i - w * 3])
1249 } else {
1250 let a = u32::from(out[i - 3]);
1251 let b = u32::from(out[i - w * 3]);
1252 let cc = u32::from(out[i - w * 3 - 3]);
1253 kind.predict(a, b, cc)
1254 };
1255 out[i] = (pred + u32::from(res[i])).wrapping_rem_euclid(256) as u8;
1256 }
1257 }
1258 }
1259 out
1260}
1261
1262fn decode_pred_walk(
1269 raw: &[u8],
1270 width: u32,
1271 height: u32,
1272 tile_count: u32,
1273 planar: bool,
1274 kind: PredKind,
1275) -> Result<Vec<u8>> {
1276 let cols = div_ceil(width, TILE_SIZE);
1285 let mut pixels = vec![0u8; width as usize * height as usize * 3];
1286 let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
1287 let mut ip = 0usize;
1288 for i in 0..tile_count {
1289 let row = i / cols;
1290 let col = i % cols;
1291 let tw = TILE_SIZE.min(width - col * TILE_SIZE);
1292 let th = TILE_SIZE.min(height - row * TILE_SIZE);
1293 let n = (tw * th * 3) as usize;
1294 if ip >= raw.len() {
1295 return Err(Error::fmt_msg(format!("eof at tile {i}")));
1296 }
1297 let mode = raw[ip];
1298 ip += 1;
1299 let tile = match mode {
1300 0 => {
1301 if ip + n > raw.len() {
1302 return Err(Error::fmt_msg("body"));
1303 }
1304 let body = raw[ip..ip + n].to_vec();
1305 ip += n;
1306 let body = if planar {
1307 crate::mnf6::planar_unpack(&body)
1308 } else {
1309 body
1310 };
1311 let mut recon = vec![0u8; n];
1314 let gy0 = row as usize * TILE_SIZE as usize;
1315 let gx0 = col as usize * TILE_SIZE as usize;
1316 for y in 0..th as usize {
1317 for x in 0..tw as usize {
1318 for c in 0..3usize {
1319 let k = (y * tw as usize + x) * 3 + c;
1320 let gy = gy0 + y;
1321 let gx = gx0 + x;
1322 let tl = |ggy: usize, ggx: usize| -> u32 {
1323 if ggy >= gy0 && ggx >= gx0 {
1324 u32::from(
1325 recon[((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
1326 )
1327 } else {
1328 u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
1329 }
1330 };
1331 let pred: u32 = if gy == 0 && gx == 0 {
1332 127
1333 } else if gy == 0 {
1334 if gx - 1 < gx0 {
1335 u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
1336 } else {
1337 u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
1338 }
1339 } else if gx == 0 {
1340 if gy - 1 < gy0 {
1341 u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
1342 } else {
1343 u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
1344 }
1345 } else {
1346 let a = tl(gy, gx - 1);
1347 let b = tl(gy - 1, gx);
1348 let cc = tl(gy - 1, gx - 1);
1349 kind.predict(a, b, cc)
1350 };
1351 recon[k] = (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
1352 }
1353 }
1354 }
1355 recon
1356 }
1357 _ => {
1358 return Err(Error::fmt_msg(format!(
1363 "tile {i}: mode {mode} under PRED_PLANE/LEFT — not yet admitted"
1364 )));
1365 }
1366 };
1367 decoded.push((tw, th, tile.clone()));
1368 for y_ in 0..th as usize {
1369 let py = row as usize * TILE_SIZE as usize + y_;
1370 for x_ in 0..tw as usize {
1371 let pxx = col as usize * TILE_SIZE as usize + x_;
1372 let d = (py * width as usize + pxx) * 3;
1373 let s = (y_ * tw as usize + x_) * 3;
1374 pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
1375 }
1376 }
1377 }
1378 if ip != raw.len() {
1379 return Err(Error::fmt_msg("extra"));
1380 }
1381 Ok(pixels)
1382}
1383
1384#[cfg(test)]
1385mod gate30 {
1386 use super::*;
1387 use crate::mnf6::Coder;
1388 use std::path::Path;
1389 fn load(name: &str) -> (Dims, Vec<u8>) {
1390 let p = format!("{}/testdata/{}", env!("CARGO_MANIFEST_DIR"), name);
1391 let img = image::open(Path::new(&p))
1392 .unwrap_or_else(|e| panic!("{name}: {e}"))
1393 .to_rgb8();
1394 (
1395 Dims {
1396 width: img.width(),
1397 height: img.height(),
1398 },
1399 img.into_raw(),
1400 )
1401 }
1402 #[test]
1405 #[ignore]
1406 fn gate_all_arms_real_wire() {
1407 let files = [
1408 "gradient_320x240.png",
1409 "noise_256x256.png",
1410 "repeat_256x256.png",
1411 "screenshot_640x400.png",
1412 "solid_256x256.png",
1413 "heldout/blob_224x160.png",
1414 "heldout/blocks_192x128.png",
1415 "heldout/document_200x280.png",
1416 "heldout/plaid_192.png",
1417 "heldout/poster_208x144.png",
1418 "heldout-photos/autumn_160x120_s3100006.png",
1419 "heldout-photos/dusk_160x120_s3100003.png",
1420 "heldout-photos/dusk_224x160_s3100002.png",
1421 "heldout-photos/forest_192x144_s3100001.png",
1422 "heldout-photos/forest_224x160_s3100005.png",
1423 "heldout-photos/forest_224x160_s3100008.png",
1424 "heldout-photos/ocean_192x144_s3100007.png",
1425 "heldout-photos/sand_192x144_s3100004.png",
1426 "heldout-photos/slate_160x120_s3100009.png",
1427 "heldout-photos/terrain_160x120_s3100000.png",
1428 "photos/autumn_192x144_s900001.png",
1429 "photos/dusk_160x120_s900003.png",
1430 "photos/forest_224x160_s900002.png",
1431 "photos/ocean_192x144_s900007.png",
1432 "photos/sand_160x120_s900000.png",
1433 "photos/slate_192x144_s900004.png",
1434 "photos/slate_224x160_s900008.png",
1435 "photos/stone_160x120_s900006.png",
1436 "photos/terrain_160x120_s900009.png",
1437 "photos/terrain_224x160_s900005.png",
1438 ];
1439 let mut old_tot = 0usize;
1440 let mut new_tot = 0usize;
1441 let mut fires = 0usize;
1442 for name in files {
1443 let (d, px) = load(name);
1444 let mut old = usize::MAX;
1446 for kind in [PredKind::None, PredKind::Med] {
1447 for (backend, coder) in [
1448 (BACKEND_ZSTD19, Coder::Zstd(19)),
1449 (BACKEND_ZLIB, Coder::Zlib),
1450 ] {
1451 for planar in [false, true] {
1452 let med = kind == PredKind::Med;
1453 let (file, _) = encode_stream_coder(
1454 &px,
1455 d,
1456 true,
1457 true,
1458 kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
1459 planar,
1460 med,
1461 kind,
1462 &coder,
1463 backend,
1464 0,
1465 )
1466 .unwrap();
1467 let _ = &file;
1469 old = old.min(file.len());
1470 }
1471 }
1472 }
1473 for (backend, _coder) in [
1474 (BACKEND_ZSTD19, Coder::Zstd(19)),
1475 (BACKEND_ZLIB, Coder::Zlib),
1476 ] {
1477 let (tw, _) =
1478 encode_tokens_coder_cap(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS, 16, 0)
1479 .unwrap();
1480 old = old.min(tw.len());
1481 }
1482 let mut new_ = old;
1484 let mut new_win = String::new();
1485 for kind in [PredKind::Plane, PredKind::Left] {
1486 for (backend, coder) in [
1487 (BACKEND_ZSTD19, Coder::Zstd(19)),
1488 (BACKEND_ZLIB, Coder::Zlib),
1489 ] {
1490 let (file, _) = encode_stream_coder(
1491 &px,
1492 d,
1493 true,
1494 true,
1495 kind.flag_bit(),
1496 false,
1497 false,
1498 kind,
1499 &coder,
1500 backend,
1501 0,
1502 )
1503 .unwrap();
1504 let (dd, out, _) =
1506 decode_mnf8(&file).unwrap_or_else(|e| panic!("{name} {kind:?}: {e}"));
1507 if out != px {
1508 let pos = out
1509 .iter()
1510 .zip(&px)
1511 .position(|(a, b)| a != b)
1512 .expect("differs");
1513 panic!(
1514 "{name} {kind:?} NOT exact: first diff byte {pos} (pixel y={}, x={}, c={}): dec={} enc={}",
1515 pos / 3 / d.width as usize, pos / 3 % d.width as usize, pos % 3,
1516 out[pos], px[pos]
1517 );
1518 }
1519 let _ = dd;
1520 if file.len() < new_ {
1521 new_ = file.len();
1522 new_win = format!("{kind:?}-{backend}");
1523 }
1524 }
1525 }
1526 if new_ < old {
1527 fires += 1;
1528 }
1529 old_tot += old;
1530 new_tot += new_;
1531 println!(
1532 "{name:44} old={old:>8} new={new_:>8} {:+6} {}",
1533 new_ as i64 - old as i64,
1534 new_win
1535 );
1536 }
1537 println!(
1538 "TOTAL old={old_tot} new={new_tot} delta={:+} fires={fires}",
1539 new_tot as i64 - old_tot as i64
1540 );
1541 assert!(new_tot < old_tot, "aggregate must strictly improve");
1542 }
1543
1544 #[test]
1545 fn branch_arms_roundtrip_and_pick_fires() {
1546 let path = format!(
1550 "{}/testdata/heldout/poster_208x144.png",
1551 env!("CARGO_MANIFEST_DIR")
1552 );
1553 let img = image::open(std::path::Path::new(&path)).unwrap().to_rgb8();
1554 let d = Dims {
1555 width: img.width(),
1556 height: img.height(),
1557 };
1558 let px = img.into_raw();
1559 let (buf, st, _) = encode_mnf8(&px, d, true, Profile::Balanced);
1560 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1561 assert_eq!(out, px, "picked arm decodes exactly");
1562 assert!(
1563 st.chosen_flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1564 "poster must pick a branch arm (picked {:#x})",
1565 st.chosen_flags
1566 );
1567 let (bufold, _, _) = {
1569 let mut best: Option<(Vec<u8>, u32)> = None;
1571 for (backend, coder) in [(BACKEND_ZSTD19, Coder::Zstd(19))] {
1572 for kind in [PredKind::None, PredKind::Med] {
1573 for planar in [false, true] {
1574 let (f, _) = encode_stream_coder(
1575 &px,
1576 d,
1577 true,
1578 true,
1579 kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
1580 planar,
1581 kind == PredKind::Med,
1582 kind,
1583 &coder,
1584 backend,
1585 0,
1586 )
1587 .unwrap();
1588 if best.as_ref().is_none_or(|b| f.len() < b.0.len()) {
1589 best = Some((f, kind.flag_bit()));
1590 }
1591 }
1592 }
1593 let (t, _) =
1594 encode_tokens_coder_cap(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS, 16, 0)
1595 .unwrap();
1596 if best.as_ref().is_none_or(|b| t.len() < b.0.len()) {
1597 best = Some((t, FLAG_TOKENS));
1598 }
1599 }
1600 let (f, fl) = best.unwrap();
1601 (f, fl, 0u32)
1602 };
1603 assert!(
1604 buf.len() <= bufold.len(),
1605 "new pick ({}) must not exceed the old pick ({})",
1606 buf.len(),
1607 bufold.len()
1608 );
1609 }
1610
1611 #[test]
1612 fn rejects_multiple_predictors() {
1613 let mut f = vec![0u8; HEADER8];
1615 f[0..4].copy_from_slice(b"MNF8");
1616 f[4..8].copy_from_slice(&16u32.to_le_bytes());
1617 f[8..12].copy_from_slice(&16u32.to_le_bytes());
1618 f[12..16].copy_from_slice(&16u32.to_le_bytes());
1619 f[16..20].copy_from_slice(&1u32.to_le_bytes());
1620 f[20..24].copy_from_slice(&0u32.to_le_bytes());
1621 f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_PRED_PLANE).to_le_bytes());
1622 f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1623 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1624 assert!(
1625 r.is_ok() && r.unwrap().is_err(),
1626 "two predictors must reject"
1627 );
1628 f[24..28].copy_from_slice(&(FLAG_TOKENS | FLAG_PRED_LEFT).to_le_bytes());
1629 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1630 assert!(r.is_ok() && r.unwrap().is_err(), "tokens+left must reject");
1631 }
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636 use super::*;
1637
1638 fn roundtrip(px: &[u8], dims: Dims, profile: Profile) -> u32 {
1639 let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
1640 let (d, out, _) = decode_mnf8(&buf).expect("decode");
1641 assert_eq!(d, dims);
1642 assert_eq!(
1643 out, px,
1644 "profile {profile:?} backend {backend} flags {st:?}"
1645 );
1646 backend
1647 }
1648
1649 #[test]
1650 fn mnf8_balanced_roundtrip_all_classes() {
1651 for (w, h, seed) in [(16u32, 16u32, 1), (48, 33, 2), (31, 17, 4), (1, 1, 5)] {
1652 let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
1653 let mut rnd = move || {
1654 s ^= s << 13;
1655 s ^= s >> 7;
1656 s ^= s << 17;
1657 s
1658 };
1659 let px: Vec<u8> = (0..w as usize * h as usize * 3)
1660 .map(|_| (rnd() & 0xFF) as u8)
1661 .collect();
1662 roundtrip(
1663 &px,
1664 Dims {
1665 width: w,
1666 height: h,
1667 },
1668 Profile::Balanced,
1669 );
1670 }
1671 }
1672
1673 #[test]
1674 fn mnf8_maximum_never_loses_to_mnf7() {
1675 for name in [
1678 "screenshot_640x400.png",
1679 "solid_256x256.png",
1680 "gradient_320x240.png",
1681 ] {
1682 let path = format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"));
1683 let img = image::open(&path)
1684 .unwrap_or_else(|e| panic!("{name}: {e}"))
1685 .to_rgb8();
1686 let d = Dims {
1687 width: img.width(),
1688 height: img.height(),
1689 };
1690 let px = img.into_raw();
1691 let (buf8, _, backend) = encode_mnf8(&px, d, true, Profile::Maximum);
1692 let (buf7, _) = crate::mnf6::encode_mnf7(&px, d, true);
1693 assert!(
1696 buf8.len() <= buf7.len() + 4,
1697 "{name}: MNF8({backend}) {} > MNF7(+4) {}",
1698 buf8.len(),
1699 buf7.len()
1700 );
1701 let (_, out, _) = decode_mnf8(&buf8).expect("decode");
1702 assert_eq!(out, px, "{name}: exact");
1703 }
1704 }
1705
1706 #[test]
1707 fn mnf8_rejects() {
1708 let mut f = vec![0u8; 32];
1709 f[0..4].copy_from_slice(b"MNF8");
1710 f[4..8].copy_from_slice(&16u32.to_le_bytes());
1711 f[8..12].copy_from_slice(&16u32.to_le_bytes());
1712 f[12..16].copy_from_slice(&16u32.to_le_bytes());
1713 f[16..20].copy_from_slice(&1u32.to_le_bytes());
1714 f[28..32].copy_from_slice(&7u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "unknown backend rejected");
1717 f[28..32].copy_from_slice(&3u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved backend rejected");
1719 f[28..32].copy_from_slice(&2u32.to_le_bytes()); assert!(decode_mnf8(&f).is_err(), "reserved brotli backend rejected");
1721 f[28..32].copy_from_slice(&2u32.to_le_bytes());
1723 let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1724 assert!(r.is_ok() && r.unwrap().is_err(), "must error, not panic");
1725 f[28..32].copy_from_slice(&0u32.to_le_bytes());
1726 f[24] = 0b100; assert!(decode_mnf8(&f).is_err(), "reserved flag rejected");
1728 f[0] = b'X';
1730 f[24] = 0;
1731 assert!(decode_mnf8(&f).is_err(), "magic");
1732 }
1733
1734 #[test]
1735 fn mnf8_balanced_picks_zstd_not_hardcoded() {
1736 let (w, h) = (64u32, 64u32);
1740 let mut px = vec![0u8; (w * h * 3) as usize];
1741 for y in 0..h {
1742 for x in 0..w {
1743 let i = ((y * w + x) * 3) as usize;
1744 px[i] = (x * 4) as u8;
1745 px[i + 1] = (y * 4) as u8;
1746 px[i + 2] = 128;
1747 }
1748 }
1749 let (buf, st, backend) = encode_mnf8(
1750 &px,
1751 Dims {
1752 width: w,
1753 height: h,
1754 },
1755 false,
1756 Profile::Balanced,
1757 );
1758 assert_eq!(backend, BACKEND_ZSTD19);
1759 assert!(
1766 st.chosen_flags & !0b1111 == 0,
1767 "balanced wrote a legal flags id, got {:#x}",
1768 st.chosen_flags
1769 );
1770 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1771 assert_eq!(out, px);
1772 }
1773
1774 #[test]
1775 fn mnf8_no_geom_no_flips() {
1776 let (w, h) = (32u32, 32u32); let mut px = vec![0u8; (w * h * 3) as usize];
1781 for y in 0..h {
1782 for x in 0..w {
1783 let i = ((y * w + x) * 3) as usize;
1784 px[i] = (x * 7 % 256) as u8;
1785 px[i + 1] = (y * 3 % 256) as u8;
1786 px[i + 2] = 5;
1787 }
1788 }
1789 let tile_row = 16usize;
1791 let tile_col = 0usize;
1792 let src_off = (tile_row * w as usize + tile_col) * 3;
1793 let tile = px[src_off..src_off + 16 * 16 * 3].to_vec();
1794 let flipped = apply_transform(1, 16, 16, &tile);
1795 px[src_off..src_off + 16 * 16 * 3].copy_from_slice(&flipped);
1796 let stacked = px.clone();
1797 let d = Dims {
1798 width: w,
1799 height: h,
1800 };
1801 let (buf, st, _) = encode_mnf8(&stacked, d, false, Profile::Balanced);
1802 assert_eq!(st.ref_geom, 0, "no flips allowed with use_geom=false");
1804 assert_eq!(st.geom_nontrivial, 0);
1805 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1806 assert_eq!(out, stacked);
1807 }
1808
1809 #[test]
1810 fn mnf8_no_atlas_arm() {
1811 let d = Dims {
1814 width: 32,
1815 height: 32,
1816 };
1817 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1818 .map(|i| (i * 7 % 256) as u8)
1819 .collect();
1820 let (buf, st, _) = encode_mnf8_noclimb(&px, d);
1821 assert_eq!(
1822 st.ref_exact + st.ref_geom + st.ref_exact_color + st.ref_geom_color,
1823 0,
1824 "no-atlas arm must emit zero references"
1825 );
1826 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1827 assert_eq!(out, px);
1828 }
1829
1830 fn encode_mnf8_noclimb(px: &[u8], d: Dims) -> (Vec<u8>, MnF6Stats, u32) {
1832 let mut best: Option<(Vec<u8>, MnF6Stats, u32)> = None;
1834 for flags in [0u32, FLAG_PLANAR] {
1835 let planar = flags & FLAG_PLANAR != 0;
1836 let (file, st) = encode_stream_coder(
1837 px,
1838 d,
1839 false,
1840 false,
1841 flags,
1842 planar,
1843 false,
1844 PredKind::None,
1845 &Coder::Zstd(19),
1846 BACKEND_ZSTD19,
1847 0,
1848 )
1849 .expect("encode");
1850 if best.as_ref().is_none_or(|b| file.len() < b.0.len()) {
1851 best = Some((file, st, BACKEND_ZSTD19));
1852 }
1853 }
1854 let (f, s, b) = best.expect("variant");
1855 (f, s, b)
1856 }
1857
1858 #[test]
1859 fn mnf8_med_flag_roundtrip_photo_like() {
1860 let (w, h) = (64u32, 64u32);
1865 let mut px = vec![0u8; (w * h * 3) as usize];
1866 for y in 0..h {
1867 for x in 0..w {
1868 let i = ((y * w + x) * 3) as usize;
1869 px[i] = (x * 4) as u8;
1870 px[i + 1] = (y * 4) as u8;
1871 px[i + 2] = ((x + y) / 2) as u8;
1872 }
1873 }
1874 let d = Dims {
1875 width: w,
1876 height: h,
1877 };
1878 let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1879 assert!(
1884 st.chosen_flags & !0b1111 == 0,
1885 "chosen flags must be a legal MNF8 id, got {:#x}",
1886 st.chosen_flags
1887 );
1888 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1889 if out != px {
1890 let first = out
1891 .iter()
1892 .zip(&px)
1893 .position(|(a, b)| a != b)
1894 .expect("differs");
1895 let (b_y, rem) = (first / 3 / (w as usize), (first / 3) % (w as usize));
1896 panic!(
1897 "not exact: first diff at byte {first} (pixel ({b_y},{rem}), ch {}): out={:?} px={:?}",
1898 first % 3,
1899 &out[first.saturating_sub(4)..first + 4],
1900 &px[first.saturating_sub(4)..first + 4]
1901 );
1902 }
1903 assert_eq!(out, px, "exact reconstruction under the measured flags");
1904 }
1905
1906 #[test]
1907 fn mnf8_med_flag_picked_on_photo_like_smoothing() {
1908 let (w, h) = (64u32, 64u32);
1912 let mut px = vec![0u8; (w * h * 3) as usize];
1913 for y in 0..h {
1914 for x in 0..w {
1915 let i = ((y * w + x) * 3) as usize;
1916 px[i] = (x * 4 + y * 2) as u8;
1918 px[i + 1] = (y * 6) as u8;
1919 px[i + 2] = ((x + y) * 3) as u8;
1920 }
1921 }
1922 let d = Dims {
1923 width: w,
1924 height: h,
1925 };
1926 let (_, st, _) = encode_mnf8(
1927 &px,
1928 d,
1929 false, Profile::Balanced,
1931 );
1932 assert!(
1937 st.chosen_flags & (FLAG_PRED_MED | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1938 "smooth no-repeat bodies must select a predictor (picked {:#x})",
1939 st.chosen_flags
1940 );
1941 let (_, out, _) =
1942 decode_mnf8(&encode_mnf8(&px, d, false, Profile::Balanced).0).expect("decode");
1943 assert_eq!(out, px, "exact under the measured predictor pick");
1944 }
1945
1946 #[test]
1947 fn mnf8_forced_mode4_mode5_records() {
1948 let mut s: u64 = 42;
1953 let mut rnd = move || {
1954 s ^= s << 13;
1955 s ^= s >> 7;
1956 s ^= s << 17;
1957 s
1958 };
1959 let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
1960 let ta_t = crate::mnf6::modcolor_forward(&a);
1961 let mut tb_t = ta_t.clone();
1962 let ramp = [0u8, 1, 2, 255];
1963 for i in (1..tb_t.len()).step_by(3) {
1964 tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
1965 }
1966 let b = crate::mnf6::modcolor_inverse(&tb_t);
1967 let mut two = a.clone();
1968 two.extend_from_slice(&b);
1969 let d2 = Dims {
1970 width: 16,
1971 height: 32,
1972 };
1973 let (buf, st, _) = encode_mnf8(&two, d2, true, Profile::Balanced);
1974 assert!(
1975 st.ref_exact_color > 0,
1976 "expected mode 4 (or a color-geom below) selected, stats: exact={} geom={} ec={} gc={}",
1977 st.ref_exact,
1978 st.ref_geom,
1979 st.ref_exact_color,
1980 st.ref_geom_color
1981 );
1982 let (_, out, _) = decode_mnf8(&buf).expect("decode");
1983 assert_eq!(out, two, "mode-4 record reconstructs exactly through MNF8");
1984
1985 let flipped = apply_transform(1, 16, 16, &a);
1987 let tb_t2 = crate::mnf6::modcolor_forward(&flipped);
1988 let mut tc_t = tb_t2;
1989 for i in (1..tc_t.len()).step_by(3) {
1990 tc_t[i] = tc_t[i].wrapping_add(ramp[(i / 3) % 4]);
1991 }
1992 let c = crate::mnf6::modcolor_inverse(&tc_t);
1993 let mut three = flipped.clone();
1994 three.extend_from_slice(&c);
1995 let d3 = Dims {
1996 width: 16,
1997 height: 32,
1998 };
1999 let (buf3, st3, _) = encode_mnf8(&three, d3, true, Profile::Balanced);
2000 assert!(
2001 st3.ref_exact_color > 0 || st3.ref_geom_color > 0,
2002 "expected a color-domain reference (exact or geom), stats: ec={} gc={}",
2003 st3.ref_exact_color,
2004 st3.ref_geom_color
2005 );
2006 let (_, out3, _) = decode_mnf8(&buf3).expect("decode");
2007 assert_eq!(out3, three);
2008 }
2009
2010 fn roundtrip_tokens(px: &[u8], dims: Dims, profile: Profile) -> (u32, u32) {
2013 let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
2014 let (d, out, _) = decode_mnf8(&buf).expect("decode");
2015 assert_eq!(d, dims);
2016 assert_eq!(
2017 out, px,
2018 "profile {profile:?} backend {backend} flags {:#x} literal {} matches {}",
2019 st.chosen_flags, st.literal, st.ref_exact
2020 );
2021 (st.literal, st.ref_exact)
2022 }
2023
2024 #[test]
2025 fn token_wire_unit_roundtrip_and_errors() {
2026 use crate::tokens::*;
2027 let (w, h) = (24usize, 16usize);
2029 let mut plane = vec![0u8; w * h * 3];
2030 for i in 0..w * h {
2031 plane[i * 3] = (i % 251) as u8;
2032 plane[i * 3 + 1] = ((i * 7) % 251) as u8;
2033 }
2034 for k in 300..384 {
2035 plane[k * 3..k * 3 + 3].copy_from_slice(&[9, 9, 9]);
2036 }
2037 let (wire, st) = encode_plane(&plane, w, h).expect("encode");
2038 assert!(st.matches >= 1, "planted structures must fire: {st:?}");
2039 let back = decode_plane(&wire, w, h).expect("decode");
2040 assert_eq!(back, plane);
2041 let mut bad = wire.clone();
2043 bad.push(2); assert!(decode_plane(&bad, w, h).is_err());
2045 assert!(decode_plane(&wire[..wire.len() - 1], w, h).is_err());
2046 let mut bad = wire.clone();
2047 bad.push(TAG_LITERAL);
2048 bad.extend_from_slice(&[1, 2, 3]);
2049 assert!(decode_plane(&bad, w, h).is_err()); }
2051
2052 #[test]
2053 fn mnf8_token_flag_picked_on_repeating_residual() {
2054 let (w, h) = (256u32, 256u32);
2059 let mut px = vec![0u8; (w * h * 3) as usize];
2060 for y in 0..h {
2061 for x in 0..w {
2062 let i = ((y * w + x) * 3) as usize;
2063 px[i] = (y * 3) as u8;
2065 px[i + 1] = (y / 4) as u8;
2066 px[i + 2] = 200;
2067 }
2068 }
2069 let d = Dims {
2070 width: w,
2071 height: h,
2072 };
2073 let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
2074 assert!(
2079 st.chosen_flags & (FLAG_TOKENS | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
2080 "repeating-residual class must pick a measured-winning arm (picked {:#x})",
2081 st.chosen_flags
2082 );
2083 let (_, out, _) = decode_mnf8(&buf).expect("decode");
2084 assert_eq!(out, px, "wire reconstructs exactly under the pick");
2085 }
2086
2087 #[test]
2088 fn mnf8_token_flag_not_picked_on_noise() {
2089 let (w, h) = (64u32, 64u32);
2093 let mut s: u64 = 0x5EED;
2094 let mut rnd = move || {
2095 s ^= s << 13;
2096 s ^= s >> 7;
2097 s ^= s << 17;
2098 s
2099 };
2100 let px: Vec<u8> = (0..w as usize * h as usize * 3)
2101 .map(|_| (rnd() & 0xFF) as u8)
2102 .collect();
2103 let d = Dims {
2104 width: w,
2105 height: h,
2106 };
2107 let (_, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
2108 assert_eq!(
2109 st.chosen_flags & FLAG_TOKENS,
2110 0,
2111 "noise class must NOT pick tokens (picked {:#x})",
2112 st.chosen_flags
2113 );
2114 roundtrip_tokens(&px, d, Profile::Balanced);
2115 }
2116
2117 #[test]
2118 fn mnf8_tokens_decode_rejects_malformed_wire() {
2119 let f = vec![0u8; HEADER8 + 7];
2123 let mut f = f;
2124 f[0..4].copy_from_slice(b"MNF8");
2125 f[4..8].copy_from_slice(&16u32.to_le_bytes());
2126 f[8..12].copy_from_slice(&16u32.to_le_bytes());
2127 f[12..16].copy_from_slice(&16u32.to_le_bytes());
2128 f[16..20].copy_from_slice(&1u32.to_le_bytes());
2129 f[20..24].copy_from_slice(&7u32.to_le_bytes());
2130 f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_TOKENS).to_le_bytes());
2131 f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
2132 f[28..32].copy_from_slice(&BACKEND_ZLIB.to_le_bytes());
2134 let res: Vec<u8> = vec![0u8; 16 * 16 * 3];
2135 let (wire, _) = crate::tokens::encode_plane(&res, 16, 16).expect("wire");
2136 let mut wire_bad = wire.clone();
2138 wire_bad[0] = 7;
2139 let comp: Vec<u8>;
2140 {
2141 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
2142 std::io::Write::write_all(&mut e, &wire_bad).unwrap();
2143 comp = e.finish().unwrap();
2144 }
2145 let mut file = Vec::with_capacity(HEADER8 + comp.len());
2146 file.extend_from_slice(&f[..HEADER8]);
2147 let pl = comp.len() as u32;
2148 file[20..24].copy_from_slice(&pl.to_le_bytes());
2149 file.extend_from_slice(&comp);
2150 let r = std::panic::catch_unwind(|| decode_mnf8(&file));
2151 assert!(
2152 r.is_ok() && r.unwrap().is_err(),
2153 "corrupt wire: Err not panic"
2154 );
2155 let comp2: Vec<u8>;
2157 {
2158 let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
2159 std::io::Write::write_all(&mut e, &wire).unwrap();
2160 comp2 = e.finish().unwrap();
2161 }
2162 let mut file2 = Vec::with_capacity(HEADER8 + comp2.len());
2163 file2.extend_from_slice(&f[..HEADER8]);
2164 file2[20..24].copy_from_slice(&(comp2.len() as u32).to_le_bytes());
2165 file2.extend_from_slice(&comp2);
2166 let (_, out, _) = decode_mnf8(&file2).expect("valid token wire decodes");
2167 assert_eq!(out, pred_residual_invert(&res, 16, 16, PredKind::Med));
2168 }
2169}
2170
2171#[cfg(test)]
2172mod channel_diff {
2173 use super::*;
2174 use crate::mnf6::Coder;
2175 use std::path::Path;
2176
2177 fn load(name: &str) -> (Dims, Vec<u8>) {
2178 let p = format!("{}/testdata/{}", env!("CARGO_MANIFEST_DIR"), name);
2179 let img = image::open(Path::new(&p))
2180 .unwrap_or_else(|e| panic!("{name}: {e}"))
2181 .to_rgb8();
2182 (
2183 Dims {
2184 width: img.width(),
2185 height: img.height(),
2186 },
2187 img.into_raw(),
2188 )
2189 }
2190
2191 #[test]
2194 fn cd_roundtrip_all_predictor_shapes() {
2195 let (d, px) = load("gradient_320x240.png");
2196 for kind in [
2197 PredKind::None,
2198 PredKind::Med,
2199 PredKind::Plane,
2200 PredKind::Left,
2201 ] {
2202 for planar in [false, true] {
2203 for (backend, coder) in [
2204 (BACKEND_ZLIB, Coder::Zlib),
2205 (BACKEND_ZSTD19, Coder::Zstd(19)),
2206 ] {
2207 let flags =
2208 kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 } | FLAG_CHANNEL_DIFF;
2209 let (file, _) = encode_stream_coder(
2210 &px,
2211 d,
2212 true,
2213 false,
2214 flags,
2215 planar,
2216 kind == PredKind::Med,
2217 kind,
2218 &coder,
2219 backend,
2220 1,
2221 )
2222 .expect("encode");
2223 let (dd, out, _) = decode_mnf8(&file).expect("decode");
2224 assert_eq!(dd, d);
2225 assert_eq!(out, px, "cd roundtrip {kind:?} planar={planar}");
2226 }
2227 }
2228 }
2229 let flags = FLAG_PRED_MED | FLAG_TOKENS | FLAG_CHANNEL_DIFF;
2231 for cap in [16, tokens::MAX_CANDIDATES] {
2232 let (file, _) = encode_tokens_coder_cap(&px, d, BACKEND_ZSTD19, flags, cap, 1)
2233 .expect("token encode");
2234 let (dd, out, _) = decode_mnf8(&file).expect("decode");
2235 assert_eq!(dd, d);
2236 assert_eq!(out, px, "cd token roundtrip cap={cap}");
2237 }
2238 }
2239
2240 #[test]
2243 fn cd_pick_never_worse() {
2244 for name in ["photos/slate_224x160_s900008.png", "gradient_320x240.png"] {
2245 let (d, px) = load(name);
2246 let (cd_file, _, _) = encode_mnf8(&px, d, false, Profile::Maximum);
2247 let candidates: Vec<(u32, Coder)> = backend_candidates();
2250 let mut old = usize::MAX;
2251 for (backend, coder) in candidates {
2252 for kind in [
2253 PredKind::None,
2254 PredKind::Med,
2255 PredKind::Plane,
2256 PredKind::Left,
2257 ] {
2258 for planar in [false, true] {
2259 let flags = kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 };
2260 let (file, _) = encode_stream_coder(
2261 &px,
2262 d,
2263 true,
2264 false,
2265 flags,
2266 planar,
2267 kind == PredKind::Med,
2268 kind,
2269 &coder,
2270 backend,
2271 0,
2272 )
2273 .expect("encode");
2274 old = old.min(file.len());
2275 }
2276 }
2277 for cap in [16, tokens::MAX_CANDIDATES] {
2278 let (file, _) = encode_tokens_coder_cap(
2279 &px,
2280 d,
2281 backend,
2282 FLAG_PRED_MED | FLAG_TOKENS,
2283 cap,
2284 0,
2285 )
2286 .expect("encode");
2287 old = old.min(file.len());
2288 }
2289 }
2290 assert!(
2291 cd_file.len() <= old,
2292 "{name}: pick {} above old {old}",
2293 cd_file.len()
2294 );
2295 }
2296 }
2297
2298 #[test]
2302 #[ignore]
2303 fn cd_corpus_gate() {
2304 let files = [
2305 "gradient_320x240.png",
2306 "noise_256x256.png",
2307 "repeat_256x256.png",
2308 "screenshot_640x400.png",
2309 "solid_256x256.png",
2310 "heldout/blob_224x160.png",
2311 "heldout/blocks_192x128.png",
2312 "heldout/document_200x280.png",
2313 "heldout/plaid_192.png",
2314 "heldout/poster_208x144.png",
2315 "heldout-photos/autumn_160x120_s3100006.png",
2316 "heldout-photos/dusk_160x120_s3100003.png",
2317 "heldout-photos/dusk_224x160_s3100002.png",
2318 "heldout-photos/forest_192x144_s3100001.png",
2319 "heldout-photos/forest_224x160_s3100005.png",
2320 "heldout-photos/forest_224x160_s3100008.png",
2321 "heldout-photos/ocean_192x144_s3100007.png",
2322 "heldout-photos/sand_192x144_s3100004.png",
2323 "heldout-photos/slate_160x120_s3100009.png",
2324 "heldout-photos/terrain_160x120_s3100000.png",
2325 "photos/autumn_192x144_s900001.png",
2326 "photos/dusk_160x120_s900003.png",
2327 "photos/forest_224x160_s900002.png",
2328 "photos/ocean_192x144_s900007.png",
2329 "photos/sand_160x120_s900000.png",
2330 "photos/slate_192x144_s900004.png",
2331 "photos/slate_224x160_s900008.png",
2332 "photos/stone_160x120_s900006.png",
2333 "photos/terrain_160x120_s900009.png",
2334 "photos/terrain_224x160_s900005.png",
2335 ];
2336 let mut tot = 0usize;
2337 let mut cd_fires = 0usize;
2338 for name in files {
2339 let (d, px) = load(name);
2340 let (file, st, _) = encode_mnf8(&px, d, false, crate::mnf8::Profile::Maximum);
2341 assert_eq!(decode_mnf8(&file).unwrap().1, px, "{name}");
2342 tot += file.len();
2343 let reference = cd_reference(st.chosen_flags);
2344 cd_fires += usize::from(reference != 0);
2345 if reference != 0 {
2346 println!("{name}: ref={reference}, {}", file.len());
2347 }
2348 }
2349 println!("corpus: total={tot} cd_fires={cd_fires}");
2350 assert!(cd_fires >= 10, "cd must fire on the photographic class");
2351 }
2352
2353 #[test]
2356 fn cd_malformed_rejects() {
2357 let (d, px) = load("gradient_320x240.png");
2358 let flags = FLAG_CHANNEL_DIFF;
2359 let (file, _) = encode_stream_coder(
2360 &px,
2361 d,
2362 true,
2363 false,
2364 flags,
2365 false,
2366 false,
2367 PredKind::None,
2368 &Coder::Zstd(19),
2369 BACKEND_ZSTD19,
2370 1,
2371 )
2372 .expect("encode");
2373 for cut in [40, 41, 60, file.len() / 2] {
2375 let mut t = file.clone();
2376 t.truncate(cut.min(file.len()));
2377 assert!(
2378 decode_mnf8(&t).is_err(),
2379 "truncated cd file must reject at {cut}"
2380 );
2381 }
2382 let mut t = file.clone();
2386 let last = t.len() - 1;
2387 t[last] = t[last].wrapping_add(1);
2388 let _ = decode_mnf8(&t); }
2391}