1use std::io::Read;
21
22use crate::codec::{apply_transform, div_ceil, tile_sig, Atlas, Dims, RANK_CANDIDATES, TILE_SIZE};
23use crate::error::{Error, Result};
24use flate2::write::ZlibEncoder;
25use flate2::Compression;
26use std::io::Write;
27
28pub const HEADER6: usize = 28;
30pub const FLAG_PLANAR: u32 = 1;
32pub const FLAG_ZSTD: u32 = 2;
34pub const FLAGS_RESERVED: u32 = !0b11;
36
37pub enum Coder {
39 Zlib,
41 Zstd(i32),
43}
44
45pub fn compress_stream(coder: &Coder, buf: &[u8]) -> Vec<u8> {
47 match coder {
48 Coder::Zlib => {
49 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
50 e.write_all(buf).expect("zlib encode");
51 e.finish().expect("zlib finish")
52 }
53 Coder::Zstd(level) => {
54 let mut e = zstd::stream::Encoder::new(Vec::new(), *level).expect("zstd");
55 e.include_checksum(true).expect("zstd checksum flag");
57 e.write_all(buf).expect("zstd write");
58 e.finish().expect("zstd finish")
59 }
60 }
61}
62
63pub fn proxy_cost(coder: &Coder, buf: &[u8]) -> u64 {
65 match coder {
66 Coder::Zlib => compress_stream(coder, buf).len() as u64,
67 Coder::Zstd(_) => compress_stream(&Coder::Zstd(3), buf).len() as u64,
69 }
70}
71
72pub fn pack9(values: &[u16]) -> Vec<u8> {
76 let mut out = Vec::with_capacity((values.len() * 9).div_ceil(8) + 1);
77 let mut acc: u32 = 0;
78 let mut bits = 0u32;
79 for &v in values {
80 debug_assert!(v < 512, "9-bit overflow");
81 acc |= (v as u32) << bits;
82 bits += 9;
83 while bits >= 8 {
84 out.push((acc & 0xFF) as u8);
85 acc >>= 8;
86 bits -= 8;
87 }
88 }
89 if bits > 0 {
90 out.push((acc & 0xFF) as u8);
91 }
92 out
93}
94
95pub fn unpack9(bytes: &[u8], n: usize) -> Result<Vec<u16>> {
97 let need = (n * 9).div_ceil(8);
98 if bytes.len() < need {
99 return Err(Error::fmt_msg("9-bit plane truncated"));
100 }
101 let mut out = Vec::with_capacity(n);
102 let mut acc: u32 = 0;
103 let mut bits = 0u32;
104 let mut bi = 0usize;
105 for _ in 0..n {
106 while bits < 9 {
107 if bi >= bytes.len() {
108 return Err(Error::fmt_msg("9-bit plane truncated"));
109 }
110 acc |= (bytes[bi] as u32) << bits;
111 bi += 1;
112 bits += 8;
113 }
114 out.push((acc & 0x1FF) as u16);
115 acc >>= 9;
116 bits -= 9;
117 }
118 Ok(out)
119}
120
121pub fn ycocg_forward(rgb: &[u8]) -> (Vec<u8>, Vec<u16>, Vec<u16>) {
125 let n = rgb.len() / 3;
126 let (mut y, mut co, mut cg) = (
127 Vec::with_capacity(n),
128 Vec::with_capacity(n),
129 Vec::with_capacity(n),
130 );
131 for px in rgb.as_chunks::<3>().0 {
132 let (r, g, b) = (px[0] as i32, px[1] as i32, px[2] as i32);
133 let co_v = r - b;
134 let tmp = b + (co_v >> 1);
135 let cg_v = g - tmp;
136 let y_v = tmp + (cg_v >> 1);
137 debug_assert!((0..=255).contains(&y_v), "y {y_v}");
138 debug_assert!((-255..=255).contains(&co_v));
139 debug_assert!((-255..=255).contains(&cg_v));
140 y.push(y_v as u8);
141 co.push((co_v + 256) as u16);
142 cg.push((cg_v + 256) as u16);
143 }
144 (y, co, cg)
145}
146
147pub fn ycocg_inverse(y: &[u8], co: &[u16], cg: &[u16]) -> Vec<u8> {
149 let n = y.len();
150 let mut out = vec![0u8; n * 3];
151 for i in 0..n {
152 let y_v = y[i] as i32;
153 let co_v = co[i] as i32 - 256;
154 let cg_v = cg[i] as i32 - 256;
155 let tmp = y_v - (cg_v >> 1);
156 let g = tmp + cg_v;
157 let b = tmp - (co_v >> 1);
158 let r = tmp + co_v - (co_v >> 1);
159 out[i * 3] = r.clamp(0, 255) as u8;
160 out[i * 3 + 1] = g.clamp(0, 255) as u8;
161 out[i * 3 + 2] = b.clamp(0, 255) as u8;
162 }
163 out
164}
165
166pub fn planar_pack(data: &[u8]) -> Vec<u8> {
170 let n = data.len() / 3;
171 let mut out = Vec::with_capacity(data.len());
172 for c in 0..3 {
173 for i in 0..n {
174 out.push(data[i * 3 + c]);
175 }
176 }
177 out
178}
179
180pub fn planar_unpack(data: &[u8]) -> Vec<u8> {
182 let n = data.len() / 3;
183 let mut out = vec![0u8; data.len()];
184 for c in 0..3 {
185 for i in 0..n {
186 out[i * 3 + c] = data[c * n + i];
187 }
188 }
189 out
190}
191
192pub mod modcolor {
204 pub fn forward(rgb: [u8; 3]) -> [u8; 3] {
206 let [r, g, b] = rgb;
207 let co = r.wrapping_sub(b);
208 let t = b.wrapping_add(co >> 1);
209 let cg = g.wrapping_sub(t);
210 let y = t.wrapping_add(cg >> 1);
211 [y, co, cg]
212 }
213
214 pub fn inverse(ycocg: [u8; 3]) -> [u8; 3] {
216 let [y, co, cg] = ycocg;
217 let t = y.wrapping_sub(cg >> 1);
218 let g = cg.wrapping_add(t);
219 let b = t.wrapping_sub(co >> 1);
220 let r = b.wrapping_add(co);
221 [r, g, b]
222 }
223}
224
225pub fn modcolor_forward(rgb: &[u8]) -> Vec<u8> {
227 let mut out = vec![0u8; rgb.len()];
228 for (o, px) in out
229 .as_chunks_mut::<3>()
230 .0
231 .iter_mut()
232 .zip(rgb.as_chunks::<3>().0)
233 {
234 let t = modcolor::forward(*px);
235 *o = t;
236 }
237 out
238}
239
240pub fn modcolor_forward_iter(rgb: &[u8]) -> Vec<u8> {
242 let mut out = vec![0u8; rgb.len()];
243 for (o, px) in out
244 .as_chunks_mut::<3>()
245 .0
246 .iter_mut()
247 .zip(rgb.as_chunks::<3>().0)
248 {
249 let t = modcolor::forward(*px);
250 *o = t;
251 }
252 out
253}
254
255pub fn modcolor_inverse(ycocg: &[u8]) -> Vec<u8> {
257 let mut out = vec![0u8; ycocg.len()];
258 for (o, px) in out
259 .as_chunks_mut::<3>()
260 .0
261 .iter_mut()
262 .zip(ycocg.as_chunks::<3>().0)
263 {
264 let t = modcolor::inverse(*px);
265 o[0] = t[0];
266 o[1] = t[1];
267 o[2] = t[2];
268 }
269 out
270}
271
272#[derive(Debug, Clone, Default)]
274pub struct MnF6Stats {
275 pub literal: u32,
277 pub ycocg_literal: u32,
279 pub ref_exact: u32,
281 pub ref_geom: u32,
283 pub ref_exact_color: u32,
285 pub ref_geom_color: u32,
287 pub geom_nontrivial: u32,
289 pub chosen_flags: u32,
291 pub modes: Vec<u8>,
293 pub refs: Vec<Option<(u32, u8)>>,
295}
296
297enum TileChoice {
298 Literal(Vec<u8>),
299 YCoCg(Vec<u8>),
300 Exact(u32, Vec<u8>),
301 Geom(u32, u8, Vec<u8>),
302 ExactColor(u32, Vec<u8>), GeomColor(u32, u8, Vec<u8>), }
305
306fn flag_variants() -> Vec<(u32, bool, Coder)> {
308 vec![
309 (0, false, Coder::Zlib),
310 (FLAG_ZSTD, false, Coder::Zstd(19)),
311 (FLAG_PLANAR, true, Coder::Zlib),
312 (FLAG_PLANAR | FLAG_ZSTD, true, Coder::Zstd(19)),
313 ]
314}
315
316pub fn encode_mnf7(pixels: &[u8], dims: Dims, use_geom: bool) -> (Vec<u8>, MnF6Stats) {
319 let tiles = crate::codec::split_tiles(pixels, dims);
320 let mut best: Option<(Vec<u8>, u32, MnF6Stats)> = None;
323
324 for (flags, planar, coder) in flag_variants() {
325 let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
326 let mut stats = MnF6Stats::default();
327 let mut atlas = Atlas::new();
328
329 for tile in &tiles {
330 let sig = tile_sig(tile.width, tile.height, &tile.data);
331 let pack = |rgb: &[u8]| -> Vec<u8> {
332 if planar {
333 planar_pack(rgb)
334 } else {
335 rgb.to_vec()
336 }
337 };
338 let n = tile.nbytes();
339
340 let lit_body = pack(&tile.data);
341 let mut choice: TileChoice = TileChoice::Literal(lit_body.clone());
342 let mut best_cost = 1 + proxy_cost(&coder, &lit_body);
343
344 {
346 let (cy, cco, ccg) = ycocg_forward(&tile.data);
347 let mut m3 = Vec::with_capacity(n + 160);
348 m3.extend_from_slice(&cy);
349 m3.extend_from_slice(&pack9(&cco));
350 m3.extend_from_slice(&pack9(&ccg));
351 let m3_cost = 1 + proxy_cost(&coder, &m3);
352 if m3_cost < best_cost {
353 best_cost = m3_cost;
354 choice = TileChoice::YCoCg(m3);
355 }
356 }
357
358 let transforms: &[u8] = if use_geom && tile.width == tile.height {
360 &[0, 1, 2, 3, 4, 5, 6, 7]
361 } else {
362 &[0, 1, 2, 3]
363 };
364 let do_atlas = tiles.len() > 1;
365 if do_atlas {
366 for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
367 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
368 for &t in transforms {
369 let base = if t == 0 {
370 ref_rgb.clone()
371 } else {
372 apply_transform(t, tile.width, tile.height, ref_rgb)
373 };
374 let mut r = vec![0u8; n];
375 for i in 0..n {
376 r[i] = tile.data[i].wrapping_sub(base[i]);
377 }
378 let body = pack(&r);
379 let meta: u64 = if t == 0 { 5 } else { 6 };
380 let cost = meta + proxy_cost(&coder, &body);
381 if cost < best_cost {
382 best_cost = cost;
383 choice = if t == 0 {
384 TileChoice::Exact(prior, body)
385 } else {
386 TileChoice::Geom(prior, t, body)
387 };
388 }
389 let mut r_c = vec![0u8; n];
391 let tile_t = modcolor_forward(&tile.data);
392 let base_t = modcolor_forward(&base);
393 for i in 0..n {
394 r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
395 }
396 let body_c = pack(&r_c);
397 let cost_c = meta + 1 + proxy_cost(&coder, &body_c);
398 if cost_c < best_cost {
399 best_cost = cost_c;
400 choice = if t == 0 {
401 TileChoice::ExactColor(prior, body_c)
402 } else {
403 TileChoice::GeomColor(prior, t, body_c)
404 };
405 }
406 }
407 }
408 }
409
410 match &choice {
411 TileChoice::Literal(_) => {
412 stats.literal += 1;
413 stats.modes.push(0);
414 stats.refs.push(None);
415 raw.push(0u8);
416 }
417 TileChoice::YCoCg(_) => {
418 stats.ycocg_literal += 1;
419 stats.modes.push(3);
420 stats.refs.push(None);
421 raw.push(3u8);
422 }
423 TileChoice::Exact(p, _) => {
424 stats.ref_exact += 1;
425 stats.modes.push(1);
426 stats.refs.push(Some((*p, 0)));
427 raw.push(1u8);
428 raw.extend_from_slice(&p.to_le_bytes());
429 }
430 TileChoice::Geom(p, t, _) => {
431 stats.ref_geom += 1;
432 if *t != 0 {
433 stats.geom_nontrivial += 1;
434 }
435 stats.modes.push(2);
436 stats.refs.push(Some((*p, *t)));
437 raw.push(2u8);
438 raw.extend_from_slice(&p.to_le_bytes());
439 raw.push(*t);
440 }
441 TileChoice::ExactColor(p, _) => {
442 stats.ref_exact_color += 1;
443 stats.modes.push(4);
444 stats.refs.push(Some((*p, 0)));
445 raw.push(4u8);
446 raw.extend_from_slice(&p.to_le_bytes());
447 }
448 TileChoice::GeomColor(p, t, _) => {
449 stats.ref_geom_color += 1;
450 if *t != 0 {
451 stats.geom_nontrivial += 1;
452 }
453 stats.modes.push(5);
454 stats.refs.push(Some((*p, *t)));
455 raw.push(5u8);
456 raw.extend_from_slice(&p.to_le_bytes());
457 raw.push(*t);
458 }
459 }
460 match &choice {
461 TileChoice::Literal(b) | TileChoice::YCoCg(b) => raw.extend_from_slice(b),
462 TileChoice::Exact(_, b)
463 | TileChoice::Geom(_, _, b)
464 | TileChoice::ExactColor(_, b)
465 | TileChoice::GeomColor(_, _, b) => raw.extend_from_slice(b),
466 }
467 atlas.insert(tile);
468 }
469
470 let payload = compress_stream(&coder, &raw);
471 let mut file = Vec::with_capacity(HEADER6 + payload.len());
472 file.extend_from_slice(b"MNF7");
473 file.extend_from_slice(&dims.width.to_le_bytes());
474 file.extend_from_slice(&dims.height.to_le_bytes());
475 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
476 file.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
477 file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
478 file.extend_from_slice(&flags.to_le_bytes());
479 file.extend_from_slice(&payload);
480 let size = file.len();
481 if best.as_ref().is_none_or(|b| size < b.0.len()) {
482 stats.chosen_flags = flags;
483 best = Some((file, flags, stats));
484 }
485 }
486
487 let (file, flags, mut stats) = best.expect("at least one variant");
488 stats.chosen_flags = flags;
489 (file, stats)
490}
491
492#[derive(Debug, Clone, Copy)]
498pub struct DecodeLimits {
499 pub max_pixels: u64,
501 pub max_tiles: u32,
503 pub max_decompressed: u64,
505}
506
507impl Default for DecodeLimits {
508 fn default() -> Self {
509 DecodeLimits {
510 max_pixels: 1 << 26,
511 max_tiles: u32::MAX,
512 max_decompressed: u64::MAX, }
514 }
515}
516
517pub fn decode_mnf7_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
520 decode_mnf7_inner(buf, lim)
521}
522
523pub fn decode_mnf7(buf: &[u8]) -> Result<(Dims, Vec<u8>)> {
525 decode_mnf7_inner(buf, &DecodeLimits::default())
526}
527
528fn decode_mnf7_inner(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
529 if buf.len() < HEADER6 {
530 return Err(Error::fmt_msg("short"));
531 }
532 if buf[0..4] != *b"MNF7" {
533 return Err(Error::fmt_msg("magic"));
534 }
535 let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
536 let width = u32le(4);
537 let height = u32le(8);
538 if width == 0 || height == 0 {
539 return Err(Error::fmt_msg("dims"));
540 }
541 if width as u64 * height as u64 > lim.max_pixels {
542 return Err(Error::Limits {
543 which: "max_pixels",
544 limit: lim.max_pixels,
545 actual: width as u64 * height as u64,
546 });
547 }
548 if u32le(12) != TILE_SIZE {
549 return Err(Error::fmt_msg("tilesize"));
550 }
551 let cols = div_ceil(width, TILE_SIZE);
552 let rows = div_ceil(height, TILE_SIZE);
553 let tile_count = u32le(16);
554 if tile_count != cols * rows {
555 return Err(Error::fmt_msg("tilecount"));
556 }
557 if tile_count > lim.max_tiles {
558 return Err(Error::Limits {
559 which: "max_tiles",
560 limit: u64::from(lim.max_tiles),
561 actual: u64::from(tile_count),
562 });
563 }
564 let payload_len = u32le(20);
565 if payload_len > (1 << 31) - 1 {
566 return Err(Error::fmt_msg("payloadlen"));
567 }
568 if buf.len() != HEADER6 + payload_len as usize {
569 return Err(Error::fmt_msg("filelen"));
570 }
571 let flags = u32le(24);
572 if flags & FLAGS_RESERVED != 0 {
573 return Err(Error::fmt_msg("reserved-flags"));
574 }
575 let planar = flags & FLAG_PLANAR != 0;
576 let zlib_in = flags & FLAG_ZSTD == 0;
577
578 let zlib_data = &buf[HEADER6..];
579 let mode3_max: u64 = 1 + 256 + 2 * u64::from((256u32 * 9).div_ceil(8));
586 let per_tile_max = 1 + 5 + (TILE_SIZE * TILE_SIZE * 3) as u64;
587 let limit = tile_count as u64 * per_tile_max.max(mode3_max) + per_tile_max + mode3_max;
588 let raw: Vec<u8> = if zlib_in {
589 let mut d = flate2::read::ZlibDecoder::new(zlib_data).take(limit + 1);
590 let mut v = Vec::new();
591 d.read_to_end(&mut v)
592 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
593 v
594 } else {
595 let d = zstd::stream::read::Decoder::new(zlib_data)
596 .map_err(|_| Error::Decompress("zstd init"))?;
597 let mut d = d;
600 d.window_log_max(23)
601 .map_err(|_| Error::Decompress("zstd window param"))?;
602 let mut d = d.take(limit + 1);
603 let mut v = Vec::new();
604 d.read_to_end(&mut v)
605 .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
606 v
607 };
608 if raw.len() as u64 > limit {
609 return Err(Error::fmt_msg("bomb"));
610 }
611
612 let pixels = decode_record_walk(&raw, width, height, tile_count, planar, false)?;
613 Ok((Dims { width, height }, pixels))
614}
615
616pub fn decode_record_walk(
621 raw: &[u8],
622 width: u32,
623 height: u32,
624 tile_count: u32,
625 planar: bool,
626 med: bool,
627) -> Result<Vec<u8>> {
628 let cols = div_ceil(width, TILE_SIZE);
629 let mut pixels = vec![0u8; width as usize * height as usize * 3];
630 let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
631 let mut ip = 0usize;
632 for i in 0..tile_count {
633 let row = i / cols;
634 let col = i % cols;
635 let tw = TILE_SIZE.min(width - col * TILE_SIZE);
636 let th = TILE_SIZE.min(height - row * TILE_SIZE);
637 let n = (tw * th * 3) as usize;
638
639 if ip >= raw.len() {
640 return Err(Error::fmt_msg(format!("eof at tile {i}")));
641 }
642 let mode = raw[ip];
643 ip += 1;
644
645 let tile = match mode {
646 0 => {
647 if ip + n > raw.len() {
648 return Err(Error::fmt_msg("body"));
649 }
650 let body = raw[ip..ip + n].to_vec();
651 ip += n;
652 let body = if planar { planar_unpack(&body) } else { body };
653 if med {
654 let mut recon = vec![0u8; n];
663 let gy0 = row as usize * TILE_SIZE as usize;
664 let gx0 = col as usize * TILE_SIZE as usize;
665 for y in 0..th as usize {
666 for x in 0..tw as usize {
667 for c in 0..3usize {
668 let k = (y * tw as usize + x) * 3 + c;
669 let gy = gy0 + y;
670 let gx = gx0 + x;
671 let tl = |ggy: usize, ggx: usize| -> u32 {
673 if ggy >= gy0 && ggx >= gx0 {
674 u32::from(
675 recon
676 [((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
677 )
678 } else {
679 u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
680 }
681 };
682 let pred: u32 = if gy == 0 && gx == 0 {
683 127
684 } else if gy == 0 {
685 if gx - 1 < gx0 {
687 u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
688 } else {
689 u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
690 }
691 } else if gx == 0 {
692 if gy - 1 < gy0 {
694 u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
695 } else {
696 u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
697 }
698 } else {
699 let a = tl(gy, gx - 1);
700 let b = tl(gy - 1, gx);
701 let cc = tl(gy - 1, gx - 1);
702 med_predict_pixel(a, b, cc)
703 };
704 recon[k] =
705 (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
706 }
707 }
708 }
709 recon
710 } else {
711 body
712 }
713 }
714 3 => {
715 let np = n / 3;
717 let n9 = (np * 9).div_ceil(8);
718 if ip + np + 2 * n9 > raw.len() {
719 return Err(Error::fmt_msg(format!("tile {i}: body")));
720 }
721 let y = raw[ip..ip + np].to_vec();
722 let co9 = &raw[ip + np..ip + np + n9];
723 let cg9 = &raw[ip + np + n9..ip + np + 2 * n9];
724 ip += np + 2 * n9;
725 let co = unpack9(co9, np)?;
726 let cg = unpack9(cg9, np)?;
727 ycocg_inverse(&y, &co, &cg)
728 }
729 1 | 2 | 4 | 5 => {
730 let meta = if mode == 2 || mode == 5 { 5 } else { 4 };
731 if ip + meta > raw.len() {
732 return Err(Error::fmt_msg("refmeta"));
733 }
734 let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
735 let tr = if mode == 2 || mode == 5 {
736 raw[ip + 4]
737 } else {
738 0
739 };
740 ip += meta;
741 if idx >= i as usize {
742 return Err(Error::fmt_msg("reforder"));
743 }
744 if tr >= 8 {
745 return Err(Error::fmt_msg("tranform-range"));
746 }
747 let (rw, rh, ref_rgb) = &decoded[idx];
748 if *rw != tw || *rh != th {
749 return Err(Error::fmt_msg("refshape"));
750 }
751 if tw != th && tr >= 4 {
752 return Err(Error::fmt_msg("xform-shape"));
753 }
754 if ip + n > raw.len() {
755 return Err(Error::fmt_msg("body"));
756 }
757 let body = raw[ip..ip + n].to_vec();
758 ip += n;
759 let resid = if planar { planar_unpack(&body) } else { body };
760 let base = if tr == 0 {
761 ref_rgb.clone()
762 } else {
763 apply_transform(tr, tw, th, ref_rgb)
764 };
765 match mode {
766 1 | 2 => base
767 .iter()
768 .zip(resid.iter())
769 .map(|(a, r)| a.wrapping_add(*r))
770 .collect(),
771 4 | 5 => {
773 let base_t = modcolor_forward(&base);
774 let recon_t: Vec<u8> = base_t
775 .iter()
776 .zip(resid.iter())
777 .map(|(a, r)| a.wrapping_add(*r))
778 .collect();
779 modcolor_inverse(&recon_t)
780 }
781 _ => unreachable!(),
782 }
783 }
784 _ => {
785 return Err(Error::fmt_msg(format!(
786 "tile {i}: mode byte {mode:#04x} pos {ip}"
787 )))
788 }
789 };
790 decoded.push((tw, th, tile.clone()));
791
792 for y_ in 0..th as usize {
793 let py = row as usize * TILE_SIZE as usize + y_;
794 for x in 0..tw as usize {
795 let pxx = col as usize * TILE_SIZE as usize + x;
796 let d = (py * width as usize + pxx) * 3;
797 let s = (y_ * tw as usize + x) * 3;
798 pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
799 }
800 }
801 }
802 if ip != raw.len() {
803 return Err(Error::fmt_msg("extra"));
804 }
805 Ok(pixels)
807}
808
809pub fn med_predict(rgb: &[u8], w: usize, h: usize) -> Vec<u8> {
815 assert_eq!(rgb.len(), w * h * 3, "rgb size");
816 let mut out = vec![0u8; rgb.len()];
817 for y in 0..h {
818 for x in 0..w {
819 for c in 0..3usize {
820 let i = (y * w + x) * 3 + c;
821 if x == 0 && y == 0 {
822 out[i] = 127;
823 } else if y == 0 {
824 out[i] = rgb[i - 3];
825 } else if x == 0 {
826 out[i] = rgb[i - w * 3];
827 } else {
828 let a = i32::from(rgb[i - 3]);
829 let b = i32::from(rgb[i - w * 3]);
830 let cc = i32::from(rgb[i - w * 3 - 3]);
831 let plane = a + b - cc;
832 let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
833 let p = if flat {
834 plane
835 } else {
836 let pa = (plane - a).abs();
837 let pb = (plane - b).abs();
838 let pcc = (plane - cc).abs();
839 if pa <= pb && pa <= pcc {
840 a
841 } else if pb <= pcc {
842 b
843 } else {
844 cc
845 }
846 };
847 out[i] = p.clamp(0, 255) as u8;
848 }
849 }
850 }
851 }
852 out
853}
854
855#[inline]
856fn med_predict_pixel(a: u32, b: u32, cc: u32) -> u32 {
857 let plane = a as i64 + b as i64 - cc as i64;
858 let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
859 if flat {
860 plane.clamp(0, 255) as u32
864 } else {
865 let pa = (plane - a as i64).abs();
866 let pb = (plane - b as i64).abs();
867 let pcc = (plane - cc as i64).abs();
868 if pa <= pb && pa <= pcc {
869 a
870 } else if pb <= pcc {
871 b
872 } else {
873 cc
874 }
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 fn roundtrip_mnf7(px: &[u8], dims: Dims, use_geom: bool) {
883 let (buf, st) = encode_mnf7(px, dims, use_geom);
884 let (d, out) = decode_mnf7(&buf).expect("decode");
885 assert_eq!(d, dims);
886 assert_eq!(out, px, "flags {:#b}", st.chosen_flags);
887 }
888
889 #[test]
890 fn bit9_roundtrip() {
891 let vals: Vec<u16> = (0..300).map(|i| (i * 137) % 512).collect();
892 let packed = pack9(&vals);
893 assert_eq!(unpack9(&packed, vals.len()).expect("unpack"), vals);
894 for v in [0u16, 1, 511, 256, 255] {
896 let p = pack9(&[v]);
897 assert_eq!(unpack9(&p, 1).expect("u")[0], v);
898 }
899 }
900
901 #[test]
902 fn ycocg_exhaustive_sample() {
903 for v in (0..=0xFFFFFF).step_by(41) {
904 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
905 let (y, co, cg) = ycocg_forward(&px);
906 assert_eq!(ycocg_inverse(&y, &co, &cg), px, "{px:?}");
907 let py = pack9(&co);
909 let pg = pack9(&cg);
910 let co2 = unpack9(&py, 1).expect("co");
911 let cg2 = unpack9(&pg, 1).expect("cg");
912 assert_eq!(ycocg_inverse(&y, &co2, &cg2), px, "{px:?}");
913 }
914 }
915
916 #[test]
917 fn mnf6_roundtrip_all_classes() {
918 for (w, h, seed) in [
919 (16u32, 16u32, 1),
920 (48, 33, 2),
921 (64, 64, 3),
922 (31, 17, 4),
923 (1, 1, 5),
924 ] {
925 let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
926 let mut rnd = move || {
927 s ^= s << 13;
928 s ^= s >> 7;
929 s ^= s << 17;
930 s
931 };
932 let px: Vec<u8> = (0..w as usize * h as usize * 3)
933 .map(|_| (rnd() & 0xFF) as u8)
934 .collect();
935 roundtrip_mnf7(
936 &px,
937 Dims {
938 width: w,
939 height: h,
940 },
941 true,
942 );
943 }
944 }
945
946 #[test]
947 fn mnf7_modcolor_is_bijective_exhaustive() {
948 if cfg!(debug_assertions) {
953 for v in (0..=0xFFFFFFu32).step_by(17) {
954 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
955 assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
956 }
957 for r in [0u8, 1, 127, 128, 254, 255] {
958 for g in [0u8, 85, 170, 255] {
959 for b in [0u8, 128, 255] {
960 let px = [r, g, b];
961 assert_eq!(modcolor::inverse(modcolor::forward(px)), px);
962 }
963 }
964 }
965 } else {
966 let mut checked: u64 = 0;
967 for v in 0..=0xFFFFFFu32 {
968 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
969 assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
970 checked += 1;
971 }
972 assert_eq!(checked, 16_777_216, "exhaustive sweep count");
973 }
974 }
975
976 #[test]
977 fn mnf7_color_domain_used_when_it_wins() {
978 let mut s: u64 = 42;
982 let mut rnd = move || {
983 s ^= s << 13;
984 s ^= s >> 7;
985 s ^= s << 17;
986 s
987 };
988 let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
989
990 let ta_t = modcolor_forward(&a);
991 let mut tb_t = ta_t.clone();
992 let ramp = [0u8, 1, 2, 255]; for i in (1..tb_t.len()).step_by(3) {
994 tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
995 }
996 let b = modcolor_inverse(&tb_t);
997
998 let mut two = a.clone();
999 two.extend_from_slice(&b);
1000 let d2 = Dims {
1001 width: 16,
1002 height: 32,
1003 };
1004 let (buf, st) = encode_mnf7(&two, d2, true);
1005 let (_, out) = decode_mnf7(&buf).expect("decode");
1006 assert_eq!(out, two);
1007 let color_modes = st.ref_exact_color + st.ref_geom_color;
1008 assert!(
1009 color_modes > 0,
1010 "expected color-domain references, got {}",
1011 color_modes
1012 );
1013 }
1014
1015 #[test]
1016 fn mnf6_rate_aware_picks_flags() {
1017 let (w, h) = (64u32, 64u32);
1019 let mut px = vec![0u8; (w * h * 3) as usize];
1020 for y in 0..h {
1021 for x in 0..w {
1022 let i = ((y * w + x) * 3) as usize;
1023 px[i] = (x * 4) as u8;
1024 px[i + 1] = (y * 4) as u8;
1025 px[i + 2] = 128;
1026 }
1027 }
1028 let (buf, st) = encode_mnf7(
1029 &px,
1030 Dims {
1031 width: w,
1032 height: h,
1033 },
1034 false,
1035 );
1036 assert_eq!(
1037 st.chosen_flags & FLAG_PLANAR,
1038 FLAG_PLANAR,
1039 "gradient wants planar"
1040 );
1041 let (_, out) = decode_mnf7(&buf).expect("decode");
1042 assert_eq!(out, px);
1043 }
1044
1045 #[test]
1046 fn mnf6_rejects() {
1047 assert!(decode_mnf7(&[]).is_err());
1048 assert!(decode_mnf7(&[0u8; 27]).is_err());
1049 assert!(decode_mnf7(b"MNF5\x01\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00").is_err(), "mnf5 magic");
1050 let mut f = vec![0u8; 28];
1052 f[0..4].copy_from_slice(b"MNF6");
1053 f[24] = 0b100; assert!(decode_mnf7(&f).is_err(), "reserved flag");
1055 }
1056
1057 #[test]
1058 fn mnf6_beats_mnf5_on_gradient() {
1059 use crate::codec::{build_header, compress_payload, encode_tiles};
1062 let (w, h) = (96u32, 96u32);
1063 let mut px = vec![0u8; (w * h * 3) as usize];
1064 for y in 0..h {
1065 for x in 0..w {
1066 let i = ((y * w + x) * 3) as usize;
1067 px[i] = (x * 2) as u8;
1068 px[i + 1] = (y * 2) as u8;
1069 px[i + 2] = ((x + y) / 2) as u8;
1070 }
1071 }
1072 let d = Dims {
1073 width: w,
1074 height: h,
1075 };
1076 let tiles = crate::codec::split_tiles(&px, d);
1077 let raw = encode_tiles(&tiles, true, true);
1078 let z = compress_payload(&raw);
1079 let mut m5 = build_header(d, tiles.len() as u32, z.len() as u32);
1080 m5.extend_from_slice(&z);
1081 let (m6, _) = encode_mnf7(&px, d, true);
1082 assert!(decode_mnf7(&m6).is_ok());
1083 assert!(
1084 m6.len() <= m5.len(),
1085 "MNF6/7 ({}) should not lose to MNF5 ({}) on gradient",
1086 m6.len(),
1087 m5.len()
1088 );
1089 }
1090 #[test]
1091 fn d8_atlas_full_roundtrip() {
1092 let (w, h) = (64u32, 64u32);
1098 let mut px = vec![0u8; (w * h * 3) as usize];
1099 for y in 0..h {
1103 for x in 0..w {
1104 let i = ((y * w + x) * 3) as usize;
1105 let band = ((x / 4) as u16 * 13) as u8;
1106 px[i] = band;
1107 px[i + 1] = ((y / 4) as u16 * 11) as u8;
1108 px[i + 2] = band ^ (((y / 4) as u16 * 29) as u8);
1109 }
1110 }
1111 let mut stacked = px.clone();
1113 for t in [1u8, 2, 3, 4, 5, 6, 7] {
1114 let transformed = crate::codec::apply_transform(t, w, h, &px);
1115 stacked.extend_from_slice(&transformed);
1116 }
1117 let d = Dims {
1118 width: w,
1119 height: h * 8,
1120 };
1121 let (buf, st) = encode_mnf7(&stacked, d, true);
1122 assert!(
1125 st.ref_geom > 0,
1126 "no geometric references emitted: literal={} exact={} geom={} nontrivial={}",
1127 st.literal,
1128 st.ref_exact,
1129 st.ref_geom,
1130 st.geom_nontrivial
1131 );
1132 assert!(
1133 st.geom_nontrivial > 0,
1134 "only identity transforms used — test cannot verify D8 matching"
1135 );
1136 let (dims, out) = decode_mnf7(&buf).expect("full-file decode");
1137 assert_eq!((dims.width, dims.height), (w, h * 8));
1138 assert_eq!(out, stacked, "MNF7 D8 atlas roundtrip");
1139 }
1141}