planetarium/
export.rs

1//! Planetarium
2//! ===========
3//!
4//! Canvas image export support definitions
5//! ---------------------------------------
6//!
7//! Defines an enum for the supported image export formats
8//! and image export methods for `Canvas`.
9//!
10//! Defines a custom error enum type `EncoderError`.
11
12mod raw;
13
14#[cfg(feature = "png")]
15mod png;
16
17use crate::{Canvas, Pixel};
18
19/// Canvas image window coordinates
20///
21/// Defines a rectangular window on the canvas to export the image from.
22///
23/// The window origin is defined by its the upper left corner.
24///
25/// Basic operations
26/// ----------------
27///
28/// ```
29/// use planetarium::Window;
30///
31/// // Create a new rectangular window with origin at (0, 0).
32/// let wnd1 = Window::new(128, 64);
33///
34/// // Move the window origin to (250, 150).
35/// let wnd2 = wnd1.at(250, 150);
36///
37/// // Check the resulting string representation.
38/// assert_eq!(wnd2.to_string(), "(250, 150)+(128, 64)");
39/// ```
40///
41/// Conversions
42/// -----------
43///
44/// ```
45/// # use planetarium::Window;
46/// // From a tuple of tuples representing the origin coordinates
47/// // and window dimensions
48/// let wnd1 = Window::from(((100, 200), (128, 128)));
49///
50/// // Check the resulting string representation.
51/// assert_eq!(wnd1.to_string(), "(100, 200)+(128, 128)");
52/// ```
53#[derive(Debug, Clone, Copy)]
54pub struct Window {
55    /// Window origin X coordinate
56    pub x: u32,
57    /// Window origin Y coordinate
58    pub y: u32,
59    /// Width in X direction
60    pub w: u32,
61    /// Height in Y direction
62    pub h: u32,
63}
64
65/// Exportable canvas image formats
66#[derive(Debug, Clone, Copy)]
67#[non_exhaustive]
68pub enum ImageFormat {
69    // Internal encoders:
70    /// 8-bit gamma-compressed grayscale RAW
71    RawGamma8Bpp,
72    /// 10-bit linear light grayscale little-endian RAW
73    RawLinear10BppLE,
74    /// 12-bit linear light grayscale little-endian RAW
75    RawLinear12BppLE,
76
77    // Require "png" feature:
78    /// 8-bit gamma-compressed grayscale PNG
79    PngGamma8Bpp,
80    /// 16-bit linear light grayscale PNG
81    PngLinear16Bpp,
82}
83
84/// Image export encoder error type
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86#[non_exhaustive]
87pub enum EncoderError {
88    /// Requested image format not supported
89    NotImplemented,
90    /// Requested image window is out of bounds
91    BrokenWindow,
92    /// Requested image subsampling factors are too large or zero
93    InvalidSubsamplingRate,
94}
95
96/// Canvas window image scanlines iterator
97///
98/// Yields the window image pixel spans as `&[Pixel]` slices.
99///
100/// Usage
101/// -----
102///
103/// ```
104/// use planetarium::{Canvas, Window};
105///
106/// let c = Canvas::new(100, 100);
107///
108/// // Define a 10x10 window rectangle with origin at (50, 50).
109/// let wnd = Window::new(10, 10).at(50, 50);
110///
111/// // Iterate over the window image scanlines yielding 10-pixel spans.
112/// for span in c.window_spans(wnd).unwrap() {
113///     // Dummy check
114///     assert_eq!(span, [0u16; 10]);
115/// }
116/// ```
117pub struct WindowSpans<'a> {
118    /// Source canvas object
119    canvas: &'a Canvas,
120
121    /// Canvas window rectangle
122    window: Window,
123
124    /// Current scanline index
125    scanline: u32,
126}
127
128impl<'a> Iterator for WindowSpans<'a> {
129    /// Image pixel span type
130    type Item = &'a [Pixel];
131
132    /// Iterates over the window image scanlines and returns the resulting
133    /// image pixel spans as `&'a [Pixel]`.
134    fn next(&mut self) -> Option<Self::Item> {
135        // Terminate when the current scanline is outside of the window rectangle.
136        if self.scanline >= self.window.y + self.window.h {
137            return None;
138        }
139
140        // Calculate the current pixel span indexes.
141        let base = (self.canvas.width * self.scanline + self.window.x) as usize;
142        let end = base + self.window.w as usize;
143
144        self.scanline += 1;
145
146        Some(&self.canvas.pixbuf[base..end])
147    }
148
149    fn size_hint(&self) -> (usize, Option<usize>) {
150        let size = (self.window.y + self.window.h - self.scanline) as usize;
151
152        (size, Some(size))
153    }
154}
155
156impl<'a> ExactSizeIterator for WindowSpans<'a> {}
157
158impl From<((u32, u32), (u32, u32))> for Window {
159    /// Creates a window from a tuple `((x, y), (w, h))`.
160    fn from(tuple: ((u32, u32), (u32, u32))) -> Self {
161        let ((x, y), (w, h)) = tuple;
162
163        Window { x, y, w, h }
164    }
165}
166
167impl std::fmt::Display for Window {
168    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
169        write!(f, "({}, {})+({}, {})", self.x, self.y, self.w, self.h)
170    }
171}
172
173impl std::fmt::Display for EncoderError {
174    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175        // FIXME: Put full length error descriptions here.
176        write!(f, "{self:?}")
177    }
178}
179
180impl std::error::Error for EncoderError {}
181
182impl Window {
183    /// Creates a new window with given dimensions located at the origin.
184    #[must_use]
185    pub fn new(width: u32, height: u32) -> Self {
186        Window {
187            x: 0,
188            y: 0,
189            w: width,
190            h: height,
191        }
192    }
193
194    /// Moves the window origin to the given origin coordinates.
195    #[must_use]
196    pub fn at(&self, x: u32, y: u32) -> Window {
197        let w = self.w;
198        let h = self.h;
199
200        Window { x, y, w, h }
201    }
202
203    /// Checks if the window rectangle is inside the canvas rectangle.
204    #[must_use]
205    fn is_inside(&self, width: u32, height: u32) -> bool {
206        self.x + self.w <= width && self.y + self.h <= height
207    }
208
209    /// Returns the total number of pixels in the window.
210    #[must_use]
211    fn len(&self) -> usize {
212        (self.w * self.h) as usize
213    }
214}
215
216/// Maximum supported image subsampling factor value
217const MAX_SUBSAMPLING_RATE: u32 = 16;
218
219/// Validates user-provided image subsampling factors
220///
221/// # Errors
222///
223/// Returns [`EncoderError::InvalidSubsamplingRate`] if the image subsampling
224/// factors are too large or zero.
225fn validate_subsampling_rate(factors: (u32, u32)) -> Result<(), EncoderError> {
226    let is_valid = |x| x > 0 && x <= MAX_SUBSAMPLING_RATE;
227
228    if is_valid(factors.0) && is_valid(factors.1) {
229        Ok(())
230    } else {
231        Err(EncoderError::InvalidSubsamplingRate)
232    }
233}
234
235impl Canvas {
236    /// Returns an iterator over the canvas window image scanlines.
237    ///
238    /// The iteration starts from the window origin and goes in the positive Y direction.
239    /// Each window scanline is represented as a pixel span (`&[Pixel]` slice).
240    ///
241    /// # Errors
242    ///
243    /// Returns `None` is the window rectangle origin or dimensions
244    /// are out of the canvas bounds.
245    #[must_use]
246    pub fn window_spans(&self, window: Window) -> Option<WindowSpans<'_>> {
247        if !window.is_inside(self.width, self.height) {
248            return None;
249        }
250
251        let canvas = self;
252
253        // Start iterating from the window origin.
254        let scanline = window.y;
255
256        let iter = WindowSpans {
257            canvas,
258            window,
259            scanline,
260        };
261
262        Some(iter)
263    }
264
265    /// Exports the canvas contents in the requested image format.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`EncoderError::NotImplemented`] if the requested image format
270    /// is not yet supported.
271    #[cfg(not(feature = "png"))]
272    pub fn export_image(&self, format: ImageFormat) -> Result<Vec<u8>, EncoderError> {
273        // Export the entire canvas.
274        let window = Window::new(self.width, self.height);
275
276        match format {
277            ImageFormat::RawGamma8Bpp => self.export_raw8bpp(window),
278            ImageFormat::RawLinear10BppLE => self.export_raw1xbpp::<10>(window),
279            ImageFormat::RawLinear12BppLE => self.export_raw1xbpp::<12>(window),
280            _ => Err(EncoderError::NotImplemented),
281        }
282    }
283
284    /// Exports the canvas window image in the requested image format.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`EncoderError::NotImplemented`] if the requested image format
289    /// is not yet supported.
290    ///
291    /// Returns [`EncoderError::BrokenWindow`] if the window rectangle origin
292    /// or dimensions are out of the canvas bounds.
293    #[cfg(not(feature = "png"))]
294    pub fn export_window_image(
295        &self,
296        window: Window,
297        format: ImageFormat,
298    ) -> Result<Vec<u8>, EncoderError> {
299        if !window.is_inside(self.width, self.height) {
300            return Err(EncoderError::BrokenWindow);
301        }
302
303        match format {
304            ImageFormat::RawGamma8Bpp => self.export_raw8bpp(window),
305            ImageFormat::RawLinear10BppLE => self.export_raw1xbpp::<10>(window),
306            ImageFormat::RawLinear12BppLE => self.export_raw1xbpp::<12>(window),
307            _ => Err(EncoderError::NotImplemented),
308        }
309    }
310
311    /// Exports the subsampled canvas image in the requested image format.
312    ///
313    /// The integer subsampling factors in X and Y directions
314    /// are passed in `factors`.
315    ///
316    /// # Errors
317    ///
318    /// Returns [`EncoderError::NotImplemented`] if the requested image format
319    /// is not yet supported.
320    #[cfg(not(feature = "png"))]
321    pub fn export_subsampled_image(
322        &self,
323        factors: (u32, u32),
324        format: ImageFormat,
325    ) -> Result<Vec<u8>, EncoderError> {
326        validate_subsampling_rate(factors)?;
327
328        match format {
329            ImageFormat::RawGamma8Bpp => self.export_sub_raw8bpp(factors),
330            ImageFormat::RawLinear10BppLE => self.export_sub_raw1xbpp::<10>(factors),
331            ImageFormat::RawLinear12BppLE => self.export_sub_raw1xbpp::<12>(factors),
332            _ => Err(EncoderError::NotImplemented),
333        }
334    }
335
336    /// Exports the canvas contents in the requested image format.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`EncoderError::NotImplemented`] if the requested image format
341    /// is not yet supported.
342    #[cfg(feature = "png")]
343    pub fn export_image(&self, format: ImageFormat) -> Result<Vec<u8>, EncoderError> {
344        // Export the entire canvas.
345        let window = Window::new(self.width, self.height);
346
347        match format {
348            ImageFormat::RawGamma8Bpp => self.export_raw8bpp(window),
349            ImageFormat::RawLinear10BppLE => self.export_raw1xbpp::<10>(window),
350            ImageFormat::RawLinear12BppLE => self.export_raw1xbpp::<12>(window),
351            ImageFormat::PngGamma8Bpp => self.export_png8bpp(window),
352            ImageFormat::PngLinear16Bpp => self.export_png16bpp(window),
353        }
354    }
355
356    /// Exports the canvas window image in the requested image format.
357    ///
358    /// # Errors
359    ///
360    /// Returns [`EncoderError::NotImplemented`] if the requested image format
361    /// is not yet supported.
362    ///
363    /// Returns [`EncoderError::BrokenWindow`] if the window rectangle origin
364    /// or dimensions are out of the canvas bounds.
365    #[cfg(feature = "png")]
366    pub fn export_window_image(
367        &self,
368        window: Window,
369        format: ImageFormat,
370    ) -> Result<Vec<u8>, EncoderError> {
371        if !window.is_inside(self.width, self.height) {
372            return Err(EncoderError::BrokenWindow);
373        }
374
375        match format {
376            ImageFormat::RawGamma8Bpp => self.export_raw8bpp(window),
377            ImageFormat::RawLinear10BppLE => self.export_raw1xbpp::<10>(window),
378            ImageFormat::RawLinear12BppLE => self.export_raw1xbpp::<12>(window),
379            ImageFormat::PngGamma8Bpp => self.export_png8bpp(window),
380            ImageFormat::PngLinear16Bpp => self.export_png16bpp(window),
381        }
382    }
383
384    /// Exports the subsampled canvas image in the requested image format.
385    ///
386    /// The integer subsampling factors in X and Y directions
387    /// are passed in `factors`.
388    ///
389    /// # Errors
390    ///
391    /// Returns [`EncoderError::NotImplemented`] if the requested image format
392    /// is not yet supported.
393    #[cfg(feature = "png")]
394    pub fn export_subsampled_image(
395        &self,
396        factors: (u32, u32),
397        format: ImageFormat,
398    ) -> Result<Vec<u8>, EncoderError> {
399        validate_subsampling_rate(factors)?;
400
401        match format {
402            ImageFormat::RawGamma8Bpp => self.export_sub_raw8bpp(factors),
403            ImageFormat::RawLinear10BppLE => self.export_sub_raw1xbpp::<10>(factors),
404            ImageFormat::RawLinear12BppLE => self.export_sub_raw1xbpp::<12>(factors),
405            ImageFormat::PngGamma8Bpp => self.export_sub_png8bpp(factors),
406            ImageFormat::PngLinear16Bpp => self.export_sub_png16bpp(factors),
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::SpotShape;
415
416    #[cfg(not(feature = "png"))]
417    #[test]
418    fn image_format_error() {
419        let c = Canvas::new(0, 0);
420
421        assert_eq!(
422            c.export_image(ImageFormat::PngGamma8Bpp),
423            Err(EncoderError::NotImplemented)
424        );
425    }
426
427    #[test]
428    fn broken_window_error() {
429        let c = Canvas::new(10, 10);
430        let wnd = Window::new(8, 8).at(5, 5);
431
432        assert_eq!(
433            c.export_window_image(wnd, ImageFormat::RawGamma8Bpp),
434            Err(EncoderError::BrokenWindow)
435        );
436    }
437
438    #[test]
439    fn subsampling_rate_error() {
440        let c = Canvas::new(0, 0);
441
442        assert_eq!(
443            c.export_subsampled_image((1, 0), ImageFormat::RawGamma8Bpp),
444            Err(EncoderError::InvalidSubsamplingRate)
445        );
446        assert_eq!(
447            c.export_subsampled_image((0, 1), ImageFormat::RawLinear10BppLE),
448            Err(EncoderError::InvalidSubsamplingRate)
449        );
450        assert_eq!(
451            c.export_subsampled_image((4, 17), ImageFormat::RawLinear12BppLE),
452            Err(EncoderError::InvalidSubsamplingRate)
453        );
454    }
455
456    #[test]
457    fn window_ops() {
458        let wnd = Window::new(128, 64).at(200, 100);
459
460        assert_eq!(wnd.len(), 128 * 64);
461        assert!(wnd.is_inside(400, 500));
462        assert!(!wnd.is_inside(100, 100));
463        assert!(!wnd.at(300, 100).is_inside(400, 500));
464    }
465
466    #[test]
467    fn get_window_spans() {
468        let mut c = Canvas::new(100, 100);
469
470        c.add_spot((50.75, 50.5), SpotShape::default(), 1.0);
471        c.draw();
472
473        let wnd1 = Window::new(4, 3).at(50, 50);
474
475        let mut vec = Vec::new();
476        for span in c.window_spans(wnd1).unwrap() {
477            vec.extend_from_slice(span);
478        }
479
480        assert_eq!(
481            vec,
482            [542, 18087, 1146, 0, 542, 18087, 1146, 0, 193, 731, 0, 0]
483        );
484
485        let wnd2 = wnd1.at(48, 51);
486
487        vec.clear();
488        for span in c.window_spans(wnd2).unwrap() {
489            vec.extend_from_slice(span);
490        }
491
492        assert_eq!(vec, [0, 0, 542, 18087, 0, 0, 193, 731, 0, 0, 0, 0]);
493    }
494
495    #[test]
496    fn broken_windows() {
497        let c = Canvas::new(100, 100);
498
499        let wnd1 = Window::new(4, 100).at(50, 50);
500        assert!(c.window_spans(wnd1).is_none());
501
502        let wnd2 = Window::new(4, 5).at(100, 100);
503        assert!(c.window_spans(wnd2).is_none());
504
505        let wnd3 = Window::new(1, 1).at(100, 100);
506        assert!(c.window_spans(wnd3).is_none());
507
508        let wnd4 = Window::new(0, 0).at(100, 100);
509        let mut spans = c.window_spans(wnd4).unwrap();
510        assert_eq!(spans.len(), 0);
511        assert!(spans.next().is_none());
512    }
513}