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