Skip to main content

oxframe_core/
frame.rs

1//! Frames as pixels, and the one interface through which the editor, the
2//! CLI and the exporter get them: [`FrameSource`]. None of them needs to
3//! know what draws the frames (2D nodes today, other modules later).
4
5/// 8-bit sRGB RGBA, straight alpha, row-major, no padding.
6#[derive(Clone, PartialEq, Eq)]
7pub struct Image {
8    /// Width in pixels.
9    pub width: u32,
10    /// Height in pixels.
11    pub height: u32,
12    /// `width * height * 4` bytes.
13    pub data: Vec<u8>,
14}
15
16impl Image {
17    /// Encodes the image as PNG (8-bit RGBA, straight alpha).
18    ///
19    /// # Panics
20    ///
21    /// If `data` is not `width * height * 4` bytes — a bug in whoever
22    /// built the image.
23    #[must_use]
24    pub fn encode_png(&self) -> Vec<u8> {
25        assert_eq!(
26            self.data.len(),
27            self.width as usize * self.height as usize * 4,
28            "Image data must be width * height * 4 bytes"
29        );
30        let mut out = Vec::new();
31        let mut encoder = png::Encoder::new(&mut out, self.width, self.height);
32        encoder.set_color(png::ColorType::Rgba);
33        encoder.set_depth(png::BitDepth::Eight);
34        let mut writer = encoder.write_header().expect("in-memory PNG header");
35        writer
36            .write_image_data(&self.data)
37            .expect("in-memory PNG data");
38        writer.finish().expect("in-memory PNG");
39        out
40    }
41
42    /// Reads an 8-bit RGBA or RGB PNG, such as one [`encode_png`](Self::encode_png)
43    /// wrote. `None` for anything else or a broken file.
44    #[must_use]
45    pub fn decode_png(bytes: &[u8]) -> Option<Image> {
46        let mut reader = png::Decoder::new(std::io::Cursor::new(bytes))
47            .read_info()
48            .ok()?;
49        let mut buf = vec![0; reader.output_buffer_size()?];
50        let info = reader.next_frame(&mut buf).ok()?;
51        if info.bit_depth != png::BitDepth::Eight {
52            return None;
53        }
54        let data = match info.color_type {
55            png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
56            png::ColorType::Rgb => buf[..info.buffer_size()]
57                .chunks_exact(3)
58                .flat_map(|p| [p[0], p[1], p[2], 255])
59                .collect(),
60            _ => return None,
61        };
62        Some(Image {
63            width: info.width,
64            height: info.height,
65            data,
66        })
67    }
68}
69
70impl std::fmt::Debug for Image {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "Image({}x{})", self.width, self.height)
73    }
74}
75
76/// A timeline that can draw any of its frames: "give me frame `n`".
77///
78/// Frames are addressed by index (time `n / fps`) so that every caller
79/// gets exactly the same pixels for the same frame, in any order.
80pub trait FrameSource {
81    /// Frames per second.
82    fn fps(&self) -> u32;
83    /// Native size in pixels.
84    fn size(&self) -> (u32, u32);
85    /// Length in frames.
86    fn total_frames(&self) -> u32;
87    /// Draws `frame` at `size` pixels (scaled to fit and centered).
88    fn render(&mut self, frame: u32, size: (u32, u32)) -> Image;
89}