This adds a new palette sorting algorithm that attempts to minimise entropy by an approximate solution to the Traveling Salesman Problem. The algorithm comes from "An efficient Re-indexing algorithm for color-mapped images" by Battiato et al (https://ieeexplore.ieee.org/document/1344033). It's fast and effective and works in addition to the luma sort (which remains the single most effective sort). In order to keep lower presets fast though, I've only enabled this for o3 and higher. Results on a set of 190 indexed images at `-o5`: 18,932,727 bytes - master 18,578,306 bytes - PR 18,559,863 bytes - PR + #509 (These images may be particularly suited to alternative sorting methods - the gains here are not necessarily what should be expected on average) Note I looked into the 120 different palette sorting methods from TruePNG, as mentioned in #74 (and seen in action in the Zopfli KrzYmod fork). They're... largely ineffective. The combination of all 120 methods are outperformed by just the existing luma sort plus this new one. That's not to say there's nothing further to be gained from them, but trying to brute force all the combinations definitely seems like a bad idea. There are other algorithms I hope to explore in future... @ace-dent Thought this might interest you UPDATE: I realised a quick tweak to alpha values in the luma sort can provide a great improvement on images with transparency. The following numbers were taken with PR #509 as base. `-o2`: 19,065,549 bytes - base (luma sort) 18,949,747 bytes - modified luma sort `-o5`: 18,922,165 bytes - base (luma sort) 18,559,863 bytes - new sorting algorithm + luma sort 18,544,813 bytes - new sorting algorithm + modified luma sort
159 lines
5.8 KiB
Rust
159 lines
5.8 KiB
Rust
use crate::evaluate::Evaluator;
|
|
use crate::png::PngImage;
|
|
use crate::Deadline;
|
|
use crate::Deflaters;
|
|
use crate::Options;
|
|
use std::sync::Arc;
|
|
|
|
pub mod alpha;
|
|
use crate::alpha::*;
|
|
pub mod bit_depth;
|
|
use crate::bit_depth::*;
|
|
pub mod color;
|
|
use crate::color::*;
|
|
pub mod palette;
|
|
use crate::palette::*;
|
|
|
|
pub(crate) fn perform_reductions(
|
|
mut png: Arc<PngImage>,
|
|
opts: &Options,
|
|
deadline: &Deadline,
|
|
eval: &Evaluator,
|
|
) -> Arc<PngImage> {
|
|
let mut evaluation_added = false;
|
|
|
|
// At low compression levels, skip some transformations which are less likely to be effective
|
|
// This currently affects optimization presets 0-2
|
|
let cheap = match opts.deflate {
|
|
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
|
_ => false,
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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, opts.scale_16) {
|
|
png = Arc::new(reduced);
|
|
}
|
|
}
|
|
|
|
// Attempt to reduce RGB to grayscale
|
|
// This is just removal of bytes and does not need to be evaluated
|
|
if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() {
|
|
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
|
|
png = Arc::new(reduced);
|
|
}
|
|
}
|
|
|
|
// Attempt to expand the bit depth to 8
|
|
// This does need to be evaluated but will be done so later when it gets reduced again
|
|
if opts.bit_depth_reduction && !deadline.passed() {
|
|
if let Some(reduced) = expanded_bit_depth_to_8(&png) {
|
|
png = Arc::new(reduced);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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 alpha removal
|
|
if opts.color_type_reduction && !deadline.passed() {
|
|
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
|
|
png = Arc::new(reduced);
|
|
// 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 {
|
|
baseline = png.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attempt to sort the palette
|
|
if opts.palette_reduction && !deadline.passed() {
|
|
if let Some(reduced) = sorted_palette(&png) {
|
|
png = Arc::new(reduced);
|
|
eval.try_image(png.clone());
|
|
evaluation_added = true;
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// 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, opts.grayscale_reduction) {
|
|
// 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();
|
|
}
|
|
indexed = Some(new);
|
|
}
|
|
}
|
|
|
|
// Attempt to sort the palette using an alternative method
|
|
if !cheap && opts.palette_reduction && !deadline.passed() {
|
|
if let Some(reduced) = sorted_palette_battiato(&png) {
|
|
eval.try_image(Arc::new(reduced));
|
|
evaluation_added = true;
|
|
}
|
|
}
|
|
|
|
// Attempt to reduce to a lower bit depth
|
|
if opts.bit_depth_reduction && !deadline.passed() {
|
|
// 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)
|
|
.or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)));
|
|
if let Some(reduced) = reduced {
|
|
eval.try_image(Arc::new(reduced));
|
|
evaluation_added = true;
|
|
}
|
|
}
|
|
|
|
if evaluation_added {
|
|
eval.try_image(baseline.clone());
|
|
}
|
|
baseline
|
|
}
|