use std::io::Write;
use color::{Pixel, PixelMode};
use raster::Bitmap;
use crate::EncodeError;
pub fn write_png<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
match P::MODE {
PixelMode::Rgb8 => write_png_rgb(bitmap, out),
PixelMode::Mono8 => write_png_gray(bitmap, out),
PixelMode::Xbgr8 => write_png_rgba(bitmap, out),
PixelMode::Bgr8 | PixelMode::Cmyk8 | PixelMode::DeviceN8 | PixelMode::Mono1 => {
Err(EncodeError::UnsupportedMode(
"unsupported mode for PNG: convert to Rgb8/Gray8/Rgba8 first",
))
}
}
}
fn png_encoder<W: Write>(
out: W,
width: u32,
height: u32,
color: ::png::ColorType,
depth: ::png::BitDepth,
) -> Result<::png::Writer<W>, EncodeError> {
let mut encoder = ::png::Encoder::new(out, width, height);
encoder.set_color(color);
encoder.set_depth(depth);
encoder.set_filter(::png::FilterType::Paeth);
encoder.set_compression(::png::Compression::Fast);
Ok(encoder.write_header()?)
}
fn pack_rows<P: Pixel>(bitmap: &Bitmap<P>, bytes_per_pixel: usize) -> Result<Vec<u8>, EncodeError> {
let w = bitmap.width as usize;
let h = bitmap.height as usize;
let total = w
.checked_mul(h)
.and_then(|wh| wh.checked_mul(bytes_per_pixel))
.ok_or(EncodeError::UnsupportedMode(
"image too large: pixel buffer would overflow usize",
))?;
let mut buf = vec![0u8; total];
let row_len = w * bytes_per_pixel;
for y in 0..bitmap.height {
let row = bitmap.row_bytes(y);
let dst_off = y as usize * row_len;
buf[dst_off..dst_off + row_len].copy_from_slice(&row[..row_len]);
}
Ok(buf)
}
fn write_png_rgb<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
let w = bitmap.width as usize;
let h = bitmap.height as usize;
if bitmap.has_alpha() {
let total = w.checked_mul(h).and_then(|wh| wh.checked_mul(4)).ok_or(
EncodeError::UnsupportedMode("image too large: RGBA buffer would overflow usize"),
)?;
let mut buf = vec![0u8; total];
for y in 0..bitmap.height {
let rgb = bitmap.row_bytes(y);
let alpha = bitmap
.alpha_row(y)
.expect("has_alpha is true but alpha_row returned None");
let row_off = y as usize * w * 4;
for i in 0..w {
buf[row_off + i * 4] = rgb[i * 3];
buf[row_off + i * 4 + 1] = rgb[i * 3 + 1];
buf[row_off + i * 4 + 2] = rgb[i * 3 + 2];
buf[row_off + i * 4 + 3] = alpha[i];
}
}
let mut writer = png_encoder(
out,
bitmap.width,
bitmap.height,
::png::ColorType::Rgba,
::png::BitDepth::Eight,
)?;
writer.write_image_data(&buf)?;
} else {
let buf = pack_rows(bitmap, 3)?;
let mut writer = png_encoder(
out,
bitmap.width,
bitmap.height,
::png::ColorType::Rgb,
::png::BitDepth::Eight,
)?;
writer.write_image_data(&buf)?;
}
Ok(())
}
fn write_png_gray<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
let buf = pack_rows(bitmap, 1)?;
let mut writer = png_encoder(
out,
bitmap.width,
bitmap.height,
::png::ColorType::Grayscale,
::png::BitDepth::Eight,
)?;
writer.write_image_data(&buf)?;
Ok(())
}
fn write_png_rgba<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
let w = bitmap.width as usize;
let h = bitmap.height as usize;
let total =
w.checked_mul(h)
.and_then(|wh| wh.checked_mul(4))
.ok_or(EncodeError::UnsupportedMode(
"image too large: RGBA buffer would overflow usize",
))?;
let mut buf = vec![0u8; total];
for y in 0..bitmap.height {
let row = bitmap.row_bytes(y);
let row_off = y as usize * w * 4;
for i in 0..w {
let src = i * 4;
let dst = row_off + i * 4;
buf[dst] = row[src + 3]; buf[dst + 1] = row[src + 2]; buf[dst + 2] = row[src + 1]; buf[dst + 3] = row[src]; }
}
let mut writer = png_encoder(
out,
bitmap.width,
bitmap.height,
::png::ColorType::Rgba,
::png::BitDepth::Eight,
)?;
writer.write_image_data(&buf)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use color::{Cmyk8, Gray8, 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 make_gray_bitmap(w: u32, h: u32, fill: u8) -> Bitmap<Gray8> {
let mut bmp = Bitmap::new(w, h, 1, false);
for y in 0..h {
bmp.row_bytes_mut(y).fill(fill);
}
bmp
}
fn decode_png(data: &[u8]) -> (u32, u32, Vec<u8>) {
let decoder = ::png::Decoder::new(std::io::Cursor::new(data));
let mut reader = decoder.read_info().expect("png decode header");
let mut buf = vec![0u8; reader.output_buffer_size()];
let frame = reader.next_frame(&mut buf).expect("png decode frame");
let info = reader.info();
(info.width, info.height, buf[..frame.buffer_size()].to_vec())
}
#[test]
fn rgb_png_roundtrip() {
let bmp = make_rgb_bitmap(4, 2, [100, 150, 200]);
let mut out = Vec::new();
write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (4, 2));
assert_eq!(pixels.len(), 24, "4×2 pixels × 3 bytes");
for chunk in pixels.chunks_exact(3) {
assert_eq!(chunk, &[100, 150, 200], "pixel mismatch");
}
}
#[test]
fn gray_png_roundtrip() {
let bmp = make_gray_bitmap(3, 3, 77);
let mut out = Vec::new();
write_png::<Gray8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (3, 3));
assert!(pixels.iter().all(|&v| v == 77), "grayscale pixel mismatch");
}
#[test]
fn rgb_with_alpha_writes_rgba_png() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(2, 1, 1, true);
let row = bmp.row_bytes_mut(0);
row[..6].copy_from_slice(&[255, 0, 0, 255, 0, 0]);
if let Some(a) = bmp.alpha_plane_mut() {
a.fill(128);
}
let mut out = Vec::new();
write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (2, 1));
assert_eq!(pixels.len(), 8, "2 pixels × 4 bytes (RGBA)");
assert_eq!(&pixels[..4], &[255, 0, 0, 128], "pixel 0 RGBA");
assert_eq!(&pixels[4..8], &[255, 0, 0, 128], "pixel 1 RGBA");
}
#[test]
fn stride_padding_not_included() {
let bmp: Bitmap<Gray8> = Bitmap::new(3, 1, 4, false);
let mut out = Vec::new();
write_png::<Gray8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (3, 1));
assert_eq!(
pixels.len(),
3,
"stride padding must not appear in PNG output"
);
}
#[test]
fn rgba8_png_roundtrip_asymmetric_2x2() {
let mut bmp: Bitmap<Rgba8> = Bitmap::new(2, 2, 1, false);
bmp.row_bytes_mut(0)
.copy_from_slice(&[200, 30, 20, 10, 210, 60, 50, 40]);
bmp.row_bytes_mut(1)
.copy_from_slice(&[220, 90, 80, 70, 230, 120, 110, 100]);
let mut out = Vec::new();
write_png::<Rgba8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (2, 2));
assert_eq!(pixels.len(), 16, "2×2 pixels × 4 bytes (RGBA)");
assert_eq!(&pixels[0..4], &[10, 20, 30, 200], "row 0 px 0");
assert_eq!(&pixels[4..8], &[40, 50, 60, 210], "row 0 px 1");
assert_eq!(&pixels[8..12], &[70, 80, 90, 220], "row 1 px 0");
assert_eq!(&pixels[12..16], &[100, 110, 120, 230], "row 1 px 1");
}
#[test]
fn rgb_with_alpha_multi_row_promotion() {
let mut bmp: Bitmap<Rgb8> = Bitmap::new(2, 2, 1, true);
bmp.row_bytes_mut(0)
.copy_from_slice(&[10, 20, 30, 40, 50, 60]);
bmp.row_bytes_mut(1)
.copy_from_slice(&[70, 80, 90, 100, 110, 120]);
let alpha = bmp.alpha_plane_mut().expect("alpha plane present");
alpha.copy_from_slice(&[200, 210, 220, 230]);
let mut out = Vec::new();
write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
let (w, h, pixels) = decode_png(&out);
assert_eq!((w, h), (2, 2));
assert_eq!(pixels.len(), 16);
assert_eq!(&pixels[0..4], &[10, 20, 30, 200]);
assert_eq!(&pixels[4..8], &[40, 50, 60, 210]);
assert_eq!(&pixels[8..12], &[70, 80, 90, 220]);
assert_eq!(&pixels[12..16], &[100, 110, 120, 230]);
}
#[test]
fn cmyk_returns_unsupported_error() {
let bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
let mut out = Vec::new();
let result = write_png::<Cmyk8, _>(&bmp, &mut out);
assert!(
matches!(result, Err(EncodeError::UnsupportedMode(_))),
"Cmyk8 should return UnsupportedMode for PNG"
);
}
}