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 let a = p[3] as f32 / 255.0;
38
39 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
55pub struct Image<'a> {
57 block: Option<Block<'a>>,
59 style: Style,
61 img: Option<RgbaImage>,
63 img_fn: Option<Box<dyn Fn(usize, usize) -> Result<RgbaImage, Error>>>,
65 color_mode: ColorMode,
67 alignment: Alignment,
69}
70
71impl<'a> Image<'a> {
72 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 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 pub fn block(mut self, block: Block<'a>) -> Image<'a> {
100 self.block = Some(block);
101 self
102 }
103
104 pub fn color_mode(mut self, color_mode: ColorMode) -> Image<'a> {
106 self.color_mode = color_mode;
107 self
108 }
109
110 pub fn style(mut self, style: Style) -> Image<'a> {
112 self.style = style;
113 self
114 }
115
116 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 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 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 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}