1use std::io::{self, Write};
18
19use color::{Pixel, PixelMode, convert::cmyk_to_rgb};
20use raster::Bitmap;
21
22use crate::EncodeError;
23
24pub fn write_ppm<P: Pixel, W: Write>(bitmap: &Bitmap<P>, mut out: W) -> Result<(), EncodeError> {
35 match P::MODE {
36 PixelMode::Mono1 | PixelMode::Mono8 => {
37 return Err(EncodeError::UnsupportedMode(
38 "grayscale/mono bitmap: use write_pgm instead",
39 ));
40 }
41 PixelMode::Rgb8
42 | PixelMode::Bgr8
43 | PixelMode::Xbgr8
44 | PixelMode::Cmyk8
45 | PixelMode::DeviceN8 => {}
46 }
47
48 write_ppm_header(&mut out, bitmap.width, bitmap.height)?;
49 write_ppm_pixels::<P, W>(bitmap, &mut out)?;
50 out.flush()?;
51 Ok(())
52}
53
54fn write_ppm_header<W: Write>(out: &mut W, width: u32, height: u32) -> io::Result<()> {
56 writeln!(out, "P6")?;
57 writeln!(out, "{width} {height}")?;
58 writeln!(out, "255")?;
59 Ok(())
60}
61
62fn write_ppm_pixels<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: &mut W) -> io::Result<()> {
64 let w = bitmap.width as usize;
66 let mut rgb_row = vec![0u8; w * 3];
67
68 for y in 0..bitmap.height {
69 let src = bitmap.row_bytes(y);
70 convert_row_to_rgb::<P>(src, &mut rgb_row, w);
71 out.write_all(&rgb_row)?;
72 }
73 Ok(())
74}
75
76#[inline]
81fn convert_row_to_rgb<P: Pixel>(src: &[u8], dst: &mut [u8], width: usize) {
82 match P::MODE {
83 PixelMode::Rgb8 => {
84 dst[..width * 3].copy_from_slice(&src[..width * 3]);
86 }
87 PixelMode::Bgr8 => {
88 for (i, chunk) in src[..width * 3].chunks_exact(3).enumerate() {
90 dst[i * 3] = chunk[2]; dst[i * 3 + 1] = chunk[1]; dst[i * 3 + 2] = chunk[0]; }
94 }
95 PixelMode::Xbgr8 => {
96 for (i, chunk) in src[..width * 4].chunks_exact(4).enumerate() {
98 dst[i * 3] = chunk[3]; dst[i * 3 + 1] = chunk[2]; dst[i * 3 + 2] = chunk[1]; }
102 }
103 PixelMode::Cmyk8 => {
104 for (i, chunk) in src[..width * 4].chunks_exact(4).enumerate() {
106 let (r, g, b) = cmyk_to_rgb(chunk[0], chunk[1], chunk[2], chunk[3]);
107 dst[i * 3] = r;
108 dst[i * 3 + 1] = g;
109 dst[i * 3 + 2] = b;
110 }
111 }
112 PixelMode::DeviceN8 => {
113 for (i, chunk) in src[..width * 8].chunks_exact(8).enumerate() {
115 let (r, g, b) = cmyk_to_rgb(chunk[0], chunk[1], chunk[2], chunk[3]);
116 dst[i * 3] = r;
117 dst[i * 3 + 1] = g;
118 dst[i * 3 + 2] = b;
119 }
120 }
121 PixelMode::Mono1 | PixelMode::Mono8 => {
122 unreachable!("convert_row_to_rgb: mono modes are screened by write_ppm");
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use color::{Cmyk8, DeviceN8, Rgb8, Rgba8};
135 use raster::Bitmap;
136
137 fn make_rgb_bitmap(w: u32, h: u32, fill: [u8; 3]) -> Bitmap<Rgb8> {
138 let mut bmp = Bitmap::new(w, h, 1, false);
139 for y in 0..h {
140 let row = bmp.row_bytes_mut(y);
141 for chunk in row.chunks_exact_mut(3) {
142 chunk.copy_from_slice(&fill);
143 }
144 }
145 bmp
146 }
147
148 fn header_len(out: &[u8]) -> usize {
150 let mut newlines = 0usize;
152 for (i, &b) in out.iter().enumerate() {
153 if b == b'\n' {
154 newlines += 1;
155 if newlines == 3 {
156 return i + 1;
157 }
158 }
159 }
160 panic!("malformed PPM header");
161 }
162
163 #[test]
164 fn rgb_ppm_header_and_pixels() {
165 let bmp = make_rgb_bitmap(2, 1, [255, 128, 0]);
166 let mut out = Vec::new();
167 write_ppm::<Rgb8, _>(&bmp, &mut out).unwrap();
168
169 let expected_header = b"P6\n2 1\n255\n";
170 assert!(
171 out.starts_with(expected_header),
172 "header mismatch: {:?}",
173 &out[..expected_header.len().min(out.len())]
174 );
175
176 let pixels = &out[expected_header.len()..];
177 assert_eq!(pixels.len(), 6, "2 pixels × 3 bytes");
178 assert_eq!(&pixels[..3], &[255, 128, 0]);
179 assert_eq!(&pixels[3..6], &[255, 128, 0]);
180 }
181
182 #[test]
183 fn rgba8_xbgr_ppm_channel_swap() {
184 let mut bmp: Bitmap<Rgba8> = Bitmap::new(1, 1, 1, false);
186 bmp.row_bytes_mut(0).copy_from_slice(&[255, 10, 20, 30]);
188 let mut out = Vec::new();
189 write_ppm::<Rgba8, _>(&bmp, &mut out).unwrap();
190 let hlen = header_len(&out);
191 assert_eq!(
192 &out[hlen..],
193 &[30, 20, 10],
194 "Xbgr8 must become RGB (channels swapped)"
195 );
196 }
197
198 #[test]
199 fn cmyk_black_converts_to_rgb_black() {
200 let mut bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
201 bmp.row_bytes_mut(0).copy_from_slice(&[0, 0, 0, 255]);
203 let mut out = Vec::new();
204 write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
205 let hlen = header_len(&out);
206 assert_eq!(&out[hlen..], &[0, 0, 0], "CMYK black → RGB (0,0,0)");
207 }
208
209 #[test]
210 fn cmyk_white_converts_to_rgb_white() {
211 let mut bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
212 bmp.row_bytes_mut(0).copy_from_slice(&[0, 0, 0, 0]);
214 let mut out = Vec::new();
215 write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
216 let hlen = header_len(&out);
217 assert_eq!(&out[hlen..], &[255, 255, 255], "CMYK white → RGB white");
218 }
219
220 #[test]
221 fn devicen_uses_only_cmyk_portion() {
222 let mut bmp: Bitmap<DeviceN8> = Bitmap::new(1, 1, 1, false);
223 bmp.row_bytes_mut(0)
225 .copy_from_slice(&[0, 0, 0, 0, 99, 99, 99, 99]);
226 let mut out = Vec::new();
227 write_ppm::<DeviceN8, _>(&bmp, &mut out).unwrap();
228 let hlen = header_len(&out);
229 assert_eq!(
230 &out[hlen..],
231 &[255, 255, 255],
232 "DeviceN spot channels must be ignored"
233 );
234 }
235
236 #[test]
237 fn stride_padding_not_written() {
238 let bmp: Bitmap<Rgb8> = Bitmap::new(1, 1, 4, false);
240 let mut out = Vec::new();
241 write_ppm::<Rgb8, _>(&bmp, &mut out).unwrap();
242 let hlen = header_len(&out);
243 assert_eq!(
245 out.len() - hlen,
246 3,
247 "stride padding must not appear in PPM output"
248 );
249 }
250
251 #[test]
252 fn mono8_returns_unsupported_error() {
253 use color::Gray8;
254 let bmp: Bitmap<Gray8> = Bitmap::new(1, 1, 1, false);
255 let mut out = Vec::new();
256 let result = write_ppm::<Gray8, _>(&bmp, &mut out);
257 assert!(
258 matches!(result, Err(EncodeError::UnsupportedMode(_))),
259 "Gray8 should return UnsupportedMode"
260 );
261 }
262
263 #[test]
264 fn cmyk_to_rgb_clamped() {
265 let (r, g, b) = cmyk_to_rgb(200, 0, 0, 100);
267 assert_eq!(r, 0, "negative result must clamp to 0");
268 assert_eq!(g, 155, "magenta=0, k=100: 255-0-100=155");
269 assert_eq!(b, 155, "yellow=0, k=100: 255-0-100=155");
270 }
271
272 #[test]
273 fn cmyk_to_rgb_asymmetric_channels() {
274 let (r, g, b) = cmyk_to_rgb(10, 50, 200, 0);
277 assert_eq!(r, 245, "cyan=10, k=0: 255-10=245");
278 assert_eq!(g, 205, "magenta=50, k=0: 255-50=205");
279 assert_eq!(b, 55, "yellow=200, k=0: 255-200=55");
280
281 let (r, g, b) = cmyk_to_rgb(40, 80, 120, 30);
283 assert_eq!(r, 185, "255-40-30=185");
284 assert_eq!(g, 145, "255-80-30=145");
285 assert_eq!(b, 105, "255-120-30=105");
286 }
287
288 #[test]
289 fn cmyk_ppm_multi_pixel_layout() {
290 let mut bmp: Bitmap<Cmyk8> = Bitmap::new(2, 2, 1, false);
293 bmp.row_bytes_mut(0)
295 .copy_from_slice(&[10, 0, 0, 0, 0, 20, 0, 0]);
296 bmp.row_bytes_mut(1)
298 .copy_from_slice(&[0, 0, 30, 0, 40, 50, 60, 0]);
299 let mut out = Vec::new();
300 write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
301 let hlen = header_len(&out);
302 let pixels = &out[hlen..];
303 assert_eq!(pixels.len(), 12, "2×2 × 3 RGB bytes");
304 assert_eq!(&pixels[0..3], &[245, 255, 255], "row 0 px 0");
305 assert_eq!(&pixels[3..6], &[255, 235, 255], "row 0 px 1");
306 assert_eq!(&pixels[6..9], &[255, 255, 225], "row 1 px 0");
307 assert_eq!(&pixels[9..12], &[215, 205, 195], "row 1 px 1");
308 }
309
310 #[test]
311 fn rgba8_xbgr_ppm_multi_pixel_channel_swap() {
312 let mut bmp: Bitmap<Rgba8> = Bitmap::new(2, 1, 1, false);
316 bmp.row_bytes_mut(0)
318 .copy_from_slice(&[255, 10, 20, 30, 240, 11, 22, 33]);
319 let mut out = Vec::new();
320 write_ppm::<Rgba8, _>(&bmp, &mut out).unwrap();
321 let hlen = header_len(&out);
322 assert_eq!(&out[hlen..], &[30, 20, 10, 33, 22, 11], "two pixels RGB");
323 }
324
325 #[test]
326 fn devicen_ignores_spot_channels_per_pixel() {
327 let mut bmp: Bitmap<DeviceN8> = Bitmap::new(2, 1, 1, false);
331 bmp.row_bytes_mut(0)
334 .copy_from_slice(&[10, 0, 0, 0, 99, 99, 99, 99, 0, 40, 80, 0, 88, 88, 88, 88]);
335 let mut out = Vec::new();
336 write_ppm::<DeviceN8, _>(&bmp, &mut out).unwrap();
337 let hlen = header_len(&out);
338 assert_eq!(
339 &out[hlen..],
340 &[245, 255, 255, 255, 215, 175],
341 "spot bytes must be skipped per pixel; only CMYK drives the RGB output"
342 );
343 }
344}