rasterrocket_encode/
pbm.rs1use std::io::{self, Write};
8
9use color::{Pixel, PixelMode};
10use raster::Bitmap;
11
12use crate::EncodeError;
13
14pub 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; let row_bytes_out = w.div_ceil(8); 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]; packed.fill(0);
49 for (i, &px) in pixels.iter().enumerate() {
50 if px != 0 {
51 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 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 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 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 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}