1use crate::core::Camera;
7use crate::plots::Figure;
8use crate::styling::PlotThemeConfig;
9use std::io::Cursor;
10use std::path::Path;
11
12pub struct ImageExporter {
14 settings: ImageExportSettings,
16 theme: Option<PlotThemeConfig>,
18}
19
20#[derive(Debug, Clone)]
22pub struct ImageExportSettings {
23 pub width: u32,
25 pub height: u32,
27 pub samples: u32,
29 pub background_color: [f32; 4],
31 pub quality: f32,
33 pub include_metadata: bool,
35}
36
37#[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 pub async fn new() -> Result<Self, String> {
62 Self::with_settings(ImageExportSettings::default()).await
63 }
64
65 pub async fn with_settings(settings: ImageExportSettings) -> Result<Self, String> {
67 Ok(Self {
68 settings,
69 theme: None,
70 })
71 }
72
73 pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
75 self.theme = Some(theme);
76 }
77
78 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 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 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 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 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 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 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 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 pub fn set_settings(&mut self, settings: ImageExportSettings) {
252 self.settings = settings;
253 }
254
255 pub fn settings(&self) -> &ImageExportSettings {
257 &self.settings
258 }
259}