Async reduction evaluator

This commit is contained in:
Kornel Lesiński 2019-01-14 21:37:38 +00:00 committed by Kornel Lesiński
parent 90f8e9d7ff
commit 8fd84dd4ff
6 changed files with 180 additions and 110 deletions

96
src/evaluate.rs Normal file
View file

@ -0,0 +1,96 @@
//! Check if a reduction makes file smaller, and keep best reductions.
//! Works asynchronously when possible
use atomicmin::AtomicMin;
use deflate;
use png::PngData;
use png::PngImage;
use png::STD_COMPRESSION;
use png::STD_FILTERS;
use png::STD_STRATEGY;
use png::STD_WINDOW;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use std::sync::mpsc::*;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
/// Collect image versions and pick one that compresses best
pub struct Evaluator {
/// images are sent to the thread for evaluation
eval_send: Option<SyncSender<(Arc<PngImage>, bool)>>,
// the thread helps evaluate images asynchronously
eval_thread: thread::JoinHandle<Option<PngData>>,
}
impl Evaluator {
pub fn new() -> Self {
// queue size ensures we're not using too much memory for pending reductions
let (tx, rx) = sync_channel(4);
Self {
eval_send: Some(tx),
eval_thread: thread::spawn(move || Self::evaluate_images(rx)),
}
}
/// Wait for all evaluations to finish and return smallest reduction
/// Or `None` if all reductions were worse than baseline.
pub fn get_result(mut self) -> Option<PngData> {
let _ = self.eval_send.take(); // disconnect the sender, breaking the loop in the thread
self.eval_thread.join().expect("eval thread")
}
/// Set baseline image. It will be used only to measure minimum compression level required
pub fn set_baseline(&self, image: Arc<PngImage>) {
self.try_image_inner(image, false)
}
/// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) {
self.try_image_inner(image, true)
}
fn try_image_inner(&self, image: Arc<PngImage>, is_reduction: bool) {
self.eval_send.as_ref().expect("not finished yet").send((image, is_reduction)).expect("send")
}
/// Main loop of evaluation thread
fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, bool)>) -> Option<PngData> {
let best_candidate_size = AtomicMin::new(None);
let best_result = Mutex::new(None);
// ends when sender is dropped
for (image, is_reduction) in from_channel.iter() {
#[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let filters_iter = STD_FILTERS.iter();
filters_iter.for_each(|&f| {
if let Ok(idat_data) = deflate::deflate(
&image.filter_image(f),
STD_COMPRESSION,
STD_STRATEGY,
STD_WINDOW,
&best_candidate_size,
) {
let mut res = best_result.lock().unwrap();
if best_candidate_size.get().map_or(true, |len| len >= idat_data.len()) {
best_candidate_size.set_min(idat_data.len());
*res = if is_reduction {
Some((PngData {
idat_data,
raw: Arc::clone(&image),
}, f))
} else {
None
};
}
}
});
}
best_result.into_inner().expect("filters should be done")
.map(|(img, _)| img)
}
}

View file

