use std::{
cmp::{max, min},
marker::PhantomData,
};
use image::{DynamicImage, ImageBuffer, Rgba, imageops};
use protocol::{ImageSource, Protocol};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
pub mod errors;
pub mod picker;
pub mod protocol;
pub mod thread;
pub use image::imageops::FilterType;
type Result<T> = std::result::Result<T, errors::Errors>;
pub type FontSize = (u16, u16);
pub struct Image<'a> {
image: &'a Protocol,
}
impl<'a> Image<'a> {
pub fn new(image: &'a Protocol) -> Self {
Self { image }
}
}
impl Widget for Image<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
self.image.render(area, buf);
}
}
pub trait ResizeEncodeRender {
fn resize_encode_render(&mut self, resize: &Resize, area: Rect, buf: &mut Buffer) {
if let Some(rect) = self.needs_resize(resize, area) {
self.resize_encode(resize, rect);
}
self.render(area, buf);
}
fn resize_encode(&mut self, resize: &Resize, area: Rect);
fn render(&mut self, area: Rect, buf: &mut Buffer);
fn needs_resize(&self, resize: &Resize, area: Rect) -> Option<Rect>;
}
pub struct StatefulImage<T>
where
T: ResizeEncodeRender,
{
resize: Resize,
phantom: PhantomData<T>,
}
impl<T> Default for StatefulImage<T>
where
T: ResizeEncodeRender,
{
fn default() -> Self {
Self::new()
}
}
impl<T> StatefulImage<T>
where
T: ResizeEncodeRender,
{
pub const fn resize(self, resize: Resize) -> Self {
Self { resize, ..self }
}
pub const fn new() -> Self {
Self {
resize: Resize::Fit(None),
phantom: PhantomData,
}
}
}
impl<T> StatefulWidget for StatefulImage<T>
where
T: ResizeEncodeRender,
{
type State = T;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.width == 0 || area.height == 0 {
return;
}
state.resize_encode_render(&self.resize, area, buf);
}
}
#[derive(Debug, Clone)]
pub enum Resize {
Fit(Option<FilterType>),
Crop(Option<CropOptions>),
Scale(Option<FilterType>),
}
impl Default for Resize {
fn default() -> Self {
Self::Fit(None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CropOptions {
pub clip_top: bool,
pub clip_left: bool,
}
impl Resize {
fn resize(
&self,
source: &ImageSource,
font_size: FontSize,
area: Rect,
background_color: Rgba<u8>,
) -> DynamicImage {
let width = (area.width * font_size.0) as u32;
let height = (area.height * font_size.1) as u32;
let mut image = self.resize_image(source, width, height);
if image.width() != width || image.height() != height {
let mut bg: DynamicImage =
ImageBuffer::from_pixel(width, height, background_color).into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
}
image
}
pub fn needs_resize(
&self,
image: &ImageSource,
font_size: FontSize,
current: Rect,
area: Rect,
force: bool,
) -> Option<Rect> {
let desired = image.desired;
if !force
&& !matches!(self, &Resize::Scale(_))
&& desired.width <= area.width
&& desired.height <= area.height
&& desired == current
{
let width = (desired.width * font_size.0) as u32;
let height = (desired.height * font_size.1) as u32;
if image.image.width() == width || image.image.height() == height {
return None;
}
}
let rect = self.render_area(image, font_size, area);
debug_assert!(rect.width <= area.width, "needs_resize exceeds area width");
debug_assert!(
rect.height <= area.height,
"needs_resize exceeds area height"
);
if force || rect != current {
return Some(rect);
}
None
}
pub fn render_area(&self, image: &ImageSource, font_size: FontSize, available: Rect) -> Rect {
let (width, height) = self.needs_resize_pixels(
&image.image,
(available.width as u32) * (font_size.0 as u32),
(available.height as u32) * (font_size.1 as u32),
);
ImageSource::round_pixel_size_to_cells(width, height, font_size)
}
fn resize_image(&self, source: &ImageSource, width: u32, height: u32) -> DynamicImage {
const DEFAULT_FILTER_TYPE: FilterType = FilterType::Nearest;
const DEFAULT_CROP_OPTIONS: CropOptions = CropOptions {
clip_top: false,
clip_left: false,
};
let image = &source.image;
match self {
Self::Fit(filter_type) | Self::Scale(filter_type) => {
image.resize(width, height, filter_type.unwrap_or(DEFAULT_FILTER_TYPE))
}
Self::Crop(options) => {
let options = options.as_ref().unwrap_or(&DEFAULT_CROP_OPTIONS);
let y = if options.clip_top {
image.height().saturating_sub(height)
} else {
0
};
let x = if options.clip_left {
image.width().saturating_sub(width)
} else {
0
};
image.crop_imm(x, y, width, height)
}
}
}
fn needs_resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> (u32, u32) {
match self {
Self::Fit(_) => fit_area_proportionally(
image.width(),
image.height(),
min(width, image.width()),
min(height, image.height()),
),
Self::Crop(_) => (min(image.width(), width), min(image.height(), height)),
Self::Scale(_) => fit_area_proportionally(image.width(), image.height(), width, height),
}
}
}
fn fit_area_proportionally(width: u32, height: u32, nwidth: u32, nheight: u32) -> (u32, u32) {
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;
(u32::MAX, max((height as f64 * ratio).round() as u32, 1))
} else if nh > u64::from(u16::MAX) {
let ratio = u16::MAX as f64 / height as f64;
(max((width as f64 * ratio).round() as u32, 1), u32::MAX)
} else {
(nw as u32, nh as u32)
}
}
#[cfg(test)]
mod tests {
use image::{ImageBuffer, Rgba};
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 _, Rgba::<u8>([255, 0, 0, 255])).into();
ImageSource::new(image, FONT_SIZE, [0, 0, 0, 0].into())
}
fn r(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
#[test]
fn needs_resize_fit() {
let resize = Resize::Fit(None);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(101, 101), FONT_SIZE, r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(80, 100), FONT_SIZE, r(8, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(99, 99), r(8, 10), false);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(99, 99), r(10, 8), false);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(&s(100, 50), FONT_SIZE, r(99, 99), r(4, 4), false);
assert_eq!(Some(r(4, 2)), to);
let to = resize.needs_resize(&s(50, 100), FONT_SIZE, r(99, 99), r(4, 4), false);
assert_eq!(Some(r(2, 4)), to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(8, 8), r(11, 11), false);
assert_eq!(Some(r(10, 10)), to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(10, 10), r(11, 11), false);
assert_eq!(None, to);
}
#[test]
fn needs_resize_crop() {
let resize = Resize::Crop(None);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(10, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(80, 100), FONT_SIZE, r(8, 10), r(10, 10), false);
assert_eq!(None, to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(10, 10), r(8, 10), false);
assert_eq!(Some(r(8, 10)), to);
let to = resize.needs_resize(&s(100, 100), FONT_SIZE, r(10, 10), r(10, 8), false);
assert_eq!(Some(r(10, 8)), to);
}
}