diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index a1b8e68b..cd8952f6 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -1,4 +1,4 @@ -use crate::colors::ColorType; +use crate::colors::{BitDepth, ColorType}; use crate::headers::IhdrData; use crate::png::PngImage; @@ -37,37 +37,61 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option { } #[must_use] -pub fn reduced_alpha_channel(png: &PngImage) -> Option { +pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option { let target_color_type = match png.ihdr.color_type { ColorType::GrayscaleAlpha => ColorType::Grayscale, ColorType::RGBA => ColorType::RGB, _ => return None, }; - let byte_depth = png.ihdr.bit_depth.as_u8() >> 3; - let channels = png.channels_per_pixel(); + let byte_depth = (png.ihdr.bit_depth.as_u8() >> 3) as usize; + let channels = png.channels_per_pixel() as usize; let bpp = channels * byte_depth; - let bpp_mask = bpp - 1; - if 0 != bpp & bpp_mask { - return None; - } let colored_bytes = bpp - byte_depth; + + // If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels. + // In case this occurs, we want to try and find an unused color we can use for the tRNS chunk. + // Rather than an exhaustive search, we will just keep track of 256 shades of gray, which should cover many cases. + let mut has_transparency = false; + let mut used_colors = vec![false; 256]; + for line in png.scan_lines() { - for (i, &byte) in line.data.iter().enumerate() { - if i as u8 & bpp_mask >= colored_bytes && byte != 255 { + for pixel in line.data.chunks(bpp) { + if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) { + // Fully transparent, we may be able to reduce with tRNS + has_transparency = true; + } else if pixel.iter().skip(colored_bytes).any(|b| *b != 255) { + // Partially transparent, the image is not reducible return None; + } else if optimize_alpha && pixel.iter().take(colored_bytes).all(|b| *b == pixel[0]) { + // Opaque shade of gray, we can't use this color for tRNS + used_colors[pixel[0] as usize] = true; } } } + let transparency_pixel = if has_transparency { + // If no unused color was found we will have to fail here + // Otherwise, proceed to construct the tRNS chunk + let unused_color = used_colors.iter().position(|b| !*b)? as u8; + Some(match png.ihdr.bit_depth { + BitDepth::Sixteen => vec![unused_color; colored_bytes], + // 8-bit is still stored as 16-bit, with the high byte set to 0 + _ => [0, unused_color].repeat(colored_bytes), + }) + } else { + None + }; + let mut raw_data = Vec::with_capacity(png.data.len()); for line in png.scan_lines() { raw_data.push(line.filter); - for (i, &byte) in line.data.iter().enumerate() { - if i as u8 & bpp_mask >= colored_bytes { - continue; - } - - raw_data.push(byte); + for pixel in line.data.chunks(bpp) { + match transparency_pixel { + Some(ref trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => { + raw_data.resize(raw_data.len() + colored_bytes, trns[1]); + } + _ => raw_data.extend_from_slice(&pixel[0..colored_bytes]), + }; } } @@ -86,7 +110,7 @@ pub fn reduced_alpha_channel(png: &PngImage) -> Option { ..png.ihdr }, aux_headers, - transparency_pixel: None, + transparency_pixel, palette: None, }) } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 9f8273fd..4bf2ec6e 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -213,7 +213,7 @@ pub fn reduce_color_type( } else { None } - .or_else(|| reduced_alpha_channel(&reduced)) + .or_else(|| reduced_alpha_channel(&reduced, optimize_alpha)) { reduced = Cow::Owned(r); } else if let Some(r) = reduce_to_palette(&reduced) { @@ -223,7 +223,9 @@ pub fn reduce_color_type( } if reduced.ihdr.color_type == ColorType::GrayscaleAlpha { - if let Some(r) = reduced_alpha_channel(&reduced).or_else(|| reduce_to_palette(&reduced)) { + if let Some(r) = + reduced_alpha_channel(&reduced, optimize_alpha).or_else(|| reduce_to_palette(&reduced)) + { reduced = Cow::Owned(r); should_reduce_bit_depth = true; } diff --git a/tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png b/tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png new file mode 100644 index 00000000..0ef1ada1 Binary files /dev/null and b/tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png differ diff --git a/tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png b/tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png new file mode 100644 index 00000000..a339411d Binary files /dev/null and b/tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png differ diff --git a/tests/files/rgba_16_should_be_rgb_trns_16.png b/tests/files/rgba_16_should_be_rgb_trns_16.png new file mode 100644 index 00000000..5174bd31 Binary files /dev/null and b/tests/files/rgba_16_should_be_rgb_trns_16.png differ diff --git a/tests/files/rgba_8_should_be_rgb_trns_8.png b/tests/files/rgba_8_should_be_rgb_trns_8.png new file mode 100644 index 00000000..783ccf29 Binary files /dev/null and b/tests/files/rgba_8_should_be_rgb_trns_8.png differ diff --git a/tests/reduction.rs b/tests/reduction.rs index 6f34942e..e6ec5a2a 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -130,6 +130,30 @@ fn rgba_8_should_be_rgb_8() { ); } +#[test] +fn rgba_16_should_be_rgb_trns_16() { + test_it_converts( + "tests/files/rgba_16_should_be_rgb_trns_16.png", + true, + ColorType::RGBA, + BitDepth::Sixteen, + ColorType::RGB, + BitDepth::Sixteen, + ); +} + +#[test] +fn rgba_8_should_be_rgb_trns_8() { + test_it_converts( + "tests/files/rgba_8_should_be_rgb_trns_8.png", + true, + ColorType::RGBA, + BitDepth::Eight, + ColorType::RGB, + BitDepth::Eight, + ); +} + #[test] fn rgba_16_should_be_palette_8() { test_it_converts( @@ -694,6 +718,30 @@ fn grayscale_8_should_be_grayscale_8() { ); } +#[test] +fn grayscale_alpha_16_should_be_grayscale_trns_16() { + test_it_converts( + "tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png", + true, + ColorType::GrayscaleAlpha, + BitDepth::Sixteen, + ColorType::Grayscale, + BitDepth::Sixteen, + ); +} + +#[test] +fn grayscale_alpha_8_should_be_grayscale_trns_8() { + test_it_converts( + "tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png", + true, + ColorType::GrayscaleAlpha, + BitDepth::Eight, + ColorType::Grayscale, + BitDepth::Eight, + ); +} + #[test] fn small_files() { let input = PathBuf::from("tests/files/small_files.png");