Skip to main content

rasterrocket_encode/
png.rs

1//! PNG encoder (via the `png` crate).
2//!
3//! Supports `Rgb8`, `Gray8`, and `Rgba8` bitmaps directly.
4//! Other modes return [`EncodeError::UnsupportedMode`] — convert to one
5//! of the above before encoding.
6//!
7//! PNG is lossless and handles transparency (alpha plane) correctly.
8//! Use this format in preference to PPM when the bitmap has an alpha plane.
9
10use std::io::Write;
11
12use color::{Pixel, PixelMode};
13use raster::Bitmap;
14
15use crate::EncodeError;
16
17/// Write `bitmap` to `out` as a PNG image.
18///
19/// The sink `out` is consumed; wrap in `std::io::BufWriter` if buffering is
20/// needed.
21///
22/// Supported pixel modes:
23///
24/// | Mode | PNG colour type |
25/// |------|----------------|
26/// | `Rgb8` | `RGB` (24-bit) |
27/// | `Gray8` / `Mono8` | `Grayscale` (8-bit) |
28/// | `Rgba8` | `RGBA` (32-bit) |
29///
30/// # Alpha plane
31///
32/// For `Rgb8` bitmaps: if the bitmap has an alpha plane, each RGB row is
33/// interleaved with the corresponding alpha bytes and written as RGBA PNG.
34///
35/// For `Rgba8` bitmaps: the alpha stored within each pixel is used; any
36/// separate alpha plane is ignored.
37///
38/// # Errors
39///
40/// Returns [`EncodeError::UnsupportedMode`] for CMYK, BGR, XBGR, `DeviceN`,
41/// or `Mono1` bitmaps.
42/// Returns [`EncodeError::Io`] or [`EncodeError::PngEncoder`] on failure.
43pub fn write_png<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
44    match P::MODE {
45        PixelMode::Rgb8 => write_png_rgb(bitmap, out),
46        PixelMode::Mono8 => write_png_gray(bitmap, out),
47        // Rgba8 is stored as Xbgr8 in the Pixel trait implementation.
48        PixelMode::Xbgr8 => write_png_rgba(bitmap, out),
49        PixelMode::Bgr8 | PixelMode::Cmyk8 | PixelMode::DeviceN8 | PixelMode::Mono1 => {
50            Err(EncodeError::UnsupportedMode(
51                "unsupported mode for PNG: convert to Rgb8/Gray8/Rgba8 first",
52            ))
53        }
54    }
55}
56
57/// Build a configured [`png::Writer`] ready to accept image data.
58fn png_encoder<W: Write>(
59    out: W,
60    width: u32,
61    height: u32,
62    color: ::png::ColorType,
63    depth: ::png::BitDepth,
64) -> Result<::png::Writer<W>, EncodeError> {
65    let mut encoder = ::png::Encoder::new(out, width, height);
66    encoder.set_color(color);
67    encoder.set_depth(depth);
68    // Paeth filter gives good compression for photographic/gradient content.
69    encoder.set_filter(::png::FilterType::Paeth);
70    // Fast (zlib level 1) keeps encode time low across hundreds of pages.
71    // The size penalty vs Default is small for rendered grayscale content
72    // because the Paeth predictor already decorrelates most of the signal.
73    encoder.set_compression(::png::Compression::Fast);
74    Ok(encoder.write_header()?)
75}
76
77/// Pack pixel rows contiguously (no stride padding) into a new `Vec<u8>`.
78///
79/// `bytes_per_pixel` is the number of source bytes per pixel to copy.
80/// Returns an error if the allocation would overflow `usize`.
81fn pack_rows<P: Pixel>(bitmap: &Bitmap<P>, bytes_per_pixel: usize) -> Result<Vec<u8>, EncodeError> {
82    let w = bitmap.width as usize;
83    let h = bitmap.height as usize;
84    let total = w
85        .checked_mul(h)
86        .and_then(|wh| wh.checked_mul(bytes_per_pixel))
87        .ok_or(EncodeError::UnsupportedMode(
88            "image too large: pixel buffer would overflow usize",
89        ))?;
90    let mut buf = vec![0u8; total];
91    let row_len = w * bytes_per_pixel;
92    for y in 0..bitmap.height {
93        let row = bitmap.row_bytes(y);
94        let dst_off = y as usize * row_len;
95        buf[dst_off..dst_off + row_len].copy_from_slice(&row[..row_len]);
96    }
97    Ok(buf)
98}
99
100/// Write an `Rgb8` bitmap as PNG, promoting to RGBA if an alpha plane is present.
101fn write_png_rgb<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
102    let w = bitmap.width as usize;
103    let h = bitmap.height as usize;
104
105    if bitmap.has_alpha() {
106        // Promote to RGBA: interleave pixel RGB with alpha plane bytes.
107        let total = w.checked_mul(h).and_then(|wh| wh.checked_mul(4)).ok_or(
108            EncodeError::UnsupportedMode("image too large: RGBA buffer would overflow usize"),
109        )?;
110        let mut buf = vec![0u8; total];
111        for y in 0..bitmap.height {
112            let rgb = bitmap.row_bytes(y);
113            // alpha_row returns None only when has_alpha is false — checked above.
114            let alpha = bitmap
115                .alpha_row(y)
116                .expect("has_alpha is true but alpha_row returned None");
117            let row_off = y as usize * w * 4;
118            for i in 0..w {
119                buf[row_off + i * 4] = rgb[i * 3];
120                buf[row_off + i * 4 + 1] = rgb[i * 3 + 1];
121                buf[row_off + i * 4 + 2] = rgb[i * 3 + 2];
122                buf[row_off + i * 4 + 3] = alpha[i];
123            }
124        }
125        let mut writer = png_encoder(
126            out,
127            bitmap.width,
128            bitmap.height,
129            ::png::ColorType::Rgba,
130            ::png::BitDepth::Eight,
131        )?;
132        writer.write_image_data(&buf)?;
133    } else {
134        let buf = pack_rows(bitmap, 3)?;
135        let mut writer = png_encoder(
136            out,
137            bitmap.width,
138            bitmap.height,
139            ::png::ColorType::Rgb,
140            ::png::BitDepth::Eight,
141        )?;
142        writer.write_image_data(&buf)?;
143    }
144
145    Ok(())
146}
147
148/// Write a `Gray8` bitmap as PNG.
149fn write_png_gray<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
150    let buf = pack_rows(bitmap, 1)?;
151    let mut writer = png_encoder(
152        out,
153        bitmap.width,
154        bitmap.height,
155        ::png::ColorType::Grayscale,
156        ::png::BitDepth::Eight,
157    )?;
158    writer.write_image_data(&buf)?;
159    Ok(())
160}
161
162/// Write an `Rgba8`-equivalent bitmap (stored as `Xbgr8`) as PNG RGBA.
163///
164/// `Rgba8` uses the `Xbgr8` pixel mode internally; the channel layout in memory
165/// is [X(=A), B, G, R] (little-endian 32-bit), so we must swap to [R, G, B, A].
166fn write_png_rgba<P: Pixel, W: Write>(bitmap: &Bitmap<P>, out: W) -> Result<(), EncodeError> {
167    let w = bitmap.width as usize;
168    let h = bitmap.height as usize;
169    let total =
170        w.checked_mul(h)
171            .and_then(|wh| wh.checked_mul(4))
172            .ok_or(EncodeError::UnsupportedMode(
173                "image too large: RGBA buffer would overflow usize",
174            ))?;
175    let mut buf = vec![0u8; total];
176    for y in 0..bitmap.height {
177        let row = bitmap.row_bytes(y);
178        let row_off = y as usize * w * 4;
179        // Source layout: [X/A, B, G, R] per pixel (Xbgr8 / little-endian 32-bit).
180        for i in 0..w {
181            let src = i * 4;
182            let dst = row_off + i * 4;
183            buf[dst] = row[src + 3]; // R ← src[3]
184            buf[dst + 1] = row[src + 2]; // G ← src[2]
185            buf[dst + 2] = row[src + 1]; // B ← src[1]
186            buf[dst + 3] = row[src]; // A ← src[0]  (was X)
187        }
188    }
189    let mut writer = png_encoder(
190        out,
191        bitmap.width,
192        bitmap.height,
193        ::png::ColorType::Rgba,
194        ::png::BitDepth::Eight,
195    )?;
196    writer.write_image_data(&buf)?;
197    Ok(())
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use color::{Cmyk8, Gray8, Rgb8, Rgba8};
204    use raster::Bitmap;
205
206    fn make_rgb_bitmap(w: u32, h: u32, fill: [u8; 3]) -> Bitmap<Rgb8> {
207        let mut bmp = Bitmap::new(w, h, 1, false);
208        for y in 0..h {
209            let row = bmp.row_bytes_mut(y);
210            for chunk in row.chunks_exact_mut(3) {
211                chunk.copy_from_slice(&fill);
212            }
213        }
214        bmp
215    }
216
217    fn make_gray_bitmap(w: u32, h: u32, fill: u8) -> Bitmap<Gray8> {
218        let mut bmp = Bitmap::new(w, h, 1, false);
219        for y in 0..h {
220            bmp.row_bytes_mut(y).fill(fill);
221        }
222        bmp
223    }
224
225    /// Decode a PNG from bytes and return `(width, height, raw_pixels)`.
226    fn decode_png(data: &[u8]) -> (u32, u32, Vec<u8>) {
227        let decoder = ::png::Decoder::new(std::io::Cursor::new(data));
228        let mut reader = decoder.read_info().expect("png decode header");
229        let mut buf = vec![0u8; reader.output_buffer_size()];
230        let frame = reader.next_frame(&mut buf).expect("png decode frame");
231        let info = reader.info();
232        (info.width, info.height, buf[..frame.buffer_size()].to_vec())
233    }
234
235    #[test]
236    fn rgb_png_roundtrip() {
237        let bmp = make_rgb_bitmap(4, 2, [100, 150, 200]);
238        let mut out = Vec::new();
239        write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
240
241        let (w, h, pixels) = decode_png(&out);
242        assert_eq!((w, h), (4, 2));
243        assert_eq!(pixels.len(), 24, "4×2 pixels × 3 bytes");
244        for chunk in pixels.chunks_exact(3) {
245            assert_eq!(chunk, &[100, 150, 200], "pixel mismatch");
246        }
247    }
248
249    #[test]
250    fn gray_png_roundtrip() {
251        let bmp = make_gray_bitmap(3, 3, 77);
252        let mut out = Vec::new();
253        write_png::<Gray8, _>(&bmp, &mut out).unwrap();
254
255        let (w, h, pixels) = decode_png(&out);
256        assert_eq!((w, h), (3, 3));
257        assert!(pixels.iter().all(|&v| v == 77), "grayscale pixel mismatch");
258    }
259
260    #[test]
261    fn rgb_with_alpha_writes_rgba_png() {
262        // Bitmap with alpha plane: every pixel [255,0,0] with alpha 128.
263        let mut bmp: Bitmap<Rgb8> = Bitmap::new(2, 1, 1, true);
264        let row = bmp.row_bytes_mut(0);
265        row[..6].copy_from_slice(&[255, 0, 0, 255, 0, 0]);
266        if let Some(a) = bmp.alpha_plane_mut() {
267            a.fill(128);
268        }
269
270        let mut out = Vec::new();
271        write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
272
273        let (w, h, pixels) = decode_png(&out);
274        assert_eq!((w, h), (2, 1));
275        assert_eq!(pixels.len(), 8, "2 pixels × 4 bytes (RGBA)");
276        assert_eq!(&pixels[..4], &[255, 0, 0, 128], "pixel 0 RGBA");
277        assert_eq!(&pixels[4..8], &[255, 0, 0, 128], "pixel 1 RGBA");
278    }
279
280    #[test]
281    fn stride_padding_not_included() {
282        // width=3 with pad=4 → stride=4 for Gray8.
283        let bmp: Bitmap<Gray8> = Bitmap::new(3, 1, 4, false);
284        let mut out = Vec::new();
285        write_png::<Gray8, _>(&bmp, &mut out).unwrap();
286        let (w, h, pixels) = decode_png(&out);
287        assert_eq!((w, h), (3, 1));
288        assert_eq!(
289            pixels.len(),
290            3,
291            "stride padding must not appear in PNG output"
292        );
293    }
294
295    #[test]
296    fn rgba8_png_roundtrip_asymmetric_2x2() {
297        // 2×2 with four distinct pixels so every byte position is constrained.
298        // Rgba8 stores as Xbgr8 in memory: [X/A, B, G, R] per pixel.
299        let mut bmp: Bitmap<Rgba8> = Bitmap::new(2, 2, 1, false);
300        // (R, G, B, A) per pixel, written into memory as [A, B, G, R]:
301        // p00 = (10, 20, 30, 200), p01 = (40, 50, 60, 210),
302        // p10 = (70, 80, 90, 220), p11 = (100, 110, 120, 230).
303        bmp.row_bytes_mut(0)
304            .copy_from_slice(&[200, 30, 20, 10, 210, 60, 50, 40]);
305        bmp.row_bytes_mut(1)
306            .copy_from_slice(&[220, 90, 80, 70, 230, 120, 110, 100]);
307
308        let mut out = Vec::new();
309        write_png::<Rgba8, _>(&bmp, &mut out).unwrap();
310
311        let (w, h, pixels) = decode_png(&out);
312        assert_eq!((w, h), (2, 2));
313        assert_eq!(pixels.len(), 16, "2×2 pixels × 4 bytes (RGBA)");
314        // Decoded PNG is RGBA in row-major order.
315        assert_eq!(&pixels[0..4], &[10, 20, 30, 200], "row 0 px 0");
316        assert_eq!(&pixels[4..8], &[40, 50, 60, 210], "row 0 px 1");
317        assert_eq!(&pixels[8..12], &[70, 80, 90, 220], "row 1 px 0");
318        assert_eq!(&pixels[12..16], &[100, 110, 120, 230], "row 1 px 1");
319    }
320
321    #[test]
322    fn rgb_with_alpha_multi_row_promotion() {
323        // The 2×1 rgb_with_alpha_writes_rgba_png test only exercises a single
324        // row; this 2×2 fixture verifies the per-row offset math is correct
325        // for at least two rows of RGB+alpha promotion.
326        let mut bmp: Bitmap<Rgb8> = Bitmap::new(2, 2, 1, true);
327        // Row 0 RGB: [10, 20, 30, 40, 50, 60]; Row 1 RGB: [70, 80, 90, 100, 110, 120].
328        bmp.row_bytes_mut(0)
329            .copy_from_slice(&[10, 20, 30, 40, 50, 60]);
330        bmp.row_bytes_mut(1)
331            .copy_from_slice(&[70, 80, 90, 100, 110, 120]);
332        let alpha = bmp.alpha_plane_mut().expect("alpha plane present");
333        alpha.copy_from_slice(&[200, 210, 220, 230]);
334
335        let mut out = Vec::new();
336        write_png::<Rgb8, _>(&bmp, &mut out).unwrap();
337        let (w, h, pixels) = decode_png(&out);
338        assert_eq!((w, h), (2, 2));
339        assert_eq!(pixels.len(), 16);
340        assert_eq!(&pixels[0..4], &[10, 20, 30, 200]);
341        assert_eq!(&pixels[4..8], &[40, 50, 60, 210]);
342        assert_eq!(&pixels[8..12], &[70, 80, 90, 220]);
343        assert_eq!(&pixels[12..16], &[100, 110, 120, 230]);
344    }
345
346    #[test]
347    fn cmyk_returns_unsupported_error() {
348        let bmp: Bitmap<Cmyk8> = Bitmap::new(1, 1, 1, false);
349        let mut out = Vec::new();
350        let result = write_png::<Cmyk8, _>(&bmp, &mut out);
351        assert!(
352            matches!(result, Err(EncodeError::UnsupportedMode(_))),
353            "Cmyk8 should return UnsupportedMode for PNG"
354        );
355    }
356}