shellshot/
image_renderer.rs

1use 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/// `ImageRenderer` is responsible for rendering a `ScreenBuilder` into an image
33/// using the provided window decoration and rendering metrics.
34#[derive(Debug)]
35pub struct ImageRenderer {
36    canvas: Canvas,
37    metrics: WindowMetrics,
38    window_decoration: Box<dyn WindowDecoration>,
39}
40
41impl ImageRenderer {
42    /// Renders a `ScreenBuilder` into an `RgbaImage` using the provided window decoration.
43    ///
44    /// # Arguments
45    ///
46    /// * `screen` - The screen content to render.
47    /// * `window_decoration` - A boxed `WindowDecoration` implementation to draw window chrome.
48    ///
49    /// # Returns
50    ///
51    /// A Result containing the rendered `RgbaImage` or an `ImageRendererError`.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if:
56    /// - Font loading fails
57    /// - Canvas initialization fails
58    /// - Image creation fails
59    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}