use std::{
collections::hash_map::DefaultHasher,
fmt::Write,
hash::{Hash, Hasher},
};
use image::{DynamicImage, ImageBuffer, Rgba, imageops};
use ratatui::{
buffer::Buffer,
layout::{Rect, Size},
};
use self::{
halfblocks::Halfblocks,
iterm2::Iterm2,
kitty::{Kitty, StatefulKitty},
sixel::Sixel,
};
use crate::{FontSize, ResizeEncodeRender, Result};
use super::Resize;
pub mod halfblocks;
pub mod iterm2;
pub mod kitty;
pub mod sixel;
pub(crate) trait ProtocolTrait: Send + Sync {
fn render(&self, area: Rect, buf: &mut Buffer);
fn size(&self) -> Size;
}
trait StatefulProtocolTrait: ProtocolTrait {
fn resize_encode(&mut self, img: DynamicImage, size: Size) -> Result<()>;
}
#[derive(Clone)]
pub enum Protocol {
Halfblocks(Halfblocks),
Sixel(Sixel),
Kitty(Kitty),
ITerm2(Iterm2),
}
impl Protocol {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
let inner: &dyn ProtocolTrait = match self {
Self::Halfblocks(halfblocks) => halfblocks,
Self::Sixel(sixel) => sixel,
Self::Kitty(kitty) => kitty,
Self::ITerm2(iterm2) => iterm2,
};
inner.render(area, buf);
}
pub fn size(&self) -> Size {
let inner: &dyn ProtocolTrait = match self {
Self::Halfblocks(halfblocks) => halfblocks,
Self::Sixel(sixel) => sixel,
Self::Kitty(kitty) => kitty,
Self::ITerm2(iterm2) => iterm2,
};
inner.size()
}
pub fn needs_placeholder(&self, area: Rect) -> Option<Rect> {
let image_size = self.size();
if area.width < image_size.width
|| area.height < image_size.height
&& (matches!(self, Self::Sixel(_)) || matches!(self, Self::Halfblocks(_)))
{
let mut placeholder_area = area;
placeholder_area.width = placeholder_area.width.min(image_size.width);
placeholder_area.height = placeholder_area.height.min(image_size.height);
return Some(placeholder_area);
}
None
}
}
pub struct StatefulProtocol {
source: ImageSource,
font_size: FontSize,
hash: u64,
protocol_type: StatefulProtocolType,
last_encoding_result: Option<Result<()>>,
}
#[derive(Clone)]
pub enum StatefulProtocolType {
Halfblocks(Halfblocks),
Sixel(Sixel),
Kitty(StatefulKitty),
ITerm2(Iterm2),
}
impl StatefulProtocolType {
fn inner_trait(&self) -> &dyn StatefulProtocolTrait {
match self {
Self::Halfblocks(halfblocks) => halfblocks,
Self::Sixel(sixel) => sixel,
Self::Kitty(kitty) => kitty,
Self::ITerm2(iterm2) => iterm2,
}
}
fn inner_trait_mut(&mut self) -> &mut dyn StatefulProtocolTrait {
match self {
Self::Halfblocks(halfblocks) => halfblocks,
Self::Sixel(sixel) => sixel,
Self::Kitty(kitty) => kitty,
Self::ITerm2(iterm2) => iterm2,
}
}
}
impl StatefulProtocol {
pub fn new(
image: DynamicImage,
font_size: FontSize,
background_color: Option<Rgba<u8>>,
protocol_type: StatefulProtocolType,
) -> Self {
let source = ImageSource::new(image, font_size, background_color);
Self {
source,
font_size,
hash: u64::default(),
protocol_type,
last_encoding_result: None,
}
}
pub fn size_for(&self, resize: Resize, size: Size) -> Size {
resize.size_for(&self.source.image, self.font_size, size)
}
pub fn protocol_type(&self) -> &StatefulProtocolType {
&self.protocol_type
}
pub fn protocol_type_owned(self) -> StatefulProtocolType {
self.protocol_type
}
pub fn last_encoding_result(&mut self) -> Option<Result<()>> {
self.last_encoding_result.take()
}
pub fn background_color(&self) -> Option<Rgba<u8>> {
self.source.background_color
}
fn last_encoding_area(&self) -> Size {
self.protocol_type.inner_trait().size()
}
}
impl ResizeEncodeRender for StatefulProtocol {
fn resize_encode(&mut self, resize: &Resize, size: Size) {
if size.width == 0 || size.height == 0 {
return;
}
let img = resize.resize(
&self.source.image,
self.font_size,
size,
self.background_color(),
);
let result = self
.protocol_type
.inner_trait_mut()
.resize_encode(img, size);
if result.is_ok() {
self.hash = self.source.hash
}
self.last_encoding_result = Some(result)
}
fn render(&mut self, area: Rect, buf: &mut Buffer) {
self.protocol_type.inner_trait_mut().render(area, buf);
}
fn needs_resize(&self, resize: &Resize, size: Size) -> Option<Size> {
resize.needs_resize(
&self.source.image,
Some(self.source.desired),
self.font_size,
Some(self.last_encoding_area()),
size,
self.source.hash != self.hash,
)
}
}
#[derive(Clone)]
struct ImageSource {
pub image: DynamicImage,
pub desired: Size,
pub hash: u64,
pub background_color: Option<Rgba<u8>>,
}
impl ImageSource {
pub fn new(
mut image: DynamicImage,
font_size: FontSize,
background_color: Option<Rgba<u8>>,
) -> ImageSource {
let desired = Resize::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();
if let Some(background_color) = background_color
&& background_color.0[3] != 0
{
let mut bg: DynamicImage =
ImageBuffer::from_pixel(image.width(), image.height(), background_color).into();
imageops::overlay(&mut bg, &image, 0, 0);
image = bg;
}
ImageSource {
image,
desired,
hash,
background_color,
}
}
}
pub(crate) fn clear_area(data: &mut String, escape: &str, width: u16, height: u16) {
if height == 1 {
write!(data, "{escape}[{width}X").unwrap();
} else {
for _ in 0..height {
write!(data, "{escape}[{width}X{escape}[1B").unwrap();
}
write!(data, "{escape}[{height}A").unwrap();
}
}