Further small reduction improvements (#504)

This commit is contained in:
andrews05 2023-05-17 12:53:05 +12:00 committed by GitHub
parent 9a500941d8
commit d8b7ebaf47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 533 additions and 342 deletions

View file

@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
b.iter(|| color::reduced_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(|| color::reduce_rgb_to_grayscale(&png.raw));
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -179,7 +179,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
color::reduce_rgb_to_grayscale(&png.raw)
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
@ -192,7 +192,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
let png = PngData::new(&input, false).unwrap();
b.iter(|| {
color::reduce_rgb_to_grayscale(&png.raw)
color::reduced_rgb_to_grayscale(&png.raw)
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
});
}
@ -204,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -212,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(|| color::reduce_rgb_to_grayscale(&png.raw));
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
}
#[bench]
@ -220,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(|| color::reduce_to_palette(&png.raw));
b.iter(|| color::reduced_to_indexed(&png.raw));
}
#[bench]
@ -228,7 +228,27 @@ 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(|| color::reduce_to_palette(&png.raw));
b.iter(|| color::reduced_to_indexed(&png.raw));
}
#[bench]
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/grayscale_8_should_be_palette_8.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::reduced_to_indexed(&png.raw));
}
#[bench]
fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_grayscale_8.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| color::indexed_to_channels(&png.raw));
}
#[bench]
@ -238,7 +258,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::optimized_palette(&png.raw, false));
b.iter(|| palette::reduced_palette(&png.raw, false));
}
#[bench]
@ -248,7 +268,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::optimized_palette(&png.raw, false));
b.iter(|| palette::reduced_palette(&png.raw, false));
}
#[bench]
@ -258,7 +278,17 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::optimized_palette(&png.raw, false));
b.iter(|| palette::reduced_palette(&png.raw, false));
}
#[bench]
fn reductions_palette_sort(b: &mut Bencher) {
let input = test::black_box(PathBuf::from(
"tests/files/palette_8_should_be_palette_8.png",
));
let png = PngData::new(&input, false).unwrap();
b.iter(|| palette::sorted_palette(&png.raw));
}
#[bench]

View file

