1use crate::error::{Error, Result};
6
7use flate2::read::ZlibDecoder;
8use flate2::write::ZlibEncoder;
9use flate2::Compression;
10use std::io::{Read, Write};
11
12pub const HEADER_SIZE: usize = 24;
14pub const TILE_SIZE: u32 = 16;
16pub const ATLAS_CAPACITY: usize = 128;
18pub const RANK_CANDIDATES: usize = 8;
20pub const NUM_TRANSFORMS: u8 = 8;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Dims {
26 pub width: u32,
28 pub height: u32,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Mode {
35 Literal,
37 Reference(u32),
39 GeomReference(u32, u8), }
42
43impl Mode {
44 pub fn to_byte(self) -> u8 {
46 match self {
47 Mode::Literal => 0,
48 Mode::Reference(_) => 1,
49 Mode::GeomReference(_, _) => 2,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct Tile {
57 pub data: Vec<u8>, pub width: u32,
61 pub height: u32,
63}
64
65impl Tile {
66 pub fn nbytes(&self) -> usize {
68 self.width as usize * self.height as usize * 3
69 }
70}
71
72#[inline]
76#[allow(clippy::manual_is_multiple_of)]
77pub fn div_ceil(a: u32, b: u32) -> u32 {
78 a / b + u32::from(a % b != 0)
79}
80
81pub fn tile_grid(dims: Dims) -> (u32, u32) {
83 (
84 div_ceil(dims.width, TILE_SIZE),
85 div_ceil(dims.height, TILE_SIZE),
86 )
87}
88
89pub fn tile_dims(dims: Dims, t: u32) -> (u32, u32) {
91 let (cols, rows) = tile_grid(dims);
92 let tx = t % cols;
93 let ty = t / cols;
94 debug_assert!(tx < cols && ty < rows, "tile index out of grid");
95 let tw = TILE_SIZE.min(dims.width - tx * TILE_SIZE);
96 let th = TILE_SIZE.min(dims.height - ty * TILE_SIZE);
97 (tw, th)
98}
99
100#[derive(Default)]
105pub struct Atlas {
106 pub(crate) patches: Vec<(u32, u32, Vec<u8>)>, sigs: Vec<Sig>, }
109
110impl Atlas {
111 pub fn new() -> Self {
113 Atlas::default()
114 }
115
116 pub fn insert(&mut self, tile: &Tile) {
118 let sig = tile_sig(tile.width, tile.height, &tile.data);
119 self.insert_with_sig(tile, sig);
120 }
121
122 pub fn insert_with_sig(&mut self, tile: &Tile, sig: Sig) {
125 self.sigs.push(sig);
126 self.patches
127 .push((tile.width, tile.height, tile.data.clone()));
128 }
129
130 pub fn rank_by_sig(&self, sig: &Sig, w: u32, h: u32, top: usize) -> Vec<u32> {
136 let want = (w, h);
137 let window_start = self.patches.len().saturating_sub(ATLAS_CAPACITY);
138 let mut cands: Vec<(u64, u32)> = self
139 .sigs
140 .iter()
141 .enumerate()
142 .skip(window_start)
143 .filter(|(i, _)| {
144 let (pw, ph, _) = &self.patches[*i];
145 (*pw, *ph) == want
146 })
147 .map(|(i, s)| (sig_dist(s, sig), u32::try_from(i).unwrap_or(u32::MAX)))
148 .collect();
149 cands.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
150 cands.truncate(top);
151 cands.into_iter().map(|(_, i)| i).collect()
152 }
153}
154
155#[inline]
156fn rgb_mean(rgb: &[u8]) -> (i64, i64, i64) {
157 let n = (rgb.len() / 3) as i64;
158 let mut s = (0i64, 0i64, 0i64);
159 for [r, g, b] in rgb.as_chunks::<3>().0 {
160 s.0 += i64::from(*r);
161 s.1 += i64::from(*g);
162 s.2 += i64::from(*b);
163 }
164 (s.0 / n, s.1 / n, s.2 / n)
165}
166
167#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
172pub struct Sig {
173 mean: (i64, i64, i64),
174 gdx: i64,
175 gdy: i64,
176 luma: i64,
177 var: i64,
178}
179
180fn luma(px: &[u8]) -> i64 {
181 (299 * px[0] as i64 + 587 * px[1] as i64 + 114 * px[2] as i64) / 1000
182}
183
184pub fn tile_sig(w: u32, h: u32, data: &[u8]) -> Sig {
186 let n = (w * h) as i64;
187 let l = data
188 .as_chunks::<3>()
189 .0
190 .iter()
191 .map(|px| luma(px.as_slice()))
192 .collect::<Vec<i64>>();
193 let (wu, hu) = (w as usize, h as usize);
194 let mut gdx = 0i64;
195 let mut gdy = 0i64;
196 for y in 0..hu {
197 for x in 0..wu {
198 if x + 1 < wu {
199 gdx += (l[y * wu + x] - l[y * wu + x + 1]).abs();
200 }
201 if y + 1 < hu {
202 gdy += (l[y * wu + x] - l[(y + 1) * wu + x]).abs();
203 }
204 }
205 }
206 let mean_l = l.iter().sum::<i64>() / n;
207 let var = l.iter().map(|v| (v - mean_l) * (v - mean_l)).sum::<i64>() / n;
208 Sig {
209 mean: rgb_mean(data),
210 gdx: gdx / n,
211 gdy: gdy / n,
212 luma: mean_l,
213 var,
214 }
215}
216
217fn sig_dist(a: &Sig, b: &Sig) -> u64 {
218 let rgb =
219 (a.mean.0 - b.mean.0).pow(2) + (a.mean.1 - b.mean.1).pow(2) + (a.mean.2 - b.mean.2).pow(2);
220 let grad = (((a.gdx - b.gdx).abs() + (a.gdy - b.gdy).abs()) as u64)
221 .min(((a.gdy - b.gdx).abs() + (a.gdx - b.gdy).abs()) as u64);
222 let lum = (a.luma - b.luma).pow(2) as u64 + (a.var - b.var).pow(2) as u64;
223 rgb as u64 + 4 * grad + lum
224}
225
226pub fn transform_dims(t: u8, w: u32, h: u32) -> (u32, u32) {
232 match t {
233 4..=7 => (h, w),
234 _ => (w, h),
235 }
236}
237
238pub fn transform_is_valid_for(t: u8, w: u32, h: u32) -> bool {
242 if t >= NUM_TRANSFORMS {
243 return false;
244 }
245 w == h || t <= 3
246}
247
248fn transform_source(t: u8, w: u32, h: u32, x: u32, y: u32) -> (u32, u32) {
249 match t {
251 0 => (x, y),
252 1 => (w - 1 - x, y),
253 2 => (x, h - 1 - y),
254 3 => (w - 1 - x, h - 1 - y),
255 4 => (y, h - 1 - x),
256 _ => unreachable!("composite transforms are composed in apply_transform"),
257 }
258}
259
260pub fn apply_transform(t: u8, w: u32, h: u32, data: &[u8]) -> Vec<u8> {
262 assert!(t < NUM_TRANSFORMS, "invalid transform {t}");
263 if t >= 5 {
264 let (w2, h2) = transform_dims(4, w, h);
265 let base = apply_transform(4, w, h, data);
266 return match t {
267 5 => apply_transform(1, w2, h2, &base),
268 6 => apply_transform(3, w2, h2, &base),
269 _ => apply_transform(1, w2, h2, &apply_transform(3, w2, h2, &base)),
270 };
271 }
272 let (ow, oh) = transform_dims(t, w, h);
273 let mut out = vec![0u8; (ow as usize) * (oh as usize) * 3];
274 for y in 0..oh {
275 for x in 0..ow {
276 let (sx, sy) = transform_source(t, w, h, x, y);
277 let so = ((sy * w + sx) as usize) * 3;
278 let d = ((y * ow + x) as usize) * 3;
279 out[d..d + 3].copy_from_slice(&data[so..so + 3]);
280 }
281 }
282 out
283}
284
285pub fn tile_proxy_cost(bytes: &[u8]) -> u64 {
289 let mut e = ZlibEncoder::new(Vec::new(), Compression::fast());
290 e.write_all(bytes).ok();
291 e.finish().map(|v| v.len() as u64).unwrap_or(u64::MAX)
292}
293
294pub fn encode_tiles(tiles: &[Tile], use_atlas: bool, use_geom: bool) -> Vec<u8> {
300 encode_stats(tiles, use_atlas, use_geom).0
301}
302
303pub fn encode_stats(tiles: &[Tile], use_atlas: bool, use_geom: bool) -> (Vec<u8>, GeomStats) {
305 let mut out: Vec<u8> =
306 Vec::with_capacity(tiles.len() * (TILE_SIZE as usize * TILE_SIZE as usize * 3 + 6));
307 let mut atlas = Atlas::new();
308 let mut stats = GeomStats::default();
309
310 for tile in tiles {
311 let n = tile.nbytes();
312 let sig = tile_sig(tile.width, tile.height, &tile.data);
313 let mut best_mode = Mode::Literal;
314 let mut best_bytes = tile.data.clone();
315 let lit_cost = 1 + tile_proxy_cost(&tile.data);
318 let mut best_cost = lit_cost;
319
320 if use_atlas {
321 let cands = atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES);
323 let transforms: &[u8] = if !use_geom {
326 &[0]
327 } else if tile.width == tile.height {
328 &[0, 1, 2, 3, 4, 5, 6, 7]
329 } else {
330 &[0, 1, 2, 3]
331 };
332 for prior in cands {
333 let shape = (tile.width, tile.height);
334 for &t in transforms {
335 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
336 let transformed = if t == 0 {
337 ref_rgb.clone()
338 } else {
339 apply_transform(t, shape.0, shape.1, ref_rgb)
340 };
341 let mut residual = vec![0u8; n];
342 for i in 0..n {
343 residual[i] = tile.data[i].wrapping_sub(transformed[i]);
344 }
345 let meta_cost = 5 + u64::from(t != 0);
347 let cost = meta_cost + tile_proxy_cost(&residual);
348 if cost < best_cost {
349 best_cost = cost;
350 best_mode = Mode::GeomReference(prior, t);
351 best_bytes = residual;
352 }
353 }
354 }
355 }
358
359 let emit = match best_mode {
361 Mode::GeomReference(p, 0) => Mode::Reference(p),
362 other => other,
363 };
364 match emit {
365 Mode::Literal => stats.literal += 1,
366 Mode::Reference(_) => stats.ref_exact += 1,
367 Mode::GeomReference(_, t) => {
368 stats.ref_geom += 1;
369 if t != 0 {
370 stats.geom_nontrivial += 1;
371 }
372 }
373 }
374 stats.modes.push(emit.to_byte());
375 stats.savings.push(lit_cost as i64 - best_cost as i64);
376 stats.refs.push(match emit {
377 Mode::Reference(p) => Some((p, 0)),
378 Mode::GeomReference(p, t) => Some((p, t)),
379 Mode::Literal => None,
380 });
381 out.push(emit.to_byte());
382 match emit {
383 Mode::Reference(prior) => out.extend_from_slice(&prior.to_le_bytes()),
384 Mode::GeomReference(prior, t) => {
385 out.extend_from_slice(&prior.to_le_bytes());
386 out.push(t);
387 }
388 Mode::Literal => {}
389 }
390 out.extend_from_slice(&best_bytes);
391 atlas.insert_with_sig(tile, sig);
392 }
393 (out, stats)
394}
395
396pub fn compress_payload(raw: &[u8]) -> Vec<u8> {
398 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
399 e.write_all(raw).expect("zlib encode");
400 e.finish().expect("zlib finish")
401}
402
403#[derive(Debug, Clone, Default)]
406pub struct GeomStats {
407 pub literal: u32,
409 pub ref_exact: u32,
411 pub ref_geom: u32,
413 pub geom_nontrivial: u32, pub modes: Vec<u8>, pub savings: Vec<i64>, pub refs: Vec<Option<(u32, u8)>>, }
422
423pub fn build_header(dims: Dims, tile_count: u32, payload_len: u32) -> Vec<u8> {
425 let mut h = Vec::with_capacity(HEADER_SIZE);
426 h.extend_from_slice(b"MNF5");
427 h.extend_from_slice(&dims.width.to_le_bytes());
428 h.extend_from_slice(&dims.height.to_le_bytes());
429 h.extend_from_slice(&TILE_SIZE.to_le_bytes());
430 h.extend_from_slice(&tile_count.to_le_bytes());
431 h.extend_from_slice(&payload_len.to_le_bytes());
432 debug_assert_eq!(h.len(), HEADER_SIZE);
433 h
434}
435
436#[derive(Debug)]
438pub struct DecodeResult {
439 pub dims: Dims,
441 pub pixels: Vec<u8>,
443}
444
445#[derive(Debug, Clone, Copy)]
449pub struct DecodeLimits {
450 pub max_pixels: u64,
452 pub max_tiles: u32,
454 pub max_decompressed: u64,
456 pub max_total_memory: u64, }
459
460impl Default for DecodeLimits {
461 fn default() -> Self {
462 DecodeLimits {
463 max_pixels: 1 << 26,
464 max_tiles: u32::MAX,
465 max_decompressed: u64::MAX, max_total_memory: u64::MAX,
467 }
468 }
469}
470
471pub fn decode(buf: &[u8]) -> Result<DecodeResult> {
473 decode_with_limits(buf, &DecodeLimits::default())
474}
475
476pub fn encode_mnf5(pixels: &[u8], dims: Dims, use_atlas: bool, use_geom: bool) -> Result<Vec<u8>> {
482 let tiles = split_tiles(pixels, dims);
483 let (raw, _stats) = encode_stats(&tiles, use_atlas, use_geom);
484 let payload = compress_payload(&raw);
485 let mut buf = build_header(dims, tiles.len() as u32, payload.len() as u32);
486 buf.extend_from_slice(&payload);
487 Ok(buf)
488}
489
490pub fn decode_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<DecodeResult> {
492 if buf.len() < HEADER_SIZE {
493 return Err(Error::fmt_at("truncated header", buf.len() as u64));
494 }
495 if &buf[0..4] != b"MNF5" {
496 return Err(Error::fmt_at("bad magic (want MNF5)", 0));
497 }
498 let (w, h) = read_dims(&buf[4..12])?;
499 if w as u64 * h as u64 > lim.max_pixels {
500 return Err(Error::Limits {
501 which: "max_pixels",
502 limit: lim.max_pixels,
503 actual: w as u64 * h as u64,
504 });
505 }
506 let ts = u32::from_le_bytes(buf[12..16].try_into().unwrap());
507 let tile_count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
508 if ts != TILE_SIZE {
509 return Err(Error::fmt_msg(format!("unsupported tile size {ts}")));
510 }
511 if tile_count as u64 > lim.max_tiles as u64 {
512 return Err(Error::Limits {
513 which: "max_tiles",
514 limit: u64::from(lim.max_tiles),
515 actual: tile_count as u64,
516 });
517 }
518 let payload_len = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize;
519 let (cols, rows) = tile_grid(Dims {
520 width: w,
521 height: h,
522 });
523 let expected = cols * rows;
524 if tile_count != expected {
525 return Err(Error::fmt_msg(format!(
526 "tile_count {tile_count} != expected {expected} for {w}x{h}"
527 )));
528 }
529 let _rows = rows;
530 if payload_len > MAX_PAYLOAD {
531 return Err(Error::fmt_msg("payload too large"));
532 }
533 if (buf.len() - HEADER_SIZE) < payload_len {
534 return Err(Error::fmt_msg("payload shorter than header-declared len"));
535 }
536 if buf.len() != HEADER_SIZE + payload_len {
539 return Err(Error::fmt_msg(format!(
540 "file is {} bytes but header declares {} + {payload_len} payload",
541 buf.len(),
542 HEADER_SIZE
543 )));
544 }
545 let payload = &buf[HEADER_SIZE..HEADER_SIZE + payload_len];
546
547 let px_bytes_total = (w as u64) * (h as u64) * 3;
552 let decomp_limit = px_bytes_total
553 .checked_add(6 * tile_count as u64)
554 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?;
555 let mut dz = ZlibDecoder::new(payload).take(decomp_limit + 1);
556 let mut raw = Vec::new();
557 dz.read_to_end(&mut raw)
558 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
559 if raw.len() as u64 > decomp_limit {
560 return Err(Error::Limits {
561 which: "max_decompressed",
562 limit: decomp_limit,
563 actual: raw.len() as u64,
564 });
565 }
566
567 if raw.len() < 4 * tile_count as usize {
571 return Err(Error::fmt_msg(
572 "decompressed stream shorter than per-tile minimum",
573 ));
574 }
575
576 let total_px = w as usize * h as usize * 3;
577 let mem_needed = (total_px as u64) * 2;
580 if mem_needed > lim.max_total_memory {
581 return Err(Error::Limits {
582 which: "max_total_memory",
583 limit: lim.max_total_memory,
584 actual: mem_needed,
585 });
586 }
587 let mut out = vec![0u8; total_px];
588 let mut patches: Vec<(u32, u32, Vec<u8>)> = Vec::new();
590 let mut cursor = 0usize;
591
592 for t in 0..tile_count {
593 let (tw, th) = tile_dims(
594 Dims {
595 width: w,
596 height: h,
597 },
598 t,
599 );
600 let n = (tw * th * 3) as usize;
601 if cursor + 1 > raw.len() {
602 return Err(Error::fmt_at(
603 format!("tile {t}: truncated mode byte"),
604 u64::from(t),
605 ));
606 }
607 let mode = raw[cursor];
608 cursor += 1;
609 let data: Vec<u8> = match mode {
610 0 => {
611 if cursor + n > raw.len() {
612 return Err(Error::fmt_at(
613 format!("tile {t}: truncated literal payload"),
614 u64::from(t),
615 ));
616 }
617 let d = raw[cursor..cursor + n].to_vec();
618 cursor += n;
619 d
620 }
621 1 => {
622 if cursor + 4 > raw.len() {
623 return Err(Error::fmt_at(
624 format!("tile {t}: truncated reference index"),
625 u64::from(t),
626 ));
627 }
628 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
629 cursor += 4;
630 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
631 Error::fmt_msg(format!(
632 "tile {t}: reference to tile {idx} >= {len} decoded",
633 len = patches.len(),
634 t = t,
635 idx = idx
636 ))
637 })?;
638 if *rw != tw || *rh != th {
639 return Err(Error::fmt_at(
640 format!(
641 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
642 t = t,
643 idx = idx,
644 rw = rw,
645 rh = rh,
646 tw = tw,
647 th = th
648 ),
649 u64::from(t),
650 ));
651 }
652 if cursor + n > raw.len() {
653 return Err(Error::fmt_at(
654 format!("tile {t}: truncated residual"),
655 u64::from(t),
656 ));
657 }
658 let residual = &raw[cursor..cursor + n];
659 let mut d = vec![0u8; n];
660 for i in 0..n {
661 d[i] = ref_rgb[i].wrapping_add(residual[i]);
662 }
663 cursor += n;
664 d
665 }
666 2 => {
667 if cursor + 5 > raw.len() {
668 return Err(Error::fmt_at(
669 format!("tile {t}: truncated geometric reference"),
670 u64::from(t),
671 ));
672 }
673 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
674 let tr = raw[cursor + 4];
675 cursor += 5;
676 if !transform_is_valid_for(tr, tw, th) {
677 return Err(Error::fmt_at(
678 format!(
679 "tile {t}: invalid transform {tr} for {tw}x{th} tile",
680 t = t,
681 tr = tr,
682 tw = tw,
683 th = th
684 ),
685 u64::from(t),
686 ));
687 }
688 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
689 Error::fmt_msg(format!(
690 "tile {t}: reference to tile {idx} >= {len} decoded",
691 len = patches.len(),
692 t = t,
693 idx = idx
694 ))
695 })?;
696 if *rw != tw || *rh != th {
697 return Err(Error::fmt_at(
698 format!(
699 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
700 t = t,
701 idx = idx,
702 rw = rw,
703 rh = rh,
704 tw = tw,
705 th = th
706 ),
707 u64::from(t),
708 ));
709 }
710 if cursor + n > raw.len() {
711 return Err(Error::fmt_at(
712 format!("tile {t}: truncated residual"),
713 u64::from(t),
714 ));
715 }
716 let residual = &raw[cursor..cursor + n];
717 let transformed = if tr == 0 {
718 ref_rgb.clone()
719 } else {
720 apply_transform(tr, tw, th, ref_rgb)
721 };
722 let mut d = vec![0u8; n];
723 for i in 0..n {
724 d[i] = transformed[i].wrapping_add(residual[i]);
725 }
726 cursor += n;
727 d
728 }
729 m => {
730 return Err(Error::fmt_at(
731 format!("tile {t}: invalid mode byte {m}"),
732 u64::from(t),
733 ))
734 }
735 };
736 blit_tile(
737 &mut out,
738 Dims {
739 width: w,
740 height: h,
741 },
742 t,
743 &data,
744 tw,
745 th,
746 );
747 patches.push((tw, th, data));
748 }
749 if cursor != raw.len() {
750 return Err(Error::fmt_msg(format!(
751 "trailing bytes after last tile: {cursor} != {}",
752 raw.len()
753 )));
754 }
755 Ok(DecodeResult {
756 dims: Dims {
757 width: w,
758 height: h,
759 },
760 pixels: out,
761 })
762}
763
764const MAX_PAYLOAD: usize = 1 << 31; fn read_dims(b: &[u8]) -> Result<(u32, u32)> {
767 let w = u32::from_le_bytes(b[0..4].try_into().unwrap());
768 let h = u32::from_le_bytes(b[4..8].try_into().unwrap());
769 if w == 0 || h == 0 {
770 return Err(Error::fmt_msg("zero dimensions"));
771 }
772 if (w as u64) * (h as u64) > (1 << 26) {
775 return Err(Error::fmt_msg("image too large"));
776 }
777 Ok((w, h))
778}
779
780pub fn split_tiles(pixels: &[u8], d: Dims) -> Vec<Tile> {
783 let (cols, rows) = tile_grid(d);
784 (0..cols * rows)
785 .map(|t| {
786 let (tw, th) = tile_dims(d, t);
787 let mut data = vec![0u8; (tw * th * 3) as usize];
788 let tx = (t % cols) * TILE_SIZE;
789 let ty = (t / cols) * TILE_SIZE;
790 for row in 0..th as usize {
791 for col in 0..tw as usize {
792 let py = ty as usize + row;
793 let px = tx as usize + col;
794 let dst = (row * tw as usize + col) * 3;
795 let src = (py * d.width as usize + px) * 3;
796 data[dst..dst + 3].copy_from_slice(&pixels[src..src + 3]);
797 }
798 }
799 Tile {
800 data,
801 width: tw,
802 height: th,
803 }
804 })
805 .collect()
806}
807
808fn blit_tile(out: &mut [u8], dims: Dims, t: u32, data: &[u8], tw: u32, th: u32) {
809 let (cols, _) = tile_grid(dims);
810 let tx = (t % cols) * TILE_SIZE;
811 let ty = (t / cols) * TILE_SIZE;
812 for row in 0..th as usize {
813 let py = ty as usize + row;
814 for col in 0..tw as usize {
815 let px = tx as usize + col;
816 let dst = (py * dims.width as usize + px) * 3;
817 let src = (row * tw as usize + col) * 3;
818 out[dst..dst + 3].copy_from_slice(&data[src..src + 3]);
819 }
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826
827 fn dims(w: u32, h: u32) -> Dims {
828 Dims {
829 width: w,
830 height: h,
831 }
832 }
833
834 fn roundtrip(d: Dims, pixels: &[u8], use_atlas: bool) -> Vec<u8> {
835 let tiles = split_tiles(pixels, d);
836 let raw = encode_tiles(&tiles, use_atlas, true);
837 let payload = compress_payload(&raw);
838 let header = build_header(d, tiles.len() as u32, payload.len() as u32);
839 let mut buf = header;
840 buf.extend_from_slice(&payload);
841 let res = decode(&buf).expect("decode ok");
842 assert_eq!(res.dims, d);
843 res.pixels
844 }
845
846 fn solid(w: u32, h: u32, c: [u8; 3]) -> Vec<u8> {
847 (0..w * h).flat_map(|_| c).collect()
848 }
849
850 fn gradient(w: u32, h: u32) -> Vec<u8> {
851 (0..w * h)
852 .flat_map(|i| {
853 [
854 (i % 256) as u8,
855 ((i / w) % 256) as u8,
856 ((i / 7) % 256) as u8,
857 ]
858 })
859 .collect()
860 }
861
862 #[test]
863 fn encode_mnf5_one_call_roundtrip() {
864 let (w, h) = (48u32, 36u32); let mut px = vec![0u8; (w * h * 3) as usize];
866 for (i, b) in px.iter_mut().enumerate() {
867 *b = (i % 251) as u8;
868 }
869 let d = Dims {
870 width: w,
871 height: h,
872 };
873 let buf = encode_mnf5(&px, d, true, true).expect("encode");
874 let res = decode(&buf).expect("decode");
875 assert_eq!(res.pixels, px, "one-call MNF5 exact");
876 assert_eq!((res.dims.width, res.dims.height), (w, h));
877 }
878
879 #[test]
880 fn grid_math() {
881 assert_eq!(tile_grid(dims(16, 16)), (1, 1));
882 assert_eq!(tile_grid(dims(17, 16)), (2, 1));
883 assert_eq!(tile_grid(dims(31, 33)), (2, 3));
884 assert_eq!(tile_dims(dims(31, 33), 1), (15, 16));
885 assert_eq!(tile_dims(dims(31, 33), 2), (16, 16));
886 assert_eq!(tile_dims(dims(31, 33), 5), (15, 1));
887 assert_eq!(div_ceil(0, 16), 0); }
889
890 #[test]
891 fn roundtrip_solid_16x16_atlas() {
892 let px = solid(16, 16, [10, 20, 30]);
893 assert_eq!(roundtrip(dims(16, 16), &px, true), px);
894 }
895
896 #[test]
897 fn roundtrip_solid_16x16_no_atlas() {
898 let px = solid(16, 16, [200, 100, 50]);
899 assert_eq!(roundtrip(dims(16, 16), &px, false), px);
900 }
901
902 #[test]
903 fn roundtrip_odd_dims() {
904 for (w, h) in [
905 (31u32, 33u32),
906 (17u32, 1u32),
907 (1u32, 17u32),
908 (1u32, 1u32),
909 (48u32, 47u32),
910 ] {
911 let px = gradient(w, h);
912 let dec = roundtrip(dims(w, h), &px, true);
913 assert_eq!(dec.len(), (w * h * 3) as usize, "len {w}x{h}");
914 assert_eq!(dec, px, "pixels {w}x{h}");
915 }
916 }
917
918 #[test]
919 fn roundtrip_random_pixels() {
920 let mut s: u32 = 12345;
922 let mut rnd = || {
923 s = s.wrapping_mul(1103515245).wrapping_add(12345);
924 (s >> 16) as u8
925 };
926 let w = 64;
927 let h = 48;
928 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
929 assert_eq!(roundtrip(dims(w, h), &px, true), px);
930 }
931
932 #[test]
933 fn roundtrip_uniform_atlas_matches_noatlas() {
934 let px = solid(64, 64, [7, 7, 7]);
935 assert_eq!(roundtrip(dims(64, 64), &px, true), px);
936 assert_eq!(roundtrip(dims(64, 64), &px, false), px);
937 }
938
939 #[test]
940 fn decode_rejects_bad_inputs() {
941 assert!(decode(&[]).is_err(), "empty");
942 assert!(decode(&[0u8; 23]).is_err(), "short header");
943 let mut bad_magic = build_header(dims(16, 16), 1, 0);
944 bad_magic[0] = b'X';
945 assert!(decode(&bad_magic).is_err(), "bad magic");
946 let bad_tile_size = {
947 let mut h = build_header(dims(16, 16), 1, 0);
948 h[12] = 15; h
950 };
951 assert!(decode(&bad_tile_size).is_err(), "tile size");
952 let bad_count = build_header(dims(16, 16), 5, 0);
953 assert!(decode(&bad_count).is_err(), "tile count");
954 assert!(
955 decode(&build_header(dims(0, 16), 1, 0)).is_err(),
956 "zero dim"
957 );
958 assert!(
959 read_dims(&build_header(dims(0, 16), 1, 0)[4..12]).is_err(),
960 "read_dims rejects zero width"
961 );
962 let d = dims(32, 16);
964 let tiles = split_tiles(&solid(32, 16, [1, 2, 3]), d);
965 let mut raw = encode_tiles(&tiles, true, false);
966 raw.truncate(raw.len() - 5); let truncated = {
968 let payload = compress_payload(&raw);
969 let mut b = build_header(d, 2, payload.len() as u32);
970 b.extend_from_slice(&payload);
971 b
972 };
973 assert!(decode(&truncated).is_err(), "truncated tile data");
974 let mut raw_ref = vec![1u8];
976 raw_ref.extend_from_slice(&3u32.to_le_bytes());
977 raw_ref.extend_from_slice(&[0u8; 16 * 16 * 3]);
978 let dang = {
979 let payload = compress_payload(&raw_ref);
980 let mut b = build_header(dims(16, 16), 1, payload.len() as u32);
981 b.extend_from_slice(&payload);
982 b
983 };
984 assert!(decode(&dang).is_err(), "dangling reference");
985 }
986
987 #[test]
988 fn decoder_rejects_oversized_dims() {
989 let h = build_header(
990 Dims {
991 width: 1 << 24,
992 height: 1 << 24,
993 },
994 0,
995 0,
996 );
997 assert!(decode(&h).is_err(), "oversized dims");
998 }
999
1000 #[test]
1001 fn atlas_reference_shape_mismatch_rejected() {
1002 let d = Dims {
1004 width: 16,
1005 height: 24,
1006 };
1007 let mut raw = Vec::new();
1008 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
1010 raw.push(1u8); raw.extend_from_slice(&0u32.to_le_bytes());
1012 raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
1013 let payload = compress_payload(&raw);
1014 let mut b = build_header(d, 2, payload.len() as u32);
1015 b.extend_from_slice(&payload);
1016 let err = decode(&b).unwrap_err();
1017 assert!(
1018 err.to_string().contains("shaped"),
1019 "want shape mismatch, got: {err}"
1020 );
1021 }
1022
1023 #[test]
1024 fn encoder_picks_reference_when_residual_compresses() {
1025 let mut s: u32 = 999;
1030 let mut rnd = || {
1031 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1032 (s >> 16) as u8
1033 };
1034 let noisy: Vec<u8> = (0..32 * 16 * 3).map(|_| rnd()).collect();
1035 let tiles = vec![
1036 Tile {
1037 data: noisy.clone(),
1038 width: 32,
1039 height: 16,
1040 },
1041 Tile {
1042 data: noisy.clone(),
1043 width: 32,
1044 height: 16,
1045 },
1046 ];
1047 let raw_tiles = encode_tiles(&tiles, true, false);
1048 let t1 = 1 + 32 * 16 * 3; assert_eq!(raw_tiles[0], 0, "tile 0 literal");
1050 assert_eq!(
1051 raw_tiles[t1], 1,
1052 "tile 1 should be an exact reference (mode 1)"
1053 );
1054 let idx = u32::from_le_bytes(raw_tiles[t1 + 1..t1 + 5].try_into().unwrap());
1055 assert_eq!(idx, 0, "reference points at tile 0");
1056 assert!(raw_tiles[t1 + 5..].iter().all(|&b| b == 0));
1058 }
1059
1060 #[test]
1061 fn d8_transforms_roundtrip_lossless() {
1062 for (w, h) in [
1067 (16u32, 16u32),
1068 (30u32, 30u32),
1069 (16u32, 24u32),
1070 (31u32, 33u32),
1071 ] {
1072 let mut s: u32 = 777;
1073 let mut rnd = || {
1074 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1075 (s >> 16) as u8
1076 };
1077 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
1078 for t0 in 0..NUM_TRANSFORMS {
1079 if !transform_is_valid_for(t0, w, h) {
1080 continue;
1081 }
1082 let base = apply_transform(t0, w, h, &px);
1083 let base_tile = Tile {
1084 data: base.clone(),
1085 width: w,
1086 height: h,
1087 };
1088 let target_tile = Tile {
1089 data: px.clone(),
1090 width: w,
1091 height: h,
1092 };
1093 let raw = encode_tiles(&[base_tile, target_tile], true, true);
1094 let t1 = 1 + (w * h * 3) as usize;
1095 let mode = raw[t1];
1096 let emitted_t = match mode {
1097 0 => panic!("tile 1 must reference: exact inverse exists (t0={t0}, {w}x{h})"),
1098 1 => 0u8,
1099 2 => raw[t1 + 5],
1100 m => panic!("invalid mode {m}"),
1101 };
1102 assert!(
1103 transform_is_valid_for(emitted_t, w, h),
1104 "emitted transform {emitted_t} invalid for {w}x{h} (t0={t0})"
1105 );
1106 assert_eq!(
1108 apply_transform(emitted_t, w, h, &base),
1109 px,
1110 "T_{emitted_t}(T_{t0}(px)) != px for {w}x{h}"
1111 );
1112 }
1113 }
1114 }
1115
1116 #[test]
1117 fn geom_reference_transform4_nonsquare_rejected() {
1118 let d = Dims {
1120 width: 16,
1121 height: 24,
1122 };
1123 let mut raw = Vec::new();
1124 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
1126 raw.push(2u8); raw.extend_from_slice(&0u32.to_le_bytes());
1128 raw.push(4u8); raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
1130 let payload = compress_payload(&raw);
1131 let mut b = build_header(d, 2, payload.len() as u32);
1132 b.extend_from_slice(&payload);
1133 let err = decode(&b).unwrap_err();
1134 assert!(
1135 err.to_string().contains("invalid transform"),
1136 "want transform rejection, got: {err}"
1137 );
1138 }
1139
1140 #[test]
1141 fn nonsquare_partial_tiles_use_shape_keeping_subset() {
1142 let d = Dims {
1144 width: 31,
1145 height: 33,
1146 }; let mut s: u32 = 4242;
1148 let mut rnd = || {
1149 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1150 (s >> 16) as u8
1151 };
1152 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1153 .map(|_| rnd())
1154 .collect();
1155 assert_eq!(roundtrip(d, &px, true), px);
1156 }
1157
1158 #[test]
1159 fn no_atlas_uses_literals() {
1160 let d = Dims {
1161 width: 32,
1162 height: 16,
1163 };
1164 let tiles = split_tiles(&solid(32, 16, [9, 9, 9]), d);
1165 let raw = encode_tiles(&tiles, false, false);
1166 assert_eq!(raw[0], 0);
1167 assert_eq!(
1168 raw[1 + 16 * 16 * 3],
1169 0,
1170 "tile 1 literal when atlas disabled"
1171 );
1172 }
1173
1174 #[test]
1175 fn proxy_cost_monotone() {
1176 assert!(tile_proxy_cost(&[0u8; 16]) <= tile_proxy_cost(&[5u8; 32]));
1177 }
1178}