Fix #6969: Rasterize to Grayscale or Bitmap makes a grayscale image

This commit is contained in:
Sven Lafebre
2026-11-12 23:04:50 +01:00
parent 71d717b1da
commit 499a74cedb
6 changed files with 168 additions and 8 deletions
+15
View File
@@ -53,6 +53,21 @@ pub fn encode(rgba: &[u8], width: u32, height: u32, o: &PngOptions) -> Result<Ve
write(&pixels, width, height, Layout { channels, bits: 8, color_type }, &[], o)
}
/// Encode grey levels with alpha (two bytes a pixel, `width`×`height`, row-major) as a greyscale
/// PNG: grey and alpha (colour type 4), or grey alone (colour type 0) when every pixel is opaque.
pub fn encode_gray(gray_alpha: &[u8], width: u32, height: u32, o: &PngOptions) -> Result<Vec<u8>, String> {
check_size(width, height)?;
if gray_alpha.len() as u64 != u64::from(width) * u64::from(height) * 2 {
return Err("PNG encoding failed: pixel buffer doesn't match the image size".into());
}
let px = gray_alpha.as_chunks::<2>().0;
if px.iter().all(|p| p[1] == 255) {
let levels: Vec<u8> = px.iter().map(|p| p[0]).collect();
return write(&levels, width, height, Layout { channels: 1, bits: 8, color_type: 0 }, &[], o);
}
write(gray_alpha, width, height, Layout { channels: 2, bits: 8, color_type: 4 }, &[], o)
}
/// Encode a palette image as an indexed PNG (colour type 3: `PLTE`, and `tRNS` for its
/// transparent entry) at 1, 2, 4 or 8 bits per pixel, as few as the palette needs.
pub fn encode_indexed(img: &Indexed, o: &PngOptions) -> Result<Vec<u8>, String> {
+35
View File
@@ -67,6 +67,41 @@ fn png_refuses_a_buffer_of_the_wrong_size() {
assert!(encode(&[], 0, 0, &PngOptions::default()).is_err());
}
/// Object › Rasterize's Grayscale and Bitmap images (#6969): a grey PNG of one channel when
/// opaque and grey and alpha otherwise; Bitmap's black and white as a 1-bit TIFF when opaque,
/// else as a grey PNG with alpha. Odd widths keep each row on a byte.
#[test]
fn gray_and_bilevel_images_store_gray() {
let header = |file: &[u8]| (file[24], file[25]);
let gray = |file: &[u8]| image::load_from_memory(file).unwrap().to_luma_alpha8().into_raw();
// 3 × 2, premultiplied: black, red, light grey; white, mid grey, almost black.
let mut img = Rendered {
width: 3,
height: 2,
pixels: [[0, 0, 0], [255, 0, 0], [200; 3], [255; 3], [128; 3], [127; 3]].iter().flat_map(|&[r, g, b]| [r, g, b, 255]).collect(),
};
let levels: Vec<u8> = [0u8, 54, 200, 255, 128, 127].iter().flat_map(|&g| [g, 255]).collect();
let file = img.to_gray_png().unwrap();
assert_eq!(header(&file), (8, 0), "8-bit grey");
assert_eq!(gray(&file), levels);
let (mime, file) = img.to_bilevel(150.0).unwrap();
assert_eq!(mime, "image/tiff");
let mut d = ::tiff::decoder::Decoder::new(std::io::Cursor::new(&file)).unwrap();
assert_eq!(d.colortype().unwrap(), ::tiff::ColorType::Gray(1));
let bw: Vec<u8> = [0u8, 0, 255, 255, 255, 0].iter().flat_map(|&g| [g, 255]).collect();
assert_eq!(gray(&file), bw, "from 128 up is white");
// Half-covered white over the light grey; a clear pixel is white.
img.pixels[8..12].copy_from_slice(&[100, 100, 100, 100]);
img.pixels[20..24].copy_from_slice(&[0; 4]);
let file = img.to_gray_png().unwrap();
assert_eq!(header(&file), (8, 4), "grey and alpha");
assert_eq!(gray(&file), [0, 255, 54, 255, 255, 100, 255, 255, 128, 255, 255, 0]);
let (mime, file) = img.to_bilevel(150.0).unwrap();
assert_eq!((mime, header(&file)), ("image/png", (8, 4)), "no 1-bit grey with alpha");
assert_eq!(gray(&file), [0, 255, 0, 255, 255, 100, 255, 255, 255, 255, 255, 0], "still black and white");
assert!(super::png::encode_gray(&levels[..10], 3, 2, &PngOptions::default()).is_err());
}
/// A 40×30 pt document with a black circle off the pixel grid.
fn circle_doc() -> Document {
let mut d = Document::new(40.0, 30.0);