@ -70,13 +70,21 @@ impl ColorType {
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
}
#[inline]
pub(crate) fn is_grayscale(&self) -> bool {
matches!(
self,
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
)
}
#[inline]
pub(crate) fn has_alpha(&self) -> bool {
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
}
#[inline]
pub fn has_trns(&self) -> bool {
pub(crate) fn has_trns(&self) -> bool {
match self {
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
ColorType::RGB { transparent_color } => transparent_color.is_some(),

View file

@ -564,7 +564,12 @@ fn optimize_png(
// Do this first so that reductions can ignore certain chunks such as bKGD
perform_strip(png, opts);
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) {
let max_size = if opts.force {
None
} else {
Some(png.estimated_output_size())
};
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, max_size) {
png.raw = new_png.raw;
png.idat_data = new_png.idat_data;
}
@ -611,7 +616,7 @@ fn optimize_raw(
mut png: Arc<PngImage>,
opts: &Options,
deadline: Arc<Deadline>,
max_idat_size: Option<usize>,
max_size: Option<usize>,
) -> Option<PngData> {
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
let eval_compression = 5;
@ -673,15 +678,10 @@ fn optimize_raw(
};
if trial.compression > 0 && trial.compression <= eval_compression {
// No further compression required
let idat_data = eval_result.image.idat_data;
if opts.force || idat_data.len() < max_idat_size.unwrap_or(usize::MAX) {
Some((trial, idat_data))
} else {
None
}
Some((trial, eval_result.image.idat_data))
} else {
debug!("Trying: {}", trial.filter);
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
let best_size = AtomicMin::new(max_size);
perform_trial(&eval_result.image.filtered, opts, trial, &best_size)
}
} else {
@ -712,7 +712,7 @@ fn optimize_raw(
debug!("Trying: {} filters", results.len());
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
let best_size = AtomicMin::new(max_size);
let results_iter = results.into_par_iter().with_max_len(1);
let best = results_iter.filter_map(|trial| {
if deadline.passed() {
@ -730,34 +730,37 @@ fn optimize_raw(
})
};
if let Some((opts, idat_data)) = best {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
opts.compression,
opts.filter,
idat_data.len()
);
return Some(PngData {
if let Some((trial, idat_data)) = best {
let image = PngData {
raw: png,
// The filtered data has not been retained here, but we don't need to return it
filtered: vec![],
idat_data,
});
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
trial.compression,
trial.filter,
image.idat_data.len()
);
return Some(image);
}
}
} else if let Some(result) = eval_result {
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
// we should still check if the evaluator compressed the baseline smaller than the original.
let idat_data = &result.image.idat_data;
if idat_data.len() < max_idat_size.unwrap_or(usize::MAX) {
let image = result.image;
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
debug!(
" zc = {} f = {:8} {} bytes",
eval_compression,
result.filter,
idat_data.len()
image.idat_data.len()
);
return Some(result.image);
return Some(image);
}
}

View file

@ -186,28 +186,12 @@ impl PngData {
match &self.raw.ihdr.color_type {
ColorType::Indexed { palette } => {
let mut palette_data = Vec::with_capacity(palette.len() * 3);
let mut max_palette_size = 1 << (self.raw.ihdr.bit_depth as u8);
// Ensure bKGD color doesn't get truncated from palette
if let Some(&idx) = self.raw.aux_headers.get(b"bKGD").and_then(|b| b.first()) {
max_palette_size = max_palette_size.max(idx as usize + 1);
}
for px in palette.iter().take(max_palette_size) {
for px in palette {
palette_data.extend_from_slice(px.rgb().as_slice());
}
write_png_block(b"PLTE", &palette_data, &mut output);
let num_transparent = palette.iter().take(max_palette_size).enumerate().fold(
0,
|prev, (index, px)| {
if px.a == 255 {
prev
} else {
index + 1
}
},
);
if num_transparent > 0 {
let trns_data: Vec<_> =
palette[0..num_transparent].iter().map(|px| px.a).collect();
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
let trns_data: Vec<_> = palette[0..=last_trns].iter().map(|px| px.a).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
}

View file

@ -1,103 +1,88 @@
use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData;
use crate::png::PngImage;
use indexmap::IndexMap;
use rgb::{ComponentMap, FromSlice, RGBA, RGBA8};
use indexmap::IndexSet;
use rgb::alt::Gray;
use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8};
use rustc_hash::FxHasher;
use std::hash::{BuildHasherDefault, Hash};
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
type FxIndexSet<V> = IndexSet<V, BuildHasherDefault<FxHasher>>;
fn reduce_scanline_to_palette<T>(
/// Maximum size difference between indexed and channels to consider a candidate for evaluation
pub const INDEXED_MAX_DIFF: usize = 20000;
fn build_palette<T>(
iter: impl IntoIterator<Item = T>,
palette: &mut FxIndexMap<T, u8>,
reduced: &mut Vec<u8>,
) -> bool
) -> Option<FxIndexSet<T>>
where
T: Eq + Hash,
{
let mut palette = FxIndexSet::default();
palette.reserve(257);
for pixel in iter {
let idx = if let Some(&idx) = palette.get(&pixel) {
idx
} else {
let len = palette.len();
if len == 256 {
return false;
}
let idx = len as u8;
palette.insert(pixel, idx);
idx
};
reduced.push(idx);
let (idx, _) = palette.insert_full(pixel);
if idx == 256 {
return None;
}
reduced.push(idx as u8);
}
true
Some(palette)
}
#[must_use]
pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 {
pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len());
let mut palette = FxIndexMap::default();
palette.reserve(257);
let ok = if let ColorType::RGB { transparent_color } = png.ihdr.color_type {
// Convert the RGB16 transparency to RGB8
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
reduce_scanline_to_palette(
png.data.as_rgb().iter().cloned().map(|px| {
px.alpha(if Some(px) != transparency_pixel {
255
} else {
0
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
return None;
}
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
let mut palette: Vec<_> = match png.ihdr.color_type {
ColorType::Grayscale { transparent_shade } => {
let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?;
// Convert the Gray16 transparency to Gray8
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
pmap.into_iter()
.map(|px| {
RGB::from(px).alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
}),
&mut palette,
&mut raw_data,
)
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha {
reduce_scanline_to_palette(
png.data.as_gray_alpha().iter().cloned().map(|px| RGBA {
r: px.0,
g: px.0,
b: px.0,
a: px.1,
}),
&mut palette,
&mut raw_data,
)
} else {
debug_assert_eq!(png.ihdr.color_type, ColorType::RGBA);
reduce_scanline_to_palette(
png.data.as_rgba().iter().cloned(),
&mut palette,
&mut raw_data,
)
.collect()
}
ColorType::RGB { transparent_color } => {
let pmap = build_palette(png.data.as_rgb().iter().cloned(), &mut raw_data)?;
// Convert the RGB16 transparency to RGB8
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
pmap.into_iter()
.map(|px| {
px.alpha(if Some(px) != transparency_pixel {
255
} else {
0
})
})
.collect()
}
ColorType::GrayscaleAlpha => {
let pmap = build_palette(png.data.as_gray_alpha().iter().cloned(), &mut raw_data)?;
pmap.into_iter().map(RGBA::from).collect()
}
ColorType::RGBA => {
let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?;
pmap.into_iter().collect()
}
_ => return None,
};
if !ok {
return None;
}
let num_transparent = palette
.iter()
.filter_map(|(px, &idx)| {
if px.a != 255 {
Some(idx as usize + 1)
} else {
None
}
})
.max();
let trns_size = num_transparent.map_or(0, |n| n + 8);
let headers_size = palette.len() * 3 + 8 + trns_size;
if raw_data.len() + headers_size > png.data.len() {
// Reduction would result in a larger image
return None;
}
let mut aux_headers = png.aux_headers.clone();
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
if let Some(bkgd_header) = aux_headers.remove(b"bKGD") {
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(
@ -106,7 +91,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
bkgd_header[5],
255,
))
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 {
} else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 {
Some(RGBA8::new(
bkgd_header[1],
bkgd_header[1],
@ -117,16 +102,15 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
None
};
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]);
let idx = palette.iter().position(|&px| px == bg).or_else(|| {
if palette.len() < 256 {
palette.push(bg);
Some(palette.len() - 1)
} else {
None // No space in palette to store the bg as an index
}
})?;
aux_headers.insert(*b"bKGD", vec![idx as u8]);
}
}
@ -135,17 +119,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect());
}
let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()];
for (color, idx) in palette {
palette_vec[idx as usize] = color;
}
Some(PngImage {
data: raw_data,
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: palette_vec,
},
color_type: ColorType::Indexed { palette },
..png.ihdr
},
aux_headers,
@ -153,7 +130,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
}
#[must_use]
pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
if !png.ihdr.color_type.is_rgb() {
return None;
}
@ -204,3 +181,67 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
aux_headers,
})
}
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
#[must_use]
pub fn indexed_to_channels(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
_ => return None,
};
// Determine which channels are required
let is_gray = palette.iter().all(|c| c.r == c.g && c.g == c.b);
let has_alpha = palette.iter().any(|c| c.a != 255);
let color_type = match (is_gray, has_alpha) {
(false, true) => ColorType::RGBA,
(false, false) => ColorType::RGB {
transparent_color: None,
},
(true, true) => ColorType::GrayscaleAlpha,
(true, false) => ColorType::Grayscale {
transparent_shade: None,
},
};
// Don't proceed if output would be too much larger
let out_size = color_type.channels_per_pixel() as usize * png.data.len();
if out_size - png.data.len() > INDEXED_MAX_DIFF {
return None;
}
// Construct the new data
let black = RGBA::new(0, 0, 0, 255);
let ch_start = if is_gray { 2 } else { 0 };
let ch_end = if has_alpha { 3 } else { 2 };
let mut data = Vec::with_capacity(out_size);
for b in &png.data {
let color = palette.get(*b as usize).unwrap_or(&black);
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
}
// Update bKGD if it exists
let mut aux_headers = png.aux_headers.clone();
if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) {
if let Some(color) = palette.get(idx as usize) {
let bkgd = if is_gray {
vec![0, color.r]
} else {
vec![0, color.r, 0, color.g, 0, color.b]
};
aux_headers.insert(*b"bKGD", bkgd);
}
}
Some(PngImage {
ihdr: IhdrData {
color_type,
..png.ihdr
},
data,
aux_headers,
})
}

View file

@ -51,7 +51,16 @@ pub(crate) fn perform_reductions(
// Attempt to reduce RGB to grayscale
// 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) {
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
png = Arc::new(reduced);
reduction_occurred = true;
}
}
// Attempt to reduce the palette
// This may change bytes but should always be beneficial
if opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
png = Arc::new(reduced);
reduction_occurred = true;
}
@ -65,9 +74,9 @@ pub(crate) fn perform_reductions(
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() {
// For small differences, if a tRNS chunk is required then enter this into the evaluator
// Otherwise it is mostly just removal of bytes and should become the baseline
if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 {
eval.try_image(png.clone());
evaluation_added = true;
} else {
@ -77,33 +86,52 @@ pub(crate) fn perform_reductions(
}
}
// Attempt to reduce the palette size
// Attempt to sort the palette
if opts.palette_reduction && !deadline.passed() {
if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) {
if let Some(reduced) = sorted_palette(&png) {
png = Arc::new(reduced);
eval.try_image(png.clone());
evaluation_added = true;
}
}
// Attempt to reduce to palette
// Attempt to convert from indexed to channels
// This may give a better result due to dropping the PLTE chunk
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());
if let Some(reduced) = indexed_to_channels(&png) {
// This result should not be passed on to subsequent reductions
eval.try_image(Arc::new(reduced));
evaluation_added = true;
}
}
// Attempt to reduce to indexed
let mut indexed = None;
if opts.color_type_reduction && !deadline.passed() {
if let Some(reduced) = reduced_to_indexed(&png) {
// Make sure the palette gets sorted (but don't bother evaluating both results)
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
// For relatively small differences, enter this into the evaluator
// Otherwise we're confident enough for it to become the baseline
if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF {
eval.try_image(new.clone());
evaluation_added = true;
} else {
baseline = new.clone();
reduction_occurred = true;
}
indexed = Some(new);
}
}
// 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());
// Try reducing the previous png, falling back to the indexed one if it exists
// This allows a grayscale depth reduction to be preferred over an indexed depth reduction
let reduced = reduced_bit_depth_8_or_less(&png, 1)
.or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png, 1)));
if let Some(reduced) = reduced {
eval.try_image(Arc::new(reduced));
evaluation_added = true;
}
}

