tui_image/
lib.rs

1use failure::Error;
2use image::{imageops::resize, imageops::FilterType, RgbaImage};
3use std::cmp::{max, min};
4use tui::buffer::Buffer;
5use tui::layout::{Alignment, Rect};
6use tui::style::{Color, Style};
7use tui::widgets::{Block, Widget};
8
9pub enum ColorMode {
10	Luma,
11	Rgb,
12}
13
14const BLOCK_LIGHT: char = '\u{2591}';
15const BLOCK_MEDIUM: char = '\u{2592}';
16const BLOCK_DARK: char = '\u{2593}';
17const BLOCK_FULL: char = '\u{2588}';
18
19/// A tui-rs Widget which displays an image.
20pub struct Image<'a> {
21	/// A block to wrap the widget in
22	block: Option<Block<'a>>,
23	/// Widget style
24	style: Style,
25	/// Image to display
26	img: Option<RgbaImage>,
27	/// Function returning image to display
28	img_fn: Option<Box<dyn Fn(usize, usize) -> Result<RgbaImage, Error>>>,
29	/// Color mode
30	color_mode: ColorMode,
31	/// Alignment of the image
32	alignment: Alignment,
33}
34
35impl<'a> Image<'a> {
36	/// Construct an Image widget with a single image.
37	pub fn with_img(img: RgbaImage) -> Image<'a> {
38		Image {
39			block: None,
40			style: Default::default(),
41			img: Some(img),
42			img_fn: None,
43			color_mode: ColorMode::Luma,
44			alignment: Alignment::Center,
45		}
46	}
47
48	/// Construct an Image widget with a function which can be called to obtain an image of the correct size.
49	pub fn with_img_fn(
50		img_fn: impl Fn(usize, usize) -> Result<RgbaImage, Error> + 'static,
51	) -> Image<'a> {
52		Image {
53			block: None,
54			style: Default::default(),
55			img: None,
56			img_fn: Some(Box::new(img_fn)),
57			color_mode: ColorMode::Luma,
58			alignment: Alignment::Center,
59		}
60	}
61
62	/// Set the widget to use the provided block.
63	pub fn block(mut self, block: Block<'a>) -> Image<'a> {
64		self.block = Some(block);
65		self
66	}
67
68	/// Set the color mode used to render the image.
69	pub fn color_mode(mut self, color_mode: ColorMode) -> Image<'a> {
70		self.color_mode = color_mode;
71		self
72	}
73
74	/// Set the widget style.
75	pub fn style(mut self, style: Style) -> Image<'a> {
76		self.style = style;
77		self
78	}
79
80	/// Set the widget alignment.
81	pub fn alignment(mut self, alignment: Alignment) -> Image<'a> {
82		self.alignment = alignment;
83		self
84	}
85
86	fn draw_img(&self, area: Rect, buf: &mut Buffer, img: &RgbaImage) {
87		// TODO: add other fixed colours
88		let bg_rgb = match self.style.bg {
89			Some(Color::Black) => vec![0f32, 0f32, 0f32],
90			Some(Color::White) => vec![1f32, 1f32, 1f32],
91			Some(Color::Rgb(r, g, b)) => {
92				vec![r as f32 / 255f32, g as f32 / 255f32, b as f32 / 255f32]
93			}
94			_ => vec![0f32, 0f32, 0f32],
95		};
96
97		// calc offset
98
99		let ox = max(
100			0,
101			min(
102				area.width as i32 - 1,
103				match self.alignment {
104					Alignment::Center => (area.width as i32 - img.width() as i32) / 2i32,
105					Alignment::Left => 0i32,
106					Alignment::Right => area.width as i32 - img.width() as i32,
107				},
108			),
109		) as u16;
110		let oy = max(
111			0,
112			min(
113				area.height - 1,
114				(area.height - (img.height() / 2) as u16) / 2,
115			),
116		) as u16;
117
118		// draw
119
120		for y in oy..(oy + min((img.height() / 2) as u16, area.height - 1)) {
121			for x in ox..min(ox + img.width() as u16, area.width - 1) {
122				let p = img.get_pixel((x - ox) as u32, 2 * (y - oy) as u32);
123
124				// composite onto background
125				let a = p[3] as f32 / 255.0;
126				let r = p[0] as f32 * a / 255.0 + bg_rgb[0] * (1f32 - a);
127				let g = p[1] as f32 * a / 255.0 + bg_rgb[1] * (1f32 - a);
128				let b = p[2] as f32 * a / 255.0 + bg_rgb[2] * (1f32 - a);
129
130				let cell = buf.get_mut(area.left() + x, area.top() + y);
131
132				match self.color_mode {
133					ColorMode::Luma => {
134						let luma = r * 0.3 + g * 0.59 + b * 0.11;
135						let luma_u8 = (5.0 * luma) as u8;
136						if luma_u8 == 0 {
137							continue;
138						}
139
140						cell.set_char(match luma_u8 {
141							1 => BLOCK_LIGHT,
142							2 => BLOCK_MEDIUM,
143							3 => BLOCK_DARK,
144							_ => BLOCK_FULL,
145						});
146					}
147					ColorMode::Rgb => {
148						cell.set_char(BLOCK_FULL).set_fg(Color::Rgb(
149							(255.0 * r) as u8,
150							(255.0 * g) as u8,
151							(255.0 * b) as u8,
152						));
153					}
154				}
155			}
156		}
157	}
158}
159
160impl<'a> Widget for Image<'a> {
161	fn render(mut self, area: Rect, buf: &mut Buffer) {
162		let area = match self.block.take() {
163			Some(b) => {
164				let inner_area = b.inner(area);
165				b.render(area, buf);
166				inner_area
167			}
168			None => area,
169		};
170
171		if area.width < 1 || area.height < 1 {
172			return;
173		}
174
175		buf.set_style(area, self.style);
176
177		if let Some(ref img) = self.img {
178			if img.width() > area.width as u32 || img.height() / 2 > area.height as u32 {
179				let scaled = resize(
180					img,
181					area.width as u32,
182					2 * area.height as u32,
183					FilterType::Nearest,
184				);
185				self.draw_img(area, buf, &scaled)
186			} else {
187				self.draw_img(area, buf, img)
188			}
189		} else if let Some(ref img_fn) = self.img_fn {
190			if let Ok(img) = img_fn(area.width as usize, 2 * area.height as usize) {
191				self.draw_img(area, buf, &img);
192			}
193		}
194	}
195}