use std::{
cmp::{max, min},
marker::PhantomData,
};
use image::{DynamicImage, ImageBuffer, Rgba, imageops};
use protocol::Protocol;
use ratatui::{
buffer::Buffer,
layout::{Rect, Size},
widgets::{StatefulWidget, Widget},
};
pub mod errors;
pub mod picker;
pub mod protocol;
pub mod sliced;
pub mod thread;
pub use image::imageops::FilterType;
type Result<T> = std::result::Result<T, errors::Errors>;
#[derive(Copy, Clone, Debug)]
pub struct FontSize {
pub width: u16,
pub height: u16,
}
impl FontSize {
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
}
impl From<(u16, u16)> for FontSize {
fn from((width, height): (u16, u16)) -> Self {
Self::new(width, height)
}
}
pub struct Image<'a> {
image: &'a Protocol,
allow_clipping: bool,
}
impl<'a> Image<'a> {
pub fn new(image: &'a Protocol) -> Self {
Self {
image,
allow_clipping: false,
}
}
pub fn allow_clipping(mut self, allow: bool) -> Self {
self.allow_clipping = allow;
self
}
}
impl Widget for Image<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
if !self.allow_clipping
&& (self.image.size().width > area.width || self.image.size().height > area.height)
{
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.into()) {
self.resize_encode(resize, rect);
}
self.render(area, buf);
}
fn resize_encode(&mut self, resize: &Resize, size: Size);
fn render(&mut self, area: Rect, buf: &mut Buffer);
fn needs_resize(&self, resize: &Resize, size: Size) -> Option<Size>;
}
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,
}
const DEFAULT_BACKGROUND: Rgba<u8> = Rgba([0, 0, 0, 0]);
impl Resize {
pub fn resize(
&self,
image: &DynamicImage,
font_size: FontSize,
size: Size,
background_color: Option<Rgba<u8>>,
) -> DynamicImage {
let width = (size.width * font_size.width) as u32;
let height = (size.height * font_size.height) as u32;
let mut image = self.resize_pixels(image, width, height);
if image.width() != width || image.height() != height {
let mut bg: DynamicImage = ImageBuffer::from_pixel(
width,
height,
background_color.unwrap_or(DEFAULT_BACKGROUND),
)
.into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
}
image
}
pub fn size_for(&self, image: &DynamicImage, font_size: FontSize, available: Size) -> Size {
let (width, height) = self.needs_resize_pixels(
image,
(available.width as u32) * (font_size.width as u32),
(available.height as u32) * (font_size.height as u32),
);
Self::round_pixel_size_to_cells(width, height, font_size)
}
pub fn natural_size(image: &DynamicImage, font_size: FontSize) -> Size {
Self::round_pixel_size_to_cells(image.width(), image.height(), font_size)
}
pub(crate) fn needs_resize(
&self,
image: &DynamicImage,
desired: Option<Size>,
font_size: FontSize,
current: Option<Size>,
target: Size,
force: bool,
) -> Option<Size> {
let desired = desired.unwrap_or_else(|| Self::natural_size(image, font_size));
if !force
&& !matches!(self, &Resize::Scale(_))
&& desired.width <= target.width
&& desired.height <= target.height
&& (current.is_none() || current == Some(desired))
{
let width = (desired.width * font_size.width) as u32;
let height = (desired.height * font_size.height) as u32;
if image.width() == width || image.height() == height {
return None;
}
}
let rect = self.size_for(image, font_size, target);
debug_assert!(
rect.width <= target.width,
"needs_resize exceeds area width"
);
debug_assert!(
rect.height <= target.height,
"needs_resize exceeds area height"
);
if force || Some(rect) != current {
return Some(rect);
}
None
}
fn resize_pixels(&self, image: &DynamicImage, width: u32, height: u32) -> DynamicImage {
const DEFAULT_FILTER_TYPE: FilterType = FilterType::Nearest;
const DEFAULT_CROP_OPTIONS: CropOptions = CropOptions {
clip_top: false,
clip_left: false,
};
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 round_pixel_size_to_cells(img_width: u32, img_height: u32, font_size: FontSize) -> Size {
let width = (img_width as f32 / font_size.width as f32).ceil() as u16;
let height = (img_height as f32 / font_size.height as f32).ceil() as u16;
Size::new(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 = FontSize::new(10, 10);
fn s(w: u16, h: u16) -> DynamicImage {
let image: DynamicImage =
ImageBuffer::from_pixel(w as _, h as _, Rgba::<u8>([255, 0, 0, 255])).into();
image
}
fn r(w: u16, h: u16) -> Size {
Size::new(w, h)
}
#[test]
fn needs_resize_fit() {
let resize = Resize::Fit(None);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(10, 10)),
r(10, 10),
false,
);
assert_eq!(None, to);
let to = resize.needs_resize(
&s(101, 101),
None,
FONT_SIZE,
Some(r(10, 10)),
r(10, 10),
false,
);
assert_eq!(None, to);
let to = resize.needs_resize(
&s(80, 100),
None,
FONT_SIZE,
Some(r(8, 10)),
r(10, 10),
false,
);
assert_eq!(None, to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(99, 99)),
r(8, 10),
false,
);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(99, 99)),
r(10, 8),
false,
);
assert_eq!(Some(r(8, 8)), to);
let to = resize.needs_resize(
&s(100, 50),
None,
FONT_SIZE,
Some(r(99, 99)),
r(4, 4),
false,
);
assert_eq!(Some(r(4, 2)), to);
let to = resize.needs_resize(
&s(50, 100),
None,
FONT_SIZE,
Some(r(99, 99)),
r(4, 4),
false,
);
assert_eq!(Some(r(2, 4)), to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(8, 8)),
r(11, 11),
false,
);
assert_eq!(Some(r(10, 10)), to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(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),
None,
FONT_SIZE,
Some(r(10, 10)),
r(10, 10),
false,
);
assert_eq!(None, to);
let to = resize.needs_resize(
&s(80, 100),
None,
FONT_SIZE,
Some(r(8, 10)),
r(10, 10),
false,
);
assert_eq!(None, to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(10, 10)),
r(8, 10),
false,
);
assert_eq!(Some(r(8, 10)), to);
let to = resize.needs_resize(
&s(100, 100),
None,
FONT_SIZE,
Some(r(10, 10)),
r(10, 8),
false,
);
assert_eq!(Some(r(10, 8)), to);
}
}