View file

@ -1,180 +1,201 @@
use crate::colors::{BitDepth, ColorType};
use crate::headers::IhdrData;
use crate::png::PngImage;
use indexmap::map::{Entry::*, IndexMap};
use indexmap::IndexSet;
use rgb::RGBA8;
/// Attempt to shrink and sort the palette, returning the optimized image if successful
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
#[must_use]
pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
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
ColorType::Indexed { palette } if palette.len() > 1 => 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 used = get_used_entries(png);
let black = RGBA8::new(0, 0, 0, 255);
let mut condensed = IndexSet::with_capacity(palette.len());
let mut palette_map = [0; 256];
let mut did_change = false;
for (i, used) in used.iter().enumerate() {
if !used {
continue;
}
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()),
}
// There are invalid files that use pixel indices beyond palette size
let color = *palette.get(i).unwrap_or(&black);
palette_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
if palette_map[i] as usize != i {
did_change = 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();
// Update bKGD if it exists, ensuring it comes last in the palette if otherwise unused
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 let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) {
if let Some(&color) = palette.get(idx as usize) {
let idx = add_color_to_set(color, &mut condensed, optimize_alpha);
aux_headers.insert(*b"bKGD", vec![idx]);
}
}
let data = if did_change {
// Reassign data bytes to new indices
let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &palette_map);
png.data.iter().map(|b| byte_map[*b as usize]).collect()
} else if condensed.len() < palette.len() {
// Data is unchanged but palette will be truncated
png.data.clone()
} else {
// Nothing has changed
return None;
};
let palette: Vec<_> = condensed.into_iter().collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: reordered_palette(palette, palette_map),
},
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data: raw_data,
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;
fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha: bool) -> u8 {
// 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;
}
let (idx, _) = set.insert_full(color);
idx as u8
}
let mut byte_map = [0_u8; 256];
// low bit-depths can be pre-computed for every byte value
fn get_used_entries(png: &PngImage) -> [bool; 256] {
let mut used = [false; 256];
match png.ihdr.bit_depth {
BitDepth::Eight => {
for byte in 0..=255usize {
byte_map[byte] = palette_map[byte].unwrap_or(0)
for &byte in &png.data {
used[byte as usize] = true;
}
}
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);
for &byte in &png.data {
used[(byte & 0x0F) as usize] = true;
used[(byte >> 4) as usize] = true;
}
}
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);
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;
}
}
_ => {}
}
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;
BitDepth::One => {
// Only two options, don't bother checking which are actually used
used[0] = true;
used[1] = true;
}
}
new_palette
_ => unreachable!(),
};
used
}
fn palette_map_to_byte_map(bit_depth: BitDepth, palette_map: &[u8; 256]) -> [u8; 256] {
// Low bit-depths can be pre-computed for every byte value
match bit_depth {
BitDepth::Eight => *palette_map,
BitDepth::Four => {
let mut byte_map = [0_u8; 256];
for byte in 0..256 {
byte_map[byte] = palette_map[byte & 0x0F] | (palette_map[byte >> 4] << 4);
}
byte_map
}
BitDepth::Two => {
let mut byte_map = [0_u8; 256];
for byte in 0..256 {
byte_map[byte] = palette_map[byte & 0x03]
| (palette_map[(byte >> 2) & 0x03] << 2)
| (palette_map[(byte >> 4) & 0x03] << 4)
| (palette_map[byte >> 6] << 6);
}
byte_map
}
_ => unreachable!(),
}
}
/// Attempt to sort the colors in the palette, returning the sorted image if successful
#[must_use]
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth == BitDepth::One {
// Don't bother trying to sort a 1-bit image
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } => palette,
_ => return None,
};
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
// If the background is the last entry in the palette we should make sure it stays last
// Otherwise an entry that's unused by the idat could prevent reduction to a lower depth
let mut aux_headers = png.aux_headers.clone();
let bkgd_idx = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned());
let bkgd_last = match bkgd_idx {
Some(idx) if idx as usize + 1 == palette.len() => enumerated.pop(),
_ => None,
};
// Sort the palette
enumerated.sort_by(|a, b| {
// Sort by ascending alpha and descending luma
let color_val = |color: &RGBA8| {
((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.1).cmp(&color_val(b.1))
});
if let Some(bkgd) = bkgd_last {
enumerated.push(bkgd);
}
// Extract the new palette and determine if anything changed
let (old_map, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
if old_map.iter().enumerate().all(|(a, b)| a == *b) {
return None;
}
// Construct the palette and byte maps and convert the data
let mut new_map = [0; 256];
for (i, &v) in old_map.iter().enumerate() {
new_map[v] = i as u8;
}
let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &new_map);
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
// Update bKGD if it exists
if let Some(idx) = bkgd_idx.map(|idx| new_map[idx as usize]) {
aux_headers.insert(*b"bKGD", vec![idx]);
}
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data,
aux_headers,
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -427,8 +427,8 @@ fn interlacing_0_to_1_small_files() {
};
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One);
assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
remove_file(output).ok();
}
@ -461,8 +461,8 @@ fn interlacing_1_to_0_small_files() {
};
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
// the depth can't be asserted reliably, because on such small file different zlib implementations pick different depth as the best
assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
remove_file(output).ok();
}

