Minor reduction improvements (#502)

* Fix bKGD conversion from gray to palette

* Allow grayscale reduction from 16 to 4 or less

* Refactor reduction evaluation sequence

* Separate palette into new file
This commit is contained in:
andrews05 2023-05-08 09:46:58 +12:00 committed by GitHub
parent a26d225d81
commit 2f622fc7bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 357 additions and 342 deletions

View file

@ -12,7 +12,7 @@ fn reductions_16_to_8_bits(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw));
}
#[bench]
@ -22,7 +22,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -32,7 +32,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -42,7 +42,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -52,7 +52,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -62,7 +62,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -72,7 +72,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -82,7 +82,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -92,7 +92,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -102,7 +102,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -112,7 +112,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -122,7 +122,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -132,7 +132,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
}
#[bench]
@ -140,7 +140,7 @@ fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
}
#[bench]
@ -148,7 +148,7 @@ fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| alpha::reduced_alpha_channel(&png.raw, false));
}
#[bench]
@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -178,7 +178,10 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| {
color::reduce_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
@ -188,7 +191,10 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| {
color::reduce_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
#[bench]
@ -198,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -206,7 +212,7 @@ fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -214,7 +220,7 @@ fn reductions_rgba_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_to_palette(&png.raw));
}
#[bench]
@ -222,7 +228,7 @@ fn reductions_rgb_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png"));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduce_color_type(&png.raw, true, false));
b.iter(|| color::reduce_to_palette(&png.raw));
}
#[bench]
@ -232,7 +238,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduced_palette(&png.raw, false));
b.iter(|| palette::optimized_palette(&png.raw, false));
}
#[bench]
@ -242,7 +248,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduced_palette(&png.raw, false));
b.iter(|| palette::optimized_palette(&png.raw, false));
}
#[bench]
@ -252,7 +258,7 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| reduced_palette(&png.raw, false));
b.iter(|| palette::optimized_palette(&png.raw, false));
}
#[bench]

View file

@ -74,6 +74,15 @@ impl ColorType {
pub fn has_alpha(&self) -> bool {
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
}
#[inline]
pub fn has_trns(&self) -> bool {
match self {
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
ColorType::RGB { transparent_color } => transparent_color.is_some(),
_ => false,
}
}
}
#[repr(u8)]

View file

