1#![allow(dead_code)]
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ThumbnailFormat {
13 Jpeg,
15 Png,
17 Webp,
19}
20
21impl ThumbnailFormat {
22 #[must_use]
24 pub fn extension(&self) -> &'static str {
25 match self {
26 ThumbnailFormat::Jpeg => "jpg",
27 ThumbnailFormat::Png => "png",
28 ThumbnailFormat::Webp => "webp",
29 }
30 }
31
32 #[must_use]
34 pub fn mime_type(&self) -> &'static str {
35 match self {
36 ThumbnailFormat::Jpeg => "image/jpeg",
37 ThumbnailFormat::Png => "image/png",
38 ThumbnailFormat::Webp => "image/webp",
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub enum ThumbnailStrategy {
46 FixedInterval,
48 SceneChange,
50 Uniform,
52 AtTimestamps(Vec<u64>),
54}
55
56#[derive(Debug, Clone)]
58pub struct ThumbnailConfig {
59 pub width: u32,
61 pub height: u32,
63 pub format: ThumbnailFormat,
65 pub quality: u8,
67 pub count: usize,
69 pub interval_strategy: ThumbnailStrategy,
71}
72
73impl ThumbnailConfig {
74 #[must_use]
76 pub fn default_web() -> Self {
77 Self {
78 width: 320,
79 height: 180,
80 format: ThumbnailFormat::Jpeg,
81 quality: 80,
82 count: 10,
83 interval_strategy: ThumbnailStrategy::Uniform,
84 }
85 }
86
87 #[must_use]
89 pub fn sprite_sheet(count: usize) -> Self {
90 Self {
91 width: 160,
92 height: 90,
93 format: ThumbnailFormat::Jpeg,
94 quality: 70,
95 count,
96 interval_strategy: ThumbnailStrategy::Uniform,
97 }
98 }
99
100 #[must_use]
102 pub fn is_valid(&self) -> bool {
103 self.width > 0 && self.height > 0 && self.count > 0
104 }
105}
106
107#[derive(Debug, Clone)]
109pub struct Thumbnail {
110 pub timestamp_ms: u64,
112 pub width: u32,
114 pub height: u32,
116 pub data: Vec<u8>,
118}
119
120impl Thumbnail {
121 #[must_use]
123 pub fn new(timestamp_ms: u64, width: u32, height: u32, data: Vec<u8>) -> Self {
124 Self {
125 timestamp_ms,
126 width,
127 height,
128 data,
129 }
130 }
131
132 #[must_use]
134 pub fn pixel_count(&self) -> usize {
135 (self.width * self.height) as usize
136 }
137
138 #[must_use]
140 pub fn expected_byte_len(&self) -> usize {
141 self.pixel_count() * 4
142 }
143}
144
145#[must_use]
154pub fn compute_thumbnail_timestamps(
155 duration_ms: u64,
156 strategy: &ThumbnailStrategy,
157 fps: f64,
158) -> Vec<u64> {
159 if duration_ms == 0 {
160 return Vec::new();
161 }
162
163 let snap = |ts: f64| -> u64 {
164 if fps > 0.0 {
165 let frame_ms = 1000.0 / fps;
166 ((ts / frame_ms).round() * frame_ms) as u64
167 } else {
168 ts as u64
169 }
170 };
171
172 match strategy {
173 ThumbnailStrategy::AtTimestamps(ts) => {
174 ts.iter().filter(|&&t| t <= duration_ms).copied().collect()
175 }
176
177 ThumbnailStrategy::Uniform => {
178 compute_uniform_timestamps(duration_ms, 10, fps)
182 }
183
184 ThumbnailStrategy::FixedInterval => {
185 let interval_ms = 10_000u64;
187 let mut ts = Vec::new();
188 let mut t = 0u64;
189 while t <= duration_ms {
190 ts.push(snap(t as f64));
191 t += interval_ms;
192 }
193 ts
194 }
195
196 ThumbnailStrategy::SceneChange => {
197 Vec::new()
200 }
201 }
202}
203
204#[must_use]
206pub fn compute_uniform_timestamps(duration_ms: u64, count: usize, fps: f64) -> Vec<u64> {
207 if count == 0 || duration_ms == 0 {
208 return Vec::new();
209 }
210
211 let snap = |ts: f64| -> u64 {
212 if fps > 0.0 {
213 let frame_ms = 1000.0 / fps;
214 ((ts / frame_ms).round() * frame_ms) as u64
215 } else {
216 ts as u64
217 }
218 };
219
220 if count == 1 {
221 return vec![snap(duration_ms as f64 / 2.0)];
222 }
223
224 (0..count)
225 .map(|i| {
226 let t = (duration_ms as f64 * i as f64) / (count - 1) as f64;
227 snap(t).min(duration_ms)
228 })
229 .collect()
230}
231
232#[allow(clippy::too_many_arguments)]
238#[must_use]
239pub fn scale_thumbnail(src: &[u8], src_w: u32, src_h: u32, dst_w: u32, dst_h: u32) -> Vec<u8> {
240 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
241 return Vec::new();
242 }
243
244 let expected_len = (src_w * src_h * 4) as usize;
245 if src.len() < expected_len {
246 return Vec::new();
247 }
248
249 let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
250
251 for dy in 0..dst_h {
252 for dx in 0..dst_w {
253 let sx = (f64::from(dx) * f64::from(src_w) / f64::from(dst_w)) as u32;
255 let sy = (f64::from(dy) * f64::from(src_h) / f64::from(dst_h)) as u32;
256
257 let src_idx = ((sy * src_w + sx) * 4) as usize;
258 let dst_idx = ((dy * dst_w + dx) * 4) as usize;
259
260 if src_idx + 3 < src.len() && dst_idx + 3 < dst.len() {
261 dst[dst_idx] = src[src_idx];
262 dst[dst_idx + 1] = src[src_idx + 1];
263 dst[dst_idx + 2] = src[src_idx + 2];
264 dst[dst_idx + 3] = src[src_idx + 3];
265 }
266 }
267 }
268
269 dst
270}
271
272#[derive(Debug, Clone)]
281pub struct SpriteSheet {
282 pub cell_width: u32,
284 pub cell_height: u32,
286 pub cols: u32,
288 pub rows: u32,
290 pub sheet_width: u32,
292 pub sheet_height: u32,
294 pub data: Vec<u8>,
296 pub timestamps_ms: Vec<u64>,
298}
299
300impl SpriteSheet {
301 #[must_use]
309 pub fn from_thumbnails(thumbnails: &[Thumbnail], cols: u32) -> Option<Self> {
310 if thumbnails.is_empty() || cols == 0 {
311 return None;
312 }
313
314 let cell_w = thumbnails[0].width;
315 let cell_h = thumbnails[0].height;
316 if cell_w == 0 || cell_h == 0 {
317 return None;
318 }
319
320 let count = thumbnails.len() as u32;
321 let rows = (count + cols - 1) / cols; let sheet_w = cols * cell_w;
323 let sheet_h = rows * cell_h;
324
325 let mut sheet = vec![0u8; (sheet_w * sheet_h * 4) as usize];
326 let mut timestamps = Vec::with_capacity(thumbnails.len());
327
328 for (idx, thumb) in thumbnails.iter().enumerate() {
329 timestamps.push(thumb.timestamp_ms);
330
331 let col = idx as u32 % cols;
332 let row = idx as u32 / cols;
333
334 let cell_data = if thumb.width == cell_w && thumb.height == cell_h {
336 thumb.data.clone()
337 } else {
338 scale_thumbnail(&thumb.data, thumb.width, thumb.height, cell_w, cell_h)
339 };
340
341 if cell_data.len() < (cell_w * cell_h * 4) as usize {
342 continue; }
344
345 let dest_x = col * cell_w;
347 let dest_y = row * cell_h;
348
349 for cy in 0..cell_h {
350 let src_row_start = (cy * cell_w * 4) as usize;
351 let src_row_end = src_row_start + (cell_w * 4) as usize;
352 let dest_row_start = ((dest_y + cy) * sheet_w * 4 + dest_x * 4) as usize;
353 let dest_row_end = dest_row_start + (cell_w * 4) as usize;
354
355 if src_row_end <= cell_data.len() && dest_row_end <= sheet.len() {
356 sheet[dest_row_start..dest_row_end]
357 .copy_from_slice(&cell_data[src_row_start..src_row_end]);
358 }
359 }
360 }
361
362 Some(SpriteSheet {
363 cell_width: cell_w,
364 cell_height: cell_h,
365 cols,
366 rows,
367 sheet_width: sheet_w,
368 sheet_height: sheet_h,
369 data: sheet,
370 timestamps_ms: timestamps,
371 })
372 }
373
374 #[must_use]
377 pub fn cell_origin(&self, idx: usize) -> (u32, u32) {
378 let col = idx as u32 % self.cols;
379 let row = idx as u32 / self.cols;
380 (col * self.cell_width, row * self.cell_height)
381 }
382
383 #[must_use]
392 pub fn to_vtt(&self, sprite_url: &str) -> String {
393 let mut vtt = String::from("WEBVTT\n\n");
394
395 for (idx, &ts_ms) in self.timestamps_ms.iter().enumerate() {
396 let next_ts_ms = self
397 .timestamps_ms
398 .get(idx + 1)
399 .copied()
400 .unwrap_or(ts_ms + 1_000); let (x, y) = self.cell_origin(idx);
403
404 let start = format_vtt_time(ts_ms);
405 let end = format_vtt_time(next_ts_ms);
406
407 vtt.push_str(&format!(
408 "{start} --> {end}\n{sprite_url}#xywh={x},{y},{w},{h}\n\n",
409 w = self.cell_width,
410 h = self.cell_height,
411 ));
412 }
413
414 vtt
415 }
416
417 #[must_use]
419 pub fn cell_count(&self) -> usize {
420 self.timestamps_ms.len()
421 }
422
423 #[must_use]
425 pub fn byte_len(&self) -> usize {
426 self.data.len()
427 }
428}
429
430#[must_use]
432pub fn format_vtt_time(ms: u64) -> String {
433 let total_secs = ms / 1_000;
434 let millis = ms % 1_000;
435 let secs = total_secs % 60;
436 let mins = (total_secs / 60) % 60;
437 let hours = total_secs / 3_600;
438 format!("{hours:02}:{mins:02}:{secs:02}.{millis:03}")
439}
440
441#[derive(Debug, Clone)]
443pub struct SpriteSheetConfig {
444 pub cell_width: u32,
446 pub cell_height: u32,
448 pub cols: u32,
450 pub count: usize,
452 pub strategy: ThumbnailStrategy,
454 pub quality: u8,
456}
457
458impl SpriteSheetConfig {
459 #[must_use]
461 pub fn default_web() -> Self {
462 Self {
463 cell_width: 160,
464 cell_height: 90,
465 cols: 5,
466 count: 100,
467 strategy: ThumbnailStrategy::Uniform,
468 quality: 70,
469 }
470 }
471
472 #[must_use]
474 pub fn high_density(count: usize) -> Self {
475 Self {
476 cell_width: 120,
477 cell_height: 68,
478 cols: 10,
479 count,
480 strategy: ThumbnailStrategy::Uniform,
481 quality: 65,
482 }
483 }
484
485 #[must_use]
487 pub fn is_valid(&self) -> bool {
488 self.cell_width > 0 && self.cell_height > 0 && self.cols > 0 && self.count > 0
489 }
490
491 #[must_use]
493 pub fn compute_timestamps(&self, duration_ms: u64, fps: f64) -> Vec<u64> {
494 match &self.strategy {
495 ThumbnailStrategy::Uniform => compute_uniform_timestamps(duration_ms, self.count, fps),
496 other => compute_thumbnail_timestamps(duration_ms, other, fps),
497 }
498 }
499}
500
501#[must_use]
508pub fn thumbnail_variance(thumb: &Thumbnail) -> f64 {
509 let pixel_count = thumb.pixel_count();
510 if pixel_count == 0 || thumb.data.len() < pixel_count * 4 {
511 return 0.0;
512 }
513
514 let mut sum = 0.0;
516 let mut sum_sq = 0.0;
517
518 for i in 0..pixel_count {
519 let offset = i * 4;
520 let r = f64::from(thumb.data[offset]);
521 let g = f64::from(thumb.data[offset + 1]);
522 let b = f64::from(thumb.data[offset + 2]);
523 let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
524 sum += lum;
525 sum_sq += lum * lum;
526 }
527
528 let n = pixel_count as f64;
529 let mean = sum / n;
530 (sum_sq / n) - (mean * mean)
531}
532
533#[must_use]
538pub fn select_smart_thumbnails(thumbnails: &[Thumbnail], count: usize) -> Vec<usize> {
539 if thumbnails.is_empty() || count == 0 {
540 return Vec::new();
541 }
542
543 let mut scored: Vec<(usize, f64)> = thumbnails
544 .iter()
545 .enumerate()
546 .map(|(i, t)| (i, thumbnail_variance(t)))
547 .collect();
548
549 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
551
552 scored.iter().take(count).map(|&(i, _)| i).collect()
553}
554
555#[derive(Debug, Clone)]
562pub struct AnimatedThumbnail {
563 pub width: u32,
565 pub height: u32,
567 pub frames: Vec<AnimatedFrame>,
569 pub total_duration_ms: u64,
571 pub loop_count: u32,
573}
574
575#[derive(Debug, Clone)]
577pub struct AnimatedFrame {
578 pub data: Vec<u8>,
580 pub duration_ms: u64,
582 pub source_timestamp_ms: u64,
584}
585
586impl AnimatedThumbnail {
587 #[must_use]
592 pub fn from_thumbnails(
593 thumbnails: &[Thumbnail],
594 frame_duration_ms: u64,
595 loop_count: u32,
596 ) -> Option<Self> {
597 if thumbnails.is_empty() || frame_duration_ms == 0 {
598 return None;
599 }
600
601 let width = thumbnails[0].width;
602 let height = thumbnails[0].height;
603 if width == 0 || height == 0 {
604 return None;
605 }
606
607 let mut frames = Vec::with_capacity(thumbnails.len());
608 let mut total_ms = 0u64;
609
610 for thumb in thumbnails {
611 let data = if thumb.width == width && thumb.height == height {
612 thumb.data.clone()
613 } else {
614 scale_thumbnail(&thumb.data, thumb.width, thumb.height, width, height)
615 };
616
617 if data.len() < (width * height * 4) as usize {
618 continue;
619 }
620
621 frames.push(AnimatedFrame {
622 data,
623 duration_ms: frame_duration_ms,
624 source_timestamp_ms: thumb.timestamp_ms,
625 });
626 total_ms += frame_duration_ms;
627 }
628
629 if frames.is_empty() {
630 return None;
631 }
632
633 Some(Self {
634 width,
635 height,
636 frames,
637 total_duration_ms: total_ms,
638 loop_count,
639 })
640 }
641
642 #[must_use]
644 pub fn frame_count(&self) -> usize {
645 self.frames.len()
646 }
647
648 #[must_use]
650 pub fn total_byte_size(&self) -> usize {
651 self.frames.iter().map(|f| f.data.len()).sum()
652 }
653
654 #[must_use]
657 pub fn from_smart_selection(
658 thumbnails: &[Thumbnail],
659 max_frames: usize,
660 frame_duration_ms: u64,
661 loop_count: u32,
662 ) -> Option<Self> {
663 let indices = select_smart_thumbnails(thumbnails, max_frames);
664 if indices.is_empty() {
665 return None;
666 }
667
668 let mut sorted_indices = indices;
670 sorted_indices.sort_by_key(|&i| thumbnails[i].timestamp_ms);
671
672 let selected: Vec<Thumbnail> = sorted_indices
673 .iter()
674 .map(|&i| thumbnails[i].clone())
675 .collect();
676
677 Self::from_thumbnails(&selected, frame_duration_ms, loop_count)
678 }
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
685pub enum ThumbnailQualityProfile {
686 Low,
688 Medium,
690 High,
692}
693
694impl ThumbnailQualityProfile {
695 #[must_use]
697 pub fn jpeg_quality(self) -> u8 {
698 match self {
699 Self::Low => 50,
700 Self::Medium => 75,
701 Self::High => 92,
702 }
703 }
704
705 #[must_use]
707 pub fn dimensions(self) -> (u32, u32) {
708 match self {
709 Self::Low => (120, 68),
710 Self::Medium => (240, 135),
711 Self::High => (480, 270),
712 }
713 }
714
715 #[must_use]
717 pub fn sprite_cols(self) -> u32 {
718 match self {
719 Self::Low => 10,
720 Self::Medium => 5,
721 Self::High => 4,
722 }
723 }
724}
725
726#[derive(Debug, Clone)]
728pub struct ThumbnailExtConfig {
729 pub base: ThumbnailConfig,
731 pub quality_profile: ThumbnailQualityProfile,
733 pub generate_sprite_sheet: bool,
735 pub generate_vtt: bool,
737 pub generate_animated: bool,
739 pub animated_frame_duration_ms: u64,
741 pub animated_max_frames: usize,
743}
744
745impl ThumbnailExtConfig {
746 #[must_use]
748 pub fn from_profile(profile: ThumbnailQualityProfile, count: usize) -> Self {
749 let (w, h) = profile.dimensions();
750 Self {
751 base: ThumbnailConfig {
752 width: w,
753 height: h,
754 format: ThumbnailFormat::Jpeg,
755 quality: profile.jpeg_quality(),
756 count,
757 interval_strategy: ThumbnailStrategy::Uniform,
758 },
759 quality_profile: profile,
760 generate_sprite_sheet: true,
761 generate_vtt: true,
762 generate_animated: false,
763 animated_frame_duration_ms: 200,
764 animated_max_frames: 15,
765 }
766 }
767
768 #[must_use]
770 pub fn with_animated(mut self, max_frames: usize, frame_duration_ms: u64) -> Self {
771 self.generate_animated = true;
772 self.animated_max_frames = max_frames;
773 self.animated_frame_duration_ms = frame_duration_ms;
774 self
775 }
776
777 #[must_use]
779 pub fn without_sprite_sheet(mut self) -> Self {
780 self.generate_sprite_sheet = false;
781 self.generate_vtt = false;
782 self
783 }
784}
785
786#[must_use]
791pub fn generate_vtt_track(
792 sprite_sheet: &SpriteSheet,
793 sprite_url: &str,
794 total_duration_ms: u64,
795) -> String {
796 let mut vtt = String::from("WEBVTT\n\n");
797
798 for (idx, &ts_ms) in sprite_sheet.timestamps_ms.iter().enumerate() {
799 let next_ts_ms = sprite_sheet
800 .timestamps_ms
801 .get(idx + 1)
802 .copied()
803 .unwrap_or(total_duration_ms);
804
805 let (x, y) = sprite_sheet.cell_origin(idx);
806 let start = format_vtt_time(ts_ms);
807 let end = format_vtt_time(next_ts_ms);
808
809 vtt.push_str(&format!(
810 "{start} --> {end}\n{sprite_url}#xywh={x},{y},{w},{h}\n\n",
811 w = sprite_sheet.cell_width,
812 h = sprite_sheet.cell_height,
813 ));
814 }
815
816 vtt
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822
823 #[test]
824 fn test_thumbnail_format_extension() {
825 assert_eq!(ThumbnailFormat::Jpeg.extension(), "jpg");
826 assert_eq!(ThumbnailFormat::Png.extension(), "png");
827 assert_eq!(ThumbnailFormat::Webp.extension(), "webp");
828 }
829
830 #[test]
831 fn test_thumbnail_format_mime_type() {
832 assert_eq!(ThumbnailFormat::Jpeg.mime_type(), "image/jpeg");
833 assert_eq!(ThumbnailFormat::Png.mime_type(), "image/png");
834 assert_eq!(ThumbnailFormat::Webp.mime_type(), "image/webp");
835 }
836
837 #[test]
838 fn test_thumbnail_config_default_web() {
839 let cfg = ThumbnailConfig::default_web();
840 assert_eq!(cfg.width, 320);
841 assert_eq!(cfg.height, 180);
842 assert_eq!(cfg.format, ThumbnailFormat::Jpeg);
843 assert_eq!(cfg.count, 10);
844 assert!(cfg.is_valid());
845 }
846
847 #[test]
848 fn test_thumbnail_config_sprite_sheet() {
849 let cfg = ThumbnailConfig::sprite_sheet(20);
850 assert_eq!(cfg.width, 160);
851 assert_eq!(cfg.height, 90);
852 assert_eq!(cfg.count, 20);
853 assert!(cfg.is_valid());
854 }
855
856 #[test]
857 fn test_thumbnail_pixel_count() {
858 let thumb = Thumbnail::new(0, 160, 90, vec![0; 160 * 90 * 4]);
859 assert_eq!(thumb.pixel_count(), 14400);
860 assert_eq!(thumb.expected_byte_len(), 57600);
861 }
862
863 #[test]
864 fn test_compute_timestamps_at_timestamps() {
865 let strategy = ThumbnailStrategy::AtTimestamps(vec![1000, 2000, 3000]);
866 let ts = compute_thumbnail_timestamps(5000, &strategy, 0.0);
867 assert_eq!(ts, vec![1000, 2000, 3000]);
868 }
869
870 #[test]
871 fn test_compute_timestamps_at_timestamps_filters_out_of_range() {
872 let strategy = ThumbnailStrategy::AtTimestamps(vec![1000, 2000, 9999]);
873 let ts = compute_thumbnail_timestamps(5000, &strategy, 0.0);
874 assert_eq!(ts, vec![1000, 2000]);
875 }
876
877 #[test]
878 fn test_compute_timestamps_zero_duration() {
879 let ts = compute_thumbnail_timestamps(0, &ThumbnailStrategy::Uniform, 24.0);
880 assert!(ts.is_empty());
881 }
882
883 #[test]
884 fn test_compute_uniform_timestamps_count() {
885 let ts = compute_uniform_timestamps(60_000, 5, 0.0);
886 assert_eq!(ts.len(), 5);
887 assert_eq!(ts[0], 0);
889 assert_eq!(ts[4], 60_000);
890 }
891
892 #[test]
893 fn test_compute_uniform_timestamps_single() {
894 let ts = compute_uniform_timestamps(10_000, 1, 0.0);
895 assert_eq!(ts.len(), 1);
896 assert_eq!(ts[0], 5000);
897 }
898
899 #[test]
900 fn test_compute_fixed_interval_timestamps() {
901 let ts = compute_thumbnail_timestamps(30_000, &ThumbnailStrategy::FixedInterval, 0.0);
903 assert_eq!(ts, vec![0, 10_000, 20_000, 30_000]);
904 }
905
906 #[test]
907 fn test_scale_thumbnail_identity() {
908 let src = vec![
910 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
915 let dst = scale_thumbnail(&src, 2, 2, 2, 2);
916 assert_eq!(dst, src);
917 }
918
919 #[test]
920 fn test_scale_thumbnail_upscale() {
921 let src = vec![100u8, 150, 200, 255];
923 let dst = scale_thumbnail(&src, 1, 1, 2, 2);
924 assert_eq!(dst.len(), 16);
925 assert_eq!(&dst[0..4], &[100, 150, 200, 255]);
927 assert_eq!(&dst[4..8], &[100, 150, 200, 255]);
928 }
929
930 #[test]
931 fn test_scale_thumbnail_zero_dimensions() {
932 let src = vec![255u8; 16];
933 assert!(scale_thumbnail(&src, 0, 2, 4, 4).is_empty());
934 assert!(scale_thumbnail(&src, 2, 2, 0, 4).is_empty());
935 }
936
937 #[test]
938 fn test_scale_thumbnail_undersized_src() {
939 let src = vec![255u8; 4]; let dst = scale_thumbnail(&src, 4, 4, 2, 2);
942 assert!(dst.is_empty());
943 }
944
945 #[test]
948 fn test_sprite_sheet_from_thumbnails_empty() {
949 assert!(SpriteSheet::from_thumbnails(&[], 5).is_none());
950 }
951
952 #[test]
953 fn test_sprite_sheet_from_thumbnails_zero_cols() {
954 let thumb = Thumbnail::new(0, 160, 90, vec![0u8; 160 * 90 * 4]);
955 assert!(SpriteSheet::from_thumbnails(&[thumb], 0).is_none());
956 }
957
958 #[test]
959 fn test_sprite_sheet_single_thumbnail() {
960 let data = vec![200u8; 4 * 4 * 4]; let thumb = Thumbnail::new(1000, 4, 4, data.clone());
962 let sheet = SpriteSheet::from_thumbnails(&[thumb], 1).expect("sheet ok");
963
964 assert_eq!(sheet.cols, 1);
965 assert_eq!(sheet.rows, 1);
966 assert_eq!(sheet.sheet_width, 4);
967 assert_eq!(sheet.sheet_height, 4);
968 assert_eq!(sheet.cell_count(), 1);
969 assert_eq!(sheet.timestamps_ms[0], 1000);
970 }
971
972 #[test]
973 fn test_sprite_sheet_four_thumbnails_two_cols() {
974 let data = vec![128u8; 2 * 2 * 4]; let thumbs: Vec<Thumbnail> = (0..4)
976 .map(|i| Thumbnail::new(i * 1000, 2, 2, data.clone()))
977 .collect();
978
979 let sheet = SpriteSheet::from_thumbnails(&thumbs, 2).expect("sheet ok");
980
981 assert_eq!(sheet.cols, 2);
982 assert_eq!(sheet.rows, 2);
983 assert_eq!(sheet.sheet_width, 4); assert_eq!(sheet.sheet_height, 4); assert_eq!(sheet.cell_count(), 4);
986 }
987
988 #[test]
989 fn test_sprite_sheet_cell_origin() {
990 let data = vec![0u8; 10 * 10 * 4];
991 let thumbs: Vec<Thumbnail> = (0..6)
992 .map(|i| Thumbnail::new(i * 1000, 10, 10, data.clone()))
993 .collect();
994 let sheet = SpriteSheet::from_thumbnails(&thumbs, 3).expect("sheet ok");
995
996 assert_eq!(sheet.cell_origin(0), (0, 0));
998 assert_eq!(sheet.cell_origin(1), (10, 0));
1000 assert_eq!(sheet.cell_origin(3), (0, 10));
1002 assert_eq!(sheet.cell_origin(5), (20, 10));
1004 }
1005
1006 #[test]
1007 fn test_sprite_sheet_vtt_basic() {
1008 let data = vec![0u8; 4 * 4 * 4];
1009 let thumbs = vec![
1010 Thumbnail::new(0, 4, 4, data.clone()),
1011 Thumbnail::new(10_000, 4, 4, data.clone()),
1012 ];
1013 let sheet = SpriteSheet::from_thumbnails(&thumbs, 2).expect("sheet ok");
1014 let vtt = sheet.to_vtt("https://cdn.example.com/sprites.jpg");
1015
1016 assert!(vtt.starts_with("WEBVTT\n\n"));
1017 assert!(vtt.contains("xywh=0,0,4,4")); assert!(vtt.contains("xywh=4,0,4,4")); assert!(vtt.contains("00:00:00.000 --> 00:00:10.000"));
1020 assert!(vtt.contains("00:00:10.000 --> 00:00:11.000"));
1021 }
1022
1023 #[test]
1024 fn test_format_vtt_time_basic() {
1025 assert_eq!(format_vtt_time(0), "00:00:00.000");
1026 assert_eq!(format_vtt_time(1_000), "00:00:01.000");
1027 assert_eq!(format_vtt_time(61_500), "00:01:01.500");
1028 assert_eq!(format_vtt_time(3_600_000), "01:00:00.000");
1029 }
1030
1031 #[test]
1032 fn test_format_vtt_time_millis() {
1033 assert_eq!(format_vtt_time(123), "00:00:00.123");
1034 assert_eq!(format_vtt_time(1_234), "00:00:01.234");
1035 }
1036
1037 #[test]
1038 fn test_sprite_sheet_config_default_web() {
1039 let cfg = SpriteSheetConfig::default_web();
1040 assert!(cfg.is_valid());
1041 assert_eq!(cfg.cell_width, 160);
1042 assert_eq!(cfg.cell_height, 90);
1043 assert_eq!(cfg.cols, 5);
1044 assert_eq!(cfg.count, 100);
1045 }
1046
1047 #[test]
1048 fn test_sprite_sheet_config_high_density() {
1049 let cfg = SpriteSheetConfig::high_density(200);
1050 assert!(cfg.is_valid());
1051 assert_eq!(cfg.count, 200);
1052 assert_eq!(cfg.cols, 10);
1053 }
1054
1055 #[test]
1056 fn test_sprite_sheet_config_timestamps() {
1057 let cfg = SpriteSheetConfig::default_web();
1058 let ts = cfg.compute_timestamps(100_000, 24.0);
1060 assert_eq!(ts.len(), cfg.count);
1061 assert!(ts[0] < 1000);
1063 }
1064
1065 #[test]
1066 fn test_sprite_sheet_byte_len() {
1067 let data = vec![255u8; 4 * 4 * 4];
1068 let thumbs = vec![Thumbnail::new(0, 4, 4, data)];
1069 let sheet = SpriteSheet::from_thumbnails(&thumbs, 1).expect("sheet ok");
1070 assert_eq!(sheet.byte_len(), 4 * 4 * 4);
1072 }
1073
1074 #[test]
1075 fn test_sprite_sheet_pixel_composition() {
1076 let red = vec![
1078 255u8, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
1079 ]; let blue = vec![
1081 0u8, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255,
1082 ]; let thumbs = vec![
1085 Thumbnail::new(0, 2, 2, red.clone()),
1086 Thumbnail::new(1000, 2, 2, blue.clone()),
1087 ];
1088 let sheet = SpriteSheet::from_thumbnails(&thumbs, 2).expect("sheet ok");
1089 assert_eq!(sheet.sheet_width, 4);
1090 assert_eq!(sheet.sheet_height, 2);
1091
1092 assert_eq!(sheet.data[0], 255, "R channel at (0,0)");
1094 assert_eq!(sheet.data[1], 0, "G channel at (0,0)");
1095 assert_eq!(sheet.data[2], 0, "B channel at (0,0)");
1096
1097 let blue_offset = 2 * 4; assert_eq!(sheet.data[blue_offset], 0, "R channel at (2,0)");
1100 assert_eq!(sheet.data[blue_offset + 2], 255, "B channel at (2,0)");
1101 }
1102
1103 #[test]
1106 fn test_thumbnail_variance_flat_image() {
1107 let data = vec![128u8, 128, 128, 255].repeat(4); let thumb = Thumbnail::new(0, 2, 2, data);
1110 let v = thumbnail_variance(&thumb);
1111 assert!(v.abs() < 1.0);
1112 }
1113
1114 #[test]
1115 fn test_thumbnail_variance_high_contrast() {
1116 let mut data = Vec::with_capacity(4 * 4);
1118 data.extend_from_slice(&[0, 0, 0, 255]); data.extend_from_slice(&[255, 255, 255, 255]); data.extend_from_slice(&[255, 255, 255, 255]); data.extend_from_slice(&[0, 0, 0, 255]); let thumb = Thumbnail::new(0, 2, 2, data);
1123 let v = thumbnail_variance(&thumb);
1124 assert!(v > 1000.0);
1125 }
1126
1127 #[test]
1128 fn test_thumbnail_variance_empty() {
1129 let thumb = Thumbnail::new(0, 0, 0, Vec::new());
1130 assert!((thumbnail_variance(&thumb)).abs() < 1e-6);
1131 }
1132
1133 #[test]
1134 fn test_select_smart_thumbnails_empty() {
1135 assert!(select_smart_thumbnails(&[], 5).is_empty());
1136 }
1137
1138 #[test]
1139 fn test_select_smart_thumbnails_picks_interesting() {
1140 let flat = vec![128u8, 128, 128, 255].repeat(4); let high_contrast = {
1142 let mut d = Vec::with_capacity(16);
1143 d.extend_from_slice(&[0, 0, 0, 255]);
1144 d.extend_from_slice(&[255, 255, 255, 255]);
1145 d.extend_from_slice(&[255, 255, 255, 255]);
1146 d.extend_from_slice(&[0, 0, 0, 255]);
1147 d
1148 };
1149
1150 let thumbs = vec![
1151 Thumbnail::new(0, 2, 2, flat.clone()),
1152 Thumbnail::new(1000, 2, 2, high_contrast),
1153 Thumbnail::new(2000, 2, 2, flat),
1154 ];
1155
1156 let selected = select_smart_thumbnails(&thumbs, 1);
1157 assert_eq!(selected.len(), 1);
1158 assert_eq!(selected[0], 1); }
1160
1161 #[test]
1162 fn test_select_smart_thumbnails_count_capped() {
1163 let data = vec![128u8, 128, 128, 255].repeat(4);
1164 let thumbs: Vec<Thumbnail> = (0..3)
1165 .map(|i| Thumbnail::new(i * 1000, 2, 2, data.clone()))
1166 .collect();
1167
1168 let selected = select_smart_thumbnails(&thumbs, 10);
1169 assert_eq!(selected.len(), 3); }
1171
1172 #[test]
1175 fn test_animated_thumbnail_empty() {
1176 assert!(AnimatedThumbnail::from_thumbnails(&[], 100, 0).is_none());
1177 }
1178
1179 #[test]
1180 fn test_animated_thumbnail_zero_duration() {
1181 let thumb = Thumbnail::new(0, 2, 2, vec![0u8; 16]);
1182 assert!(AnimatedThumbnail::from_thumbnails(&[thumb], 0, 0).is_none());
1183 }
1184
1185 #[test]
1186 fn test_animated_thumbnail_basic() {
1187 let data = vec![128u8; 4 * 4 * 4]; let thumbs: Vec<Thumbnail> = (0..3)
1189 .map(|i| Thumbnail::new(i * 1000, 4, 4, data.clone()))
1190 .collect();
1191
1192 let anim = AnimatedThumbnail::from_thumbnails(&thumbs, 200, 0)
1193 .expect("should create animated thumbnail");
1194 assert_eq!(anim.frame_count(), 3);
1195 assert_eq!(anim.total_duration_ms, 600);
1196 assert_eq!(anim.width, 4);
1197 assert_eq!(anim.height, 4);
1198 assert!(anim.total_byte_size() > 0);
1199 }
1200
1201 #[test]
1202 fn test_animated_thumbnail_smart_selection() {
1203 let flat = vec![128u8, 128, 128, 255].repeat(4);
1204 let interesting = {
1205 let mut d = Vec::with_capacity(16);
1206 d.extend_from_slice(&[0, 0, 0, 255]);
1207 d.extend_from_slice(&[255, 255, 255, 255]);
1208 d.extend_from_slice(&[200, 100, 50, 255]);
1209 d.extend_from_slice(&[50, 100, 200, 255]);
1210 d
1211 };
1212
1213 let thumbs = vec![
1214 Thumbnail::new(0, 2, 2, flat.clone()),
1215 Thumbnail::new(1000, 2, 2, interesting.clone()),
1216 Thumbnail::new(2000, 2, 2, flat.clone()),
1217 Thumbnail::new(3000, 2, 2, interesting),
1218 Thumbnail::new(4000, 2, 2, flat),
1219 ];
1220
1221 let anim =
1222 AnimatedThumbnail::from_smart_selection(&thumbs, 2, 300, 0).expect("should select");
1223 assert_eq!(anim.frame_count(), 2);
1224 assert_eq!(anim.total_duration_ms, 600);
1225 }
1226
1227 #[test]
1230 fn test_quality_profile_jpeg_quality() {
1231 assert!(
1232 ThumbnailQualityProfile::Low.jpeg_quality()
1233 < ThumbnailQualityProfile::Medium.jpeg_quality()
1234 );
1235 assert!(
1236 ThumbnailQualityProfile::Medium.jpeg_quality()
1237 < ThumbnailQualityProfile::High.jpeg_quality()
1238 );
1239 }
1240
1241 #[test]
1242 fn test_quality_profile_dimensions() {
1243 let (lw, lh) = ThumbnailQualityProfile::Low.dimensions();
1244 let (mw, mh) = ThumbnailQualityProfile::Medium.dimensions();
1245 let (hw, hh) = ThumbnailQualityProfile::High.dimensions();
1246 assert!(lw < mw);
1247 assert!(mw < hw);
1248 assert!(lh < mh);
1249 assert!(mh < hh);
1250 }
1251
1252 #[test]
1253 fn test_thumbnail_ext_config_from_profile() {
1254 let cfg = ThumbnailExtConfig::from_profile(ThumbnailQualityProfile::Medium, 50);
1255 assert_eq!(cfg.base.count, 50);
1256 assert_eq!(cfg.base.width, 240);
1257 assert!(cfg.generate_sprite_sheet);
1258 assert!(cfg.generate_vtt);
1259 assert!(!cfg.generate_animated);
1260 }
1261
1262 #[test]
1263 fn test_thumbnail_ext_config_with_animated() {
1264 let cfg = ThumbnailExtConfig::from_profile(ThumbnailQualityProfile::High, 100)
1265 .with_animated(10, 150);
1266 assert!(cfg.generate_animated);
1267 assert_eq!(cfg.animated_max_frames, 10);
1268 assert_eq!(cfg.animated_frame_duration_ms, 150);
1269 }
1270
1271 #[test]
1272 fn test_thumbnail_ext_config_without_sprite() {
1273 let cfg = ThumbnailExtConfig::from_profile(ThumbnailQualityProfile::Low, 20)
1274 .without_sprite_sheet();
1275 assert!(!cfg.generate_sprite_sheet);
1276 assert!(!cfg.generate_vtt);
1277 }
1278
1279 #[test]
1282 fn test_generate_vtt_track_basic() {
1283 let data = vec![0u8; 4 * 4 * 4];
1284 let thumbs = vec![
1285 Thumbnail::new(0, 4, 4, data.clone()),
1286 Thumbnail::new(5_000, 4, 4, data),
1287 ];
1288 let sheet = SpriteSheet::from_thumbnails(&thumbs, 2).expect("sheet ok");
1289 let vtt = generate_vtt_track(&sheet, "sprites.jpg", 10_000);
1290
1291 assert!(vtt.starts_with("WEBVTT"));
1292 assert!(vtt.contains("00:00:00.000 --> 00:00:05.000"));
1293 assert!(vtt.contains("00:00:05.000 --> 00:00:10.000"));
1294 assert!(vtt.contains("xywh=0,0,4,4"));
1295 assert!(vtt.contains("xywh=4,0,4,4"));
1296 }
1297
1298 #[test]
1299 fn test_generate_vtt_track_single_thumb() {
1300 let data = vec![0u8; 4 * 4 * 4];
1301 let thumbs = vec![Thumbnail::new(0, 4, 4, data)];
1302 let sheet = SpriteSheet::from_thumbnails(&thumbs, 1).expect("sheet ok");
1303 let vtt = generate_vtt_track(&sheet, "s.jpg", 60_000);
1304
1305 assert!(vtt.contains("00:00:00.000 --> 00:01:00.000"));
1306 }
1307}