1 /// The SVG-based implementation of the frame renderer
2 use rgb::{AsPixels, RGBA8};
3 
4 use crate::types::*;
5 
parse_color(color: vt100::Color) -> Option<String>6 fn parse_color(color: vt100::Color) -> Option<String> {
7     let (r, g, b) = super::parse_color(color)?;
8     Some(format!("#{}", base16::encode_lower(&[r, g, b])))
9 }
10 
render_frame_to_svg(frame: &TerminalFrame) -> SvgFrame11 fn render_frame_to_svg(frame: &TerminalFrame) -> SvgFrame {
12     use svg::{
13         node::{
14             element::{Rectangle, Text},
15             Text as TextNode,
16         },
17         Document,
18     };
19 
20     // Set the size of the terminal cells
21     // TODO: Make this dynamic based on the font and font-size
22     let font_size = 10;
23     let cell_width = 6;
24     let cell_height = font_size;
25 
26     // Get the size of the terminal screen
27     let (rows, cols) = frame.screen.size();
28     let doc_height = rows * cell_height;
29     let doc_width = cols * cell_width;
30 
31     // Create the svg document
32     let mut doc = Document::new()
33         .set("viewbox", (0, 0, doc_width, doc_height))
34         .set("height", doc_height)
35         .set("width", doc_width);
36 
37     // TODO: Allow custom
38     let background_color = "#000000";
39     let foreground_color = "#ffffff";
40 
41     // Draw the terminal background
42     doc = doc.add(
43         Rectangle::new()
44             .set(
45                 "style",
46                 format!(
47                     "fill:{bgcolor};fill-opacity:1;stroke:none",
48                     bgcolor = background_color
49                 ),
50             )
51             .set("x", "0")
52             .set("y", "0")
53             .set("width", doc_width)
54             .set("height", doc_height),
55     );
56 
57     // Iterate through each cell
58     for row in 0..rows {
59         for col in 0..cols {
60             // Get the cell
61             let cell = frame.screen.cell(row, col).unwrap_or_else(|| {
62                 panic!(
63                     "Missing cell at position ({}, {}) in frame at {}",
64                     row, col, frame.time
65                 )
66             });
67 
68             // If the cell has a background color
69             if let Some(bg_color) = parse_color(cell.bgcolor()) {
70                 doc = doc.add(
71                     Rectangle::new()
72                         .set("x", (col * cell_width).to_string())
73                         .set("y", (row * cell_height).to_string())
74                         .set("width", cell_width.to_string())
75                         .set("height", cell_height.to_string())
76                         .set(
77                             "style",
78                             format!(
79                                 "fill:{bgcolor};fill-opacity:1;stroke:none",
80                                 bgcolor = bg_color
81                             ),
82                         ),
83                 );
84             }
85             // If the cell is not empty
86             let contents = cell.contents();
87             if contents != "" && contents != " " {
88                 let text_color =
89                     parse_color(cell.fgcolor()).unwrap_or_else(|| foreground_color.into());
90                 // Add the cell's text to the SVG
91                 doc = doc.add(
92                     Text::new()
93                         .add(TextNode::new(contents))
94                         .set("x", (col * cell_width).to_string())
95                         .set(
96                             "y",
97                             ((row + 1) * cell_height - 3/* TODO: Fix for text position */)
98                                 .to_string(),
99                         )
100                         .set("width", cell_width.to_string())
101                         .set("height", cell_height.to_string())
102                         .set(
103                             "style",
104                             format!(
105                                 "font-size: {font_size}px; \
106                                 font-family: monospace; \
107                                 fill: {color};",
108                                 // font = font_family,
109                                 font_size = font_size,
110                                 color = text_color,
111                             ),
112                         ),
113                 );
114             }
115         }
116     }
117 
118     // std::fs::create_dir_all("out-svg.gitignore").expect("TODO");
119     // svg::save(format!("out-svg.gitignore/{}.svg", frame.time), &doc).expect("TODO");
120 
121     SvgFrame {
122         index: frame.index,
123         time: frame.time,
124         doc,
125         width: doc_width,
126         height: doc_height,
127     }
128 }
129 
render_frame_to_png(frame: TerminalFrame) -> RgbaFrame130 pub(crate) fn render_frame_to_png(frame: TerminalFrame) -> RgbaFrame {
131     use resvg::prelude::*;
132     // Get the SVG render of the frame
133     let svg_doc = render_frame_to_svg(&frame);
134 
135     let opt = resvg::Options::default();
136     let rtree = usvg::Tree::from_str(&svg_doc.doc.to_string(), &opt.usvg).expect("TODO");
137     let backend = resvg::default_backend();
138     let mut img = backend.render_to_image(&rtree, &opt).expect("TODO");
139 
140     // std::fs::create_dir_all("out-png.gitignore").expect("TODO");
141     // img.save_png(&std::path::PathBuf::from(format!(
142     //     "out-png.gitignore/{}.png",
143     //     frame.time
144     // )));
145 
146     // Collect image
147     let rgba8_pixels = img.make_rgba_vec();
148     let rgba8_pixels: Vec<RGBA8> = rgba8_pixels
149         .as_slice()
150         .as_pixels()
151         .iter()
152         .map(Clone::clone)
153         .collect();
154 
155     RgbaFrame {
156         time: frame.time,
157         index: frame.index,
158         image: imgref::Img::new(
159             rgba8_pixels,
160             // TODO: avoid using `as`
161             svg_doc.width as usize,
162             svg_doc.height as usize,
163         ),
164     }
165 }
166