shellshot/
image_renderer.rs1use ab_glyph::PxScale;
2use image::RgbaImage;
3use termwiz::surface::Surface;
4use thiserror::Error;
5use tracing::info;
6use unicode_width::UnicodeWidthChar;
7
8use crate::constants::{FONT_SIZE, IMAGE_QUALITY_MULTIPLIER};
9use crate::image_renderer::canvas::Canvas;
10use crate::image_renderer::render_size::{calculate_char_size, calculate_image_size};
11use crate::window_decoration::{WindowDecoration, WindowMetrics};
12
13pub mod canvas;
14pub mod render_size;
15mod utils;
16
17#[derive(Debug, Error)]
18pub enum ImageRendererError {
19 #[error("Failed to load font")]
20 FontLoadError,
21
22 #[error("Numeric conversion failed: {0}")]
23 Conversion(#[from] std::num::TryFromIntError),
24
25 #[error("Failed to initialize canvas")]
26 CanvasInitFailed,
27
28 #[error("Failed to create final image from raw data")]
29 ImageCreationFailed,
30}
31
32#[derive(Debug)]
35pub struct ImageRenderer {
36 canvas: Canvas,
37 metrics: WindowMetrics,
38 window_decoration: Box<dyn WindowDecoration>,
39}
40
41impl ImageRenderer {
42 pub fn render_image(
60 command: &[String],
61 screen: &Surface,
62 window_decoration: Box<dyn WindowDecoration>,
63 ) -> Result<RgbaImage, ImageRendererError> {
64 let mut renderer = Self::create_renderer(command, screen, window_decoration)?;
65 renderer.compose_image(command, screen)
66 }
67
68 fn create_renderer(
69 command: &[String],
70 screen: &Surface,
71 window_decoration: Box<dyn WindowDecoration>,
72 ) -> Result<Self, ImageRendererError> {
73 let font = window_decoration.font()?;
74
75 let scale = PxScale::from((FONT_SIZE * IMAGE_QUALITY_MULTIPLIER) as f32);
76 let char_size = calculate_char_size(&font.regular, scale);
77 let command_line = window_decoration.build_command_line(&command.join(" "));
78
79 let metrics = window_decoration.compute_metrics(char_size);
80 let image_size = calculate_image_size(&command_line, screen, &metrics, char_size);
81 let canvas = Canvas::new(image_size.width, image_size.height, font.clone(), scale)?;
82
83 Ok(Self {
84 canvas,
85 metrics,
86 window_decoration,
87 })
88 }
89
90 fn compose_image(
91 &mut self,
92 command: &[String],
93 screen: &Surface,
94 ) -> Result<RgbaImage, ImageRendererError> {
95 self.window_decoration
96 .draw_window(&mut self.canvas, &self.metrics)?;
97
98 self.draw_command_line(command)?;
99
100 self.draw_terminal_content(screen)?;
101
102 info!("Rendering final screenshot...");
103
104 let final_image = self.canvas.to_final_image()?;
105
106 Ok(final_image)
107 }
108
109 fn draw_command_line(&mut self, command: &[String]) -> Result<(), ImageRendererError> {
110 let start_x = self.metrics.border_width + self.metrics.padding;
111 let start_y =
112 self.metrics.border_width + self.metrics.title_bar_height + self.metrics.padding;
113
114 let color_palette = self.window_decoration.get_color_palette();
115
116 let command_line = self
117 .window_decoration
118 .build_command_line(&command.join(" "));
119
120 let y = i32::try_from(start_y)?;
121 let mut x_offset = 0;
122 for cell in &command_line {
123 let x = i32::try_from(start_x + x_offset)?;
124
125 let text = cell.str();
126
127 self.canvas
128 .draw_text(text, x, y, &color_palette, cell.attrs());
129
130 let text_width = text
131 .chars()
132 .map(|ch| ch.width().unwrap_or(0))
133 .sum::<usize>();
134 x_offset += self.canvas.char_width() * u32::try_from(text_width)?;
135 }
136
137 Ok(())
138 }
139
140 fn draw_terminal_content(&mut self, screen: &Surface) -> Result<(), ImageRendererError> {
141 let start_x = self.metrics.border_width + self.metrics.padding;
142 let start_y =
143 self.metrics.border_width + self.metrics.title_bar_height + self.metrics.padding;
144
145 let color_palette = self.window_decoration.get_color_palette();
146
147 for (row_idx, line) in screen.screen_lines().iter().enumerate() {
148 let row_idx = u32::try_from(row_idx + 1)?;
149 let y = i32::try_from(start_y + row_idx * self.canvas.char_height())?;
150
151 let mut x_offset = 0;
152 for cell in line.visible_cells() {
153 let x = i32::try_from(start_x + x_offset)?;
154
155 let text = cell.str();
156
157 self.canvas
158 .draw_text(text, x, y, &color_palette, cell.attrs());
159
160 let text_width = text
161 .chars()
162 .map(|ch| ch.width().unwrap_or(0))
163 .sum::<usize>();
164 x_offset += self.canvas.char_width() * u32::try_from(text_width)?;
165 }
166 }
167
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use termwiz::surface::Change;
175
176 use crate::window_decoration::create_window_decoration;
177
178 use super::*;
179
180 fn create_mock_surface() -> Surface {
181 let mut surface = Surface::new(10, 5);
182 surface.add_change(Change::Text("echo test".to_string()));
183 surface
184 }
185
186 #[test]
187 fn test_render_image_with_mock_screen() {
188 let window_decoration = create_window_decoration(None);
189
190 let surface = create_mock_surface();
191
192 let command = vec!["echo".to_string(), "test".to_string()];
193
194 let result = ImageRenderer::render_image(&command, &surface, window_decoration);
195
196 assert!(result.is_ok(), "ImageRenderer failed to render mock screen");
197
198 let image = result.unwrap();
199
200 assert!(image.width() > 0, "Rendered image width should be > 0");
201 assert!(image.height() > 0, "Rendered image height should be > 0");
202 }
203}