use std::io::{self, Write};
use color::{Pixel, PixelMode, convert::cmyk_to_rgb};
use raster::Bitmap;
use crate::EncodeError;
pub fn write_ppm<P: Pixel, W: Write>(bitmap: &Bitmap<P>, mut out: W) -> Result<(), EncodeError> {
match P::MODE {
PixelMode::Mono1 | PixelMode::Mono8 => {
return Err(EncodeError::UnsupportedMode(
"grayscale/mono bitmap: use write_pgm instead",
));
}
PixelMode::Rgb8
| PixelMode::Bgr8
| PixelMode::Xbgr8
| PixelMode::Cmyk8
| PixelMode::DeviceN8 => {}
}
write_ppm_header(&mut out, bitmap.width, bitmap.height)?;
write_ppm_pixels::<P, W>(bitmap, &mut out)?;
out.flush()?;
Ok(())
}
fn write_ppm_header<W: Write>(out: &mut W, width: u32, height: u32) -> io::Result<()> {
writeln!(out, "P6")?;
writeln!(out, "{width} {height}")?;
writeln!(out, "255")?;
Ok(())
}
fn write_ppm_pixels<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: &mut W) -> io::Result<()> {
let w = bitmap.width as usize;
let mut rgb_row = vec![0u8; w * 3];
for y in 0..bitmap.height {
let src = bitmap.row_bytes(y);
convert_row_to_rgb::<P>(src, &mut rgb_row, w);
out.write_all(&rgb_row)?;
}
Ok(())
}
#[inline]
fn convert_row_to_rgb<P: Pixel>(src: &[u8], dst: &mut [u8], width: usize) {
match P::MODE {
PixelMode::Rgb8 => {
dst[..width * 3].copy_from_slice(&src[..width * 3]);
}
PixelMode::Bgr8 => {
for (i, chunk) in src[..width * 3].chunks_exact(3).enumerate() {
dst[i * 3] = chunk[2]; dst[i * 3 + 1] = chunk[1]; dst[i * 3 + 2] = chunk[0]; }
}
PixelMode::Xbgr8 => {
for (i, chunk) in src[..width * 4].chunks_exact(4).enumerate() {
dst[i * 3] = chunk[3]; dst[i * 3 + 1] = chunk[2]; dst[i * 3 + 2] = chunk[1]; }
}
PixelMode::Cmyk8 => {
for (i, chunk) in src[..width * 4].chunks_exact(4).enumerate() {
let (r, g, b) = cmyk_to_rgb(chunk[0], chunk[1], chunk[2], chunk[3]);
dst[i * 3] = r;
dst[i * 3 + 1] = g;
dst[i * 3 + 2] = b;
}
}
PixelMode::DeviceN8 => {
for (i, chunk) in src[..width * 8].chunks_exact(8).enumerate() {
let (r, g, b) = cmyk_to_rgb(chunk[0], chunk[1], chunk[2], chunk[3]);
dst[i * 3] = r;
dst[i * 3 + 1] = g;
dst[i * 3 + 2] = b;
}
}
PixelMode::Mono1 | PixelMode::Mono8 => {
unreachable!("convert_row_to_rgb: mono modes are screened by write_ppm");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use color::{Cmyk8, DeviceN8, Rgb8, Rgba8};
use raster::Bitmap;
fn make_rgb_bitmap(w: u32, h: u32, fill: [u8; 3]) -> Bitmap<Rgb8> {
let mut bmp = Bitmap::new(w, h, 1, false);
for y in 0..h {
let row = bmp.row_bytes_mut(y);
for chunk in row.chunks_exact_mut(3) {
chunk.copy_from_slice(&fill);
}
}
bmp
}
fn header_len(out: &[u8]) -> usize {
let mut newlines = 0usize;
for (i, &b) in out.iter().enumerate() {
if b == b'\n' {
newlines += 1;
if newlines == 3 {
return i + 1;
}
}
}
panic!("malformed PPM header");
}
#[test]
fn rgb_ppm_header_and_pixels() {
let bmp = make_rgb_bitmap(2, 1, [255, 128, 0]);
let mut out = Vec::new();
write_ppm::<Rgb8, _>(&bmp, &mut out).unwrap();
let expected_header = b"P6\n2 1\n255\n";
assert!(
out.starts_with(expected_header),
"header mismatch: {:?}",
&out[..expected_header.len().min(out.len())]
);
let pixels = &out[expected_header.len()..];
assert_eq!(pixels.len(), 6, "2 pixels × 3 bytes");
assert_eq!(&pixels[..3], &[255, 128, 0]);
assert_eq!(&pixels[3..6], &[255, 128, 0]);
}
#[test]
fn rgba8_xbgr_ppm_channel_swap() {
let mut bmp: Bitmap<Rgba8> = Bitmap::new(1, 1, 1, false);
bmp.row_bytes_mut(0).copy_from_slice(&[255, 10, 20, 30]);
let mut out = Vec::new();
write_ppm::<Rgba8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(
&out[hlen..],
&[30, 20, 10],
"Xbgr8 must become RGB (channels swapped)"
);
}
#[test]
fn cmyk_black_converts_to_rgb_black() {
let mut bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
bmp.row_bytes_mut(0).copy_from_slice(&[0, 0, 0, 255]);
let mut out = Vec::new();
write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(&out[hlen..], &[0, 0, 0], "CMYK black → RGB (0,0,0)");
}
#[test]
fn cmyk_white_converts_to_rgb_white() {
let mut bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
bmp.row_bytes_mut(0).copy_from_slice(&[0, 0, 0, 0]);
let mut out = Vec::new();
write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(&out[hlen..], &[255, 255, 255], "CMYK white → RGB white");
}
#[test]
fn devicen_uses_only_cmyk_portion() {
let mut bmp: Bitmap<DeviceN8> = Bitmap::new(1, 1, 1, false);
bmp.row_bytes_mut(0)
.copy_from_slice(&[0, 0, 0, 0, 99, 99, 99, 99]);
let mut out = Vec::new();
write_ppm::<DeviceN8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(
&out[hlen..],
&[255, 255, 255],
"DeviceN spot channels must be ignored"
);
}
#[test]
fn stride_padding_not_written() {
let bmp: Bitmap<Rgb8> = Bitmap::new(1, 1, 4, false);
let mut out = Vec::new();
write_ppm::<Rgb8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(
out.len() - hlen,
3,
"stride padding must not appear in PPM output"
);
}
#[test]
fn mono8_returns_unsupported_error() {
use color::Gray8;
let bmp: Bitmap<Gray8> = Bitmap::new(1, 1, 1, false);
let mut out = Vec::new();
let result = write_ppm::<Gray8, _>(&bmp, &mut out);
assert!(
matches!(result, Err(EncodeError::UnsupportedMode(_))),
"Gray8 should return UnsupportedMode"
);
}
#[test]
fn cmyk_to_rgb_clamped() {
let (r, g, b) = cmyk_to_rgb(200, 0, 0, 100);
assert_eq!(r, 0, "negative result must clamp to 0");
assert_eq!(g, 155, "magenta=0, k=100: 255-0-100=155");
assert_eq!(b, 155, "yellow=0, k=100: 255-0-100=155");
}
#[test]
fn cmyk_to_rgb_asymmetric_channels() {
let (r, g, b) = cmyk_to_rgb(10, 50, 200, 0);
assert_eq!(r, 245, "cyan=10, k=0: 255-10=245");
assert_eq!(g, 205, "magenta=50, k=0: 255-50=205");
assert_eq!(b, 55, "yellow=200, k=0: 255-200=55");
let (r, g, b) = cmyk_to_rgb(40, 80, 120, 30);
assert_eq!(r, 185, "255-40-30=185");
assert_eq!(g, 145, "255-80-30=145");
assert_eq!(b, 105, "255-120-30=105");
}
#[test]
fn cmyk_ppm_multi_pixel_layout() {
let mut bmp: Bitmap<Cmyk8> = Bitmap::new(2, 2, 1, false);
bmp.row_bytes_mut(0)
.copy_from_slice(&[10, 0, 0, 0, 0, 20, 0, 0]);
bmp.row_bytes_mut(1)
.copy_from_slice(&[0, 0, 30, 0, 40, 50, 60, 0]);
let mut out = Vec::new();
write_ppm::<Cmyk8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
let pixels = &out[hlen..];
assert_eq!(pixels.len(), 12, "2×2 × 3 RGB bytes");
assert_eq!(&pixels[0..3], &[245, 255, 255], "row 0 px 0");
assert_eq!(&pixels[3..6], &[255, 235, 255], "row 0 px 1");
assert_eq!(&pixels[6..9], &[255, 255, 225], "row 1 px 0");
assert_eq!(&pixels[9..12], &[215, 205, 195], "row 1 px 1");
}
#[test]
fn rgba8_xbgr_ppm_multi_pixel_channel_swap() {
let mut bmp: Bitmap<Rgba8> = Bitmap::new(2, 1, 1, false);
bmp.row_bytes_mut(0)
.copy_from_slice(&[255, 10, 20, 30, 240, 11, 22, 33]);
let mut out = Vec::new();
write_ppm::<Rgba8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(&out[hlen..], &[30, 20, 10, 33, 22, 11], "two pixels RGB");
}
#[test]
fn devicen_ignores_spot_channels_per_pixel() {
let mut bmp: Bitmap<DeviceN8> = Bitmap::new(2, 1, 1, false);
bmp.row_bytes_mut(0)
.copy_from_slice(&[10, 0, 0, 0, 99, 99, 99, 99, 0, 40, 80, 0, 88, 88, 88, 88]);
let mut out = Vec::new();
write_ppm::<DeviceN8, _>(&bmp, &mut out).unwrap();
let hlen = header_len(&out);
assert_eq!(
&out[hlen..],
&[245, 255, 255, 255, 215, 175],
"spot bytes must be skipped per pixel; only CMYK drives the RGB output"
);
}
}