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