Draft raw api
This commit is contained in:
parent
a7be8751dc
commit
b20a14d5b2
2 changed files with 175 additions and 135 deletions
|
|
@ -81,7 +81,7 @@ impl Evaluator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for all evaluations to finish and return smallest reduction
|
/// Wait for all evaluations to finish and return smallest reduction
|
||||||
/// Or `None` if all reductions were worse than baseline.
|
/// Or `None` if the queue is empty.
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
pub fn get_best_candidate(self) -> Option<Candidate> {
|
pub fn get_best_candidate(self) -> Option<Candidate> {
|
||||||
let (eval_send, eval_recv) = self.eval_channel;
|
let (eval_send, eval_recv) = self.eval_channel;
|
||||||
|
|
|
||||||
308
src/lib.rs
308
src/lib.rs
|
|
@ -446,6 +446,19 @@ pub fn optimize_from_memory(data: &[u8], opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform optimization on the raw image data using the options provided
|
||||||
|
pub fn optimize_from_raw(raw: PngImage, opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
|
info!("Processing from raw image data");
|
||||||
|
|
||||||
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
|
|
||||||
|
if let Some(png) = optimize_raw(Arc::new(raw), opts, deadline, None) {
|
||||||
|
return Ok(png.output());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(PngError::new("Failed to optimize input data"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
|
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
|
||||||
/// Defines options to be used for a single compression trial
|
/// Defines options to be used for a single compression trial
|
||||||
struct TrialOptions {
|
struct TrialOptions {
|
||||||
|
|
@ -474,141 +487,10 @@ fn optimize_png(
|
||||||
|
|
||||||
// Do this first so that reductions can ignore certain chunks such as bKGD
|
// Do this first so that reductions can ignore certain chunks such as bKGD
|
||||||
perform_strip(png, opts);
|
perform_strip(png, opts);
|
||||||
let stripped_png = png.clone();
|
|
||||||
|
|
||||||
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
|
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) {
|
||||||
let eval_compression = 5;
|
png.raw = new_png.raw;
|
||||||
// None and Bigrams work well together, especially for alpha reductions
|
png.idat_data = new_png.idat_data;
|
||||||
let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams};
|
|
||||||
// This will collect all versions of images and pick one that compresses best
|
|
||||||
let eval = Evaluator::new(
|
|
||||||
deadline.clone(),
|
|
||||||
eval_filters.clone(),
|
|
||||||
eval_compression,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
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 {
|
|
||||||
reduction_occurred = true;
|
|
||||||
}
|
|
||||||
Some(result.filter)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if reduction_occurred {
|
|
||||||
report_format("Reducing image to ", &png.raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.idat_recoding || reduction_occurred {
|
|
||||||
let mut filters = opts.filter.clone();
|
|
||||||
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_filter.is_some());
|
|
||||||
let best: Option<TrialWithData> = if fast_eval {
|
|
||||||
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
|
||||||
|
|
||||||
if eval_filter.is_some() {
|
|
||||||
// Some filters have already been evaluated, we don't need to try them again
|
|
||||||
filters = filters.difference(&eval_filters).cloned().collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filters.is_empty() {
|
|
||||||
trace!("Evaluating: {} filters", filters.len());
|
|
||||||
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
|
|
||||||
if eval_filter.is_some() {
|
|
||||||
eval.set_best_size(png.idat_data.len());
|
|
||||||
}
|
|
||||||
eval.try_image(png.raw.clone());
|
|
||||||
if let Some(result) = eval.get_best_candidate() {
|
|
||||||
*png = result.image;
|
|
||||||
eval_filter = Some(result.filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let trial = TrialOptions {
|
|
||||||
filter: eval_filter.unwrap(),
|
|
||||||
compression: match opts.deflate {
|
|
||||||
Deflaters::Libdeflater { compression } => compression,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if trial.compression > 0 && trial.compression <= eval_compression {
|
|
||||||
// No further compression required
|
|
||||||
if png.idat_data.len() < idat_original_size || opts.force {
|
|
||||||
Some((trial, png.idat_data.clone()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("Trying: {}", trial.filter);
|
|
||||||
let original_len = idat_original_size;
|
|
||||||
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
|
|
||||||
perform_trial(&png.filtered, opts, trial, &best_size)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Perform full compression trials of selected filters and determine the best
|
|
||||||
|
|
||||||
if filters.is_empty() {
|
|
||||||
// Pick a filter automatically
|
|
||||||
if png.raw.ihdr.bit_depth as u8 >= 8 {
|
|
||||||
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
|
||||||
filters.insert(RowFilter::Bigrams);
|
|
||||||
} else {
|
|
||||||
// Otherwise delta filters generally don't work well, so just stick with None
|
|
||||||
filters.insert(RowFilter::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut results: Vec<TrialOptions> = Vec::with_capacity(filters.len());
|
|
||||||
|
|
||||||
for f in &filters {
|
|
||||||
results.push(TrialOptions {
|
|
||||||
filter: *f,
|
|
||||||
compression: match opts.deflate {
|
|
||||||
Deflaters::Libdeflater { compression } => compression,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Trying: {} filters", results.len());
|
|
||||||
|
|
||||||
let original_len = idat_original_size;
|
|
||||||
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
|
|
||||||
let results_iter = results.into_par_iter().with_max_len(1);
|
|
||||||
let best = results_iter.filter_map(|trial| {
|
|
||||||
if deadline.passed() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let filtered = &png.raw.filter_image(trial.filter, opts.optimize_alpha);
|
|
||||||
perform_trial(filtered, opts, trial, &best_size)
|
|
||||||
});
|
|
||||||
best.reduce_with(|i, j| {
|
|
||||||
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
|
||||||
i
|
|
||||||
} else {
|
|
||||||
j
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((opts, idat_data)) = best {
|
|
||||||
png.idat_data = idat_data;
|
|
||||||
debug!("Found better combination:");
|
|
||||||
debug!(
|
|
||||||
" zc = {} f = {:8} {} bytes",
|
|
||||||
opts.compression,
|
|
||||||
opts.filter,
|
|
||||||
png.idat_data.len()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
*png = stripped_png;
|
|
||||||
}
|
|
||||||
} else if png.idat_data.len() >= idat_original_size {
|
|
||||||
*png = stripped_png;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = png.output();
|
let output = png.output();
|
||||||
|
|
@ -648,6 +530,164 @@ fn optimize_png(
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform optimization on the input image data using the options provided
|
||||||
|
fn optimize_raw(
|
||||||
|
mut png: Arc<PngImage>,
|
||||||
|
opts: &Options,
|
||||||
|
deadline: Arc<Deadline>,
|
||||||
|
max_idat_size: Option<usize>,
|
||||||
|
) -> Option<PngData> {
|
||||||
|
// 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
|
||||||
|
let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams};
|
||||||
|
// This will collect all versions of images and pick one that compresses best
|
||||||
|
let eval = Evaluator::new(
|
||||||
|
deadline.clone(),
|
||||||
|
eval_filters.clone(),
|
||||||
|
eval_compression,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let (baseline, mut reduction_occurred) =
|
||||||
|
perform_reductions(png.clone(), opts, &deadline, &eval);
|
||||||
|
png = baseline;
|
||||||
|
let mut eval_result = eval.get_best_candidate();
|
||||||
|
if let Some(ref result) = eval_result {
|
||||||
|
if result.is_reduction {
|
||||||
|
png = Arc::clone(&result.image.raw);
|
||||||
|
reduction_occurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reduction_occurred {
|
||||||
|
report_format("Reducing image to ", &png);
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.idat_recoding || reduction_occurred {
|
||||||
|
let mut filters = opts.filter.clone();
|
||||||
|
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
||||||
|
let best: Option<TrialWithData> = if fast_eval {
|
||||||
|
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
||||||
|
|
||||||
|
if eval_result.is_some() {
|
||||||
|
// Some filters have already been evaluated, we don't need to try them again
|
||||||
|
filters = filters.difference(&eval_filters).cloned().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filters.is_empty() {
|
||||||
|
trace!("Evaluating: {} filters", filters.len());
|
||||||
|
let eval = Evaluator::new(deadline, filters, eval_compression, opts.optimize_alpha);
|
||||||
|
if let Some(ref result) = eval_result {
|
||||||
|
eval.set_best_size(result.image.idat_data.len());
|
||||||
|
}
|
||||||
|
eval.try_image(png.clone());
|
||||||
|
if let Some(result) = eval.get_best_candidate() {
|
||||||
|
eval_result = Some(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We should have a result here - fail if not (e.g. deadline passed)
|
||||||
|
let eval_result = eval_result?;
|
||||||
|
|
||||||
|
let trial = TrialOptions {
|
||||||
|
filter: eval_result.filter,
|
||||||
|
compression: match opts.deflate {
|
||||||
|
Deflaters::Libdeflater { compression } => compression,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Trying: {}", trial.filter);
|
||||||
|
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
|
||||||
|
perform_trial(&eval_result.image.filtered, opts, trial, &best_size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Perform full compression trials of selected filters and determine the best
|
||||||
|
|
||||||
|
if filters.is_empty() {
|
||||||
|
// Pick a filter automatically
|
||||||
|
if png.ihdr.bit_depth as u8 >= 8 {
|
||||||
|
// Bigrams is the best all-rounder when there's at least one byte per pixel
|
||||||
|
filters.insert(RowFilter::Bigrams);
|
||||||
|
} else {
|
||||||
|
// Otherwise delta filters generally don't work well, so just stick with None
|
||||||
|
filters.insert(RowFilter::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<TrialOptions> = Vec::with_capacity(filters.len());
|
||||||
|
|
||||||
|
for f in &filters {
|
||||||
|
results.push(TrialOptions {
|
||||||
|
filter: *f,
|
||||||
|
compression: match opts.deflate {
|
||||||
|
Deflaters::Libdeflater { compression } => compression,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Trying: {} filters", results.len());
|
||||||
|
|
||||||
|
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
|
||||||
|
let results_iter = results.into_par_iter().with_max_len(1);
|
||||||
|
let best = results_iter.filter_map(|trial| {
|
||||||
|
if deadline.passed() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let filtered = &png.filter_image(trial.filter, opts.optimize_alpha);
|
||||||
|
perform_trial(filtered, opts, trial, &best_size)
|
||||||
|
});
|
||||||
|
best.reduce_with(|i, j| {
|
||||||
|
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
j
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
raw: png,
|
||||||
|
// The filtered data has not been retained here, but we don't need to return it
|
||||||
|
filtered: vec![],
|
||||||
|
idat_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
|
debug!("Found better combination:");
|
||||||
|
debug!(
|
||||||
|
" zc = {} f = {:8} {} bytes",
|
||||||
|
eval_compression,
|
||||||
|
result.filter,
|
||||||
|
idat_data.len()
|
||||||
|
);
|
||||||
|
return Some(result.image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute a compression trial
|
/// Execute a compression trial
|
||||||
fn perform_trial(
|
fn perform_trial(
|
||||||
filtered: &[u8],
|
filtered: &[u8],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue