Skip to main content

rasterrocket_encode/
pgm.rs

1//! Netpbm P5 (binary PGM) encoder.
2//!
3//! PGM stores 8-bit grayscale pixels.  Only `Gray8` / `Mono8` bitmaps are
4//! accepted; all other modes return [`EncodeError::UnsupportedMode`].
5//!
6//! The output is a standard `P5` header followed by raw luminance bytes.
7
8use std::io::{self, Write};
9
10use color::{Pixel, PixelMode};
11use raster::Bitmap;
12
13use crate::EncodeError;
14
15/// Write `bitmap` to `out` as a binary PGM (`P5`) image.
16///
17/// The sink `out` is consumed; wrap in `std::io::BufWriter` if buffering is
18/// needed.
19///
20/// # Errors
21///
22/// Returns [`EncodeError::UnsupportedMode`] for non-grayscale modes —
23/// use [`write_ppm`][crate::write_ppm] for colour bitmaps.
24/// Returns [`EncodeError::Io`] on any I/O failure.
25pub fn write_pgm<P: Pixel, W: Write>(bitmap: &Bitmap<P>, mut out: W) -> Result<(), EncodeError> {
26    match P::MODE {
27        PixelMode::Mono8 => {}
28        PixelMode::Mono1
29        | PixelMode::Rgb8
30        | PixelMode::Bgr8
31        | PixelMode::Xbgr8
32        | PixelMode::Cmyk8
33        | PixelMode::DeviceN8 => {
34            return Err(EncodeError::UnsupportedMode(
35                "non-grayscale bitmap: use write_ppm or write_png",
36            ));
37        }
38    }
39
40    write_pgm_header(&mut out, bitmap.width, bitmap.height)?;
41
42    // Each row may be wider than `width` bytes (stride padding).
43    // Write only the live pixel bytes.
44    let w = bitmap.width as usize;
45    for y in 0..bitmap.height {
46        let row = bitmap.row_bytes(y);
47        out.write_all(&row[..w])?;
48    }
49
50    out.flush()?;
51    Ok(())
52}
53
54/// Write the P5 header.
55fn write_pgm_header<W: Write>(out: &mut W, width: u32, height: u32) -> io::Result<()> {
56    writeln!(out, "P5")?;
57    writeln!(out, "{width} {height}")?;
58    writeln!(out, "255")?;
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use color::{Gray8, Rgb8};
66    use raster::Bitmap;
67
68    fn make_gray_bitmap(w: u32, h: u32, fill: u8) -> Bitmap<Gray8> {
69        let mut bmp = Bitmap::new(w, h, 1, false);
70        for y in 0..h {
71            bmp.row_bytes_mut(y).fill(fill);
72        }
73        bmp
74    }
75
76    #[test]
77    fn pgm_header_and_pixels() {
78        let bmp = make_gray_bitmap(3, 2, 128);
79        let mut out = Vec::new();
80        write_pgm::<Gray8, _>(&bmp, &mut out).unwrap();
81
82        let header = "P5\n3 2\n255\n";
83        assert!(
84            out.starts_with(header.as_bytes()),
85            "header mismatch: {:?}",
86            &out[..header.len().min(out.len())]
87        );
88        let pixels = &out[header.len()..];
89        assert_eq!(pixels.len(), 6, "3×2 = 6 pixel bytes");
90        assert!(pixels.iter().all(|&v| v == 128), "all pixels should be 128");
91    }
92
93    #[test]
94    fn stride_padding_excluded() {
95        // Bitmap with row_pad=4: stride for Gray8 (1 byte/px) at width=3 is 4.
96        let bmp_padded: Bitmap<Gray8> = Bitmap::new(3, 1, 4, false);
97        assert!(
98            bmp_padded.stride >= 3,
99            "padded stride must be at least width (sanity check on Bitmap::new)"
100        );
101
102        let mut out = Vec::new();
103        write_pgm::<Gray8, _>(&bmp_padded, &mut out).unwrap();
104        let header = "P5\n3 1\n255\n";
105        let pixels = &out[header.len()..];
106        // Must be exactly 3 bytes (width), not 4 (stride).
107        assert_eq!(
108            pixels.len(),
109            3,
110            "stride padding must not appear in PGM output"
111        );
112    }
113
114    #[test]
115    fn rgb8_returns_unsupported_error() {
116        let bmp: Bitmap<Rgb8> = Bitmap::new(1, 1, 1, false);
117        let mut out = Vec::new();
118        let result = write_pgm::<Rgb8, _>(&bmp, &mut out);
119        assert!(
120            matches!(result, Err(EncodeError::UnsupportedMode(_))),
121            "Rgb8 should return UnsupportedMode for PGM"
122        );
123    }
124}