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