1 //! The SVG-based implementation of the frame renderer
2 
3 use font_kit::{
4     canvas::{Canvas, Format, RasterizationOptions},
5     hinting::HintingOptions,
6     loaders::freetype::Font,
7     metrics::Metrics,
8 };
9 use imgref::{Img, ImgVec};
10 use lazy_static::lazy_static;
11 use pathfinder_geometry::{
12     transform2d::Transform2F,
13     vector::{Vector2F, Vector2I},
14 };
15 use rgb::{RGBA, RGBA8};
16 
17 use std::iter::FromIterator;
18 use std::sync::Arc;
19 
20 use super::parse_color;
21 use crate::{cli::CropSettings, types::*};
22 
23 lazy_static! {
24     static ref FONT_DATA: Arc<Vec<u8>> = Arc::new(Vec::from_iter(
25         include_bytes!("./fontkit/Hack-Regular.ttf")
26             .iter()
27             .map(Clone::clone)
28     ));
29     static ref FONT_METRICS: Metrics = FONT.with(|f| f.metrics());
30 }
31 
32 thread_local! {
33     // TODO clone the arc instead of cloning the iterator every time
34     static FONT: Font = Font::from_bytes(FONT_DATA.clone(), 0).expect("Could not load font");
35 }
36 
render_frame_to_png(frame: TerminalFrame, crop: Option<CropSettings>) -> RgbaFrame37 pub(crate) fn render_frame_to_png(frame: TerminalFrame, crop: Option<CropSettings>) -> RgbaFrame {
38     flame!(guard "Render Frame To PNG");
39 
40     flame!(start "Init Values");
41     let font_size = 13f32; // TODO make configurable font size
42     let (rows, cols) = frame.screen.size();
43     // TODO: Configurable background color
44     const DEFAULT_BG_COLOR: RGBA8 = RGBA::new(0, 0, 0, 255);
45 
46     let crop_rows = crop.map(|x| x.height).unwrap_or(rows);
47     let crop_cols = crop.map(|x| x.width).unwrap_or(cols);
48     let crop_top = crop.map(|x| x.top).unwrap_or(0);
49     let crop_left = crop.map(|x| x.left).unwrap_or(0);
50 
51     // Glyph rendering config
52     lazy_static! {
53         // static ref TRANS: Transform2F = Transform2F::default();
54         // TODO check hinting settings ( None might be faster with no difference in rendering )
55         static ref HINTING_OPTS: HintingOptions = HintingOptions::Vertical(5.);
56         static ref FORMAT: Format = Format::A8;
57         static ref RASTER_OPTS: RasterizationOptions = RasterizationOptions::GrayscaleAa;
58     }
59 
60     // Get font height and width
61     let raster_rect = FONT
62         .with(|f| {
63             f.raster_bounds(
64                 f.glyph_for_char('A').expect("TODO"),
65                 font_size,
66                 Transform2F::default(),
67                 *HINTING_OPTS,
68                 *RASTER_OPTS,
69             )
70         })
71         .expect("TODO");
72     let font_width = raster_rect.width();
73     let font_height = ((FONT_METRICS.ascent - FONT_METRICS.descent)
74         / FONT_METRICS.units_per_em as f32
75         * font_size)
76         .ceil() as i32;
77     let font_height_offset = (font_height - raster_rect.height()) / 2;
78     let font_transform =
79         Transform2F::from_translation(Vector2F::new(0., -font_height_offset as f32));
80 
81     let height = (crop_rows as i32 * font_height) as usize;
82     let width = (crop_cols as i32 * font_width) as usize;
83 
84     // Image to render to
85     let pixel_count = width * height;
86     let mut pixels: Vec<RGBA8> = Vec::with_capacity(pixel_count);
87     for _ in 0..pixel_count {
88         pixels.push(DEFAULT_BG_COLOR);
89     }
90     let mut image: ImgVec<RGBA8> = Img::new(pixels, width, height);
91     // TODO: Render cursor position
92     let _cursor_position = frame.screen.cursor_position();
93 
94     flame!(end "Init Values");
95 
96     flame!(start "Render Cells");
97     for (row_i, row) in (crop_top..(crop_top + crop_rows)).enumerate() {
98         for (col_i, col) in (crop_left..(crop_left + crop_cols)).enumerate() {
99             let cell = frame.screen.cell(row, col).expect("Error indexing cell");
100             let ypos = row_i as i32 * font_height;
101             let xpos = col_i as i32 * font_width;
102             let mut subimg = image.sub_image_mut(
103                 xpos as usize,
104                 ypos as usize,
105                 font_width as usize,
106                 font_height as usize,
107             );
108 
109             let cell_bg_color = parse_color(cell.bgcolor())
110                 .map(|x| RGBA::new(x.0, x.1, x.2, 255))
111                 .unwrap_or(DEFAULT_BG_COLOR);
112             let cell_fg_color = parse_color(cell.fgcolor())
113                 .map(|x| RGBA::new(x.0, x.1, x.2, 255))
114                 .unwrap_or(RGBA::new(255, 255, 255, 255));
115 
116             let real_bg_color;
117             let real_fg_color;
118             if frame.screen.cursor_position() == (row, col) {
119                 real_fg_color = cell_bg_color;
120                 real_bg_color = cell_fg_color;
121             } else {
122                 real_bg_color = cell_bg_color;
123                 real_fg_color = cell_fg_color;
124             }
125 
126             if real_bg_color != DEFAULT_BG_COLOR {
127                 for pixel in subimg.pixels_mut() {
128                     *pixel = real_bg_color;
129                 }
130             }
131 
132             if cell.has_contents() {
133                 use palette::{Blend, LinSrgba, Pixel};
134                 let mut canvas = Canvas::new(Vector2I::new(font_width, font_height), *FORMAT);
135                 let contents = cell.contents();
136                 if contents == "" {
137                     break;
138                 }
139                 let cell_char: char = contents.parse().expect("Could not parse char");
140 
141                 // TODO: We currently use `.` as a fallback char, but we should use a better one and maybe pick a
142                 // font that supports all the characters used in the TUI-rs demo.
143                 let glyph_id = FONT.with(|f| {
144                     f.glyph_for_char(cell_char)
145                         .unwrap_or_else(|| f.glyph_for_char('.').expect("TODO"))
146                 });
147 
148                 FONT.with(|f| {
149                     f.rasterize_glyph(
150                         &mut canvas,
151                         glyph_id,
152                         font_size as f32,
153                         Transform2F::from_translation(-raster_rect.origin().to_f32())
154                             * font_transform,
155                         *HINTING_OPTS,
156                         *RASTER_OPTS,
157                     )
158                 })
159                 .expect("TODO");
160 
161                 // Alpha `a` over `b`: component wize: a + b * (255 - alpha)
162                 for y in 0..font_height {
163                     let (row_start, row_end) =
164                         (y as usize * canvas.stride, (y + 1) as usize * canvas.stride);
165                     let row = &canvas.pixels[row_start..row_end];
166                     for x in 0..font_width {
167                         let alpha = row[x as usize];
168                         let bg: LinSrgba<f32> = LinSrgba::from_raw(&[
169                             real_bg_color.r,
170                             real_bg_color.g,
171                             real_bg_color.b,
172                             255,
173                         ])
174                         .into_format();
175                         let fg: LinSrgba<f32> = LinSrgba::from_raw(&[
176                             real_fg_color.r,
177                             real_fg_color.g,
178                             real_fg_color.b,
179                             alpha,
180                         ])
181                         .into_format();
182                         let out: [u8; 4] = fg.over(bg).into_format().into_raw();
183                         subimg[(x as usize, y as usize)] = RGBA8::new(out[0], out[1], out[2], 255);
184                     }
185                 }
186             }
187         }
188     }
189     flame!(end "Render Cells");
190 
191     flame!(start "Create Image");
192     // for y in 0..height {
193     //     // flame!(guard "Write Pixel");
194     //     let (row_start, row_end) = (y as usize * canvas.stride, (y + 1) as usize * canvas.stride);
195     //     let row = &canvas.pixels[row_start..row_end];
196     //     for x in 0..width {
197     //         let a = row[x as usize];
198     //         image[(x, y)] = RGBA8::new(a, a, a, 255);
199     //     }
200     // }
201     flame!(end "Create Image");
202 
203     #[cfg(feature = "flamegraph")]
204     flame::dump_html(
205         &mut std::fs::File::create("fontkitrender-flamegraph.gitignore.html").unwrap(),
206     )
207     .unwrap();
208 
209     RgbaFrame {
210         time: frame.time,
211         index: frame.index,
212         image,
213     }
214 }
215