shellshot/
image_renderer.rs1use 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#[derive(Debug)]
33pub struct ImageRenderer {
34 canvas: Canvas,
35 metrics: WindowMetrics,
36 window_decoration: Box<dyn WindowDecoration>,
37}
38
39impl ImageRenderer {
40 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}