Skip to main content

rasterrocket_encode/
pbm.rs

1//! Netpbm P4 (binary PBM) encoder.
2//!
3//! PBM stores 1-bit monochrome pixels, MSB first, rows padded to whole bytes.
4//! Only `Gray8` / `Mono8` bitmaps are accepted; 0 maps to white (0 bit),
5//! non-zero maps to black (1 bit), per the P4 spec.
6
7use std::io::{self, Write};
8
9use color::{Pixel, PixelMode};
10use raster::Bitmap;
11
12use crate::EncodeError;
13
14/// Write `bitmap` to `out` as a binary PBM (`P4`) image.
15///
16/// Input pixels are `Gray8`: 0 → white (0 bit), 1–255 → black (1 bit).
17/// Rows are MSB-packed and padded to whole bytes, matching the P4 spec.
18///
19/// # Errors
20///
21/// Returns [`EncodeError::UnsupportedMode`] for non-grayscale modes.
22/// Returns [`EncodeError::Io`] on any I/O failure.
23pub fn write_pbm<P: Pixel, W: Write>(bitmap: &Bitmap<P>, mut out: W) -> Result<(), EncodeError> {
24    match P::MODE {
25        PixelMode::Mono8 => {}
26        PixelMode::Mono1
27        | PixelMode::Rgb8
28        | PixelMode::Bgr8
29        | PixelMode::Xbgr8
30        | PixelMode::Cmyk8
31        | PixelMode::DeviceN8 => {
32            return Err(EncodeError::UnsupportedMode(
33                "write_pbm accepts only Gray8 (Mono8) bitmaps",
34            ));
35        }
36    }
37
38    write_pbm_header(&mut out, bitmap.width, bitmap.height)?;
39
40    let w = bitmap.width as usize; // u32 → usize: lossless on all ≥32-bit targets
41    let row_bytes_out = w.div_ceil(8); // packed byte count per output row (MSB first, P4 spec)
42    let mut packed = vec![0u8; row_bytes_out];
43
44    for y in 0..bitmap.height {
45        let row = bitmap.row_bytes(y);
46        let pixels = &row[..w]; // live pixels only (exclude stride padding)
47
48        packed.fill(0);
49        for (i, &px) in pixels.iter().enumerate() {
50            if px != 0 {
51                // MSB first: pixel 0 → bit 7, pixel 7 → bit 0.
52                packed[i / 8] |= 0x80 >> (i % 8);
53            }
54        }
55        out.write_all(&packed)?;
56    }
57
58    out.flush()?;
59    Ok(())
60}
61
62fn write_pbm_header<W: Write>(out: &mut W, width: u32, height: u32) -> io::Result<()> {
63    writeln!(out, "P4")?;
64    writeln!(out, "{width} {height}")?;
65    Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use color::{Gray8, Rgb8};
72    use raster::Bitmap;
73
74    fn make_gray_bitmap(w: u32, h: u32) -> Bitmap<Gray8> {
75        Bitmap::new(w, h, 1, false)
76    }
77
78    fn gray_fill(bmp: &mut Bitmap<Gray8>, row: u32, values: &[u8]) {
79        let r = bmp.row_bytes_mut(row);
80        r[..values.len()].copy_from_slice(values);
81    }
82
83    #[test]
84    fn pbm_header_format() {
85        let bmp = make_gray_bitmap(8, 1);
86        let mut out = Vec::new();
87        write_pbm::<Gray8, _>(&bmp, &mut out).unwrap();
88        assert!(
89            out.starts_with(b"P4\n8 1\n"),
90            "header: {:?}",
91            &out[..12.min(out.len())]
92        );
93    }
94
95    #[test]
96    fn all_white_is_zero_byte() {
97        // All pixels = 0 → all white → packed byte = 0x00
98        let bmp = make_gray_bitmap(8, 1);
99        let mut out = Vec::new();
100        write_pbm::<Gray8, _>(&bmp, &mut out).unwrap();
101        let header_len = b"P4\n8 1\n".len();
102        assert_eq!(out[header_len], 0x00, "all-white row must be 0x00");
103    }
104
105    #[test]
106    fn all_black_is_ff_byte() {
107        let mut bmp = make_gray_bitmap(8, 1);
108        gray_fill(&mut bmp, 0, &[255u8; 8]);
109        let mut out = Vec::new();
110        write_pbm::<Gray8, _>(&bmp, &mut out).unwrap();
111        let header_len = b"P4\n8 1\n".len();
112        assert_eq!(out[header_len], 0xFF, "all-black row must be 0xFF");
113    }
114
115    #[test]
116    fn alternating_checkerboard() {
117        // Pixels: 255, 0, 255, 0, 255, 0, 255, 0 → bits: 1010_1010 = 0xAA
118        let mut bmp = make_gray_bitmap(8, 1);
119        gray_fill(&mut bmp, 0, &[255, 0, 255, 0, 255, 0, 255, 0]);
120        let mut out = Vec::new();
121        write_pbm::<Gray8, _>(&bmp, &mut out).unwrap();
122        let header_len = b"P4\n8 1\n".len();
123        assert_eq!(out[header_len], 0xAA);
124    }
125
126    #[test]
127    fn row_padding_when_width_not_multiple_of_8() {
128        // 3 pixels wide → 1 packed byte per row; bits beyond pixel 2 must be 0.
129        let mut bmp = make_gray_bitmap(3, 1);
130        gray_fill(&mut bmp, 0, &[255, 255, 255]);
131        let mut out = Vec::new();
132        write_pbm::<Gray8, _>(&bmp, &mut out).unwrap();
133        let header_len = b"P4\n3 1\n".len();
134        // 3 black pixels → top 3 bits set: 1110_0000 = 0xE0
135        assert_eq!(out[header_len], 0xE0, "3 black pixels must pack to 0xE0");
136        assert_eq!(
137            out.len(),
138            header_len + 1,
139            "3-pixel row occupies 1 packed byte"
140        );
141    }
142
143    #[test]
144    fn rgb8_returns_unsupported_error() {
145        let bmp: Bitmap<Rgb8> = Bitmap::new(1, 1, 1, false);
146        let result = write_pbm::<Rgb8, _>(&bmp, std::io::sink());
147        assert!(matches!(result, Err(EncodeError::UnsupportedMode(_))));
148    }
149}