View file

@ -546,6 +546,17 @@ fn interlaced_palette_1_should_be_palette_1() {
);
}
#[test]
fn interlaced_palette_8_should_be_grayscale_8() {
test_it_converts(
"tests/files/interlaced_palette_8_should_be_grayscale_8.png",
INDEXED,
BitDepth::Eight,
GRAYSCALE,
BitDepth::Eight,
);
}
#[test]
fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16() {
test_it_converts(
@ -651,8 +662,8 @@ fn interlaced_small_files() {
"tests/files/interlaced_small_files.png",
INDEXED,
BitDepth::Eight,
INDEXED,
BitDepth::One,
RGB,
BitDepth::Eight,
);
}

View file

@ -556,6 +556,42 @@ fn palette_4_should_be_palette_2() {
);
}
#[test]
fn palette_8_should_be_grayscale_8() {
test_it_converts(
"tests/files/palette_8_should_be_grayscale_8.png",
false,
INDEXED,
BitDepth::Eight,
GRAYSCALE,
BitDepth::Eight,
);
}
#[test]
fn palette_8_should_be_rgb() {
test_it_converts(
"tests/files/palette_8_should_be_rgb.png",
false,
INDEXED,
BitDepth::Eight,
RGB,
BitDepth::Eight,
);
}
#[test]
fn palette_8_should_be_rgba() {
test_it_converts(
"tests/files/palette_8_should_be_rgba.png",
false,
INDEXED,
BitDepth::Eight,
RGBA,
BitDepth::Eight,
);
}
#[test]
fn palette_2_should_be_palette_2() {
test_it_converts(
@ -808,6 +844,54 @@ fn grayscale_2_should_be_grayscale_1() {
);
}
#[test]
fn grayscale_8_should_be_palette_8() {
test_it_converts(
"tests/files/grayscale_8_should_be_palette_8.png",
false,
GRAYSCALE,
BitDepth::Eight,
INDEXED,
BitDepth::Eight,
);
}
#[test]
fn grayscale_8_should_be_palette_4() {
test_it_converts(
"tests/files/grayscale_8_should_be_palette_4.png",
false,
GRAYSCALE,
BitDepth::Eight,
INDEXED,
BitDepth::Four,
);
}
#[test]
fn grayscale_8_should_be_palette_2() {
test_it_converts(
"tests/files/grayscale_8_should_be_palette_2.png",
false,
GRAYSCALE,
BitDepth::Eight,
INDEXED,
BitDepth::Two,
);
}
#[test]
fn grayscale_8_should_be_palette_1() {
test_it_converts(
"tests/files/grayscale_8_should_be_palette_1.png",
false,
GRAYSCALE,
BitDepth::Eight,
INDEXED,
BitDepth::One,
);
}
#[test]
fn grayscale_alpha_16_should_be_grayscale_trns_16() {
test_it_converts(
@ -834,33 +918,14 @@ fn grayscale_alpha_8_should_be_grayscale_trns_8() {
#[test]
fn small_files() {
let input = PathBuf::from("tests/files/small_files.png");
let (output, opts) = get_opts(&input);
let png = PngData::new(&input, opts.fix_errors).unwrap();
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
Err(x) => panic!("{}", x),
};
let output = output.path().unwrap();
assert!(output.exists());
let png = match PngData::new(output, opts.fix_errors) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!("{}", x)
}
};
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
// depth varies depending on zlib implementation used
remove_file(output).ok();
test_it_converts(
"tests/files/small_files.png",
false,
INDEXED,
BitDepth::Eight,
RGB,
BitDepth::Eight,
);
}
#[test]