micropnm/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3/// An enum that represents a PNM image
4#[derive(Clone, Debug)]
5pub enum PNMImage<'a> {
6    /// Binary PPM (P6) image
7    PPMBinary {
8        /// The width of the image
9        width: usize,
10        /// The height of the image
11        height: usize,
12        /// The maximum pixel value of the image
13        maximum_pixel: usize,
14        /// The comment associated with the image
15        comment: &'a str,
16        /// The pixel data of the image
17        pixel_data: &'a [u8],
18    },
19}
20
21use PNMImage::*;
22
23/// Error type that represents the different PNM parsing errors
24#[derive(Debug)]
25pub enum PNMError {
26    /// The file is not in PNM format
27    NotPNMFormat,
28    /// The PNM format is not supported. Right now, only P6 is supported.
29    UnsupportedPNMFormat,
30    /// Error while parsing a UTF-8 encoded string
31    UTF8Error,
32    /// Error while parsing the image
33    ParseError {
34        /// The position of the error
35        pos: usize,
36        /// The byte that was encountered
37        got: u8,
38        /// Contextual information about the error
39        ctx: &'static str,
40    },
41}
42
43use PNMError::*;
44
45impl<'a> PNMImage<'a> {
46
47    /// Parses a PNM image from a byte array
48    ///
49    /// # Arguments
50    ///
51    /// * `bytes` - A byte array containing the PNM image data
52    ///
53    /// # Returns
54    ///
55    /// A Result object containing the parsed PNMImage if successful, otherwise a PNMError
56    pub fn from_parse<const N: usize>(bytes: &'a [u8; N]) -> Result<Self, PNMError> {
57        // magic number P6\n
58        if bytes[0] != b'P' {
59            return Err(NotPNMFormat);
60        }
61        match bytes[1] {
62            b'1' ..= b'5' => return Err(UnsupportedPNMFormat),
63            b'6' => (),
64            _ => return Err(NotPNMFormat)
65        }
66        if bytes[2] != b'\n' {
67            return Err(ParseError {
68                pos: 2,
69                got: bytes[2],
70                ctx: "expected newline.",
71            });
72        }
73        let mut idx = 3;
74
75        // comments
76        while bytes[idx] == b'#' {
77            while bytes[idx] != b'\n' {
78                idx += 1
79            }
80        }
81        let comment = if idx == 3 {
82            ""
83        } else if let Ok(header) = core::str::from_utf8(&bytes[3..idx]) {
84            header
85        } else {
86            return Err(UTF8Error);
87        };
88        idx += 1;
89
90        macro_rules! parse_dec {
91            ($stop:expr) => {{
92                let mut acc = 0;
93                while bytes[idx] != $stop {
94                    if !bytes[idx].is_ascii_digit() {
95                        return Err(ParseError {
96                            pos: idx,
97                            got: bytes[idx],
98                            ctx: "expected digit.",
99                        });
100                    }
101                    acc *= 10;
102                    acc += (bytes[idx] - b'0') as usize;
103
104                    idx += 1;
105                }
106                idx += 1;
107                acc
108            }};
109        }
110
111        // parse <width>SPC<height>\n
112        let width = parse_dec!(b' ');
113        let height = parse_dec!(b'\n');
114        // parse <maximum_pixel>\n
115        let maximum_pixel = parse_dec!(b'\n');
116
117        // rest is raw data
118        let pixel_data = &bytes[idx..N];
119
120        Ok(Self::PPMBinary {
121            width,
122            height,
123            maximum_pixel,
124            comment,
125            pixel_data,
126        })
127    }
128}
129
130impl PNMImage<'_> {
131    /// Returns the width of the PNM image.
132    pub fn width(&self) -> usize {
133        let PPMBinary{width, ..} = *self;
134        width
135    }
136
137    /// Returns the height of the PNM image.
138    pub fn height(&self) -> usize {
139        let PPMBinary{height, ..} = *self;
140        height
141    }
142
143    /// Returns the maximum pixel value of the PNM image.
144    pub fn maximum_pixel(&self) -> usize {
145        let PPMBinary{maximum_pixel, ..} = *self;
146        maximum_pixel
147    }
148
149    /// Returns the comment associated with the PNM image.
150    pub fn comment(&self) -> &str {
151        let PPMBinary{comment, ..} = *self;
152        comment
153    }
154
155    /// Returns the raw pixel bytes data of the PNM image.
156    fn pixel_data(&self) -> &[u8] {
157        let PPMBinary{pixel_data, ..} = *self;
158        pixel_data
159    }
160
161    /// Returns the RGB values of the pixel at the specified (x, y) coordinate.
162    /// Returns `None` if the pixel is outside the bounds of the image.
163    pub fn pixel_rgb(&self, x: usize, y: usize) -> Option<(u8, u8, u8)> {
164        let idx = (x + y * self.width()) * 3;
165        if idx >= self.pixel_data().len() {
166            None
167        } else {
168            Some((
169                self.pixel_data()[idx],
170                self.pixel_data()[idx + 1],
171                self.pixel_data()[idx + 2],
172            ))
173        }
174    }
175}
176
177#[cfg(test)]
178mod test {
179    use super::*;
180
181    #[test]
182    fn test() {
183        let raw_img = include_bytes!("./binary.ppm");
184        let ppm_img = PNMImage::from_parse(raw_img).unwrap();
185
186        assert_eq!(ppm_img.comment(), "# Created by GIMP version 2.10.34 PNM plug-in");
187        assert_eq!(ppm_img.width(), 64, "expecting image width 64");
188        assert_eq!(ppm_img.height(), 64, "expecting image height 64");
189
190        // overflows should be None
191        assert_eq!(ppm_img.pixel_rgb(64, 63), None);
192
193        // corners should be black
194        assert_eq!(ppm_img.pixel_rgb(0, 0), Some((0,0,0)));
195        assert_eq!(ppm_img.pixel_rgb(63, 63), Some((0,0,0)));
196        assert_eq!(ppm_img.pixel_rgb(63, 0), Some((0,0,0)));
197        assert_eq!(ppm_img.pixel_rgb(0, 63), Some((0,0,0)));
198
199        // with an offset, they should be:
200        // GREEN | RED | YELLOW
201        // RED | WHITE | RED
202        // CYAN | RED | BLUE
203        assert_eq!(ppm_img.pixel_rgb(7, 7), Some((0,255,0)));
204        assert_eq!(ppm_img.pixel_rgb(31, 7), Some((255,0,0)));
205        assert_eq!(ppm_img.pixel_rgb(56, 7), Some((255,255,0)));
206
207        assert_eq!(ppm_img.pixel_rgb(7, 31), Some((255,0,0)));
208        assert_eq!(ppm_img.pixel_rgb(31, 31), Some((255,255,255)));
209        assert_eq!(ppm_img.pixel_rgb(56, 31), Some((255,0,0)));
210
211        assert_eq!(ppm_img.pixel_rgb(7, 56), Some((0,255,255)));
212        assert_eq!(ppm_img.pixel_rgb(31, 56), Some((255,0,0)));
213        assert_eq!(ppm_img.pixel_rgb(56, 56), Some((0,0,255)));
214    }
215}