ratatui_image/protocol/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
//! Protocol backends for the widgets
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use image::{imageops, DynamicImage, ImageBuffer, Rgba};
use ratatui::{buffer::Buffer, layout::Rect};
use crate::FontSize;
use self::{
halfblocks::{Halfblocks, StatefulHalfblocks},
iterm2::{Iterm2, StatefulIterm2},
kitty::{Kitty, StatefulKitty},
sixel::{Sixel, StatefulSixel},
};
use super::Resize;
pub mod halfblocks;
pub mod iterm2;
pub mod kitty;
pub mod sixel;
trait ProtocolTrait: Send + Sync {
/// Render the currently resized and encoded data to the buffer.
fn render(&mut self, area: Rect, buf: &mut Buffer);
// Get the area of the image.
#[allow(dead_code)]
fn area(&self) -> Rect;
}
trait StatefulProtocolTrait: ProtocolTrait {
// Get the background color that fills in when resizing.
fn background_color(&self) -> Rgba<u8>;
/// Check if the current image state would need resizing (grow or shrink) for the given area.
///
/// This can be called by the UI thread to check if this [StatefulProtocol] should be sent off
/// toprotoco
/// some background thread/task to do the resizing and encoding, instead of rendering. The
/// thread should then return the [StatefulProtocol] so that it can be rendered.protoco
fn needs_resize(&mut self, resize: &Resize, area: Rect) -> Option<Rect>;
/// Resize the image and encode it for rendering. The result should be stored statefully so
/// that next call for the given area does not need to redo the work.
///
/// This can be done in a background thread, and the result is stored in this [StatefulProtocol].
fn resize_encode(&mut self, resize: &Resize, background_color: Rgba<u8>, area: Rect);
}
/// A fixed-size image protocol for the [crate::Image] widget.
#[derive(Clone)]
pub enum Protocol {
Halfblocks(Halfblocks),
Sixel(Sixel),
Kitty(Kitty),
ITerm2(Iterm2),
}
impl Protocol {
pub(crate) fn render(&mut self, area: Rect, buf: &mut Buffer) {
let inner: &mut dyn ProtocolTrait = match self {
Self::Halfblocks(halfblocks) => halfblocks,
Self::Sixel(sixel) => sixel,
Self::Kitty(kitty) => kitty,
Self::ITerm2(iterm2) => iterm2,
};
inner.render(area, buf);
}
}
/// A stateful resizing image protocol for the [crate::StatefulImage] widget.
///
/// The [create::thread::ThreadImage] widget also uses this, and is the reason why resizing is
/// split from rendering.
#[derive(Clone)]
pub enum StatefulProtocol {
Halfblocks(StatefulHalfblocks),
Sixel(StatefulSixel),
Kitty(StatefulKitty),
ITerm2(StatefulIterm2),
}
impl StatefulProtocol {
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,
}
}
pub fn background_color(&self) -> Rgba<u8> {
let proto = self.inner_trait();
proto.background_color()
}
/// Resize and encode if necessary, and render immediately.
///
/// This blocks the UI thread but requires neither threads nor async.
pub fn resize_encode_render(
&mut self,
resize: &Resize,
background_color: Rgba<u8>,
area: Rect,
buf: &mut Buffer,
) {
let proto = self.inner_trait_mut();
if let Some(rect) = proto.needs_resize(resize, area) {
proto.resize_encode(resize, background_color, rect);
}
proto.render(area, buf);
}
/// Check if the current image state would need resizing (grow or shrink) for the given area.
///
/// This can be called by the UI thread to check if this [StatefulProtocol] should be sent off
/// to some background thread/task to do the resizing and encoding, instead of rendering. The
/// thread should then return the [StatefulProtocol] so that it can be rendered.protoco
pub fn needs_resize(&mut self, resize: &Resize, area: Rect) -> Option<Rect> {
self.inner_trait_mut().needs_resize(resize, area)
}
/// Resize the image and encode it for rendering. The result should be stored statefully so
/// that next call for the given area does not need to redo the work.
///
/// This can be done in a background thread, and the result is stored in this [StatefulProtocol].
pub fn resize_encode(&mut self, resize: &Resize, background_color: Rgba<u8>, area: Rect) {
self.inner_trait_mut()
.resize_encode(resize, background_color, area)
}
/// Render the currently resized and encoded data to the buffer.
pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
self.inner_trait_mut().render(area, buf);
}
}
#[derive(Clone)]
/// Image source for [crate::protocol::StatefulProtocol]s
///
/// A `[StatefulProtocol]` needs to resize the ImageSource to its state when the available area
/// changes. A `[Protocol]` only needs it once.
///
/// # Examples
/// ```text
/// use image::{DynamicImage, ImageBuffer, Rgb};
/// use ratatui_image::ImageSource;
///
/// let image: ImageBuffer::from_pixel(300, 200, Rgb::<u8>([255, 0, 0])).into();
/// let source = ImageSource::new(image, "filename.png", (7, 14));
/// assert_eq!((43, 14), (source.rect.width, source.rect.height));
/// ```
///
pub struct ImageSource {
/// The original image without resizing.
pub image: DynamicImage,
/// The area that the [`ImageSource::image`] covers, but not necessarily fills.
pub desired: Rect,
/// TODO: document this; when image changes but it doesn't need a resize, force a render.
pub hash: u64,
/// The background color that should be used for padding or background when resizing.
pub background_color: Rgba<u8>,
}
impl ImageSource {
/// Create a new image source
pub fn new(
mut image: DynamicImage,
font_size: FontSize,
background_color: Rgba<u8>,
) -> 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();
// We only need to underlay the background color here if it's not completely transparent.
if 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,
}
}
/// Round an image pixel size to the nearest matching cell size, given a font size.
pub 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)
}
}