Skip to main content

runmat_plot/export/
image.rs

1//! Image export (PNG and raw RGBA).
2//!
3//! This module routes through `export::native_surface` to produce static image
4//! output that matches interactive/webgpu composition (scene + overlay layout).
5
6use crate::core::Camera;
7use crate::plots::Figure;
8use crate::styling::PlotThemeConfig;
9use std::io::Cursor;
10use std::path::Path;
11
12/// High-fidelity image exporter using the interactive render path.
13pub struct ImageExporter {
14    /// Export settings
15    settings: ImageExportSettings,
16    /// Optional theme override for export composition.
17    theme: Option<PlotThemeConfig>,
18}
19
20/// Image export configuration
21#[derive(Debug, Clone)]
22pub struct ImageExportSettings {
23    /// Output width in pixels
24    pub width: u32,
25    /// Output height in pixels
26    pub height: u32,
27    /// Samples for anti-aliasing (reserved; interactive path currently chooses its own)
28    pub samples: u32,
29    /// Background color [R, G, B, A] (reserved; theme/figure-driven in interactive path)
30    pub background_color: [f32; 4],
31    /// Image quality (0.0-1.0) for lossy formats (reserved)
32    pub quality: f32,
33    /// Include metadata in output (reserved)
34    pub include_metadata: bool,
35}
36
37/// Supported image formats
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum ImageFormat {
40    Png,
41    Jpeg,
42    WebP,
43    Bmp,
44}
45
46impl Default for ImageExportSettings {
47    fn default() -> Self {
48        Self {
49            width: 800,
50            height: 600,
51            samples: 4,
52            background_color: [1.0, 1.0, 1.0, 1.0],
53            quality: 0.95,
54            include_metadata: true,
55        }
56    }
57}
58
59impl ImageExporter {
60    /// Create a new image exporter.
61    pub async fn new() -> Result<Self, String> {
62        Self::with_settings(ImageExportSettings::default()).await
63    }
64
65    /// Create exporter with custom settings.
66    pub async fn with_settings(settings: ImageExportSettings) -> Result<Self, String> {
67        Ok(Self {
68            settings,
69            theme: None,
70        })
71    }
72
73    /// Set the export theme configuration.
74    pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
75        self.theme = Some(theme);
76    }
77
78    /// Export figure to PNG file.
79    pub async fn export_png<P: AsRef<Path>>(
80        &self,
81        figure: &mut Figure,
82        path: P,
83    ) -> Result<(), String> {
84        let bytes = self.render_png_bytes(figure).await?;
85        std::fs::write(path, bytes).map_err(|e| format!("Failed to save PNG: {e}"))
86    }
87
88    /// Render figure into a PNG buffer.
89    pub async fn render_png_bytes(&self, figure: &mut Figure) -> Result<Vec<u8>, String> {
90        if let Some(theme) = &self.theme {
91            crate::export::native_surface::render_figure_png_bytes_interactive_and_theme(
92                figure.clone(),
93                self.settings.width.max(1),
94                self.settings.height.max(1),
95                theme.clone(),
96            )
97            .await
98        } else {
99            crate::export::native_surface::render_figure_png_bytes_interactive(
100                figure.clone(),
101                self.settings.width.max(1),
102                self.settings.height.max(1),
103            )
104            .await
105        }
106    }
107
108    /// Render figure into raw RGBA8 bytes.
109    pub async fn render_rgba_bytes(&self, figure: &mut Figure) -> Result<Vec<u8>, String> {
110        if let Some(theme) = &self.theme {
111            crate::export::native_surface::render_figure_rgba_bytes_interactive_and_theme(
112                figure.clone(),
113                self.settings.width.max(1),
114                self.settings.height.max(1),
115                theme.clone(),
116            )
117            .await
118        } else {
119            crate::export::native_surface::render_figure_rgba_bytes_interactive(
120                figure.clone(),
121                self.settings.width.max(1),
122                self.settings.height.max(1),
123            )
124            .await
125        }
126    }
127
128    /// Render figure into a PNG buffer using an explicit camera override.
129    pub async fn render_png_bytes_with_camera(
130        &self,
131        figure: &mut Figure,
132        camera: &Camera,
133    ) -> Result<Vec<u8>, String> {
134        if let Some(theme) = &self.theme {
135            crate::export::native_surface::render_figure_png_bytes_interactive_with_camera_and_theme(
136                figure.clone(),
137                self.settings.width.max(1),
138                self.settings.height.max(1),
139                camera,
140                theme.clone(),
141            )
142            .await
143        } else {
144            crate::export::native_surface::render_figure_png_bytes_interactive_with_camera(
145                figure.clone(),
146                self.settings.width.max(1),
147                self.settings.height.max(1),
148                camera,
149            )
150            .await
151        }
152    }
153
154    /// Render figure into raw RGBA8 bytes using an explicit camera override.
155    pub async fn render_rgba_bytes_with_camera(
156        &self,
157        figure: &mut Figure,
158        camera: &Camera,
159    ) -> Result<Vec<u8>, String> {
160        if let Some(theme) = &self.theme {
161            crate::export::native_surface::render_figure_rgba_bytes_interactive_with_camera_and_theme(
162                figure.clone(),
163                self.settings.width.max(1),
164                self.settings.height.max(1),
165                camera,
166                theme.clone(),
167            )
168            .await
169        } else {
170            crate::export::native_surface::render_figure_rgba_bytes_interactive_with_camera(
171                figure.clone(),
172                self.settings.width.max(1),
173                self.settings.height.max(1),
174                camera,
175            )
176            .await
177        }
178    }
179
180    /// Render figure into a PNG buffer using per-axes camera overrides.
181    pub async fn render_png_bytes_with_axes_cameras(
182        &self,
183        figure: &mut Figure,
184        axes_cameras: &[Camera],
185    ) -> Result<Vec<u8>, String> {
186        if let Some(theme) = &self.theme {
187            crate::export::native_surface::render_figure_png_bytes_interactive_with_axes_cameras_and_theme(
188                figure.clone(),
189                self.settings.width.max(1),
190                self.settings.height.max(1),
191                axes_cameras,
192                theme.clone(),
193            )
194            .await
195        } else {
196            crate::export::native_surface::render_figure_png_bytes_interactive_with_axes_cameras(
197                figure.clone(),
198                self.settings.width.max(1),
199                self.settings.height.max(1),
200                axes_cameras,
201            )
202            .await
203        }
204    }
205
206    /// Render figure into raw RGBA8 bytes using per-axes camera overrides.
207    pub async fn render_rgba_bytes_with_axes_cameras(
208        &self,
209        figure: &mut Figure,
210        axes_cameras: &[Camera],
211    ) -> Result<Vec<u8>, String> {
212        if let Some(theme) = &self.theme {
213            crate::export::native_surface::render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
214                figure.clone(),
215                self.settings.width.max(1),
216                self.settings.height.max(1),
217                axes_cameras,
218                theme.clone(),
219            )
220            .await
221        } else {
222            crate::export::native_surface::render_figure_rgba_bytes_interactive_with_axes_cameras(
223                figure.clone(),
224                self.settings.width.max(1),
225                self.settings.height.max(1),
226                axes_cameras,
227            )
228            .await
229        }
230    }
231
232    /// Encode RGBA bytes as PNG.
233    pub fn encode_png_bytes(&self, data: &[u8]) -> Result<Vec<u8>, String> {
234        use image::{ImageBuffer, ImageOutputFormat, Rgba};
235
236        let image = ImageBuffer::<Rgba<u8>, _>::from_raw(
237            self.settings.width.max(1),
238            self.settings.height.max(1),
239            data.to_vec(),
240        )
241        .ok_or("Failed to create image buffer")?;
242
243        let mut cursor = Cursor::new(Vec::new());
244        image
245            .write_to(&mut cursor, ImageOutputFormat::Png)
246            .map_err(|e| format!("Failed to encode PNG: {e}"))?;
247        Ok(cursor.into_inner())
248    }
249
250    /// Update export settings.
251    pub fn set_settings(&mut self, settings: ImageExportSettings) {
252        self.settings = settings;
253    }
254
255    /// Get current export settings.
256    pub fn settings(&self) -> &ImageExportSettings {
257        &self.settings
258    }
259}