@ -16,6 +16,7 @@ use reduction::*;
use atomicmin::AtomicMin; use atomicmin::AtomicMin;
use crc::crc32; use crc::crc32;
use deflate::inflate; use deflate::inflate;
use evaluate::Evaluator;
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel}; use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
use png::PngImage; use png::PngImage;
use png::PngData; use png::PngData;
@ -23,12 +24,12 @@ use png::PngData;
use rayon::prelude::*; use rayon::prelude::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::borrow::Cow;
use std::fs::{copy, File}; use std::fs::{copy, File};
use std::io::{stdin, stdout, BufWriter, Read, Write}; use std::io::{stdin, stdout, BufWriter, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::sync::Arc;
pub use colors::AlphaOptim; pub use colors::AlphaOptim;
pub use deflate::Deflaters; pub use deflate::Deflaters;
@ -39,6 +40,7 @@ mod atomicmin;
mod colors; mod colors;
mod deflate; mod deflate;
mod error; mod error;
mod evaluate;
mod filters; mod filters;
mod headers; mod headers;
mod interlace; mod interlace;
@ -522,9 +524,16 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
} }
} }
let reduction_occurred = if let Some(reduced) = perform_reductions(&png.raw, opts, &deadline) { // This will collect all versions of images and pick one that compresses best
png.raw = reduced; let eval = Evaluator::new();
png.idat_data.clear(); // this field is out of date and needs to be replaced // Usually we want transformations that are smaller than the unmodified original,
// but if we're interlacing, we have to accept a possible file size increase.
if opts.interlace.is_none() {
eval.set_baseline(png.raw.clone());
}
perform_reductions(png.raw.clone(), opts, &deadline, &eval);
let reduction_occurred = if let Some(result) = eval.get_result() {
*png = result;
true true
} else { } else {
false false
@ -731,66 +740,59 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR
Err(PngError::new("The resulting image is corrupted")) Err(PngError::new("The resulting image is corrupted"))
} }
fn if_owned(cow: Cow<PngImage>) -> Option<PngImage> { fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadline, eval: &Evaluator) {
match cow {
Cow::Owned(png) => Some(png),
_ => None,
}
}
fn perform_reductions(png: &PngImage, opts: &Options, deadline: &Deadline) -> Option<PngImage> { // must be done first to evaluate rest with the correct interlacing
let mut reduced = Cow::Borrowed(png); if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) {
png = Arc::new(reduced);
eval.try_image(png.clone());
}
if deadline.passed() {
return;
}
}
if opts.palette_reduction { if opts.palette_reduction {
if let Some(r) = reduced_palette(&reduced) { if let Some(reduced) = reduced_palette(&png) {
reduced = Cow::Owned(r); png = Arc::new(reduced);
eval.try_image(png.clone());
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(&reduced); report_reduction(&png);
} }
} }
}
if deadline.passed() { if deadline.passed() {
return if_owned(reduced); return;
}
} }
if opts.bit_depth_reduction { if opts.bit_depth_reduction {
if let Some(r) = reduce_bit_depth(&reduced) { if let Some(reduced) = reduce_bit_depth(&png) {
reduced = Cow::Owned(r); png = Arc::new(reduced);
eval.try_image(png.clone());
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(&reduced); report_reduction(&png);
} }
} }
}
if deadline.passed() { if deadline.passed() {
return if_owned(reduced); return;
}
} }
if opts.color_type_reduction { if opts.color_type_reduction {
if let Some(r) = reduce_color_type(&reduced) { if let Some(reduced) = reduce_color_type(&png) {
reduced = Cow::Owned(r); png = Arc::new(reduced);
eval.try_image(png.clone());
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(&reduced); report_reduction(&png);
} }
} }
}
if let Some(interlacing) = opts.interlace {
if let Some(r) = reduced.change_interlacing(interlacing) {
reduced = Cow::Owned(r);
}
}
if deadline.passed() { if deadline.passed() {
return if_owned(reduced); return;
}
} }
if let Some(r) = try_alpha_reduction(&reduced, &opts.alphas) { try_alpha_reductions(png, &opts.alphas, eval);
reduced = Cow::Owned(r);
}
if_owned(reduced)
} }
/// Keep track of processing timeout /// Keep track of processing timeout
@ -846,29 +848,30 @@ fn report_reduction(png: &PngImage) {
/// Strip headers from the `PngData` object, as requested by the passed `Options` /// Strip headers from the `PngData` object, as requested by the passed `Options`
fn perform_strip(png: &mut PngData, opts: &Options) { fn perform_strip(png: &mut PngData, opts: &Options) {
let raw = Arc::make_mut(&mut png.raw);
match opts.strip { match opts.strip {
// Strip headers // Strip headers
Headers::None => (), Headers::None => (),
Headers::Keep(ref hdrs) => { Headers::Keep(ref hdrs) => {
png.raw.aux_headers.retain(|chunk, _| { raw.aux_headers.retain(|chunk, _| {
std::str::from_utf8(chunk) std::str::from_utf8(chunk)
.ok() .ok()
.map_or(false, |name| hdrs.contains(name)) .map_or(false, |name| hdrs.contains(name))
}); });
} }
Headers::Strip(ref hdrs) => for hdr in hdrs { Headers::Strip(ref hdrs) => for hdr in hdrs {
png.raw.aux_headers.remove(hdr.as_bytes()); raw.aux_headers.remove(hdr.as_bytes());
}, },
Headers::Safe => { Headers::Safe => {
const PRESERVED_HEADERS: [[u8; 4]; 9] = [ const PRESERVED_HEADERS: [[u8; 4]; 9] = [
*b"cHRM", *b"gAMA", *b"iCCP", *b"sBIT", *b"sRGB", *b"bKGD", *b"hIST", *b"pHYs", *b"cHRM", *b"gAMA", *b"iCCP", *b"sBIT", *b"sRGB", *b"bKGD", *b"hIST", *b"pHYs",
*b"sPLT", *b"sPLT",
]; ];
png.raw.aux_headers raw.aux_headers
.retain(|hdr, _| PRESERVED_HEADERS.contains(hdr)); .retain(|hdr, _| PRESERVED_HEADERS.contains(hdr));
} }
Headers::All => { Headers::All => {
png.raw.aux_headers = HashMap::new(); raw.aux_headers = HashMap::new();
} }
} }
@ -881,18 +884,18 @@ fn perform_strip(png: &mut PngData, opts: &Options) {
}; };
if may_replace_iccp { if may_replace_iccp {
if png.raw.aux_headers.get(b"sRGB").is_some() { if raw.aux_headers.get(b"sRGB").is_some() {
// Files aren't supposed to have both chunks, so we chose to honor sRGB // Files aren't supposed to have both chunks, so we chose to honor sRGB
png.raw.aux_headers.remove(b"iCCP"); raw.aux_headers.remove(b"iCCP");
} else if let Some(intent) = png.raw } else if let Some(intent) = raw
.aux_headers .aux_headers
.get(b"iCCP") .get(b"iCCP")
.and_then(|iccp| srgb_rendering_intent(iccp)) .and_then(|iccp| srgb_rendering_intent(iccp))
{ {
// sRGB-like profile can be safely replaced with // sRGB-like profile can be safely replaced with
// an sRGB chunk with the same rendering intent // an sRGB chunk with the same rendering intent
png.raw.aux_headers.remove(b"iCCP"); raw.aux_headers.remove(b"iCCP");
png.raw.aux_headers.insert(*b"sRGB", vec![intent]); raw.aux_headers.insert(*b"sRGB", vec![intent]);
} }
} }
} }