@ -25,7 +25,6 @@ extern crate rayon;
mod rayon;
use crate::atomicmin::AtomicMin;
use crate::colors::BitDepth;
use crate::deflate::{crc32, inflate};
use crate::evaluate::Evaluator;
use crate::png::PngData;
@ -477,23 +476,6 @@ fn optimize_png(
perform_strip(png, opts);
let stripped_png = png.clone();
// Interlacing is not part of the evaluator trials but must be done first to evaluate the rest correctly
let mut reduction_occurred = false;
if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.raw.change_interlacing(interlacing) {
png.raw = Arc::new(reduced);
reduction_occurred = true;
}
}
// If alpha optimization is enabled, perform a black alpha reduction before evaluating reductions
// This can allow reductions from alpha to indexed which may not have been possible otherwise
if opts.optimize_alpha {
if let Some(reduced) = cleaned_alpha_channel(&png.raw) {
png.raw = Arc::new(reduced);
}
}
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
let eval_compression = 5;
// None and Bigrams work well together, especially for alpha reductions
@ -505,7 +487,9 @@ fn optimize_png(
eval_compression,
false,
);
perform_reductions(png.raw.clone(), opts, &deadline, &eval);
let (baseline, mut reduction_occurred) =
perform_reductions(png.raw.clone(), opts, &deadline, &eval);
png.raw = baseline;
let mut eval_filter = if let Some(result) = eval.get_best_candidate() {
*png = result.image;
if result.is_reduction {
@ -664,66 +648,6 @@ fn optimize_png(
Ok(output)
}
fn perform_reductions(
mut png: Arc<PngImage>,
opts: &Options,
deadline: &Deadline,
eval: &Evaluator,
) {
// The eval baseline will be set from the original png only if we attempt any reductions
let baseline = png.clone();
let mut reduction_occurred = false;
if opts.palette_reduction {
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
eval.try_image(png.clone());
reduction_occurred = true;
}
if deadline.passed() {
return;
}
}
if opts.bit_depth_reduction {
if let Some(reduced) = reduce_bit_depth(&png, 1) {
let previous = png.clone();
let bits = reduced.ihdr.bit_depth;
png = Arc::new(reduced);
eval.try_image(png.clone());
if (bits == BitDepth::One || bits == BitDepth::Two)
&& previous.ihdr.bit_depth != BitDepth::Four
{
// Also try 16-color mode for all lower bits images, since that may compress better
if let Some(reduced) = reduce_bit_depth(&previous, 4) {
eval.try_image(Arc::new(reduced));
}
}
reduction_occurred = true;
}
if deadline.passed() {
return;
}
}
if opts.color_type_reduction {
if let Some(reduced) =
reduce_color_type(&png, opts.grayscale_reduction, opts.optimize_alpha)
{
png = Arc::new(reduced);
eval.try_image(png.clone());
reduction_occurred = true;
}
if deadline.passed() {
return;
}
}
if reduction_occurred {
eval.set_baseline(baseline);
}
}
/// Execute a compression trial
fn perform_trial(
filtered: &[u8],

View file

@ -2,13 +2,10 @@ use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData;
use crate::png::PngImage;
/// Attempt to reduce the bit depth of the image, returning the reduced image if successful
/// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful
#[must_use]
pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage> {
pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Sixteen {
if png.channels_per_pixel() == 1 {
return reduce_bit_depth_8_or_less(png, minimum_bits);
}
return None;
}
@ -29,11 +26,12 @@ pub fn reduce_bit_depth(png: &PngImage, minimum_bits: usize) -> Option<PngImage>
})
}
/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, returning the reduced image if successful
#[must_use]
pub fn reduce_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
assert!((1..8).contains(&minimum_bits));
let bit_depth = png.ihdr.bit_depth as usize;
if minimum_bits >= bit_depth || bit_depth > 8 {
if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 {
return None;
}
// Calculate the current number of pixels per byte

View file

@ -35,7 +35,7 @@ where
#[must_use]
pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len());
@ -98,22 +98,36 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
if bkgd_header.len() != 6 {
// malformed chunk?
return None;
}
// In bKGD 16-bit values are used even for 8-bit images
let bg = RGBA8::new(bkgd_header[1], bkgd_header[3], bkgd_header[5], 255);
let entry = if let Some(&entry) = palette.get(&bg) {
entry
} else if palette.len() < 256 {
let entry = palette.len() as u8;
palette.insert(bg, entry);
entry
let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 {
// In bKGD 16-bit values are used even for 8-bit images
Some(RGBA8::new(
bkgd_header[1],
bkgd_header[3],
bkgd_header[5],
255,
))
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 {
Some(RGBA8::new(
bkgd_header[1],
bkgd_header[1],
bkgd_header[1],
255,
))
} else {
return None; // No space in palette to store the bg as an index
None
};
aux_headers.insert(*b"bKGD", vec![entry]);
if let Some(bg) = bg {
let entry = if let Some(&entry) = palette.get(&bg) {
entry
} else if palette.len() < 256 {
let entry = palette.len() as u8;
palette.insert(bg, entry);
entry
} else {
return None; // No space in palette to store the bg as an index
};
aux_headers.insert(*b"bKGD", vec![entry]);
}
}
if let Some(sbit_header) = png.aux_headers.get(b"sBIT") {
@ -140,6 +154,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.is_rgb() {
return None;
}
let mut reduced = Vec::with_capacity(png.data.len());
let byte_depth = png.bytes_per_channel();
let bpp = png.channels_per_pixel() * byte_depth;

View file

@ -1,247 +1,115 @@
use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData;
use crate::evaluate::Evaluator;
use crate::png::PngImage;
use indexmap::map::{Entry::*, IndexMap};
use rgb::RGBA8;
use std::borrow::Cow;
use crate::Deadline;
use crate::Options;
use std::sync::Arc;
pub mod alpha;
use crate::alpha::reduced_alpha_channel;
use crate::alpha::*;
pub mod bit_depth;
use crate::bit_depth::reduce_bit_depth_8_or_less;
use crate::bit_depth::*;
pub mod color;
use crate::color::*;
pub mod palette;
use crate::palette::*;
pub(crate) use crate::alpha::cleaned_alpha_channel;
pub(crate) use crate::bit_depth::reduce_bit_depth;
pub(crate) fn perform_reductions(
mut png: Arc<PngImage>,
opts: &Options,
deadline: &Deadline,
eval: &Evaluator,
) -> (Arc<PngImage>, bool) {
let mut reduction_occurred = false;
let mut evaluation_added = false;
/// Attempt to reduce the number of colors in the palette
/// Returns `None` if palette hasn't changed
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
// Can't reduce if there is no palette
_ => return None,
};
if png.ihdr.bit_depth == BitDepth::One {
// Gains from 1-bit images will be at most 1 byte
// Not worth the CPU time
return None;
}
let mut palette_map = [None; 256];
let mut used = [false; 256];
{
// Find palette entries that are never used
match png.ihdr.bit_depth {
BitDepth::Eight => {
for &byte in &png.data {
used[byte as usize] = true;
}
}
BitDepth::Four => {
for &byte in &png.data {
used[(byte & 0x0F) as usize] = true;
used[(byte >> 4) as usize] = true;
}
}
BitDepth::Two => {
for &byte in &png.data {
used[(byte & 0x03) as usize] = true;
used[((byte >> 2) & 0x03) as usize] = true;
used[((byte >> 4) & 0x03) as usize] = true;
used[(byte >> 6) as usize] = true;
}
}
_ => unreachable!(),
}
let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect();
used_enumerated.sort_by(|a, b| {
//Sort by ascending alpha and descending luma.
let color_val = |i| {
let color = palette
.get(i)
.copied()
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
((color.a as i32) << 18)
// These are coefficients for standard sRGB to luma conversion
- i32::from(color.r) * 299
- i32::from(color.g) * 587
- i32::from(color.b) * 114
};
color_val(a.0).cmp(&color_val(b.0))
});
// Make sure the background is also included, but only after sorting since it may not be used in idat
if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) {
if !used[idx as usize] {
used_enumerated.push((idx as usize, &true));
}
}
let mut next_index = 0_u16;
let mut seen = IndexMap::with_capacity(palette.len());
for (i, used) in used_enumerated.iter().cloned() {
if !used {
continue;
}
// There are invalid files that use pixel indices beyond palette size
let mut color = palette
.get(i)
.cloned()
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
// If there are multiple fully transparent entries, reduce them into one
if optimize_alpha && color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
match seen.entry(color) {
Vacant(new) => {
palette_map[i] = Some(next_index as u8);
new.insert(next_index as u8);
next_index += 1;
}
Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()),
}
// Interlacing must be processed first in order to evaluate the rest correctly
if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) {
png = Arc::new(reduced);
reduction_occurred = true;
}
}
do_palette_reduction(png, palette, &palette_map)
}
#[must_use]
fn do_palette_reduction(
png: &PngImage,
palette: &[RGBA8],
palette_map: &[Option<u8>; 256],
) -> Option<PngImage> {
let byte_map = palette_map_to_byte_map(png, palette_map)?;
// Reassign data bytes to new indices
let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect();
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
if let Some(Some(map_to)) = bkgd_header
.first()
.and_then(|&idx| palette_map.get(idx as usize))
{
aux_headers.insert(*b"bKGD", vec![*map_to]);
// If alpha optimization is enabled, clean the alpha channel before continuing
// This can allow some color type reductions which may not have been possible otherwise
if opts.optimize_alpha && !deadline.passed() {
if let Some(reduced) = cleaned_alpha_channel(&png) {
png = Arc::new(reduced);
// This does not count as a reduction
}
}
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: reordered_palette(palette, palette_map),
},
..png.ihdr
},
data: raw_data,
aux_headers,
})
}
fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option<u8>; 256]) -> Option<[u8; 256]> {
if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) {
// No reduction necessary
return None;
}
let mut byte_map = [0_u8; 256];
// low bit-depths can be pre-computed for every byte value
match png.ihdr.bit_depth {
BitDepth::Eight => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte].unwrap_or(0)
}
}
BitDepth::Four => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte & 0x0F].unwrap_or(0)
| (palette_map[byte >> 4].unwrap_or(0) << 4);
}
}
BitDepth::Two => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte & 0x03].unwrap_or(0)
| (palette_map[(byte >> 2) & 0x03].unwrap_or(0) << 2)
| (palette_map[(byte >> 4) & 0x03].unwrap_or(0) << 4)
| (palette_map[byte >> 6].unwrap_or(0) << 6);
}
}
_ => {}
}
Some(byte_map)
}
fn reordered_palette(palette: &[RGBA8], palette_map: &[Option<u8>; 256]) -> Vec<RGBA8> {
let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize;
let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1];
for (&color, &map_to) in palette.iter().zip(palette_map.iter()) {
if let Some(map_to) = map_to {
new_palette[map_to as usize] = color;
// Attempt to reduce 16-bit to 8-bit
// This is just removal of bytes and does not need to be evaluated
if opts.bit_depth_reduction && !deadline.passed() {
if let Some(reduced) = reduced_bit_depth_16_to_8(&png) {
png = Arc::new(reduced);
reduction_occurred = true;
}
}
new_palette
}
/// Attempt to reduce the color type of the image, returning the reduced image if successful
pub fn reduce_color_type(
png: &PngImage,
grayscale_reduction: bool,
optimize_alpha: bool,
) -> Option<PngImage> {
let was_single_channel = png.channels_per_pixel() == 1;
let mut reduced = Cow::Borrowed(png);
// Go down one step at a time - maybe not the most efficient, but it's safe
// Attempt to reduce RGB to grayscale
if grayscale_reduction && reduced.ihdr.color_type.is_rgb() {
if let Some(r) = reduce_rgb_to_grayscale(&reduced) {
reduced = Cow::Owned(r);
// This is just removal of bytes and does not need to be evaluated
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduce_rgb_to_grayscale(&png) {
png = Arc::new(reduced);
reduction_occurred = true;
}
}
// Attempt grayscale alpha reduction before palette, as grayscale will typically be smaller than indexed
if reduced.ihdr.color_type == ColorType::GrayscaleAlpha {
if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) {
reduced = Cow::Owned(r);
}
}
// Now retain the current png for the evaluator baseline
// It will only be entered into the evaluator if there are also others to evaluate
let mut baseline = png.clone();
// Attempt to reduce to palette, if not already a single channel
if reduced.channels_per_pixel() != 1 {
if let Some(r) = reduce_to_palette(&reduced) {
reduced = Cow::Owned(r);
// Make sure that palette gets sorted. Ideally, this should be done within reduce_to_palette.
if let Some(r) = reduced_palette(&reduced, optimize_alpha) {
reduced = Cow::Owned(r);
// Attempt alpha removal
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
// If the reduction requires a tRNS chunk, enter this into the evaluator
// Otherwise it is just removal of bytes and should become the baseline
if png.ihdr.color_type.has_trns() {
eval.try_image(png.clone());
evaluation_added = true;
} else {
baseline = png.clone();
reduction_occurred = true;
}
}
}
// Attempt RGBA alpha reduction after palette, so it can be skipped if palette was successful
if reduced.ihdr.color_type == ColorType::RGBA {
if let Some(r) = reduced_alpha_channel(&reduced, optimize_alpha) {
reduced = Cow::Owned(r);
// Attempt to reduce the palette size
if opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
eval.try_image(png.clone());
evaluation_added = true;
}
}
// Some conversions will allow us to perform bit depth reduction that wasn't possible before
if !was_single_channel && reduced.channels_per_pixel() == 1 {
if let Some(r) = reduce_bit_depth_8_or_less(&reduced, 1) {
reduced = Cow::Owned(r);
// Attempt to reduce to palette
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduce_to_palette(&png) {
png = Arc::new(reduced);
// Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette)
if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
}
eval.try_image(png.clone());
evaluation_added = true;
}
}
match reduced {
Cow::Owned(r) => Some(r),
_ => None,
// Attempt to reduce to a lower bit depth
if opts.bit_depth_reduction && !deadline.passed() {
if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) {
png = Arc::new(reduced);
eval.try_image(png.clone());
evaluation_added = true;
}
}
if evaluation_added {
eval.set_baseline(baseline.clone());
}
(baseline, reduction_occurred)
}

180
src/reduction/palette.rs Normal file
View file

@ -0,0 +1,180 @@
use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData;
use crate::png::PngImage;
use indexmap::map::{Entry::*, IndexMap};
use rgb::RGBA8;
/// Attempt to shrink and sort the palette, returning the optimized image if successful
#[must_use]
pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
// Can't reduce if there is no palette
_ => return None,
};
if png.ihdr.bit_depth == BitDepth::One {
// Gains from 1-bit images will be at most 1 byte
// Not worth the CPU time
return None;
}
let mut palette_map = [None; 256];
let mut used = [false; 256];
{
// Find palette entries that are never used
match png.ihdr.bit_depth {
BitDepth::Eight => {
for &byte in &png.data {
used[byte as usize] = true;
}
}
BitDepth::Four => {
for &byte in &png.data {
used[(byte & 0x0F) as usize] = true;
used[(byte >> 4) as usize] = true;
}
}
BitDepth::Two => {
for &byte in &png.data {
used[(byte & 0x03) as usize] = true;
used[((byte >> 2) & 0x03) as usize] = true;
used[((byte >> 4) & 0x03) as usize] = true;
used[(byte >> 6) as usize] = true;
}
}
_ => unreachable!(),
}
let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect();
used_enumerated.sort_by(|a, b| {
//Sort by ascending alpha and descending luma.
let color_val = |i| {
let color = palette
.get(i)
.copied()
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
((color.a as i32) << 18)
// These are coefficients for standard sRGB to luma conversion
- i32::from(color.r) * 299
- i32::from(color.g) * 587
- i32::from(color.b) * 114
};
color_val(a.0).cmp(&color_val(b.0))
});
// Make sure the background is also included, but only after sorting since it may not be used in idat
if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) {
if !used[idx as usize] {
used_enumerated.push((idx as usize, &true));
}
}
let mut next_index = 0_u16;
let mut seen = IndexMap::with_capacity(palette.len());
for (i, used) in used_enumerated.iter().cloned() {
if !used {
continue;
}
// There are invalid files that use pixel indices beyond palette size
let mut color = palette
.get(i)
.cloned()
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
// If there are multiple fully transparent entries, reduce them into one
if optimize_alpha && color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
match seen.entry(color) {
Vacant(new) => {
palette_map[i] = Some(next_index as u8);
new.insert(next_index as u8);
next_index += 1;
}
Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()),
}
}
}
do_palette_reduction(png, palette, &palette_map)
}
#[must_use]
fn do_palette_reduction(
png: &PngImage,
palette: &[RGBA8],
palette_map: &[Option<u8>; 256],
) -> Option<PngImage> {
let byte_map = palette_map_to_byte_map(png, palette_map)?;
// Reassign data bytes to new indices
let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect();
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
if let Some(Some(map_to)) = bkgd_header
.first()
.and_then(|&idx| palette_map.get(idx as usize))
{
aux_headers.insert(*b"bKGD", vec![*map_to]);
}
}
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: reordered_palette(palette, palette_map),
},
..png.ihdr
},
data: raw_data,
aux_headers,
})
}
fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option<u8>; 256]) -> Option<[u8; 256]> {
if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) {
// No reduction necessary
return None;
}
let mut byte_map = [0_u8; 256];
// low bit-depths can be pre-computed for every byte value
match png.ihdr.bit_depth {
BitDepth::Eight => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte].unwrap_or(0)
}
}
BitDepth::Four => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte & 0x0F].unwrap_or(0)
| (palette_map[byte >> 4].unwrap_or(0) << 4);
}
}
BitDepth::Two => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte & 0x03].unwrap_or(0)
| (palette_map[(byte >> 2) & 0x03].unwrap_or(0) << 2)
| (palette_map[(byte >> 4) & 0x03].unwrap_or(0) << 4)
| (palette_map[byte >> 6].unwrap_or(0) << 6);
}
}
_ => {}
}
Some(byte_map)
}
fn reordered_palette(palette: &[RGBA8], palette_map: &[Option<u8>; 256]) -> Vec<RGBA8> {
let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize;
let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1];
for (&color, &map_to) in palette.iter().zip(palette_map.iter()) {
if let Some(map_to) = map_to {
new_palette[map_to as usize] = color;
}
}
new_palette
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -712,6 +712,18 @@ fn grayscale_16_should_be_grayscale_8() {
);
}
#[test]
fn grayscale_16_should_be_grayscale_1() {
test_it_converts(
"tests/files/grayscale_16_should_be_grayscale_1.png",
false,
GRAYSCALE,
BitDepth::Sixteen,
GRAYSCALE,
BitDepth::One,
);
}
#[test]
fn grayscale_8_should_be_grayscale_8() {
test_it_converts(

View file

@ -171,8 +171,8 @@ fn issue_52_04() {
None,
RGBA,
BitDepth::Eight,
INDEXED,
BitDepth::One,
RGB,
BitDepth::Eight,
);
}