use std::time::Duration;
use image::{DynamicImage, GenericImage, imageops::FilterType};
use crate::configuration::ExtractOptions;
use crate::error::UnbundleError;
use crate::unbundle::MediaFile;
use crate::video::FrameRange;
#[derive(Debug, Clone)]
#[must_use]
pub struct ThumbnailOptions {
pub columns: u32,
pub rows: u32,
pub thumbnail_width: u32,
}
impl ThumbnailOptions {
pub fn new(columns: u32, rows: u32) -> Self {
Self {
columns,
rows,
thumbnail_width: 320,
}
}
pub fn with_thumbnail_width(mut self, width: u32) -> Self {
self.thumbnail_width = width;
self
}
pub fn thumbnail_width(self, width: u32) -> Self {
self.with_thumbnail_width(width)
}
}
pub struct ThumbnailHandle;
impl ThumbnailHandle {
pub fn at_timestamp(
unbundler: &mut MediaFile,
timestamp: Duration,
max_dimension: u32,
) -> Result<DynamicImage, UnbundleError> {
log::debug!(
"Generating thumbnail at {:?} (max_dim={})",
timestamp,
max_dimension
);
let image = unbundler.video().frame_at(timestamp)?;
let (width, height) = (image.width(), image.height());
let (thumb_width, thumb_height) = fit_dimensions(width, height, max_dimension);
Ok(image.resize_exact(thumb_width, thumb_height, FilterType::Triangle))
}
pub fn at_frame(
unbundler: &mut MediaFile,
frame_number: u64,
max_dimension: u32,
) -> Result<DynamicImage, UnbundleError> {
let image = unbundler.video().frame(frame_number)?;
let (width, height) = (image.width(), image.height());
let (thumb_width, thumb_height) = fit_dimensions(width, height, max_dimension);
Ok(image.resize_exact(thumb_width, thumb_height, FilterType::Triangle))
}
pub fn grid(
unbundler: &mut MediaFile,
config: &ThumbnailOptions,
) -> Result<DynamicImage, UnbundleError> {
Self::grid_with_options(unbundler, config, &ExtractOptions::default())
}
pub fn grid_with_options(
unbundler: &mut MediaFile,
config: &ThumbnailOptions,
extraction_config: &ExtractOptions,
) -> Result<DynamicImage, UnbundleError> {
log::debug!(
"Generating {}x{} thumbnail grid (thumb_width={})",
config.columns,
config.rows,
config.thumbnail_width
);
let video_metadata = unbundler
.metadata
.video
.as_ref()
.ok_or(UnbundleError::NoVideoStream)?
.clone();
let total_thumbnails = config.columns * config.rows;
let frame_count = video_metadata.frame_count;
let step = if frame_count > total_thumbnails as u64 {
frame_count / total_thumbnails as u64
} else {
1
};
let frame_numbers: Vec<u64> = (0..total_thumbnails as u64)
.map(|index| index * step)
.filter(|number| *number < frame_count)
.collect();
let frames = unbundler
.video()
.frames_with_options(FrameRange::Specific(frame_numbers), extraction_config)?;
let scale_factor = config.thumbnail_width as f64 / video_metadata.width as f64;
let scaled_width = config.thumbnail_width;
let scaled_height = (video_metadata.height as f64 * scale_factor).round() as u32;
let grid_width = scaled_width * config.columns;
let grid_height = scaled_height * config.rows;
let mut grid = DynamicImage::new_rgb8(grid_width, grid_height);
for (index, frame) in frames.iter().enumerate() {
let column = (index as u32) % config.columns;
let row = (index as u32) / config.columns;
if row >= config.rows {
break;
}
let thumbnail = frame.resize_exact(scaled_width, scaled_height, FilterType::Triangle);
let x = column * scaled_width;
let y = row * scaled_height;
let _ = grid.copy_from(&thumbnail, x, y);
}
Ok(grid)
}
pub fn smart(
unbundler: &mut MediaFile,
sample_count: u32,
max_dimension: u32,
) -> Result<DynamicImage, UnbundleError> {
Self::smart_with_options(
unbundler,
sample_count,
max_dimension,
&ExtractOptions::default(),
)
}
pub fn smart_with_options(
unbundler: &mut MediaFile,
sample_count: u32,
max_dimension: u32,
extraction_config: &ExtractOptions,
) -> Result<DynamicImage, UnbundleError> {
log::debug!(
"Generating smart thumbnail (samples={}, max_dim={})",
sample_count,
max_dimension
);
let video_metadata = unbundler
.metadata
.video
.as_ref()
.ok_or(UnbundleError::NoVideoStream)?
.clone();
let frame_count = video_metadata.frame_count;
let count = (sample_count as u64).min(frame_count).max(1);
let step = if frame_count > count {
frame_count / count
} else {
1
};
let frame_numbers: Vec<u64> = (0..count)
.map(|i| i * step)
.filter(|n| *n < frame_count)
.collect();
let frames = unbundler.video().frames_with_options(
FrameRange::Specific(frame_numbers.clone()),
extraction_config,
)?;
let mut best_index = 0;
let mut best_variance: f64 = -1.0;
for (index, frame) in frames.iter().enumerate() {
let variance = pixel_variance(frame);
if variance > best_variance {
best_variance = variance;
best_index = index;
}
}
let best_frame_number = frame_numbers.get(best_index).copied().unwrap_or(0);
let full_image = unbundler.video().frame(best_frame_number)?;
let (width, height) = (full_image.width(), full_image.height());
let (thumb_width, thumb_height) = fit_dimensions(width, height, max_dimension);
Ok(full_image.resize_exact(thumb_width, thumb_height, FilterType::Triangle))
}
}
fn fit_dimensions(width: u32, height: u32, max_dimension: u32) -> (u32, u32) {
if width == 0 || height == 0 {
return (max_dimension, max_dimension);
}
let scale = max_dimension as f64 / width.max(height) as f64;
let new_width = ((width as f64) * scale).round() as u32;
let new_height = ((height as f64) * scale).round() as u32;
(new_width.max(1), new_height.max(1))
}
fn pixel_variance(image: &DynamicImage) -> f64 {
let gray = image.to_luma8();
let pixels = gray.as_raw();
if pixels.is_empty() {
return 0.0;
}
let count = pixels.len() as f64;
let mean: f64 = pixels.iter().map(|&p| p as f64).sum::<f64>() / count;
let variance: f64 = pixels
.iter()
.map(|&p| {
let diff = p as f64 - mean;
diff * diff
})
.sum::<f64>()
/ count;
variance
}