use std::{
path::{Path, PathBuf},
sync::{
Arc, Weak,
atomic::Ordering::Relaxed,
mpsc::{self, Receiver, TryRecvError},
},
task::Poll,
};
use anyhow::Result;
use image::{DynamicImage, Rgba, imageops::FilterType};
use ratatui::layout::Size;
use ratatui_image::FontSize;
use rayon::{ThreadPool, ThreadPoolBuilder};
use crate::{decode, output, search::Progress};
const WORKERS: usize = 2;
#[derive(Clone)]
pub struct Thumb {
pub image: DynamicImage,
pub cells: Size,
}
#[derive(Clone, Copy)]
pub enum Shape {
Fit { area: f32, room: Size },
Fill(Size),
}
impl Shape {
fn side(self, font: FontSize) -> u32 {
match self {
Shape::Fit { room, .. } => output::pixel_side(room, font),
Shape::Fill(cells) => 2 * output::pixel_side(cells, font),
}
}
fn cells(self, image: (u32, u32), font: FontSize) -> Size {
match self {
Shape::Fit { area, room } => image_cells(image, font, area, room),
Shape::Fill(cells) => cells,
}
}
}
pub struct Ticket {
thumb: Receiver<Option<Thumb>>,
_wanted: Arc<()>,
progress: Arc<Progress>,
}
impl Ticket {
pub fn poll(&self) -> Poll<Option<Thumb>> {
let thumb = match self.thumb.try_recv() {
Err(TryRecvError::Empty) => return Poll::Pending,
Ok(thumb) => thumb,
Err(TryRecvError::Disconnected) => None,
};
if thumb.is_none() {
self.progress.errors.fetch_add(1, Relaxed);
}
Poll::Ready(thumb)
}
}
pub struct Prefetch {
pool: ThreadPool,
font: FontSize,
edge: Rgba<u8>,
progress: Arc<Progress>,
}
impl Prefetch {
pub fn new(font: FontSize, edge: Rgba<u8>, progress: &Arc<Progress>) -> Result<Self> {
let pool = ThreadPoolBuilder::new()
.num_threads(WORKERS)
.panic_handler(|_| {})
.build()?;
Ok(Self {
pool,
font,
edge,
progress: progress.clone(),
})
}
pub fn thumb(&self, path: PathBuf, shape: Shape) -> Ticket {
let (tx, rx) = mpsc::sync_channel(1);
let wanted = Arc::new(());
let (font, edge, weak) = (self.font, self.edge, Arc::downgrade(&wanted));
self.pool
.spawn(move || _ = tx.send(thumbnail(&path, shape, font, edge, &weak)));
Ticket {
thumb: rx,
_wanted: wanted,
progress: self.progress.clone(),
}
}
}
fn thumbnail(
path: &Path,
shape: Shape,
font: FontSize,
edge: Rgba<u8>,
wanted: &Weak<()>,
) -> Option<Thumb> {
let alive = || wanted.strong_count() > 0;
if !alive() {
return None;
}
let image = decode::load(path, shape.side(font)).ok()?;
if !alive() {
return None;
}
let cells = shape.cells((image.width(), image.height()), font);
let width = u32::from(cells.width) * u32::from(font.width);
let height = u32::from(cells.height) * u32::from(font.height);
let mut image = image
.resize_to_fill(width, height, FilterType::Triangle)
.into_rgba8();
let line = u32::from(font.width / 5).max(1);
for (x, y, pixel) in image.enumerate_pixels_mut() {
if x < line || y < line || x >= width - line || y >= height - line {
*pixel = edge;
}
}
Some(Thumb {
image: image.into(),
cells,
})
}
fn image_cells(image: (u32, u32), font: FontSize, area: f32, room: Size) -> Size {
let aspect = image.0 as f32 / image.1 as f32 * f32::from(font.height) / f32::from(font.width);
let height = (area / aspect).sqrt();
let width = height * aspect;
let fit = (f32::from(room.width) / width)
.min(f32::from(room.height) / height)
.min(1.0);
let cells = |n: f32, max: u16| (n * fit).round().clamp(1.0, f32::from(max.max(1))) as u16;
Size::new(cells(width, room.width), cells(height, room.height))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_cells_keep_aspect_and_area() {
let font = FontSize::new(10, 20);
let landscape = image_cells((1600, 1200), font, 1200.0, Size::new(200, 50));
assert_eq!(landscape, Size::new(57, 21));
let portrait = image_cells((1200, 1600), font, 1200.0, Size::new(200, 50));
assert_eq!(portrait, Size::new(42, 28));
}
#[test]
fn image_cells_fit_the_room() {
let font = FontSize::new(10, 20);
let room = Size::new(100, 30);
assert_eq!(
image_cells((1600, 1200), font, 1e6, room),
Size::new(80, 30)
);
assert_eq!(
image_cells((1600, 1200), font, 1e6, Size::new(1, 1)),
Size::new(1, 1)
);
}
#[test]
fn fill_takes_the_cells() {
let font = FontSize::new(10, 20);
let shape = Shape::Fill(Size::new(30, 10));
assert_eq!(shape.cells((4000, 1000), font), Size::new(30, 10));
assert_eq!(shape.side(font), 600);
}
}