devela/media/image/
pnm.rs

1// devela::media::image::pnm
2//
3//! PNM is the portable anymap format Netpbm image format family (PBM, PGM, PPM)
4
5#![allow(unused, reason = "WIP")]
6
7#[cfg(feature = "alloc")]
8use crate::text::String;
9#[cfg(doc)]
10use crate::ImageError::FmtError;
11use crate::{
12    FmtWrite, ImageError,
13    ImageError::{InvalidImageSize, InvalidPixel},
14    ImageResult as Result, Mem,
15};
16
17// Helper function: Returns `InvalidPixel` as the cold path.
18#[cold] #[rustfmt::skip]
19const fn invalid_pixel<T>() -> crate::Result<T, ImageError> { Err(InvalidPixel) }
20
21#[doc = crate::TAG_NAMESPACE!()]
22/// A collection of methods for encoding and decoding
23/// <abbr title="Portable anymap format">PNM</abbr> bitmap formats.
24///
25/// - <https://en.wikipedia.org/wiki/Netpbm>
26///
27/// The PBM (Portable Bitmap), PGM (Portable Graymap), and PPM (Portable Pixmap)
28/// formats are part of the Netpbm format family. These formats are simple and
29/// straightforward for representing grayscale (PGM), black and white (PBM), and
30/// color (PPM) images in either ASCII or binary modes. Here's a brief overview:
31///
32/// - PBM (Portable Bitmap Format): Used for black and white images. It supports
33/// both ASCII (P1) and binary (P4) representations.
34///
35/// - PGM (Portable Graymap Format): Used for grayscale images. It also supports
36/// ASCII (P2) and binary (P5) representations, with the gray value typically
37/// ranging from 0 (black) to 255 (white) for 8-bit images.
38///
39/// - PPM (Portable Pixmap Format): Used for color images. Similar to PGM,
40/// it supports ASCII (P3) and binary (P6) formats. Each pixel is represented by
41/// three values (red, green, and blue), each in the range of 0 to 255 for 8-bit images.
42pub struct Pnm;
43
44impl Pnm {
45    /// Converts a `bitmap` of 1-bit bytes into PBM ASCII P1 representation.
46    ///
47    /// Expects a slice of width * height bytes equal to `0` or `1`.
48    ///
49    /// # Errors
50    /// Returns [`InvalidImageSize`] if the `bitmap` slice doesn't contain exactly
51    /// `width` * `height` elements, [`InvalidPixel`] if any pixel value is invalid
52    /// and [`FmtError`] if the string writing fails.
53    #[cfg(feature = "alloc")]
54    #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "alloc")))]
55    pub fn p1_encode_bytes(bitmap: &[u8], width: usize, height: usize) -> Result<String> {
56        if bitmap.len() != width * height {
57            return Err(InvalidImageSize(Some((width, height))));
58        }
59        let mut result = String::new();
60        writeln!(result, "P1\n{} {}", width, height)?;
61
62        // Convert each byte in `bitmap` to '0' (white) or '1' (black) ASCII
63        for row in 0..height {
64            let first_pixel = bitmap[row * width];
65            result.push(match first_pixel {
66                0 => '0',
67                1 => '1',
68                _ => return invalid_pixel(),
69            });
70
71            for col in 1..width {
72                let pixel = bitmap[row * width + col];
73                result.push(' '); // leading space on non-first-in-row pixels
74                match pixel {
75                    0 => result.push('0'),
76                    1 => result.push('1'),
77                    _ => return invalid_pixel(),
78                }
79            }
80            result.push('\n'); // End of row
81        }
82        Ok(result)
83    }
84
85    /// Converts a `bitmap` of 1-bit bits into PBM ASCII P1 representation.
86    ///
87    /// Each byte in `bitmap` represents 8 pixels, with the most significant bit (MSB)
88    /// of each byte representing the leftmost pixel.
89    ///
90    /// # Errors
91    /// Returns [`InvalidImageSize`] if the `bitmap` slice doesn't contain exactly
92    /// the number of expected bytes `width` * `height` elements
93    /// and [`FmtError`] if the string writing fails.
94    #[cfg(feature = "alloc")]
95    #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "alloc")))]
96    pub fn p1_encode_bits(bitmap: &[u8], width: usize, height: usize) -> Result<String> {
97        if bitmap.len() != Mem::bytes_from_bits(width * height) {
98            return Err(InvalidImageSize(Some((width, height))));
99        }
100        let mut result = String::new();
101        writeln!(result, "P1\n{} {}", width, height)?;
102
103        // Convert each bit in `bitmap` to '0' (white) or '1' (black) ASCII
104        for row in 0..height {
105            let first_col = 0;
106            let byte_index = (row * width + first_col) / 8;
107            let bit = 1 << (7 - (first_col % 8));
108            let first_pixel = (bitmap[byte_index] & bit) != 0;
109            result.push(if first_pixel { '1' } else { '0' });
110
111            for col in 1..width {
112                let byte_index = (row * width + col) / 8;
113                let bit = 1 << (7 - (col % 8));
114                let pixel = (bitmap[byte_index] & bit) != 0;
115                result.push(' '); // leading space on non-first-in-row pixels
116                result.push(if pixel { '1' } else { '0' });
117            }
118            result.push('\n'); // end of row
119        }
120        Ok(result)
121    }
122}
123
124#[cfg(all(test, feature = "alloc"))]
125mod tests_alloc {
126    use super::*;
127    use crate::_dep::_alloc::vec;
128
129    #[test]
130    fn p1_encode_bytes() {
131        let bitmap = vec![
132            1, 0, 1, 0, 1, 0, 1, 0, // First row
133            0, 1, 0, 1, 0, 1, 0, 1, // Second row
134        ];
135        let (w, h) = (8, 2);
136        let expected_output = "P1\n8 2\n1 0 1 0 1 0 1 0\n0 1 0 1 0 1 0 1\n";
137        let result = Pnm::p1_encode_bytes(&bitmap, w, h).expect("PNM P1 encoded");
138        assert_eq!(result, expected_output);
139    }
140    #[test]
141    fn p1_encode_bytes_invalid_size() {
142        let bitmap = vec![1, 0, 1, 0]; // Incorrect size for 2x2 image
143        let (w, h) = (3, 3);
144        let result = Pnm::p1_encode_bytes(&bitmap, w, h);
145        assert_eq!(result, Err(InvalidImageSize(Some((3, 3)))));
146    }
147    #[test]
148    fn p1_encode_bytes_invalid_pixel() {
149        let bitmap = vec![1, 0, 2, 0]; // Invalid pixel value (2)
150        let (w, h) = (2, 2);
151        let result = Pnm::p1_encode_bytes(&bitmap, w, h);
152        assert_eq!(result, Err(InvalidPixel));
153    }
154
155    #[test]
156    fn p1_encode_bits() {
157        let bitmap = vec![0b10101010, 0b01010101]; // Packed bits for 8x2 image
158        let (w, h) = (8, 2);
159        let expected_output = "P1\n8 2\n1 0 1 0 1 0 1 0\n0 1 0 1 0 1 0 1\n";
160        let result = Pnm::p1_encode_bits(&bitmap, w, h).expect("PNM P1 encoded");
161        assert_eq!(result, expected_output);
162    }
163    #[test]
164    fn p1_encode_bits_invalid_size() {
165        let bitmap = vec![0b10101010]; // Incorrect size for 8x2 image
166        let (w, h) = (8, 2);
167        let result = Pnm::p1_encode_bits(&bitmap, w, h);
168        assert_eq!(result, Err(InvalidImageSize(Some((8, 2)))));
169    }
170}