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 decode_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<DecodeResult> {
478 if buf.len() < HEADER_SIZE {
479 return Err(Error::fmt_at("truncated header", buf.len() as u64));
480 }
481 if &buf[0..4] != b"MNF5" {
482 return Err(Error::fmt_at("bad magic (want MNF5)", 0));
483 }
484 let (w, h) = read_dims(&buf[4..12])?;
485 if w as u64 * h as u64 > lim.max_pixels {
486 return Err(Error::Limits {
487 which: "max_pixels",
488 limit: lim.max_pixels,
489 actual: w as u64 * h as u64,
490 });
491 }
492 let ts = u32::from_le_bytes(buf[12..16].try_into().unwrap());
493 let tile_count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
494 if ts != TILE_SIZE {
495 return Err(Error::fmt_msg(format!("unsupported tile size {ts}")));
496 }
497 if tile_count as u64 > lim.max_tiles as u64 {
498 return Err(Error::Limits {
499 which: "max_tiles",
500 limit: u64::from(lim.max_tiles),
501 actual: tile_count as u64,
502 });
503 }
504 let payload_len = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize;
505 let (cols, rows) = tile_grid(Dims {
506 width: w,
507 height: h,
508 });
509 let expected = cols * rows;
510 if tile_count != expected {
511 return Err(Error::fmt_msg(format!(
512 "tile_count {tile_count} != expected {expected} for {w}x{h}"
513 )));
514 }
515 let _rows = rows;
516 if payload_len > MAX_PAYLOAD {
517 return Err(Error::fmt_msg("payload too large"));
518 }
519 if (buf.len() - HEADER_SIZE) < payload_len {
520 return Err(Error::fmt_msg("payload shorter than header-declared len"));
521 }
522 if buf.len() != HEADER_SIZE + payload_len {
525 return Err(Error::fmt_msg(format!(
526 "file is {} bytes but header declares {} + {payload_len} payload",
527 buf.len(),
528 HEADER_SIZE
529 )));
530 }
531 let payload = &buf[HEADER_SIZE..HEADER_SIZE + payload_len];
532
533 let px_bytes_total = (w as u64) * (h as u64) * 3;
538 let decomp_limit = px_bytes_total
539 .checked_add(6 * tile_count as u64)
540 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?;
541 let mut dz = ZlibDecoder::new(payload).take(decomp_limit + 1);
542 let mut raw = Vec::new();
543 dz.read_to_end(&mut raw)
544 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
545 if raw.len() as u64 > decomp_limit {
546 return Err(Error::Limits {
547 which: "max_decompressed",
548 limit: decomp_limit,
549 actual: raw.len() as u64,
550 });
551 }
552
553 if raw.len() < 4 * tile_count as usize {
557 return Err(Error::fmt_msg(
558 "decompressed stream shorter than per-tile minimum",
559 ));
560 }
561
562 let total_px = w as usize * h as usize * 3;
563 let mem_needed = (total_px as u64) * 2;
566 if mem_needed > lim.max_total_memory {
567 return Err(Error::Limits {
568 which: "max_total_memory",
569 limit: lim.max_total_memory,
570 actual: mem_needed,
571 });
572 }
573 let mut out = vec![0u8; total_px];
574 let mut patches: Vec<(u32, u32, Vec<u8>)> = Vec::new();
576 let mut cursor = 0usize;
577
578 for t in 0..tile_count {
579 let (tw, th) = tile_dims(
580 Dims {
581 width: w,
582 height: h,
583 },
584 t,
585 );
586 let n = (tw * th * 3) as usize;
587 if cursor + 1 > raw.len() {
588 return Err(Error::fmt_at(
589 format!("tile {t}: truncated mode byte"),
590 u64::from(t),
591 ));
592 }
593 let mode = raw[cursor];
594 cursor += 1;
595 let data: Vec<u8> = match mode {
596 0 => {
597 if cursor + n > raw.len() {
598 return Err(Error::fmt_at(
599 format!("tile {t}: truncated literal payload"),
600 u64::from(t),
601 ));
602 }
603 let d = raw[cursor..cursor + n].to_vec();
604 cursor += n;
605 d
606 }
607 1 => {
608 if cursor + 4 > raw.len() {
609 return Err(Error::fmt_at(
610 format!("tile {t}: truncated reference index"),
611 u64::from(t),
612 ));
613 }
614 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
615 cursor += 4;
616 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
617 Error::fmt_msg(format!(
618 "tile {t}: reference to tile {idx} >= {len} decoded",
619 len = patches.len(),
620 t = t,
621 idx = idx
622 ))
623 })?;
624 if *rw != tw || *rh != th {
625 return Err(Error::fmt_at(
626 format!(
627 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
628 t = t,
629 idx = idx,
630 rw = rw,
631 rh = rh,
632 tw = tw,
633 th = th
634 ),
635 u64::from(t),
636 ));
637 }
638 if cursor + n > raw.len() {
639 return Err(Error::fmt_at(
640 format!("tile {t}: truncated residual"),
641 u64::from(t),
642 ));
643 }
644 let residual = &raw[cursor..cursor + n];
645 let mut d = vec![0u8; n];
646 for i in 0..n {
647 d[i] = ref_rgb[i].wrapping_add(residual[i]);
648 }
649 cursor += n;
650 d
651 }
652 2 => {
653 if cursor + 5 > raw.len() {
654 return Err(Error::fmt_at(
655 format!("tile {t}: truncated geometric reference"),
656 u64::from(t),
657 ));
658 }
659 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
660 let tr = raw[cursor + 4];
661 cursor += 5;
662 if !transform_is_valid_for(tr, tw, th) {
663 return Err(Error::fmt_at(
664 format!(
665 "tile {t}: invalid transform {tr} for {tw}x{th} tile",
666 t = t,
667 tr = tr,
668 tw = tw,
669 th = th
670 ),
671 u64::from(t),
672 ));
673 }
674 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
675 Error::fmt_msg(format!(
676 "tile {t}: reference to tile {idx} >= {len} decoded",
677 len = patches.len(),
678 t = t,
679 idx = idx
680 ))
681 })?;
682 if *rw != tw || *rh != th {
683 return Err(Error::fmt_at(
684 format!(
685 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
686 t = t,
687 idx = idx,
688 rw = rw,
689 rh = rh,
690 tw = tw,
691 th = th
692 ),
693 u64::from(t),
694 ));
695 }
696 if cursor + n > raw.len() {
697 return Err(Error::fmt_at(
698 format!("tile {t}: truncated residual"),
699 u64::from(t),
700 ));
701 }
702 let residual = &raw[cursor..cursor + n];
703 let transformed = if tr == 0 {
704 ref_rgb.clone()
705 } else {
706 apply_transform(tr, tw, th, ref_rgb)
707 };
708 let mut d = vec![0u8; n];
709 for i in 0..n {
710 d[i] = transformed[i].wrapping_add(residual[i]);
711 }
712 cursor += n;
713 d
714 }
715 m => {
716 return Err(Error::fmt_at(
717 format!("tile {t}: invalid mode byte {m}"),
718 u64::from(t),
719 ))
720 }
721 };
722 blit_tile(
723 &mut out,
724 Dims {
725 width: w,
726 height: h,
727 },
728 t,
729 &data,
730 tw,
731 th,
732 );
733 patches.push((tw, th, data));
734 }
735 if cursor != raw.len() {
736 return Err(Error::fmt_msg(format!(
737 "trailing bytes after last tile: {cursor} != {}",
738 raw.len()
739 )));
740 }
741 Ok(DecodeResult {
742 dims: Dims {
743 width: w,
744 height: h,
745 },
746 pixels: out,
747 })
748}
749
750const MAX_PAYLOAD: usize = 1 << 31; fn read_dims(b: &[u8]) -> Result<(u32, u32)> {
753 let w = u32::from_le_bytes(b[0..4].try_into().unwrap());
754 let h = u32::from_le_bytes(b[4..8].try_into().unwrap());
755 if w == 0 || h == 0 {
756 return Err(Error::fmt_msg("zero dimensions"));
757 }
758 if (w as u64) * (h as u64) > (1 << 26) {
761 return Err(Error::fmt_msg("image too large"));
762 }
763 Ok((w, h))
764}
765
766pub fn split_tiles(pixels: &[u8], d: Dims) -> Vec<Tile> {
769 let (cols, rows) = tile_grid(d);
770 (0..cols * rows)
771 .map(|t| {
772 let (tw, th) = tile_dims(d, t);
773 let mut data = vec![0u8; (tw * th * 3) as usize];
774 let tx = (t % cols) * TILE_SIZE;
775 let ty = (t / cols) * TILE_SIZE;
776 for row in 0..th as usize {
777 for col in 0..tw as usize {
778 let py = ty as usize + row;
779 let px = tx as usize + col;
780 let dst = (row * tw as usize + col) * 3;
781 let src = (py * d.width as usize + px) * 3;
782 data[dst..dst + 3].copy_from_slice(&pixels[src..src + 3]);
783 }
784 }
785 Tile {
786 data,
787 width: tw,
788 height: th,
789 }
790 })
791 .collect()
792}
793
794fn blit_tile(out: &mut [u8], dims: Dims, t: u32, data: &[u8], tw: u32, th: u32) {
795 let (cols, _) = tile_grid(dims);
796 let tx = (t % cols) * TILE_SIZE;
797 let ty = (t / cols) * TILE_SIZE;
798 for row in 0..th as usize {
799 let py = ty as usize + row;
800 for col in 0..tw as usize {
801 let px = tx as usize + col;
802 let dst = (py * dims.width as usize + px) * 3;
803 let src = (row * tw as usize + col) * 3;
804 out[dst..dst + 3].copy_from_slice(&data[src..src + 3]);
805 }
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 fn dims(w: u32, h: u32) -> Dims {
814 Dims {
815 width: w,
816 height: h,
817 }
818 }
819
820 fn roundtrip(d: Dims, pixels: &[u8], use_atlas: bool) -> Vec<u8> {
821 let tiles = split_tiles(pixels, d);
822 let raw = encode_tiles(&tiles, use_atlas, true);
823 let payload = compress_payload(&raw);
824 let header = build_header(d, tiles.len() as u32, payload.len() as u32);
825 let mut buf = header;
826 buf.extend_from_slice(&payload);
827 let res = decode(&buf).expect("decode ok");
828 assert_eq!(res.dims, d);
829 res.pixels
830 }
831
832 fn solid(w: u32, h: u32, c: [u8; 3]) -> Vec<u8> {
833 (0..w * h).flat_map(|_| c).collect()
834 }
835
836 fn gradient(w: u32, h: u32) -> Vec<u8> {
837 (0..w * h)
838 .flat_map(|i| {
839 [
840 (i % 256) as u8,
841 ((i / w) % 256) as u8,
842 ((i / 7) % 256) as u8,
843 ]
844 })
845 .collect()
846 }
847
848 #[test]
849 fn grid_math() {
850 assert_eq!(tile_grid(dims(16, 16)), (1, 1));
851 assert_eq!(tile_grid(dims(17, 16)), (2, 1));
852 assert_eq!(tile_grid(dims(31, 33)), (2, 3));
853 assert_eq!(tile_dims(dims(31, 33), 1), (15, 16));
854 assert_eq!(tile_dims(dims(31, 33), 2), (16, 16));
855 assert_eq!(tile_dims(dims(31, 33), 5), (15, 1));
856 assert_eq!(div_ceil(0, 16), 0); }
858
859 #[test]
860 fn roundtrip_solid_16x16_atlas() {
861 let px = solid(16, 16, [10, 20, 30]);
862 assert_eq!(roundtrip(dims(16, 16), &px, true), px);
863 }
864
865 #[test]
866 fn roundtrip_solid_16x16_no_atlas() {
867 let px = solid(16, 16, [200, 100, 50]);
868 assert_eq!(roundtrip(dims(16, 16), &px, false), px);
869 }
870
871 #[test]
872 fn roundtrip_odd_dims() {
873 for (w, h) in [
874 (31u32, 33u32),
875 (17u32, 1u32),
876 (1u32, 17u32),
877 (1u32, 1u32),
878 (48u32, 47u32),
879 ] {
880 let px = gradient(w, h);
881 let dec = roundtrip(dims(w, h), &px, true);
882 assert_eq!(dec.len(), (w * h * 3) as usize, "len {w}x{h}");
883 assert_eq!(dec, px, "pixels {w}x{h}");
884 }
885 }
886
887 #[test]
888 fn roundtrip_random_pixels() {
889 let mut s: u32 = 12345;
891 let mut rnd = || {
892 s = s.wrapping_mul(1103515245).wrapping_add(12345);
893 (s >> 16) as u8
894 };
895 let w = 64;
896 let h = 48;
897 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
898 assert_eq!(roundtrip(dims(w, h), &px, true), px);
899 }
900
901 #[test]
902 fn roundtrip_uniform_atlas_matches_noatlas() {
903 let px = solid(64, 64, [7, 7, 7]);
904 assert_eq!(roundtrip(dims(64, 64), &px, true), px);
905 assert_eq!(roundtrip(dims(64, 64), &px, false), px);
906 }
907
908 #[test]
909 fn decode_rejects_bad_inputs() {
910 assert!(decode(&[]).is_err(), "empty");
911 assert!(decode(&[0u8; 23]).is_err(), "short header");
912 let mut bad_magic = build_header(dims(16, 16), 1, 0);
913 bad_magic[0] = b'X';
914 assert!(decode(&bad_magic).is_err(), "bad magic");
915 let bad_tile_size = {
916 let mut h = build_header(dims(16, 16), 1, 0);
917 h[12] = 15; h
919 };
920 assert!(decode(&bad_tile_size).is_err(), "tile size");
921 let bad_count = build_header(dims(16, 16), 5, 0);
922 assert!(decode(&bad_count).is_err(), "tile count");
923 assert!(
924 decode(&build_header(dims(0, 16), 1, 0)).is_err(),
925 "zero dim"
926 );
927 assert!(
928 read_dims(&build_header(dims(0, 16), 1, 0)[4..12]).is_err(),
929 "read_dims rejects zero width"
930 );
931 let d = dims(32, 16);
933 let tiles = split_tiles(&solid(32, 16, [1, 2, 3]), d);
934 let mut raw = encode_tiles(&tiles, true, false);
935 raw.truncate(raw.len() - 5); let truncated = {
937 let payload = compress_payload(&raw);
938 let mut b = build_header(d, 2, payload.len() as u32);
939 b.extend_from_slice(&payload);
940 b
941 };
942 assert!(decode(&truncated).is_err(), "truncated tile data");
943 let mut raw_ref = vec![1u8];
945 raw_ref.extend_from_slice(&3u32.to_le_bytes());
946 raw_ref.extend_from_slice(&[0u8; 16 * 16 * 3]);
947 let dang = {
948 let payload = compress_payload(&raw_ref);
949 let mut b = build_header(dims(16, 16), 1, payload.len() as u32);
950 b.extend_from_slice(&payload);
951 b
952 };
953 assert!(decode(&dang).is_err(), "dangling reference");
954 }
955
956 #[test]
957 fn decoder_rejects_oversized_dims() {
958 let h = build_header(
959 Dims {
960 width: 1 << 24,
961 height: 1 << 24,
962 },
963 0,
964 0,
965 );
966 assert!(decode(&h).is_err(), "oversized dims");
967 }
968
969 #[test]
970 fn atlas_reference_shape_mismatch_rejected() {
971 let d = Dims {
973 width: 16,
974 height: 24,
975 };
976 let mut raw = Vec::new();
977 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
979 raw.push(1u8); raw.extend_from_slice(&0u32.to_le_bytes());
981 raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
982 let payload = compress_payload(&raw);
983 let mut b = build_header(d, 2, payload.len() as u32);
984 b.extend_from_slice(&payload);
985 let err = decode(&b).unwrap_err();
986 assert!(
987 err.to_string().contains("shaped"),
988 "want shape mismatch, got: {err}"
989 );
990 }
991
992 #[test]
993 fn encoder_picks_reference_when_residual_compresses() {
994 let mut s: u32 = 999;
999 let mut rnd = || {
1000 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1001 (s >> 16) as u8
1002 };
1003 let noisy: Vec<u8> = (0..32 * 16 * 3).map(|_| rnd()).collect();
1004 let tiles = vec![
1005 Tile {
1006 data: noisy.clone(),
1007 width: 32,
1008 height: 16,
1009 },
1010 Tile {
1011 data: noisy.clone(),
1012 width: 32,
1013 height: 16,
1014 },
1015 ];
1016 let raw_tiles = encode_tiles(&tiles, true, false);
1017 let t1 = 1 + 32 * 16 * 3; assert_eq!(raw_tiles[0], 0, "tile 0 literal");
1019 assert_eq!(
1020 raw_tiles[t1], 1,
1021 "tile 1 should be an exact reference (mode 1)"
1022 );
1023 let idx = u32::from_le_bytes(raw_tiles[t1 + 1..t1 + 5].try_into().unwrap());
1024 assert_eq!(idx, 0, "reference points at tile 0");
1025 assert!(raw_tiles[t1 + 5..].iter().all(|&b| b == 0));
1027 }
1028
1029 #[test]
1030 fn d8_transforms_roundtrip_lossless() {
1031 for (w, h) in [
1036 (16u32, 16u32),
1037 (30u32, 30u32),
1038 (16u32, 24u32),
1039 (31u32, 33u32),
1040 ] {
1041 let mut s: u32 = 777;
1042 let mut rnd = || {
1043 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1044 (s >> 16) as u8
1045 };
1046 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
1047 for t0 in 0..NUM_TRANSFORMS {
1048 if !transform_is_valid_for(t0, w, h) {
1049 continue;
1050 }
1051 let base = apply_transform(t0, w, h, &px);
1052 let base_tile = Tile {
1053 data: base.clone(),
1054 width: w,
1055 height: h,
1056 };
1057 let target_tile = Tile {
1058 data: px.clone(),
1059 width: w,
1060 height: h,
1061 };
1062 let raw = encode_tiles(&[base_tile, target_tile], true, true);
1063 let t1 = 1 + (w * h * 3) as usize;
1064 let mode = raw[t1];
1065 let emitted_t = match mode {
1066 0 => panic!("tile 1 must reference: exact inverse exists (t0={t0}, {w}x{h})"),
1067 1 => 0u8,
1068 2 => raw[t1 + 5],
1069 m => panic!("invalid mode {m}"),
1070 };
1071 assert!(
1072 transform_is_valid_for(emitted_t, w, h),
1073 "emitted transform {emitted_t} invalid for {w}x{h} (t0={t0})"
1074 );
1075 assert_eq!(
1077 apply_transform(emitted_t, w, h, &base),
1078 px,
1079 "T_{emitted_t}(T_{t0}(px)) != px for {w}x{h}"
1080 );
1081 }
1082 }
1083 }
1084
1085 #[test]
1086 fn geom_reference_transform4_nonsquare_rejected() {
1087 let d = Dims {
1089 width: 16,
1090 height: 24,
1091 };
1092 let mut raw = Vec::new();
1093 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
1095 raw.push(2u8); raw.extend_from_slice(&0u32.to_le_bytes());
1097 raw.push(4u8); raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
1099 let payload = compress_payload(&raw);
1100 let mut b = build_header(d, 2, payload.len() as u32);
1101 b.extend_from_slice(&payload);
1102 let err = decode(&b).unwrap_err();
1103 assert!(
1104 err.to_string().contains("invalid transform"),
1105 "want transform rejection, got: {err}"
1106 );
1107 }
1108
1109 #[test]
1110 fn nonsquare_partial_tiles_use_shape_keeping_subset() {
1111 let d = Dims {
1113 width: 31,
1114 height: 33,
1115 }; let mut s: u32 = 4242;
1117 let mut rnd = || {
1118 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1119 (s >> 16) as u8
1120 };
1121 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1122 .map(|_| rnd())
1123 .collect();
1124 assert_eq!(roundtrip(d, &px, true), px);
1125 }
1126
1127 #[test]
1128 fn no_atlas_uses_literals() {
1129 let d = Dims {
1130 width: 32,
1131 height: 16,
1132 };
1133 let tiles = split_tiles(&solid(32, 16, [9, 9, 9]), d);
1134 let raw = encode_tiles(&tiles, false, false);
1135 assert_eq!(raw[0], 0);
1136 assert_eq!(
1137 raw[1 + 16 * 16 * 3],
1138 0,
1139 "tile 1 literal when atlas disabled"
1140 );
1141 }
1142
1143 #[test]
1144 fn proxy_cost_monotone() {
1145 assert!(tile_proxy_cost(&[0u8; 16]) <= tile_proxy_cost(&[5u8; 32]));
1146 }
1147}