View file

@ -13,11 +13,13 @@ use std::fs::File;
use std::io::{Read, Seek, SeekFrom}; use std::io::{Read, Seek, SeekFrom};
use std::iter::Iterator; use std::iter::Iterator;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
pub(crate) const STD_COMPRESSION: u8 = 8; pub(crate) const STD_COMPRESSION: u8 = 6;
/// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative /// Must use normal compression, as faster ones (Huffman/RLE-only) are not representative
pub(crate) const STD_STRATEGY: u8 = 0; pub(crate) const STD_STRATEGY: u8 = 0;
pub(crate) const STD_WINDOW: u8 = 15; /// OK to use a bit smalller window for evaluation
pub(crate) const STD_WINDOW: u8 = 13;
pub(crate) const STD_FILTERS: [u8; 2] = [0, 5]; pub(crate) const STD_FILTERS: [u8; 2] = [0, 5];
pub(crate) mod scan_lines; pub(crate) mod scan_lines;
@ -42,7 +44,8 @@ pub struct PngImage {
/// Contains all data relevant to a PNG image /// Contains all data relevant to a PNG image
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PngData { pub struct PngData {
pub raw: PngImage, /// Uncompressed image data
pub raw: Arc<PngImage>,
/// The filtered and compressed data of the IDAT chunk /// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>, pub idat_data: Vec<u8>,
} }
@ -122,19 +125,19 @@ impl PngData {
aux_headers.remove(b"tRNS"), aux_headers.remove(b"tRNS"),
)?; )?;
let mut png_data = Self { let mut raw = PngImage {
idat_data: idat_headers,
raw: PngImage {
ihdr: ihdr_header, ihdr: ihdr_header,
data: raw_data, data: raw_data,
palette, palette,
transparency_pixel, transparency_pixel,
aux_headers, aux_headers,
}
}; };
png_data.raw.data = png_data.raw.unfilter_image(); raw.data = raw.unfilter_image();
// Return the PngData // Return the PngData
Ok(png_data) Ok(Self {
idat_data: idat_headers,
raw: Arc::new(raw),
})
} }
/// Handle transparency header /// Handle transparency header
@ -162,7 +165,6 @@ impl PngData {
} }
} }
/// Format the `PngData` struct into a valid PNG bytestream /// Format the `PngData` struct into a valid PNG bytestream
pub fn output(&self) -> Vec<u8> { pub fn output(&self) -> Vec<u8> {
// PNG header // PNG header

View file

@ -1,56 +1,25 @@
use png::STD_STRATEGY; use evaluate::Evaluator;
use png::STD_WINDOW;
use itertools::flatten; use itertools::flatten;
use png::scan_lines::ScanLine; use png::scan_lines::ScanLine;
use std::collections::HashSet; use std::collections::HashSet;
use png::STD_COMPRESSION; use std::sync::Arc;
use png::STD_FILTERS;
use colors::AlphaOptim; use colors::AlphaOptim;
use headers::IhdrData; use headers::IhdrData;
use png::PngImage; use png::PngImage;
use colors::ColorType; use colors::ColorType;
use atomicmin::AtomicMin;
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
use rayon::prelude::*; use rayon::prelude::*;
use deflate;
pub fn try_alpha_reduction(png: &PngImage, alphas: &HashSet<AlphaOptim>) -> Option<PngImage> { pub fn try_alpha_reductions(png: Arc<PngImage>, alphas: &HashSet<AlphaOptim>, eval: &Evaluator) {
assert!(!alphas.is_empty()); assert!(!alphas.is_empty());
let alphas = alphas.iter().collect::<Vec<_>>(); let alphas = alphas.iter().collect::<Vec<_>>();
let best_size = AtomicMin::new(None);
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
let alphas_iter = alphas.par_iter().with_max_len(1); let alphas_iter = alphas.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
let alphas_iter = alphas.iter(); let alphas_iter = alphas.iter();
let best = alphas_iter alphas_iter
.filter_map(|&alpha| { .filter_map(|&alpha| filtered_alpha_channel(&png, *alpha))
let image = match filtered_alpha_channel(png, *alpha) { .for_each(|image| eval.try_image(Arc::new(image)));
Some(image) => image,
None => return None,
};
#[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))]
let filters_iter = STD_FILTERS.iter();
filters_iter
.filter_map(|f| {
deflate::deflate(
&image.filter_image(*f),
STD_COMPRESSION,
STD_STRATEGY,
STD_WINDOW,
&best_size,
).ok()
.as_ref()
.map(|l| {
best_size.set_min(l.len());
l.len()
})
}).min()
.map(|size| (size, image))
}).min_by_key(|&(size, _)| size);
best.map(|(_, image)| image)
} }
pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> { pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> {

View file

@ -14,7 +14,7 @@ pub mod color;
use color::*; use color::*;
pub use bit_depth::reduce_bit_depth; pub use bit_depth::reduce_bit_depth;
pub use alpha::try_alpha_reduction; pub use alpha::try_alpha_reductions;
/// Attempt to reduce the number of colors in the palette /// Attempt to reduce the number of colors in the palette
/// Returns `None` if palette hasn't changed /// Returns `None` if palette hasn't changed

View file

@ -718,7 +718,7 @@ fn palette_should_be_reduced_with_dupes() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 43); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (), Ok(_) => (),
@ -737,7 +737,7 @@ fn palette_should_be_reduced_with_dupes() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 35); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35);
remove_file(output).ok(); remove_file(output).ok();
} }
@ -751,7 +751,7 @@ fn palette_should_be_reduced_with_unused() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 35); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 35);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (), Ok(_) => (),
@ -770,7 +770,7 @@ fn palette_should_be_reduced_with_unused() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 33); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33);
remove_file(output).ok(); remove_file(output).ok();
} }
@ -784,7 +784,7 @@ fn palette_should_be_reduced_with_both() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 43); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 43);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (), Ok(_) => (),
@ -803,7 +803,7 @@ fn palette_should_be_reduced_with_both() {
assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed); assert_eq!(png.raw.ihdr.color_type, ColorType::Indexed);
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
assert_eq!(png.raw.palette.unwrap().len(), 33); assert_eq!(png.raw.palette.as_ref().unwrap().len(), 33);
remove_file(output).ok(); remove_file(output).ok();
} }