shellshot/
image_renderer.rs

1use ab_glyph::PxScale;
2use image::RgbaImage;
3use thiserror::Error;
4use unicode_width::UnicodeWidthChar;
5
6use crate::constants::{FONT_SIZE, QUALITY_MULTIPLIER};
7use crate::image_renderer::canvas::Canvas;
8use crate::image_renderer::render_size::{calculate_char_size, calculate_image_size};
9use crate::screen_builder::{Cell, ScreenBuilder};
10use crate::window_decoration::{WindowDecoration, WindowMetrics};
11
12pub mod canvas;
13pub mod render_size;
14
15#[derive(Debug, Error)]
16pub enum ImageRendererError {
17    #[error("Failed to load font")]
18    FontLoadError,
19
20    #[error("Numeric conversion failed: {0}")]
21    Conversion(#[from] std::num::TryFromIntError),
22
23    #[error("Failed to initialize canvas")]
24    CanvasInitFailed,
25
26    #[error("Failed to create final image from raw data")]
27    ImageCreationFailed,
28}
29
30/// `ImageRenderer` is responsible for rendering a `ScreenBuilder` into an image
31/// using the provided window decoration and rendering metrics.
32#[derive(Debug)]
33pub struct ImageRenderer {
34    canvas: Canvas,
35    metrics: WindowMetrics,
36    window_decoration: Box<dyn WindowDecoration>,
37}
38
39impl ImageRenderer {
40    /// Renders a `ScreenBuilder` into an `RgbaImage` using the provided window decoration.
41    ///
42    /// # Arguments
43    ///
44    /// * `screen` - The screen content to render.
45    /// * `window_decoration` - A boxed `WindowDecoration` implementation to draw window chrome.
46    ///
47    /// # Returns
48    ///
49    /// A Result containing the rendered `RgbaImage` or an `ImageRendererError`.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if:
54    /// - Font loading fails
55    /// - Canvas initialization fails
56    /// - Image creation fails
57    pub fn render_image(
58        screen: &ScreenBuilder,
59        window_decoration: Box<dyn WindowDecoration>,
60    ) -> Result<RgbaImage, ImageRendererError> {
61        let mut renderer = Self::create_renderer(screen, window_decoration)?;
62        renderer.compose_image(screen)
63    }
64
65    fn create_renderer(
66        screen: &ScreenBuilder,
67        window_decoration: Box<dyn WindowDecoration>,
68    ) -> Result<Self, ImageRendererError> {
69        let font = window_decoration.font()?;
70        let default_fg_color = window_decoration.default_fg_color();
71
72        let scale = PxScale::from((FONT_SIZE * QUALITY_MULTIPLIER) as f32);
73        let char_size = calculate_char_size(font, scale);
74
75        let metrics = window_decoration.compute_metrics(char_size);
76        let image_size = calculate_image_size(screen, &metrics, char_size);
77        let canvas = Canvas::new(
78            image_size.width,
79            image_size.height,
80            font.clone(),
81            default_fg_color,
82            scale,
83        )?;
84
85        Ok(Self {
86            canvas,
87            metrics,
88            window_decoration,
89        })
90    }
91
92    fn compose_image(&mut self, screen: &ScreenBuilder) -> Result<RgbaImage, ImageRendererError> {
93        self.window_decoration
94            .draw_window(&mut self.canvas, &self.metrics)?;
95
96        self.draw_terminal_content(&screen.cells)?;
97
98        let final_image = self.canvas.to_final_image()?;
99
100        Ok(final_image)
101    }
102
103    fn draw_terminal_content(&mut self, screen: &[Vec<Cell>]) -> Result<(), ImageRendererError> {
104        let start_x = self.metrics.border_width + self.metrics.padding;
105        let start_y =
106            self.metrics.border_width + self.metrics.title_bar_height + self.metrics.padding;
107
108        for (row_idx, line) in screen.iter().enumerate() {
109            let row_idx = u32::try_from(row_idx)?;
110            let y = i32::try_from(start_y + row_idx * self.canvas.char_height())?;
111
112            let mut x_offset = 0;
113            for cell in line {
114                let x = i32::try_from(start_x + x_offset)?;
115
116                self.canvas.draw_char(cell.ch, x, y, cell.fg, cell.bg);
117
118                x_offset += self.canvas.char_width() * u32::try_from(cell.ch.width().unwrap_or(0))?;
119            }
120        }
121
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_render_image_with_mock_screen() {
132        let window_decoration = crate::window_decoration::create_window_decoration(None);
133
134        let screen =
135            ScreenBuilder::from_output("test", "echo test", window_decoration.as_ref()).unwrap();
136
137        let result = ImageRenderer::render_image(&screen, window_decoration);
138
139        assert!(result.is_ok());
140        let image = result.unwrap();
141        assert!(image.width() > 0);
142        assert!(image.height() > 0);
143    }
144}