Async reduction evaluator
This commit is contained in:
parent
90f8e9d7ff
commit
8fd84dd4ff
6 changed files with 180 additions and 110 deletions
96
src/evaluate.rs
Normal file
96
src/evaluate.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
111
src/lib.rs
111
src/lib.rs
|
|
@ -16,6 +16,7 @@ use reduction::*;
|
|||
use atomicmin::AtomicMin;
|
||||
use crc::crc32;
|
||||
use deflate::inflate;
|
||||
use evaluate::Evaluator;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
||||
use png::PngImage;
|
||||
use png::PngData;
|
||||
|
|
@ -23,12 +24,12 @@ use png::PngData;
|
|||
use rayon::prelude::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::borrow::Cow;
|
||||
use std::fs::{copy, File};
|
||||
use std::io::{stdin, stdout, BufWriter, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use colors::AlphaOptim;
|
||||
pub use deflate::Deflaters;
|
||||
|
|
@ -39,6 +40,7 @@ mod atomicmin;
|
|||
mod colors;
|
||||
mod deflate;
|
||||
mod error;
|
||||
mod evaluate;
|
||||
mod filters;
|
||||
mod headers;
|
||||
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) {
|
||||
png.raw = reduced;
|
||||
png.idat_data.clear(); // this field is out of date and needs to be replaced
|
||||
// This will collect all versions of images and pick one that compresses best
|
||||
let eval = Evaluator::new();
|
||||
// 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
|
||||
} else {
|
||||
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"))
|
||||
}
|
||||
|
||||
fn if_owned(cow: Cow<PngImage>) -> Option<PngImage> {
|
||||
match cow {
|
||||
Cow::Owned(png) => Some(png),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadline, eval: &Evaluator) {
|
||||
|
||||
fn perform_reductions(png: &PngImage, opts: &Options, deadline: &Deadline) -> Option<PngImage> {
|
||||
let mut reduced = Cow::Borrowed(png);
|
||||
|
||||
if opts.palette_reduction {
|
||||
if let Some(r) = reduced_palette(&reduced) {
|
||||
reduced = Cow::Owned(r);
|
||||
if opts.verbosity == Some(1) {
|
||||
report_reduction(&reduced);
|
||||
}
|
||||
// must be done first to evaluate rest with the correct interlacing
|
||||
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 deadline.passed() {
|
||||
return if_owned(reduced);
|
||||
if opts.palette_reduction {
|
||||
if let Some(reduced) = reduced_palette(&png) {
|
||||
png = Arc::new(reduced);
|
||||
eval.try_image(png.clone());
|
||||
if opts.verbosity == Some(1) {
|
||||
report_reduction(&png);
|
||||
}
|
||||
}
|
||||
if deadline.passed() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if opts.bit_depth_reduction {
|
||||
if let Some(r) = reduce_bit_depth(&reduced) {
|
||||
reduced = Cow::Owned(r);
|
||||
if let Some(reduced) = reduce_bit_depth(&png) {
|
||||
png = Arc::new(reduced);
|
||||
eval.try_image(png.clone());
|
||||
if opts.verbosity == Some(1) {
|
||||
report_reduction(&reduced);
|
||||
report_reduction(&png);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deadline.passed() {
|
||||
return if_owned(reduced);
|
||||
if deadline.passed() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if opts.color_type_reduction {
|
||||
if let Some(r) = reduce_color_type(&reduced) {
|
||||
reduced = Cow::Owned(r);
|
||||
if let Some(reduced) = reduce_color_type(&png) {
|
||||
png = Arc::new(reduced);
|
||||
eval.try_image(png.clone());
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if deadline.passed() {
|
||||
return if_owned(reduced);
|
||||
}
|
||||
|
||||
if let Some(r) = try_alpha_reduction(&reduced, &opts.alphas) {
|
||||
reduced = Cow::Owned(r);
|
||||
}
|
||||
|
||||
if_owned(reduced)
|
||||
try_alpha_reductions(png, &opts.alphas, eval);
|
||||
}
|
||||
|
||||
/// 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`
|
||||
fn perform_strip(png: &mut PngData, opts: &Options) {
|
||||
let raw = Arc::make_mut(&mut png.raw);
|
||||
match opts.strip {
|
||||
// Strip headers
|
||||
Headers::None => (),
|
||||
Headers::Keep(ref hdrs) => {
|
||||
png.raw.aux_headers.retain(|chunk, _| {
|
||||
raw.aux_headers.retain(|chunk, _| {
|
||||
std::str::from_utf8(chunk)
|
||||
.ok()
|
||||
.map_or(false, |name| hdrs.contains(name))
|
||||
});
|
||||
}
|
||||
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 => {
|
||||
const PRESERVED_HEADERS: [[u8; 4]; 9] = [
|
||||
*b"cHRM", *b"gAMA", *b"iCCP", *b"sBIT", *b"sRGB", *b"bKGD", *b"hIST", *b"pHYs",
|
||||
*b"sPLT",
|
||||
];
|
||||
png.raw.aux_headers
|
||||
raw.aux_headers
|
||||
.retain(|hdr, _| PRESERVED_HEADERS.contains(hdr));
|
||||
}
|
||||
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 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
|
||||
png.raw.aux_headers.remove(b"iCCP");
|
||||
} else if let Some(intent) = png.raw
|
||||
raw.aux_headers.remove(b"iCCP");
|
||||
} else if let Some(intent) = raw
|
||||
.aux_headers
|
||||
.get(b"iCCP")
|
||||
.and_then(|iccp| srgb_rendering_intent(iccp))
|
||||
{
|
||||
// sRGB-like profile can be safely replaced with
|
||||
// an sRGB chunk with the same rendering intent
|
||||
png.raw.aux_headers.remove(b"iCCP");
|
||||
png.raw.aux_headers.insert(*b"sRGB", vec![intent]);
|
||||
raw.aux_headers.remove(b"iCCP");
|
||||
raw.aux_headers.insert(*b"sRGB", vec![intent]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ use std::fs::File;
|
|||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::iter::Iterator;
|
||||
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
|
||||
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) mod scan_lines;
|
||||
|
|
@ -42,7 +44,8 @@ pub struct PngImage {
|
|||
/// Contains all data relevant to a PNG image
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PngData {
|
||||
pub raw: PngImage,
|
||||
/// Uncompressed image data
|
||||
pub raw: Arc<PngImage>,
|
||||
/// The filtered and compressed data of the IDAT chunk
|
||||
pub idat_data: Vec<u8>,
|
||||
}
|
||||
|
|
@ -122,19 +125,19 @@ impl PngData {
|
|||
aux_headers.remove(b"tRNS"),
|
||||
)?;
|
||||
|
||||
let mut png_data = Self {
|
||||
idat_data: idat_headers,
|
||||
raw: PngImage {
|
||||
ihdr: ihdr_header,
|
||||
data: raw_data,
|
||||
let mut raw = PngImage {
|
||||
ihdr: ihdr_header,
|
||||
data: raw_data,
|
||||
palette,
|
||||
transparency_pixel,
|
||||
aux_headers,
|
||||
}
|
||||
};
|
||||
png_data.raw.data = png_data.raw.unfilter_image();
|
||||
raw.data = raw.unfilter_image();
|
||||
// Return the PngData
|
||||
Ok(png_data)
|
||||
Ok(Self {
|
||||
idat_data: idat_headers,
|
||||
raw: Arc::new(raw),
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle transparency header
|
||||
|
|
@ -162,7 +165,6 @@ impl PngData {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Format the `PngData` struct into a valid PNG bytestream
|
||||
pub fn output(&self) -> Vec<u8> {
|
||||
// PNG header
|
||||
|
|
|
|||
|
|
@ -1,56 +1,25 @@
|
|||
use png::STD_STRATEGY;
|
||||
use png::STD_WINDOW;
|
||||
use evaluate::Evaluator;
|
||||
use itertools::flatten;
|
||||
use png::scan_lines::ScanLine;
|
||||
use std::collections::HashSet;
|
||||
use png::STD_COMPRESSION;
|
||||
use png::STD_FILTERS;
|
||||
use std::sync::Arc;
|
||||
use colors::AlphaOptim;
|
||||
use headers::IhdrData;
|
||||
use png::PngImage;
|
||||
use colors::ColorType;
|
||||
use atomicmin::AtomicMin;
|
||||
#[cfg(feature = "parallel")]
|
||||
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());
|
||||
let alphas = alphas.iter().collect::<Vec<_>>();
|
||||
let best_size = AtomicMin::new(None);
|
||||
#[cfg(feature = "parallel")]
|
||||
let alphas_iter = alphas.par_iter().with_max_len(1);
|
||||
#[cfg(not(feature = "parallel"))]
|
||||
let alphas_iter = alphas.iter();
|
||||
let best = alphas_iter
|
||||
.filter_map(|&alpha| {
|
||||
let image = match filtered_alpha_channel(png, *alpha) {
|
||||
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)
|
||||
alphas_iter
|
||||
.filter_map(|&alpha| filtered_alpha_channel(&png, *alpha))
|
||||
.for_each(|image| eval.try_image(Arc::new(image)));
|
||||
}
|
||||
|
||||
pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub mod color;
|
|||
use color::*;
|
||||
|
||||
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
|
||||
/// Returns `None` if palette hasn't changed
|
||||
|
|
|
|||
|
|
@ -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.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) {
|
||||
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.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();
|
||||
}
|
||||
|
|
@ -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.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) {
|
||||
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.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();
|
||||
}
|
||||
|
|
@ -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.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) {
|
||||
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.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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue