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.chunks_exact(3) {
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.chunks_exact_mut(3).zip(rgb.chunks_exact(3)) {
229 let t = modcolor::forward([px[0], px[1], px[2]]);
230 o[0] = t[0];
231 o[1] = t[1];
232 o[2] = t[2];
233 }
234 out
235}
236
237pub fn modcolor_forward_iter(rgb: &[u8]) -> Vec<u8> {
239 let mut out = vec![0u8; rgb.len()];
240 for (o, px) in out.chunks_exact_mut(3).zip(rgb.chunks_exact(3)) {
241 let t = modcolor::forward([px[0], px[1], px[2]]);
242 o[0] = t[0];
243 o[1] = t[1];
244 o[2] = t[2];
245 }
246 out
247}
248
249pub fn modcolor_inverse(ycocg: &[u8]) -> Vec<u8> {
251 let mut out = vec![0u8; ycocg.len()];
252 for (o, px) in out.chunks_exact_mut(3).zip(ycocg.chunks_exact(3)) {
253 let t = modcolor::inverse([px[0], px[1], px[2]]);
254 o[0] = t[0];
255 o[1] = t[1];
256 o[2] = t[2];
257 }
258 out
259}
260
261#[derive(Debug, Clone, Default)]
263pub struct MnF6Stats {
264 pub literal: u32,
266 pub ycocg_literal: u32,
268 pub ref_exact: u32,
270 pub ref_geom: u32,
272 pub ref_exact_color: u32,
274 pub ref_geom_color: u32,
276 pub geom_nontrivial: u32,
278 pub chosen_flags: u32,
280 pub modes: Vec<u8>,
282 pub refs: Vec<Option<(u32, u8)>>,
284}
285
286enum TileChoice {
287 Literal(Vec<u8>),
288 YCoCg(Vec<u8>),
289 Exact(u32, Vec<u8>),
290 Geom(u32, u8, Vec<u8>),
291 ExactColor(u32, Vec<u8>), GeomColor(u32, u8, Vec<u8>), }
294
295fn flag_variants() -> Vec<(u32, bool, Coder)> {
297 vec![
298 (0, false, Coder::Zlib),
299 (FLAG_ZSTD, false, Coder::Zstd(19)),
300 (FLAG_PLANAR, true, Coder::Zlib),
301 (FLAG_PLANAR | FLAG_ZSTD, true, Coder::Zstd(19)),
302 ]
303}
304
305pub fn encode_mnf7(pixels: &[u8], dims: Dims, use_geom: bool) -> (Vec<u8>, MnF6Stats) {
308 let tiles = crate::codec::split_tiles(pixels, dims);
309 let mut best: Option<(Vec<u8>, u32, MnF6Stats)> = None;
312
313 for (flags, planar, coder) in flag_variants() {
314 let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
315 let mut stats = MnF6Stats::default();
316 let mut atlas = Atlas::new();
317
318 for tile in &tiles {
319 let sig = tile_sig(tile.width, tile.height, &tile.data);
320 let pack = |rgb: &[u8]| -> Vec<u8> {
321 if planar {
322 planar_pack(rgb)
323 } else {
324 rgb.to_vec()
325 }
326 };
327 let n = tile.nbytes();
328
329 let lit_body = pack(&tile.data);
330 let mut choice: TileChoice = TileChoice::Literal(lit_body.clone());
331 let mut best_cost = 1 + proxy_cost(&coder, &lit_body);
332
333 {
335 let (cy, cco, ccg) = ycocg_forward(&tile.data);
336 let mut m3 = Vec::with_capacity(n + 160);
337 m3.extend_from_slice(&cy);
338 m3.extend_from_slice(&pack9(&cco));
339 m3.extend_from_slice(&pack9(&ccg));
340 let m3_cost = 1 + proxy_cost(&coder, &m3);
341 if m3_cost < best_cost {
342 best_cost = m3_cost;
343 choice = TileChoice::YCoCg(m3);
344 }
345 }
346
347 let transforms: &[u8] = if use_geom && tile.width == tile.height {
349 &[0, 1, 2, 3, 4, 5, 6, 7]
350 } else {
351 &[0, 1, 2, 3]
352 };
353 let do_atlas = tiles.len() > 1;
354 if do_atlas {
355 for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
356 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
357 for &t in transforms {
358 let base = if t == 0 {
359 ref_rgb.clone()
360 } else {
361 apply_transform(t, tile.width, tile.height, ref_rgb)
362 };
363 let mut r = vec![0u8; n];
364 for i in 0..n {
365 r[i] = tile.data[i].wrapping_sub(base[i]);
366 }
367 let body = pack(&r);
368 let meta: u64 = if t == 0 { 5 } else { 6 };
369 let cost = meta + proxy_cost(&coder, &body);
370 if cost < best_cost {
371 best_cost = cost;
372 choice = if t == 0 {
373 TileChoice::Exact(prior, body)
374 } else {
375 TileChoice::Geom(prior, t, body)
376 };
377 }
378 let mut r_c = vec![0u8; n];
380 let tile_t = modcolor_forward(&tile.data);
381 let base_t = modcolor_forward(&base);
382 for i in 0..n {
383 r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
384 }
385 let body_c = pack(&r_c);
386 let cost_c = meta + 1 + proxy_cost(&coder, &body_c);
387 if cost_c < best_cost {
388 best_cost = cost_c;
389 choice = if t == 0 {
390 TileChoice::ExactColor(prior, body_c)
391 } else {
392 TileChoice::GeomColor(prior, t, body_c)
393 };
394 }
395 }
396 }
397 }
398
399 match &choice {
400 TileChoice::Literal(_) => {
401 stats.literal += 1;
402 stats.modes.push(0);
403 stats.refs.push(None);
404 raw.push(0u8);
405 }
406 TileChoice::YCoCg(_) => {
407 stats.ycocg_literal += 1;
408 stats.modes.push(3);
409 stats.refs.push(None);
410 raw.push(3u8);
411 }
412 TileChoice::Exact(p, _) => {
413 stats.ref_exact += 1;
414 stats.modes.push(1);
415 stats.refs.push(Some((*p, 0)));
416 raw.push(1u8);
417 raw.extend_from_slice(&p.to_le_bytes());
418 }
419 TileChoice::Geom(p, t, _) => {
420 stats.ref_geom += 1;
421 if *t != 0 {
422 stats.geom_nontrivial += 1;
423 }
424 stats.modes.push(2);
425 stats.refs.push(Some((*p, *t)));
426 raw.push(2u8);
427 raw.extend_from_slice(&p.to_le_bytes());
428 raw.push(*t);
429 }
430 TileChoice::ExactColor(p, _) => {
431 stats.ref_exact_color += 1;
432 stats.modes.push(4);
433 stats.refs.push(Some((*p, 0)));
434 raw.push(4u8);
435 raw.extend_from_slice(&p.to_le_bytes());
436 }
437 TileChoice::GeomColor(p, t, _) => {
438 stats.ref_geom_color += 1;
439 if *t != 0 {
440 stats.geom_nontrivial += 1;
441 }
442 stats.modes.push(5);
443 stats.refs.push(Some((*p, *t)));
444 raw.push(5u8);
445 raw.extend_from_slice(&p.to_le_bytes());
446 raw.push(*t);
447 }
448 }
449 match &choice {
450 TileChoice::Literal(b) | TileChoice::YCoCg(b) => raw.extend_from_slice(b),
451 TileChoice::Exact(_, b)
452 | TileChoice::Geom(_, _, b)
453 | TileChoice::ExactColor(_, b)
454 | TileChoice::GeomColor(_, _, b) => raw.extend_from_slice(b),
455 }
456 atlas.insert(tile);
457 }
458
459 let payload = compress_stream(&coder, &raw);
460 let mut file = Vec::with_capacity(HEADER6 + payload.len());
461 file.extend_from_slice(b"MNF7");
462 file.extend_from_slice(&dims.width.to_le_bytes());
463 file.extend_from_slice(&dims.height.to_le_bytes());
464 file.extend_from_slice(&TILE_SIZE.to_le_bytes());
465 file.extend_from_slice(&(tiles.len() as u32).to_le_bytes());
466 file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
467 file.extend_from_slice(&flags.to_le_bytes());
468 file.extend_from_slice(&payload);
469 let size = file.len();
470 if best.as_ref().is_none_or(|b| size < b.0.len()) {
471 stats.chosen_flags = flags;
472 best = Some((file, flags, stats));
473 }
474 }
475
476 let (file, flags, mut stats) = best.expect("at least one variant");
477 stats.chosen_flags = flags;
478 (file, stats)
479}
480
481#[derive(Debug, Clone, Copy)]
487pub struct DecodeLimits {
488 pub max_pixels: u64,
490 pub max_tiles: u32,
492 pub max_decompressed: u64,
494}
495
496impl Default for DecodeLimits {
497 fn default() -> Self {
498 DecodeLimits {
499 max_pixels: 1 << 26,
500 max_tiles: u32::MAX,
501 max_decompressed: u64::MAX, }
503 }
504}
505
506pub fn decode_mnf7_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
509 decode_mnf7_inner(buf, lim)
510}
511
512pub fn decode_mnf7(buf: &[u8]) -> Result<(Dims, Vec<u8>)> {
514 decode_mnf7_inner(buf, &DecodeLimits::default())
515}
516
517fn decode_mnf7_inner(buf: &[u8], lim: &DecodeLimits) -> Result<(Dims, Vec<u8>)> {
518 if buf.len() < HEADER6 {
519 return Err(Error::fmt_msg("short"));
520 }
521 if buf[0..4] != *b"MNF7" {
522 return Err(Error::fmt_msg("magic"));
523 }
524 let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
525 let width = u32le(4);
526 let height = u32le(8);
527 if width == 0 || height == 0 {
528 return Err(Error::fmt_msg("dims"));
529 }
530 if width as u64 * height as u64 > lim.max_pixels {
531 return Err(Error::Limits {
532 which: "max_pixels",
533 limit: lim.max_pixels,
534 actual: width as u64 * height as u64,
535 });
536 }
537 if u32le(12) != TILE_SIZE {
538 return Err(Error::fmt_msg("tilesize"));
539 }
540 let cols = div_ceil(width, TILE_SIZE);
541 let rows = div_ceil(height, TILE_SIZE);
542 let tile_count = u32le(16);
543 if tile_count != cols * rows {
544 return Err(Error::fmt_msg("tilecount"));
545 }
546 if tile_count > lim.max_tiles {
547 return Err(Error::Limits {
548 which: "max_tiles",
549 limit: u64::from(lim.max_tiles),
550 actual: u64::from(tile_count),
551 });
552 }
553 let payload_len = u32le(20);
554 if payload_len > (1 << 31) - 1 {
555 return Err(Error::fmt_msg("payloadlen"));
556 }
557 if buf.len() != HEADER6 + payload_len as usize {
558 return Err(Error::fmt_msg("filelen"));
559 }
560 let flags = u32le(24);
561 if flags & FLAGS_RESERVED != 0 {
562 return Err(Error::fmt_msg("reserved-flags"));
563 }
564 let planar = flags & FLAG_PLANAR != 0;
565 let zlib_in = flags & FLAG_ZSTD == 0;
566
567 let zlib_data = &buf[HEADER6..];
568 let mode3_max: u64 = 1 + 256 + 2 * u64::from((256u32 * 9).div_ceil(8));
575 let per_tile_max = 1 + 5 + (TILE_SIZE * TILE_SIZE * 3) as u64;
576 let limit = tile_count as u64 * per_tile_max.max(mode3_max) + per_tile_max + mode3_max;
577 let raw: Vec<u8> = if zlib_in {
578 let mut d = flate2::read::ZlibDecoder::new(zlib_data).take(limit + 1);
579 let mut v = Vec::new();
580 d.read_to_end(&mut v)
581 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
582 v
583 } else {
584 let d = zstd::stream::read::Decoder::new(zlib_data)
585 .map_err(|_| Error::Decompress("zstd init"))?;
586 let mut d = d;
589 d.window_log_max(23)
590 .map_err(|_| Error::Decompress("zstd window param"))?;
591 let mut d = d.take(limit + 1);
592 let mut v = Vec::new();
593 d.read_to_end(&mut v)
594 .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
595 v
596 };
597 if raw.len() as u64 > limit {
598 return Err(Error::fmt_msg("bomb"));
599 }
600
601 let pixels = decode_record_walk(&raw, width, height, tile_count, planar, false)?;
602 Ok((Dims { width, height }, pixels))
603}
604
605pub fn decode_record_walk(
610 raw: &[u8],
611 width: u32,
612 height: u32,
613 tile_count: u32,
614 planar: bool,
615 med: bool,
616) -> Result<Vec<u8>> {
617 let cols = div_ceil(width, TILE_SIZE);
618 let mut pixels = vec![0u8; width as usize * height as usize * 3];
619 let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
620 let mut ip = 0usize;
621 for i in 0..tile_count {
622 let row = i / cols;
623 let col = i % cols;
624 let tw = TILE_SIZE.min(width - col * TILE_SIZE);
625 let th = TILE_SIZE.min(height - row * TILE_SIZE);
626 let n = (tw * th * 3) as usize;
627
628 if ip >= raw.len() {
629 return Err(Error::fmt_msg(format!("eof at tile {i}")));
630 }
631 let mode = raw[ip];
632 ip += 1;
633
634 let tile = match mode {
635 0 => {
636 if ip + n > raw.len() {
637 return Err(Error::fmt_msg("body"));
638 }
639 let body = raw[ip..ip + n].to_vec();
640 ip += n;
641 let body = if planar { planar_unpack(&body) } else { body };
642 if med {
643 let mut recon = vec![0u8; n];
652 let gy0 = row as usize * TILE_SIZE as usize;
653 let gx0 = col as usize * TILE_SIZE as usize;
654 for y in 0..th as usize {
655 for x in 0..tw as usize {
656 for c in 0..3usize {
657 let k = (y * tw as usize + x) * 3 + c;
658 let gy = gy0 + y;
659 let gx = gx0 + x;
660 let tl = |ggy: usize, ggx: usize| -> u32 {
662 if ggy >= gy0 && ggx >= gx0 {
663 u32::from(
664 recon
665 [((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
666 )
667 } else {
668 u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
669 }
670 };
671 let pred: u32 = if gy == 0 && gx == 0 {
672 127
673 } else if gy == 0 {
674 if gx - 1 < gx0 {
676 u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
677 } else {
678 u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
679 }
680 } else if gx == 0 {
681 if gy - 1 < gy0 {
683 u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
684 } else {
685 u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
686 }
687 } else {
688 let a = tl(gy, gx - 1);
689 let b = tl(gy - 1, gx);
690 let cc = tl(gy - 1, gx - 1);
691 med_predict_pixel(a, b, cc)
692 };
693 recon[k] =
694 (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
695 }
696 }
697 }
698 recon
699 } else {
700 body
701 }
702 }
703 3 => {
704 let np = n / 3;
706 let n9 = (np * 9).div_ceil(8);
707 if ip + np + 2 * n9 > raw.len() {
708 return Err(Error::fmt_msg(format!("tile {i}: body")));
709 }
710 let y = raw[ip..ip + np].to_vec();
711 let co9 = &raw[ip + np..ip + np + n9];
712 let cg9 = &raw[ip + np + n9..ip + np + 2 * n9];
713 ip += np + 2 * n9;
714 let co = unpack9(co9, np)?;
715 let cg = unpack9(cg9, np)?;
716 ycocg_inverse(&y, &co, &cg)
717 }
718 1 | 2 | 4 | 5 => {
719 let meta = if mode == 2 || mode == 5 { 5 } else { 4 };
720 if ip + meta > raw.len() {
721 return Err(Error::fmt_msg("refmeta"));
722 }
723 let idx = u32::from_le_bytes(raw[ip..ip + 4].try_into().unwrap()) as usize;
724 let tr = if mode == 2 || mode == 5 {
725 raw[ip + 4]
726 } else {
727 0
728 };
729 ip += meta;
730 if idx >= i as usize {
731 return Err(Error::fmt_msg("reforder"));
732 }
733 if tr >= 8 {
734 return Err(Error::fmt_msg("tranform-range"));
735 }
736 let (rw, rh, ref_rgb) = &decoded[idx];
737 if *rw != tw || *rh != th {
738 return Err(Error::fmt_msg("refshape"));
739 }
740 if tw != th && tr >= 4 {
741 return Err(Error::fmt_msg("xform-shape"));
742 }
743 if ip + n > raw.len() {
744 return Err(Error::fmt_msg("body"));
745 }
746 let body = raw[ip..ip + n].to_vec();
747 ip += n;
748 let resid = if planar { planar_unpack(&body) } else { body };
749 let base = if tr == 0 {
750 ref_rgb.clone()
751 } else {
752 apply_transform(tr, tw, th, ref_rgb)
753 };
754 match mode {
755 1 | 2 => base
756 .iter()
757 .zip(resid.iter())
758 .map(|(a, r)| a.wrapping_add(*r))
759 .collect(),
760 4 | 5 => {
762 let base_t = modcolor_forward(&base);
763 let recon_t: Vec<u8> = base_t
764 .iter()
765 .zip(resid.iter())
766 .map(|(a, r)| a.wrapping_add(*r))
767 .collect();
768 modcolor_inverse(&recon_t)
769 }
770 _ => unreachable!(),
771 }
772 }
773 _ => {
774 return Err(Error::fmt_msg(format!(
775 "tile {i}: mode byte {mode:#04x} pos {ip}"
776 )))
777 }
778 };
779 decoded.push((tw, th, tile.clone()));
780
781 for y_ in 0..th as usize {
782 let py = row as usize * TILE_SIZE as usize + y_;
783 for x in 0..tw as usize {
784 let pxx = col as usize * TILE_SIZE as usize + x;
785 let d = (py * width as usize + pxx) * 3;
786 let s = (y_ * tw as usize + x) * 3;
787 pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
788 }
789 }
790 }
791 if ip != raw.len() {
792 return Err(Error::fmt_msg("extra"));
793 }
794 Ok(pixels)
796}
797
798pub fn med_predict(rgb: &[u8], w: usize, h: usize) -> Vec<u8> {
804 assert_eq!(rgb.len(), w * h * 3, "rgb size");
805 let mut out = vec![0u8; rgb.len()];
806 for y in 0..h {
807 for x in 0..w {
808 for c in 0..3usize {
809 let i = (y * w + x) * 3 + c;
810 if x == 0 && y == 0 {
811 out[i] = 127;
812 } else if y == 0 {
813 out[i] = rgb[i - 3];
814 } else if x == 0 {
815 out[i] = rgb[i - w * 3];
816 } else {
817 let a = i32::from(rgb[i - 3]);
818 let b = i32::from(rgb[i - w * 3]);
819 let cc = i32::from(rgb[i - w * 3 - 3]);
820 let plane = a + b - cc;
821 let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
822 let p = if flat {
823 plane
824 } else {
825 let pa = (plane - a).abs();
826 let pb = (plane - b).abs();
827 let pcc = (plane - cc).abs();
828 if pa <= pb && pa <= pcc {
829 a
830 } else if pb <= pcc {
831 b
832 } else {
833 cc
834 }
835 };
836 out[i] = p.clamp(0, 255) as u8;
837 }
838 }
839 }
840 }
841 out
842}
843
844#[inline]
845fn med_predict_pixel(a: u32, b: u32, cc: u32) -> u32 {
846 let plane = a as i64 + b as i64 - cc as i64;
847 let flat = (a >= b && a >= cc && b >= cc) || (a <= b && a <= cc && b <= cc);
848 if flat {
849 plane.clamp(0, 255) as u32
853 } else {
854 let pa = (plane - a as i64).abs();
855 let pb = (plane - b as i64).abs();
856 let pcc = (plane - cc as i64).abs();
857 if pa <= pb && pa <= pcc {
858 a
859 } else if pb <= pcc {
860 b
861 } else {
862 cc
863 }
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870
871 fn roundtrip_mnf7(px: &[u8], dims: Dims, use_geom: bool) {
872 let (buf, st) = encode_mnf7(px, dims, use_geom);
873 let (d, out) = decode_mnf7(&buf).expect("decode");
874 assert_eq!(d, dims);
875 assert_eq!(out, px, "flags {:#b}", st.chosen_flags);
876 }
877
878 #[test]
879 fn bit9_roundtrip() {
880 let vals: Vec<u16> = (0..300).map(|i| (i * 137) % 512).collect();
881 let packed = pack9(&vals);
882 assert_eq!(unpack9(&packed, vals.len()).expect("unpack"), vals);
883 for v in [0u16, 1, 511, 256, 255] {
885 let p = pack9(&[v]);
886 assert_eq!(unpack9(&p, 1).expect("u")[0], v);
887 }
888 }
889
890 #[test]
891 fn ycocg_exhaustive_sample() {
892 for v in (0..=0xFFFFFF).step_by(41) {
893 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
894 let (y, co, cg) = ycocg_forward(&px);
895 assert_eq!(ycocg_inverse(&y, &co, &cg), px, "{px:?}");
896 let py = pack9(&co);
898 let pg = pack9(&cg);
899 let co2 = unpack9(&py, 1).expect("co");
900 let cg2 = unpack9(&pg, 1).expect("cg");
901 assert_eq!(ycocg_inverse(&y, &co2, &cg2), px, "{px:?}");
902 }
903 }
904
905 #[test]
906 fn mnf6_roundtrip_all_classes() {
907 for (w, h, seed) in [
908 (16u32, 16u32, 1),
909 (48, 33, 2),
910 (64, 64, 3),
911 (31, 17, 4),
912 (1, 1, 5),
913 ] {
914 let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
915 let mut rnd = move || {
916 s ^= s << 13;
917 s ^= s >> 7;
918 s ^= s << 17;
919 s
920 };
921 let px: Vec<u8> = (0..w as usize * h as usize * 3)
922 .map(|_| (rnd() & 0xFF) as u8)
923 .collect();
924 roundtrip_mnf7(
925 &px,
926 Dims {
927 width: w,
928 height: h,
929 },
930 true,
931 );
932 }
933 }
934
935 #[test]
936 fn mnf7_modcolor_is_bijective_exhaustive() {
937 if cfg!(debug_assertions) {
942 for v in (0..=0xFFFFFFu32).step_by(17) {
943 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
944 assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
945 }
946 for r in [0u8, 1, 127, 128, 254, 255] {
947 for g in [0u8, 85, 170, 255] {
948 for b in [0u8, 128, 255] {
949 let px = [r, g, b];
950 assert_eq!(modcolor::inverse(modcolor::forward(px)), px);
951 }
952 }
953 }
954 } else {
955 let mut checked: u64 = 0;
956 for v in 0..=0xFFFFFFu32 {
957 let px = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
958 assert_eq!(modcolor::inverse(modcolor::forward(px)), px, "{px:?}");
959 checked += 1;
960 }
961 assert_eq!(checked, 16_777_216, "exhaustive sweep count");
962 }
963 }
964
965 #[test]
966 fn mnf7_color_domain_used_when_it_wins() {
967 let mut s: u64 = 42;
971 let mut rnd = move || {
972 s ^= s << 13;
973 s ^= s >> 7;
974 s ^= s << 17;
975 s
976 };
977 let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
978
979 let ta_t = modcolor_forward(&a);
980 let mut tb_t = ta_t.clone();
981 let ramp = [0u8, 1, 2, 255]; for i in (1..tb_t.len()).step_by(3) {
983 tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
984 }
985 let b = modcolor_inverse(&tb_t);
986
987 let mut two = a.clone();
988 two.extend_from_slice(&b);
989 let d2 = Dims {
990 width: 16,
991 height: 32,
992 };
993 let (buf, st) = encode_mnf7(&two, d2, true);
994 let (_, out) = decode_mnf7(&buf).expect("decode");
995 assert_eq!(out, two);
996 let color_modes = st.ref_exact_color + st.ref_geom_color;
997 assert!(
998 color_modes > 0,
999 "expected color-domain references, got {}",
1000 color_modes
1001 );
1002 }
1003
1004 #[test]
1005 fn mnf6_rate_aware_picks_flags() {
1006 let (w, h) = (64u32, 64u32);
1008 let mut px = vec![0u8; (w * h * 3) as usize];
1009 for y in 0..h {
1010 for x in 0..w {
1011 let i = ((y * w + x) * 3) as usize;
1012 px[i] = (x * 4) as u8;
1013 px[i + 1] = (y * 4) as u8;
1014 px[i + 2] = 128;
1015 }
1016 }
1017 let (buf, st) = encode_mnf7(
1018 &px,
1019 Dims {
1020 width: w,
1021 height: h,
1022 },
1023 false,
1024 );
1025 assert_eq!(
1026 st.chosen_flags & FLAG_PLANAR,
1027 FLAG_PLANAR,
1028 "gradient wants planar"
1029 );
1030 let (_, out) = decode_mnf7(&buf).expect("decode");
1031 assert_eq!(out, px);
1032 }
1033
1034 #[test]
1035 fn mnf6_rejects() {
1036 assert!(decode_mnf7(&[]).is_err());
1037 assert!(decode_mnf7(&[0u8; 27]).is_err());
1038 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");
1039 let mut f = vec![0u8; 28];
1041 f[0..4].copy_from_slice(b"MNF6");
1042 f[24] = 0b100; assert!(decode_mnf7(&f).is_err(), "reserved flag");
1044 }
1045
1046 #[test]
1047 fn mnf6_beats_mnf5_on_gradient() {
1048 use crate::codec::{build_header, compress_payload, encode_tiles};
1051 let (w, h) = (96u32, 96u32);
1052 let mut px = vec![0u8; (w * h * 3) as usize];
1053 for y in 0..h {
1054 for x in 0..w {
1055 let i = ((y * w + x) * 3) as usize;
1056 px[i] = (x * 2) as u8;
1057 px[i + 1] = (y * 2) as u8;
1058 px[i + 2] = ((x + y) / 2) as u8;
1059 }
1060 }
1061 let d = Dims {
1062 width: w,
1063 height: h,
1064 };
1065 let tiles = crate::codec::split_tiles(&px, d);
1066 let raw = encode_tiles(&tiles, true, true);
1067 let z = compress_payload(&raw);
1068 let mut m5 = build_header(d, tiles.len() as u32, z.len() as u32);
1069 m5.extend_from_slice(&z);
1070 let (m6, _) = encode_mnf7(&px, d, true);
1071 assert!(decode_mnf7(&m6).is_ok());
1072 assert!(
1073 m6.len() <= m5.len(),
1074 "MNF6/7 ({}) should not lose to MNF5 ({}) on gradient",
1075 m6.len(),
1076 m5.len()
1077 );
1078 }
1079 #[test]
1080 fn d8_atlas_full_roundtrip() {
1081 let (w, h) = (64u32, 64u32);
1087 let mut px = vec![0u8; (w * h * 3) as usize];
1088 for y in 0..h {
1092 for x in 0..w {
1093 let i = ((y * w + x) * 3) as usize;
1094 let band = ((x / 4) as u16 * 13) as u8;
1095 px[i] = band;
1096 px[i + 1] = ((y / 4) as u16 * 11) as u8;
1097 px[i + 2] = band ^ (((y / 4) as u16 * 29) as u8);
1098 }
1099 }
1100 let mut stacked = px.clone();
1102 for t in [1u8, 2, 3, 4, 5, 6, 7] {
1103 let transformed = crate::codec::apply_transform(t, w, h, &px);
1104 stacked.extend_from_slice(&transformed);
1105 }
1106 let d = Dims {
1107 width: w,
1108 height: h * 8,
1109 };
1110 let (buf, st) = encode_mnf7(&stacked, d, true);
1111 assert!(
1114 st.ref_geom > 0,
1115 "no geometric references emitted: literal={} exact={} geom={} nontrivial={}",
1116 st.literal,
1117 st.ref_exact,
1118 st.ref_geom,
1119 st.geom_nontrivial
1120 );
1121 assert!(
1122 st.geom_nontrivial > 0,
1123 "only identity transforms used — test cannot verify D8 matching"
1124 );
1125 let (dims, out) = decode_mnf7(&buf).expect("full-file decode");
1126 assert_eq!((dims.width, dims.height), (w, h * 8));
1127 assert_eq!(out, stacked, "MNF7 D8 atlas roundtrip");
1128 }
1130}