use std::{
cmp::{max, min},
collections::hash_map::DefaultHasher,
error::Error,
hash::{Hash, Hasher},
};
use backend::{FixedBackend, ResizeBackend};
use image::{
imageops::{self, FilterType},
DynamicImage, ImageBuffer, Rgb,
};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
pub mod backend;
pub mod picker;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
pub type FontSize = (u16, u16);
#[derive(Clone)]
pub struct ImageSource {
pub image: DynamicImage,
pub font_size: FontSize,
pub desired: Rect,
pub hash: u64,
}
impl ImageSource {
pub fn new(image: DynamicImage, font_size: FontSize) -> ImageSource {
let desired =
ImageSource::round_pixel_size_to_cells(image.width(), image.height(), font_size);
let mut state = DefaultHasher::new();
image.as_bytes().hash(&mut state);
let hash = state.finish();
ImageSource {
image,
font_size,
desired,
hash,
}
}
fn round_pixel_size_to_cells(
img_width: u32,
img_height: u32,
(char_width, char_height): FontSize,
) -> Rect {
let width = (img_width as f32 / char_width as f32).ceil() as u16;
let height = (img_height as f32 / char_height as f32).ceil() as u16;
Rect::new(0, 0, width, height)
}
}
pub struct FixedImage<'a> {
image: &'a dyn FixedBackend,
}
impl<'a> FixedImage<'a> {
pub fn new(image: &'a dyn FixedBackend) -> FixedImage<'a> {
FixedImage { image }
}
}
impl<'a> Widget for FixedImage<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
self.image.render(area, buf);
}
}
pub struct ResizeImage<'a> {
image: &'a ImageSource,
resize: Resize,
background_color: Option<Rgb<u8>>,
}
impl<'a> ResizeImage<'a> {
pub fn new(image: &'a ImageSource, background_color: Option<Rgb<u8>>) -> ResizeImage<'a> {
ResizeImage {
image,
resize: Resize::Fit,
background_color,
}
}
pub fn resize(mut self, resize: Resize) -> ResizeImage<'a> {
self.resize = resize;
self
}
}
impl<'a> StatefulWidget for ResizeImage<'a> {
type State = Box<dyn ResizeBackend>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.render(self.image, &self.resize, self.background_color, area, buf)
}
}
#[derive(Debug)]
pub enum Resize {
Fit,
Crop,
}
impl Resize {
fn resize(
&self,
source: &ImageSource,
current: Rect,
area: Rect,
background_color: Option<Rgb<u8>>,
force: bool,
) -> Option<(DynamicImage, Rect)> {
self.needs_resize(source, current, area, force).map(|rect| {
let width = (rect.width * source.font_size.0) as u32;
let height = (rect.height * source.font_size.1) as u32;
let mut image = self.resize_image(source, width, height);
if image.width() != width || image.height() != height {
static DEFAULT_BACKGROUND: Rgb<u8> = Rgb([0, 0, 0]);
let color = background_color.unwrap_or(DEFAULT_BACKGROUND);
let mut bg: DynamicImage = ImageBuffer::from_pixel(width, height, color).into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
}
(image, rect)
})
}
fn needs_resize(
&self,
image: &ImageSource,
current: Rect,
area: Rect,
force: bool,
) -> Option<Rect> {
let desired = image.desired;
if desired.width <= area.width && desired.height <= area.height && desired == current {
let width = (desired.width * image.font_size.0) as u32;
let height = (desired.height * image.font_size.1) as u32;
if !force && (image.image.width() == width || image.image.height() == height) {
return None;
}
}
let rect = self.needs_resize_rect(desired, area);
if force || rect != current {
return Some(rect);
}
None
}
fn resize_image(&self, source: &ImageSource, width: u32, height: u32) -> DynamicImage {
match self {
Self::Fit => source.image.resize(width, height, FilterType::Nearest),
Self::Crop => source.image.crop_imm(0, 0, width, height),
}
}
fn needs_resize_rect(&self, desired: Rect, area: Rect) -> Rect {
match self {
Self::Fit => {
let (width, height) = resize_pixels(
desired.width,
desired.height,
min(area.width, desired.width),
min(area.height, desired.height),
);
Rect::new(0, 0, width, height)
}
Self::Crop => Rect::new(
0,
0,
min(desired.width, area.width),
min(desired.height, area.height),
),
}
}
}
fn resize_pixels(width: u16, height: u16, nwidth: u16, nheight: u16) -> (u16, u16) {
let wratio = nwidth as f64 / width as f64;
let hratio = nheight as f64 / height as f64;
let ratio = f64::min(wratio, hratio);
let nw = max((width as f64 * ratio).round() as u64, 1);
let nh = max((height as f64 * ratio).round() as u64, 1);
if nw > u64::from(u16::MAX) {
let ratio = u16::MAX as f64 / width as f64;
(u16::MAX, max((height as f64 * ratio).round() as u16, 1))
} else if nh > u64::from(u16::MAX) {
let ratio = u16::MAX as f64 / height as f64;
(max((width as f64 * ratio).round() as u16, 1), u16::MAX)
} else {
(nw as u16, nh as u16)
}
}
#[cfg(test)]
mod tests {
use image::{ImageBuffer, Rgb};
use super::*;
const FONT_SIZE: FontSize = (10, 10);
fn s(w: u16, h: u16) -> ImageSource {
let image: DynamicImage =
ImageBuffer::from_pixel(w as _, h as _, Rgb::<u8>([255, 0, 0])).into();
ImageSource::new(image, FONT_SIZE)
}
fn r(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
#[test]
fn needs_resize_fit() {
let resize = Resize::Fit;
let to = resize.needs_resize(&s(100, 100), r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(101, 101), r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(80, 100), r(8, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(100, 100), r(99, 99), r(8, 10), false);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(&s(100, 100), r(99, 99), r(10, 8), false);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(&s(100, 50), r(99, 99), r(4, 4), false);
assert_eq!(Some(r(4, 2)), to);
let to = resize.needs_resize(&s(50, 100), r(99, 99), r(4, 4), false);
assert_eq!(Some(r(2, 4)), to);
let to = resize.needs_resize(&s(100, 100), r(8, 8), r(11, 11), false);
assert_eq!(Some(r(10, 10)), to);
let to = resize.needs_resize(&s(100, 100), r(10, 10), r(11, 11), false);
assert_eq!(None, to);
}
#[test]
fn needs_resize_crop() {
let resize = Resize::Crop;
let to = resize.needs_resize(&s(100, 100), r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(80, 100), r(8, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(100, 100), r(10, 10), r(8, 10), false);
assert_eq!(Some(r(8, 10)), to);
let to = resize.needs_resize(&s(100, 100), r(10, 10), r(10, 8), false);
assert_eq!(Some(r(10, 8)), to);
}
}