1use crate::core::Camera;
8use crate::plots::Figure;
9use crate::styling::PlotThemeConfig;
10use std::io::Cursor;
11use std::path::Path;
12
13pub struct ImageExporter {
15 settings: ImageExportSettings,
17 theme: Option<PlotThemeConfig>,
19 textmark: Option<String>,
21}
22
23#[derive(Debug, Clone)]
25pub struct ImageExportSettings {
26 pub width: u32,
28 pub height: u32,
30 pub samples: u32,
32 pub background_color: [f32; 4],
34 pub quality: f32,
36 pub include_metadata: bool,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum ImageFormat {
43 Png,
44 Jpeg,
45 WebP,
46 Bmp,
47}
48
49impl Default for ImageExportSettings {
50 fn default() -> Self {
51 Self {
52 width: 800,
53 height: 600,
54 samples: 4,
55 background_color: [1.0, 1.0, 1.0, 1.0],
56 quality: 0.95,
57 include_metadata: true,
58 }
59 }
60}
61
62impl ImageExporter {
63 pub async fn new() -> Result<Self, String> {
65 Self::with_settings(ImageExportSettings::default()).await
66 }
67
68 pub async fn with_settings(settings: ImageExportSettings) -> Result<Self, String> {
70 Ok(Self {
71 settings,
72 theme: None,
73 textmark: None,
74 })
75 }
76
77 pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
79 self.theme = Some(theme);
80 }
81
82 pub fn set_textmark(&mut self, textmark: Option<&str>) {
84 self.textmark = textmark
85 .map(str::trim)
86 .filter(|s| !s.is_empty())
87 .map(ToOwned::to_owned);
88 }
89
90 pub async fn export_png<P: AsRef<Path>>(
92 &self,
93 figure: &mut Figure,
94 path: P,
95 ) -> Result<(), String> {
96 let bytes = self.render_png_bytes(figure).await?;
97 std::fs::write(path, bytes).map_err(|e| format!("Failed to save PNG: {e}"))
98 }
99
100 pub async fn render_png_bytes(&self, figure: &mut Figure) -> Result<Vec<u8>, String> {
102 let width = self.settings.width.max(1);
103 let height = self.settings.height.max(1);
104 let gpu = if let Some(theme) = &self.theme {
105 crate::export::native_surface::render_figure_png_bytes_interactive_and_theme_and_textmark(
106 figure.clone(),
107 width,
108 height,
109 theme.clone(),
110 self.textmark.as_deref(),
111 )
112 .await
113 } else {
114 crate::export::native_surface::render_figure_png_bytes_interactive(
115 figure.clone(),
116 width,
117 height,
118 )
119 .await
120 };
121
122 match gpu {
123 Ok(bytes) => Ok(bytes),
124 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
125 crate::export::cpu_surface::render_figure_png_bytes(
126 figure.clone(),
127 width,
128 height,
129 self.theme.clone(),
130 None,
131 None,
132 self.textmark.as_deref(),
133 )
134 .await
135 }
136 Err(err) => Err(err),
137 }
138 }
139
140 pub async fn render_rgba_bytes(&self, figure: &mut Figure) -> Result<Vec<u8>, String> {
142 let width = self.settings.width.max(1);
143 let height = self.settings.height.max(1);
144 let gpu = if let Some(theme) = &self.theme {
145 crate::export::native_surface::render_figure_rgba_bytes_interactive_and_theme(
146 figure.clone(),
147 width,
148 height,
149 theme.clone(),
150 )
151 .await
152 } else {
153 crate::export::native_surface::render_figure_rgba_bytes_interactive(
154 figure.clone(),
155 width,
156 height,
157 )
158 .await
159 };
160
161 match gpu {
162 Ok(bytes) => Ok(bytes),
163 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
164 crate::export::cpu_surface::render_figure_rgba_bytes(
165 figure.clone(),
166 width,
167 height,
168 self.theme.clone(),
169 None,
170 None,
171 self.textmark.as_deref(),
172 )
173 .await
174 }
175 Err(err) => Err(err),
176 }
177 }
178
179 pub async fn render_png_bytes_with_camera(
181 &self,
182 figure: &mut Figure,
183 camera: &Camera,
184 ) -> Result<Vec<u8>, String> {
185 let width = self.settings.width.max(1);
186 let height = self.settings.height.max(1);
187 let gpu = if let Some(theme) = &self.theme {
188 crate::export::native_surface::render_figure_png_bytes_interactive_with_camera_and_theme_and_textmark(
189 figure.clone(),
190 width,
191 height,
192 camera,
193 theme.clone(),
194 self.textmark.as_deref(),
195 )
196 .await
197 } else {
198 crate::export::native_surface::render_figure_png_bytes_interactive_with_camera(
199 figure.clone(),
200 width,
201 height,
202 camera,
203 )
204 .await
205 };
206
207 match gpu {
208 Ok(bytes) => Ok(bytes),
209 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
210 crate::export::cpu_surface::render_figure_png_bytes(
211 figure.clone(),
212 width,
213 height,
214 self.theme.clone(),
215 Some(camera),
216 None,
217 self.textmark.as_deref(),
218 )
219 .await
220 }
221 Err(err) => Err(err),
222 }
223 }
224
225 pub async fn render_rgba_bytes_with_camera(
227 &self,
228 figure: &mut Figure,
229 camera: &Camera,
230 ) -> Result<Vec<u8>, String> {
231 let width = self.settings.width.max(1);
232 let height = self.settings.height.max(1);
233 let gpu = if let Some(theme) = &self.theme {
234 crate::export::native_surface::render_figure_rgba_bytes_interactive_with_camera_and_theme(
235 figure.clone(),
236 width,
237 height,
238 camera,
239 theme.clone(),
240 )
241 .await
242 } else {
243 crate::export::native_surface::render_figure_rgba_bytes_interactive_with_camera(
244 figure.clone(),
245 width,
246 height,
247 camera,
248 )
249 .await
250 };
251
252 match gpu {
253 Ok(bytes) => Ok(bytes),
254 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
255 crate::export::cpu_surface::render_figure_rgba_bytes(
256 figure.clone(),
257 width,
258 height,
259 self.theme.clone(),
260 Some(camera),
261 None,
262 self.textmark.as_deref(),
263 )
264 .await
265 }
266 Err(err) => Err(err),
267 }
268 }
269
270 pub async fn render_png_bytes_with_axes_cameras(
272 &self,
273 figure: &mut Figure,
274 axes_cameras: &[Camera],
275 ) -> Result<Vec<u8>, String> {
276 let width = self.settings.width.max(1);
277 let height = self.settings.height.max(1);
278 let gpu = if let Some(theme) = &self.theme {
279 crate::export::native_surface::render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark(
280 figure.clone(),
281 width,
282 height,
283 axes_cameras,
284 theme.clone(),
285 self.textmark.as_deref(),
286 )
287 .await
288 } else {
289 crate::export::native_surface::render_figure_png_bytes_interactive_with_axes_cameras(
290 figure.clone(),
291 width,
292 height,
293 axes_cameras,
294 )
295 .await
296 };
297
298 match gpu {
299 Ok(bytes) => Ok(bytes),
300 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
301 crate::export::cpu_surface::render_figure_png_bytes(
302 figure.clone(),
303 width,
304 height,
305 self.theme.clone(),
306 None,
307 Some(axes_cameras),
308 self.textmark.as_deref(),
309 )
310 .await
311 }
312 Err(err) => Err(err),
313 }
314 }
315
316 pub async fn render_rgba_bytes_with_axes_cameras(
318 &self,
319 figure: &mut Figure,
320 axes_cameras: &[Camera],
321 ) -> Result<Vec<u8>, String> {
322 let width = self.settings.width.max(1);
323 let height = self.settings.height.max(1);
324 let gpu = if let Some(theme) = &self.theme {
325 crate::export::native_surface::render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
326 figure.clone(),
327 width,
328 height,
329 axes_cameras,
330 theme.clone(),
331 )
332 .await
333 } else {
334 crate::export::native_surface::render_figure_rgba_bytes_interactive_with_axes_cameras(
335 figure.clone(),
336 width,
337 height,
338 axes_cameras,
339 )
340 .await
341 };
342
343 match gpu {
344 Ok(bytes) => Ok(bytes),
345 Err(err) if crate::export::native_surface::is_headless_gpu_unavailable_error(&err) => {
346 crate::export::cpu_surface::render_figure_rgba_bytes(
347 figure.clone(),
348 width,
349 height,
350 self.theme.clone(),
351 None,
352 Some(axes_cameras),
353 self.textmark.as_deref(),
354 )
355 .await
356 }
357 Err(err) => Err(err),
358 }
359 }
360
361 pub fn encode_png_bytes(&self, data: &[u8]) -> Result<Vec<u8>, String> {
363 use image::{ImageBuffer, ImageOutputFormat, Rgba};
364
365 let image = ImageBuffer::<Rgba<u8>, _>::from_raw(
366 self.settings.width.max(1),
367 self.settings.height.max(1),
368 data.to_vec(),
369 )
370 .ok_or("Failed to create image buffer")?;
371
372 let mut cursor = Cursor::new(Vec::new());
373 image
374 .write_to(&mut cursor, ImageOutputFormat::Png)
375 .map_err(|e| format!("Failed to encode PNG: {e}"))?;
376 Ok(cursor.into_inner())
377 }
378
379 pub fn set_settings(&mut self, settings: ImageExportSettings) {
381 self.settings = settings;
382 }
383
384 pub fn settings(&self) -> &ImageExportSettings {
386 &self.settings
387 }
388}