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