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 px in rgb.chunks_exact(3) {
160 s.0 += px[0] as i64;
161 s.1 += px[1] as i64;
162 s.2 += px[2] as i64;
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.chunks_exact(3).map(luma).collect::<Vec<i64>>();
188 let (wu, hu) = (w as usize, h as usize);
189 let mut gdx = 0i64;
190 let mut gdy = 0i64;
191 for y in 0..hu {
192 for x in 0..wu {
193 if x + 1 < wu {
194 gdx += (l[y * wu + x] - l[y * wu + x + 1]).abs();
195 }
196 if y + 1 < hu {
197 gdy += (l[y * wu + x] - l[(y + 1) * wu + x]).abs();
198 }
199 }
200 }
201 let mean_l = l.iter().sum::<i64>() / n;
202 let var = l.iter().map(|v| (v - mean_l) * (v - mean_l)).sum::<i64>() / n;
203 Sig {
204 mean: rgb_mean(data),
205 gdx: gdx / n,
206 gdy: gdy / n,
207 luma: mean_l,
208 var,
209 }
210}
211
212fn sig_dist(a: &Sig, b: &Sig) -> u64 {
213 let rgb =
214 (a.mean.0 - b.mean.0).pow(2) + (a.mean.1 - b.mean.1).pow(2) + (a.mean.2 - b.mean.2).pow(2);
215 let grad = (((a.gdx - b.gdx).abs() + (a.gdy - b.gdy).abs()) as u64)
216 .min(((a.gdy - b.gdx).abs() + (a.gdx - b.gdy).abs()) as u64);
217 let lum = (a.luma - b.luma).pow(2) as u64 + (a.var - b.var).pow(2) as u64;
218 rgb as u64 + 4 * grad + lum
219}
220
221pub fn transform_dims(t: u8, w: u32, h: u32) -> (u32, u32) {
227 match t {
228 4..=7 => (h, w),
229 _ => (w, h),
230 }
231}
232
233pub fn transform_is_valid_for(t: u8, w: u32, h: u32) -> bool {
237 if t >= NUM_TRANSFORMS {
238 return false;
239 }
240 w == h || t <= 3
241}
242
243fn transform_source(t: u8, w: u32, h: u32, x: u32, y: u32) -> (u32, u32) {
244 match t {
246 0 => (x, y),
247 1 => (w - 1 - x, y),
248 2 => (x, h - 1 - y),
249 3 => (w - 1 - x, h - 1 - y),
250 4 => (y, h - 1 - x),
251 _ => unreachable!("composite transforms are composed in apply_transform"),
252 }
253}
254
255pub fn apply_transform(t: u8, w: u32, h: u32, data: &[u8]) -> Vec<u8> {
257 assert!(t < NUM_TRANSFORMS, "invalid transform {t}");
258 if t >= 5 {
259 let (w2, h2) = transform_dims(4, w, h);
260 let base = apply_transform(4, w, h, data);
261 return match t {
262 5 => apply_transform(1, w2, h2, &base),
263 6 => apply_transform(3, w2, h2, &base),
264 _ => apply_transform(1, w2, h2, &apply_transform(3, w2, h2, &base)),
265 };
266 }
267 let (ow, oh) = transform_dims(t, w, h);
268 let mut out = vec![0u8; (ow as usize) * (oh as usize) * 3];
269 for y in 0..oh {
270 for x in 0..ow {
271 let (sx, sy) = transform_source(t, w, h, x, y);
272 let so = ((sy * w + sx) as usize) * 3;
273 let d = ((y * ow + x) as usize) * 3;
274 out[d..d + 3].copy_from_slice(&data[so..so + 3]);
275 }
276 }
277 out
278}
279
280pub fn tile_proxy_cost(bytes: &[u8]) -> u64 {
284 let mut e = ZlibEncoder::new(Vec::new(), Compression::fast());
285 e.write_all(bytes).ok();
286 e.finish().map(|v| v.len() as u64).unwrap_or(u64::MAX)
287}
288
289pub fn encode_tiles(tiles: &[Tile], use_atlas: bool, use_geom: bool) -> Vec<u8> {
295 encode_stats(tiles, use_atlas, use_geom).0
296}
297
298pub fn encode_stats(tiles: &[Tile], use_atlas: bool, use_geom: bool) -> (Vec<u8>, GeomStats) {
300 let mut out: Vec<u8> =
301 Vec::with_capacity(tiles.len() * (TILE_SIZE as usize * TILE_SIZE as usize * 3 + 6));
302 let mut atlas = Atlas::new();
303 let mut stats = GeomStats::default();
304
305 for tile in tiles {
306 let n = tile.nbytes();
307 let sig = tile_sig(tile.width, tile.height, &tile.data);
308 let mut best_mode = Mode::Literal;
309 let mut best_bytes = tile.data.clone();
310 let lit_cost = 1 + tile_proxy_cost(&tile.data);
313 let mut best_cost = lit_cost;
314
315 if use_atlas {
316 let cands = atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES);
318 let transforms: &[u8] = if !use_geom {
321 &[0]
322 } else if tile.width == tile.height {
323 &[0, 1, 2, 3, 4, 5, 6, 7]
324 } else {
325 &[0, 1, 2, 3]
326 };
327 for prior in cands {
328 let shape = (tile.width, tile.height);
329 for &t in transforms {
330 let (_, _, ref_rgb) = &atlas.patches[prior as usize];
331 let transformed = if t == 0 {
332 ref_rgb.clone()
333 } else {
334 apply_transform(t, shape.0, shape.1, ref_rgb)
335 };
336 let mut residual = vec![0u8; n];
337 for i in 0..n {
338 residual[i] = tile.data[i].wrapping_sub(transformed[i]);
339 }
340 let meta_cost = 5 + u64::from(t != 0);
342 let cost = meta_cost + tile_proxy_cost(&residual);
343 if cost < best_cost {
344 best_cost = cost;
345 best_mode = Mode::GeomReference(prior, t);
346 best_bytes = residual;
347 }
348 }
349 }
350 }
353
354 let emit = match best_mode {
356 Mode::GeomReference(p, 0) => Mode::Reference(p),
357 other => other,
358 };
359 match emit {
360 Mode::Literal => stats.literal += 1,
361 Mode::Reference(_) => stats.ref_exact += 1,
362 Mode::GeomReference(_, t) => {
363 stats.ref_geom += 1;
364 if t != 0 {
365 stats.geom_nontrivial += 1;
366 }
367 }
368 }
369 stats.modes.push(emit.to_byte());
370 stats.savings.push(lit_cost as i64 - best_cost as i64);
371 stats.refs.push(match emit {
372 Mode::Reference(p) => Some((p, 0)),
373 Mode::GeomReference(p, t) => Some((p, t)),
374 Mode::Literal => None,
375 });
376 out.push(emit.to_byte());
377 match emit {
378 Mode::Reference(prior) => out.extend_from_slice(&prior.to_le_bytes()),
379 Mode::GeomReference(prior, t) => {
380 out.extend_from_slice(&prior.to_le_bytes());
381 out.push(t);
382 }
383 Mode::Literal => {}
384 }
385 out.extend_from_slice(&best_bytes);
386 atlas.insert_with_sig(tile, sig);
387 }
388 (out, stats)
389}
390
391pub fn compress_payload(raw: &[u8]) -> Vec<u8> {
393 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
394 e.write_all(raw).expect("zlib encode");
395 e.finish().expect("zlib finish")
396}
397
398#[derive(Debug, Clone, Default)]
401pub struct GeomStats {
402 pub literal: u32,
404 pub ref_exact: u32,
406 pub ref_geom: u32,
408 pub geom_nontrivial: u32, pub modes: Vec<u8>, pub savings: Vec<i64>, pub refs: Vec<Option<(u32, u8)>>, }
417
418pub fn build_header(dims: Dims, tile_count: u32, payload_len: u32) -> Vec<u8> {
420 let mut h = Vec::with_capacity(HEADER_SIZE);
421 h.extend_from_slice(b"MNF5");
422 h.extend_from_slice(&dims.width.to_le_bytes());
423 h.extend_from_slice(&dims.height.to_le_bytes());
424 h.extend_from_slice(&TILE_SIZE.to_le_bytes());
425 h.extend_from_slice(&tile_count.to_le_bytes());
426 h.extend_from_slice(&payload_len.to_le_bytes());
427 debug_assert_eq!(h.len(), HEADER_SIZE);
428 h
429}
430
431#[derive(Debug)]
433pub struct DecodeResult {
434 pub dims: Dims,
436 pub pixels: Vec<u8>,
438}
439
440#[derive(Debug, Clone, Copy)]
444pub struct DecodeLimits {
445 pub max_pixels: u64,
447 pub max_tiles: u32,
449 pub max_decompressed: u64,
451 pub max_total_memory: u64, }
454
455impl Default for DecodeLimits {
456 fn default() -> Self {
457 DecodeLimits {
458 max_pixels: 1 << 26,
459 max_tiles: u32::MAX,
460 max_decompressed: u64::MAX, max_total_memory: u64::MAX,
462 }
463 }
464}
465
466pub fn decode(buf: &[u8]) -> Result<DecodeResult> {
468 decode_with_limits(buf, &DecodeLimits::default())
469}
470
471pub fn decode_with_limits(buf: &[u8], lim: &DecodeLimits) -> Result<DecodeResult> {
473 if buf.len() < HEADER_SIZE {
474 return Err(Error::fmt_at("truncated header", buf.len() as u64));
475 }
476 if &buf[0..4] != b"MNF5" {
477 return Err(Error::fmt_at("bad magic (want MNF5)", 0));
478 }
479 let (w, h) = read_dims(&buf[4..12])?;
480 if w as u64 * h as u64 > lim.max_pixels {
481 return Err(Error::Limits {
482 which: "max_pixels",
483 limit: lim.max_pixels,
484 actual: w as u64 * h as u64,
485 });
486 }
487 let ts = u32::from_le_bytes(buf[12..16].try_into().unwrap());
488 let tile_count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
489 if ts != TILE_SIZE {
490 return Err(Error::fmt_msg(format!("unsupported tile size {ts}")));
491 }
492 if tile_count as u64 > lim.max_tiles as u64 {
493 return Err(Error::Limits {
494 which: "max_tiles",
495 limit: u64::from(lim.max_tiles),
496 actual: tile_count as u64,
497 });
498 }
499 let payload_len = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize;
500 let (cols, rows) = tile_grid(Dims {
501 width: w,
502 height: h,
503 });
504 let expected = cols * rows;
505 if tile_count != expected {
506 return Err(Error::fmt_msg(format!(
507 "tile_count {tile_count} != expected {expected} for {w}x{h}"
508 )));
509 }
510 let _rows = rows;
511 if payload_len > MAX_PAYLOAD {
512 return Err(Error::fmt_msg("payload too large"));
513 }
514 if (buf.len() - HEADER_SIZE) < payload_len {
515 return Err(Error::fmt_msg("payload shorter than header-declared len"));
516 }
517 if buf.len() != HEADER_SIZE + payload_len {
520 return Err(Error::fmt_msg(format!(
521 "file is {} bytes but header declares {} + {payload_len} payload",
522 buf.len(),
523 HEADER_SIZE
524 )));
525 }
526 let payload = &buf[HEADER_SIZE..HEADER_SIZE + payload_len];
527
528 let px_bytes_total = (w as u64) * (h as u64) * 3;
533 let decomp_limit = px_bytes_total
534 .checked_add(6 * tile_count as u64)
535 .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?;
536 let mut dz = ZlibDecoder::new(payload).take(decomp_limit + 1);
537 let mut raw = Vec::new();
538 dz.read_to_end(&mut raw)
539 .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
540 if raw.len() as u64 > decomp_limit {
541 return Err(Error::Limits {
542 which: "max_decompressed",
543 limit: decomp_limit,
544 actual: raw.len() as u64,
545 });
546 }
547
548 if raw.len() < 4 * tile_count as usize {
552 return Err(Error::fmt_msg(
553 "decompressed stream shorter than per-tile minimum",
554 ));
555 }
556
557 let total_px = w as usize * h as usize * 3;
558 let mem_needed = (total_px as u64) * 2;
561 if mem_needed > lim.max_total_memory {
562 return Err(Error::Limits {
563 which: "max_total_memory",
564 limit: lim.max_total_memory,
565 actual: mem_needed,
566 });
567 }
568 let mut out = vec![0u8; total_px];
569 let mut patches: Vec<(u32, u32, Vec<u8>)> = Vec::new();
571 let mut cursor = 0usize;
572
573 for t in 0..tile_count {
574 let (tw, th) = tile_dims(
575 Dims {
576 width: w,
577 height: h,
578 },
579 t,
580 );
581 let n = (tw * th * 3) as usize;
582 if cursor + 1 > raw.len() {
583 return Err(Error::fmt_at(
584 format!("tile {t}: truncated mode byte"),
585 u64::from(t),
586 ));
587 }
588 let mode = raw[cursor];
589 cursor += 1;
590 let data: Vec<u8> = match mode {
591 0 => {
592 if cursor + n > raw.len() {
593 return Err(Error::fmt_at(
594 format!("tile {t}: truncated literal payload"),
595 u64::from(t),
596 ));
597 }
598 let d = raw[cursor..cursor + n].to_vec();
599 cursor += n;
600 d
601 }
602 1 => {
603 if cursor + 4 > raw.len() {
604 return Err(Error::fmt_at(
605 format!("tile {t}: truncated reference index"),
606 u64::from(t),
607 ));
608 }
609 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
610 cursor += 4;
611 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
612 Error::fmt_msg(format!(
613 "tile {t}: reference to tile {idx} >= {len} decoded",
614 len = patches.len(),
615 t = t,
616 idx = idx
617 ))
618 })?;
619 if *rw != tw || *rh != th {
620 return Err(Error::fmt_at(
621 format!(
622 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
623 t = t,
624 idx = idx,
625 rw = rw,
626 rh = rh,
627 tw = tw,
628 th = th
629 ),
630 u64::from(t),
631 ));
632 }
633 if cursor + n > raw.len() {
634 return Err(Error::fmt_at(
635 format!("tile {t}: truncated residual"),
636 u64::from(t),
637 ));
638 }
639 let residual = &raw[cursor..cursor + n];
640 let mut d = vec![0u8; n];
641 for i in 0..n {
642 d[i] = ref_rgb[i].wrapping_add(residual[i]);
643 }
644 cursor += n;
645 d
646 }
647 2 => {
648 if cursor + 5 > raw.len() {
649 return Err(Error::fmt_at(
650 format!("tile {t}: truncated geometric reference"),
651 u64::from(t),
652 ));
653 }
654 let idx = u32::from_le_bytes(raw[cursor..cursor + 4].try_into().unwrap());
655 let tr = raw[cursor + 4];
656 cursor += 5;
657 if !transform_is_valid_for(tr, tw, th) {
658 return Err(Error::fmt_at(
659 format!(
660 "tile {t}: invalid transform {tr} for {tw}x{th} tile",
661 t = t,
662 tr = tr,
663 tw = tw,
664 th = th
665 ),
666 u64::from(t),
667 ));
668 }
669 let (rw, rh, ref_rgb) = patches.get(idx as usize).ok_or_else(|| {
670 Error::fmt_msg(format!(
671 "tile {t}: reference to tile {idx} >= {len} decoded",
672 len = patches.len(),
673 t = t,
674 idx = idx
675 ))
676 })?;
677 if *rw != tw || *rh != th {
678 return Err(Error::fmt_at(
679 format!(
680 "tile {t}: patch {idx} shaped {rw}x{rh}, need {tw}x{th}",
681 t = t,
682 idx = idx,
683 rw = rw,
684 rh = rh,
685 tw = tw,
686 th = th
687 ),
688 u64::from(t),
689 ));
690 }
691 if cursor + n > raw.len() {
692 return Err(Error::fmt_at(
693 format!("tile {t}: truncated residual"),
694 u64::from(t),
695 ));
696 }
697 let residual = &raw[cursor..cursor + n];
698 let transformed = if tr == 0 {
699 ref_rgb.clone()
700 } else {
701 apply_transform(tr, tw, th, ref_rgb)
702 };
703 let mut d = vec![0u8; n];
704 for i in 0..n {
705 d[i] = transformed[i].wrapping_add(residual[i]);
706 }
707 cursor += n;
708 d
709 }
710 m => {
711 return Err(Error::fmt_at(
712 format!("tile {t}: invalid mode byte {m}"),
713 u64::from(t),
714 ))
715 }
716 };
717 blit_tile(
718 &mut out,
719 Dims {
720 width: w,
721 height: h,
722 },
723 t,
724 &data,
725 tw,
726 th,
727 );
728 patches.push((tw, th, data));
729 }
730 if cursor != raw.len() {
731 return Err(Error::fmt_msg(format!(
732 "trailing bytes after last tile: {cursor} != {}",
733 raw.len()
734 )));
735 }
736 Ok(DecodeResult {
737 dims: Dims {
738 width: w,
739 height: h,
740 },
741 pixels: out,
742 })
743}
744
745const MAX_PAYLOAD: usize = 1 << 31; fn read_dims(b: &[u8]) -> Result<(u32, u32)> {
748 let w = u32::from_le_bytes(b[0..4].try_into().unwrap());
749 let h = u32::from_le_bytes(b[4..8].try_into().unwrap());
750 if w == 0 || h == 0 {
751 return Err(Error::fmt_msg("zero dimensions"));
752 }
753 if (w as u64) * (h as u64) > (1 << 26) {
756 return Err(Error::fmt_msg("image too large"));
757 }
758 Ok((w, h))
759}
760
761pub fn split_tiles(pixels: &[u8], d: Dims) -> Vec<Tile> {
764 let (cols, rows) = tile_grid(d);
765 (0..cols * rows)
766 .map(|t| {
767 let (tw, th) = tile_dims(d, t);
768 let mut data = vec![0u8; (tw * th * 3) as usize];
769 let tx = (t % cols) * TILE_SIZE;
770 let ty = (t / cols) * TILE_SIZE;
771 for row in 0..th as usize {
772 for col in 0..tw as usize {
773 let py = ty as usize + row;
774 let px = tx as usize + col;
775 let dst = (row * tw as usize + col) * 3;
776 let src = (py * d.width as usize + px) * 3;
777 data[dst..dst + 3].copy_from_slice(&pixels[src..src + 3]);
778 }
779 }
780 Tile {
781 data,
782 width: tw,
783 height: th,
784 }
785 })
786 .collect()
787}
788
789fn blit_tile(out: &mut [u8], dims: Dims, t: u32, data: &[u8], tw: u32, th: u32) {
790 let (cols, _) = tile_grid(dims);
791 let tx = (t % cols) * TILE_SIZE;
792 let ty = (t / cols) * TILE_SIZE;
793 for row in 0..th as usize {
794 let py = ty as usize + row;
795 for col in 0..tw as usize {
796 let px = tx as usize + col;
797 let dst = (py * dims.width as usize + px) * 3;
798 let src = (row * tw as usize + col) * 3;
799 out[dst..dst + 3].copy_from_slice(&data[src..src + 3]);
800 }
801 }
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807
808 fn dims(w: u32, h: u32) -> Dims {
809 Dims {
810 width: w,
811 height: h,
812 }
813 }
814
815 fn roundtrip(d: Dims, pixels: &[u8], use_atlas: bool) -> Vec<u8> {
816 let tiles = split_tiles(pixels, d);
817 let raw = encode_tiles(&tiles, use_atlas, true);
818 let payload = compress_payload(&raw);
819 let header = build_header(d, tiles.len() as u32, payload.len() as u32);
820 let mut buf = header;
821 buf.extend_from_slice(&payload);
822 let res = decode(&buf).expect("decode ok");
823 assert_eq!(res.dims, d);
824 res.pixels
825 }
826
827 fn solid(w: u32, h: u32, c: [u8; 3]) -> Vec<u8> {
828 (0..w * h).flat_map(|_| c).collect()
829 }
830
831 fn gradient(w: u32, h: u32) -> Vec<u8> {
832 (0..w * h)
833 .flat_map(|i| {
834 [
835 (i % 256) as u8,
836 ((i / w) % 256) as u8,
837 ((i / 7) % 256) as u8,
838 ]
839 })
840 .collect()
841 }
842
843 #[test]
844 fn grid_math() {
845 assert_eq!(tile_grid(dims(16, 16)), (1, 1));
846 assert_eq!(tile_grid(dims(17, 16)), (2, 1));
847 assert_eq!(tile_grid(dims(31, 33)), (2, 3));
848 assert_eq!(tile_dims(dims(31, 33), 1), (15, 16));
849 assert_eq!(tile_dims(dims(31, 33), 2), (16, 16));
850 assert_eq!(tile_dims(dims(31, 33), 5), (15, 1));
851 assert_eq!(div_ceil(0, 16), 0); }
853
854 #[test]
855 fn roundtrip_solid_16x16_atlas() {
856 let px = solid(16, 16, [10, 20, 30]);
857 assert_eq!(roundtrip(dims(16, 16), &px, true), px);
858 }
859
860 #[test]
861 fn roundtrip_solid_16x16_no_atlas() {
862 let px = solid(16, 16, [200, 100, 50]);
863 assert_eq!(roundtrip(dims(16, 16), &px, false), px);
864 }
865
866 #[test]
867 fn roundtrip_odd_dims() {
868 for (w, h) in [
869 (31u32, 33u32),
870 (17u32, 1u32),
871 (1u32, 17u32),
872 (1u32, 1u32),
873 (48u32, 47u32),
874 ] {
875 let px = gradient(w, h);
876 let dec = roundtrip(dims(w, h), &px, true);
877 assert_eq!(dec.len(), (w * h * 3) as usize, "len {w}x{h}");
878 assert_eq!(dec, px, "pixels {w}x{h}");
879 }
880 }
881
882 #[test]
883 fn roundtrip_random_pixels() {
884 let mut s: u32 = 12345;
886 let mut rnd = || {
887 s = s.wrapping_mul(1103515245).wrapping_add(12345);
888 (s >> 16) as u8
889 };
890 let w = 64;
891 let h = 48;
892 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
893 assert_eq!(roundtrip(dims(w, h), &px, true), px);
894 }
895
896 #[test]
897 fn roundtrip_uniform_atlas_matches_noatlas() {
898 let px = solid(64, 64, [7, 7, 7]);
899 assert_eq!(roundtrip(dims(64, 64), &px, true), px);
900 assert_eq!(roundtrip(dims(64, 64), &px, false), px);
901 }
902
903 #[test]
904 fn decode_rejects_bad_inputs() {
905 assert!(decode(&[]).is_err(), "empty");
906 assert!(decode(&[0u8; 23]).is_err(), "short header");
907 let mut bad_magic = build_header(dims(16, 16), 1, 0);
908 bad_magic[0] = b'X';
909 assert!(decode(&bad_magic).is_err(), "bad magic");
910 let bad_tile_size = {
911 let mut h = build_header(dims(16, 16), 1, 0);
912 h[12] = 15; h
914 };
915 assert!(decode(&bad_tile_size).is_err(), "tile size");
916 let bad_count = build_header(dims(16, 16), 5, 0);
917 assert!(decode(&bad_count).is_err(), "tile count");
918 assert!(
919 decode(&build_header(dims(0, 16), 1, 0)).is_err(),
920 "zero dim"
921 );
922 assert!(
923 read_dims(&build_header(dims(0, 16), 1, 0)[4..12]).is_err(),
924 "read_dims rejects zero width"
925 );
926 let d = dims(32, 16);
928 let tiles = split_tiles(&solid(32, 16, [1, 2, 3]), d);
929 let mut raw = encode_tiles(&tiles, true, false);
930 raw.truncate(raw.len() - 5); let truncated = {
932 let payload = compress_payload(&raw);
933 let mut b = build_header(d, 2, payload.len() as u32);
934 b.extend_from_slice(&payload);
935 b
936 };
937 assert!(decode(&truncated).is_err(), "truncated tile data");
938 let mut raw_ref = vec![1u8];
940 raw_ref.extend_from_slice(&3u32.to_le_bytes());
941 raw_ref.extend_from_slice(&[0u8; 16 * 16 * 3]);
942 let dang = {
943 let payload = compress_payload(&raw_ref);
944 let mut b = build_header(dims(16, 16), 1, payload.len() as u32);
945 b.extend_from_slice(&payload);
946 b
947 };
948 assert!(decode(&dang).is_err(), "dangling reference");
949 }
950
951 #[test]
952 fn decoder_rejects_oversized_dims() {
953 let h = build_header(
954 Dims {
955 width: 1 << 24,
956 height: 1 << 24,
957 },
958 0,
959 0,
960 );
961 assert!(decode(&h).is_err(), "oversized dims");
962 }
963
964 #[test]
965 fn atlas_reference_shape_mismatch_rejected() {
966 let d = Dims {
968 width: 16,
969 height: 24,
970 };
971 let mut raw = Vec::new();
972 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
974 raw.push(1u8); raw.extend_from_slice(&0u32.to_le_bytes());
976 raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
977 let payload = compress_payload(&raw);
978 let mut b = build_header(d, 2, payload.len() as u32);
979 b.extend_from_slice(&payload);
980 let err = decode(&b).unwrap_err();
981 assert!(
982 err.to_string().contains("shaped"),
983 "want shape mismatch, got: {err}"
984 );
985 }
986
987 #[test]
988 fn encoder_picks_reference_when_residual_compresses() {
989 let mut s: u32 = 999;
994 let mut rnd = || {
995 s = s.wrapping_mul(1103515245).wrapping_add(12345);
996 (s >> 16) as u8
997 };
998 let noisy: Vec<u8> = (0..32 * 16 * 3).map(|_| rnd()).collect();
999 let tiles = vec![
1000 Tile {
1001 data: noisy.clone(),
1002 width: 32,
1003 height: 16,
1004 },
1005 Tile {
1006 data: noisy.clone(),
1007 width: 32,
1008 height: 16,
1009 },
1010 ];
1011 let raw_tiles = encode_tiles(&tiles, true, false);
1012 let t1 = 1 + 32 * 16 * 3; assert_eq!(raw_tiles[0], 0, "tile 0 literal");
1014 assert_eq!(
1015 raw_tiles[t1], 1,
1016 "tile 1 should be an exact reference (mode 1)"
1017 );
1018 let idx = u32::from_le_bytes(raw_tiles[t1 + 1..t1 + 5].try_into().unwrap());
1019 assert_eq!(idx, 0, "reference points at tile 0");
1020 assert!(raw_tiles[t1 + 5..].iter().all(|&b| b == 0));
1022 }
1023
1024 #[test]
1025 fn d8_transforms_roundtrip_lossless() {
1026 for (w, h) in [
1031 (16u32, 16u32),
1032 (30u32, 30u32),
1033 (16u32, 24u32),
1034 (31u32, 33u32),
1035 ] {
1036 let mut s: u32 = 777;
1037 let mut rnd = || {
1038 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1039 (s >> 16) as u8
1040 };
1041 let px: Vec<u8> = (0..w * h * 3).map(|_| rnd()).collect();
1042 for t0 in 0..NUM_TRANSFORMS {
1043 if !transform_is_valid_for(t0, w, h) {
1044 continue;
1045 }
1046 let base = apply_transform(t0, w, h, &px);
1047 let base_tile = Tile {
1048 data: base.clone(),
1049 width: w,
1050 height: h,
1051 };
1052 let target_tile = Tile {
1053 data: px.clone(),
1054 width: w,
1055 height: h,
1056 };
1057 let raw = encode_tiles(&[base_tile, target_tile], true, true);
1058 let t1 = 1 + (w * h * 3) as usize;
1059 let mode = raw[t1];
1060 let emitted_t = match mode {
1061 0 => panic!("tile 1 must reference: exact inverse exists (t0={t0}, {w}x{h})"),
1062 1 => 0u8,
1063 2 => raw[t1 + 5],
1064 m => panic!("invalid mode {m}"),
1065 };
1066 assert!(
1067 transform_is_valid_for(emitted_t, w, h),
1068 "emitted transform {emitted_t} invalid for {w}x{h} (t0={t0})"
1069 );
1070 assert_eq!(
1072 apply_transform(emitted_t, w, h, &base),
1073 px,
1074 "T_{emitted_t}(T_{t0}(px)) != px for {w}x{h}"
1075 );
1076 }
1077 }
1078 }
1079
1080 #[test]
1081 fn geom_reference_transform4_nonsquare_rejected() {
1082 let d = Dims {
1084 width: 16,
1085 height: 24,
1086 };
1087 let mut raw = Vec::new();
1088 raw.push(0u8); raw.extend_from_slice(&[0u8; 16 * 16 * 3]);
1090 raw.push(2u8); raw.extend_from_slice(&0u32.to_le_bytes());
1092 raw.push(4u8); raw.extend_from_slice(&[0u8; 16 * 8 * 3]);
1094 let payload = compress_payload(&raw);
1095 let mut b = build_header(d, 2, payload.len() as u32);
1096 b.extend_from_slice(&payload);
1097 let err = decode(&b).unwrap_err();
1098 assert!(
1099 err.to_string().contains("invalid transform"),
1100 "want transform rejection, got: {err}"
1101 );
1102 }
1103
1104 #[test]
1105 fn nonsquare_partial_tiles_use_shape_keeping_subset() {
1106 let d = Dims {
1108 width: 31,
1109 height: 33,
1110 }; let mut s: u32 = 4242;
1112 let mut rnd = || {
1113 s = s.wrapping_mul(1103515245).wrapping_add(12345);
1114 (s >> 16) as u8
1115 };
1116 let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1117 .map(|_| rnd())
1118 .collect();
1119 assert_eq!(roundtrip(d, &px, true), px);
1120 }
1121
1122 #[test]
1123 fn no_atlas_uses_literals() {
1124 let d = Dims {
1125 width: 32,
1126 height: 16,
1127 };
1128 let tiles = split_tiles(&solid(32, 16, [9, 9, 9]), d);
1129 let raw = encode_tiles(&tiles, false, false);
1130 assert_eq!(raw[0], 0);
1131 assert_eq!(
1132 raw[1 + 16 * 16 * 3],
1133 0,
1134 "tile 1 literal when atlas disabled"
1135 );
1136 }
1137
1138 #[test]
1139 fn proxy_cost_monotone() {
1140 assert!(tile_proxy_cost(&[0u8; 16]) <= tile_proxy_cost(&[5u8; 32]));
1141 }
1142}