Improve indexed to grayscale conversion (#674)

If no palette reduction occurred (or palette reductions were off), a
fully transparent palette entry may not have been zeroed to black. By
ensuring this gets cleaned in the `indexed_to_channels` transformation,
we may able to achieve a grayscale conversion that would otherwise have
been RGB.

This is the final piece of the puzzle in #649 to achieve better file
sizes on the first run and avoid further changes on a second run.

Results from the images in #649 (combining this PR and #673):
9.1.3 1st run: 2,191,773
9.1.3 2nd run: 2,191,629
PR 1st run: 2,191,380
PR 2nd run: 2,191,380
This commit is contained in:
andrews05 2025-02-03 06:26:18 +13:00 committed by GitHub
parent e6f84264e5
commit 174f36ad8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 34 additions and 5 deletions

View file

@ -257,7 +257,7 @@ fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| color::indexed_to_channels(&png.raw, true));
b.iter(|| color::indexed_to_channels(&png.raw, true, false));
}
#[bench]

View file

@ -138,15 +138,30 @@ pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(png: &PngImage, allow_grayscale: bool) -> Option<PngImage> {
pub fn indexed_to_channels(
png: &PngImage,
allow_grayscale: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
let mut palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette.clone(),
_ => return None,
};
// Ensure fully transparent colors are black, which can help with grayscale conversion
if optimize_alpha {
for color in &mut palette {
if color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
}
}
// Determine which channels are required
let is_gray = if allow_grayscale {
palette.iter().all(|c| c.r == c.g && c.g == c.b)

View file

@ -106,7 +106,9 @@ pub(crate) fn perform_reductions(
// Attempt to convert from indexed to channels
// This may give a better result due to dropping the PLTE chunk
if !cheap && opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = indexed_to_channels(&png, opts.grayscale_reduction) {
if let Some(reduced) =
indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha)
{
// This result should not be passed on to subsequent reductions
eval.try_image(Arc::new(reduced));
evaluation_added = true;

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View file

@ -578,6 +578,18 @@ fn palette_8_should_be_grayscale_8() {
);
}
#[test]
fn palette_2_should_be_grayscale_alpha_8() {
test_it_converts(
"tests/files/palette_2_should_be_grayscale_alpha_8.png",
true,
INDEXED,
BitDepth::Two,
GRAYSCALE_ALPHA,
BitDepth::Eight,
);
}
#[test]
fn palette_8_should_be_rgb() {
test_it_converts(