Add Predefined filter strategy
Use this to retain filters during evaluation rather than the complete filtered data
This commit is contained in:
parent
682b5d9ad0
commit
8c3758b9f4
4 changed files with 59 additions and 30 deletions
|
|
@ -23,11 +23,14 @@ use crate::{
|
||||||
|
|
||||||
pub(crate) struct Candidate {
|
pub(crate) struct Candidate {
|
||||||
pub image: Arc<PngImage>,
|
pub image: Arc<PngImage>,
|
||||||
pub data: Vec<u8>,
|
pub idat_data: Option<Vec<u8>>,
|
||||||
pub data_is_compressed: bool,
|
|
||||||
pub estimated_output_size: usize,
|
pub estimated_output_size: usize,
|
||||||
|
/// The input filter, which is retained for printing and for APNG frames.
|
||||||
pub filter: FilterStrategy,
|
pub filter: FilterStrategy,
|
||||||
// For determining tie-breaker
|
/// The filter returned by the filter function, which may be Predefined.
|
||||||
|
/// Use this for the next round to avoid recomputing the filter.
|
||||||
|
pub filter_used: FilterStrategy,
|
||||||
|
/// For determining tie-breaker
|
||||||
nth: usize,
|
nth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +39,7 @@ impl Candidate {
|
||||||
(
|
(
|
||||||
self.estimated_output_size,
|
self.estimated_output_size,
|
||||||
self.image.data.len(),
|
self.image.data.len(),
|
||||||
self.filter,
|
self.filter.clone(),
|
||||||
// Prefer the later image added (e.g. baseline, which is always added last)
|
// Prefer the later image added (e.g. baseline, which is always added last)
|
||||||
usize::MAX - self.nth,
|
usize::MAX - self.nth,
|
||||||
)
|
)
|
||||||
|
|
@ -143,21 +146,21 @@ impl Evaluator {
|
||||||
// which are dangerous to do in side Rayon's loop.
|
// which are dangerous to do in side Rayon's loop.
|
||||||
// Instead, only update (atomic) best size in real time,
|
// Instead, only update (atomic) best size in real time,
|
||||||
// and the best result later without need for locks.
|
// and the best result later without need for locks.
|
||||||
filters_iter.for_each(|&filter| {
|
filters_iter.for_each(|filter| {
|
||||||
if deadline.passed() {
|
if deadline.passed() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let filtered = image.filter_image(filter, optimize_alpha);
|
let (filtered, filter_used) = image.filter_image(filter.clone(), optimize_alpha);
|
||||||
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
let idat_data = deflater.deflate(&filtered, best_candidate_size.get());
|
||||||
if let Ok(idat_data) = idat_data {
|
if let Ok(idat_data) = idat_data {
|
||||||
let estimated_output_size = image.estimated_output_size(&idat_data);
|
let estimated_output_size = image.estimated_output_size(&idat_data);
|
||||||
// For the final round we need the IDAT data, otherwise the filtered data
|
// We only need to retain the IDAT data in the final round
|
||||||
let new = Candidate {
|
let new = Candidate {
|
||||||
image: image.clone(),
|
image: image.clone(),
|
||||||
data: if final_round { idat_data } else { filtered },
|
idat_data: if final_round { Some(idat_data) } else { None },
|
||||||
data_is_compressed: final_round,
|
|
||||||
estimated_output_size,
|
estimated_output_size,
|
||||||
filter,
|
filter: filter.clone(),
|
||||||
|
filter_used,
|
||||||
nth,
|
nth,
|
||||||
};
|
};
|
||||||
best_candidate_size.set_min(estimated_output_size);
|
best_candidate_size.set_min(estimated_output_size);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{fmt, fmt::Display, mem::transmute};
|
use std::{fmt, fmt::Display, mem::transmute};
|
||||||
|
|
||||||
/// Filtering strategy for use in [`Options`][crate::Options]
|
/// Filtering strategy for use in [`Options`][crate::Options]
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
|
||||||
pub enum FilterStrategy {
|
pub enum FilterStrategy {
|
||||||
/// Same filter for all rows
|
/// Same filter for all rows
|
||||||
Basic(RowFilter),
|
Basic(RowFilter),
|
||||||
|
|
@ -15,6 +15,8 @@ pub enum FilterStrategy {
|
||||||
BigEnt,
|
BigEnt,
|
||||||
/// Deflate compression
|
/// Deflate compression
|
||||||
Brute,
|
Brute,
|
||||||
|
/// Predefined filter for each row
|
||||||
|
Predefined(Vec<RowFilter>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for FilterStrategy {
|
impl Display for FilterStrategy {
|
||||||
|
|
@ -26,6 +28,7 @@ impl Display for FilterStrategy {
|
||||||
Self::Bigrams => "Bigrams".fmt(f),
|
Self::Bigrams => "Bigrams".fmt(f),
|
||||||
Self::BigEnt => "BigEnt".fmt(f),
|
Self::BigEnt => "BigEnt".fmt(f),
|
||||||
Self::Brute => "Brute".fmt(f),
|
Self::Brute => "Brute".fmt(f),
|
||||||
|
Self::Predefined(_) => "Predefined".fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/lib.rs
18
src/lib.rs
|
|
@ -170,7 +170,7 @@ impl RawImage {
|
||||||
|
|
||||||
let mut png = PngData {
|
let mut png = PngData {
|
||||||
raw: result.image,
|
raw: result.image,
|
||||||
idat_data: result.data,
|
idat_data: result.idat_data.unwrap(),
|
||||||
aux_chunks,
|
aux_chunks,
|
||||||
frames: Vec::new(),
|
frames: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
@ -359,7 +359,7 @@ fn optimize_png(
|
||||||
};
|
};
|
||||||
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
if let Some(result) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
||||||
png.raw = result.image;
|
png.raw = result.image;
|
||||||
png.idat_data = result.data;
|
png.idat_data = result.idat_data.unwrap();
|
||||||
recompress_frames(png, &opts, deadline, result.filter)?;
|
recompress_frames(png, &opts, deadline, result.filter)?;
|
||||||
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
|
postprocess_chunks(&mut png.aux_chunks, &png.raw.ihdr, &raw.ihdr);
|
||||||
}
|
}
|
||||||
|
|
@ -472,7 +472,7 @@ fn optimize_raw(
|
||||||
(eval_result?, eval_deflater)
|
(eval_result?, eval_deflater)
|
||||||
};
|
};
|
||||||
|
|
||||||
if result.data_is_compressed
|
if result.idat_data.is_some()
|
||||||
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
|
&& max_size.is_none_or(|max_size| result.estimated_output_size < max_size)
|
||||||
{
|
{
|
||||||
debug!("Found better result:");
|
debug!("Found better result:");
|
||||||
|
|
@ -499,7 +499,7 @@ fn perform_trials(
|
||||||
|
|
||||||
if eval_result.is_some() {
|
if eval_result.is_some() {
|
||||||
// Some filters have already been evaluated, we don't need to try them again
|
// Some filters have already been evaluated, we don't need to try them again
|
||||||
filters = filters.difference(&eval_filters).copied().collect();
|
filters = filters.difference(&eval_filters).cloned().collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filters.is_empty() {
|
if !filters.is_empty() {
|
||||||
|
|
@ -523,14 +523,14 @@ fn perform_trials(
|
||||||
// We should have a result here - fail if not (e.g. deadline passed)
|
// We should have a result here - fail if not (e.g. deadline passed)
|
||||||
let mut result = eval_result?;
|
let mut result = eval_result?;
|
||||||
|
|
||||||
if !result.data_is_compressed {
|
if result.idat_data.is_none() {
|
||||||
// Compress with the main deflater
|
// Compress with the main deflater
|
||||||
debug!("Trying filter {} with {}", result.filter, opts.deflate);
|
debug!("Trying filter {} with {}", result.filter, opts.deflate);
|
||||||
match opts.deflate.deflate(&result.data, max_size) {
|
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
|
||||||
|
match opts.deflate.deflate(&data, max_size) {
|
||||||
Ok(idat_data) => {
|
Ok(idat_data) => {
|
||||||
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
|
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
|
||||||
result.data = idat_data;
|
result.idat_data = Some(idat_data);
|
||||||
result.data_is_compressed = true;
|
|
||||||
trace!("{} bytes", result.estimated_output_size);
|
trace!("{} bytes", result.estimated_output_size);
|
||||||
}
|
}
|
||||||
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
Err(PngError::DeflatedDataTooLong(bytes)) => {
|
||||||
|
|
@ -644,7 +644,7 @@ fn recompress_frames(
|
||||||
ihdr.width = frame.width;
|
ihdr.width = frame.width;
|
||||||
ihdr.height = frame.height;
|
ihdr.height = frame.height;
|
||||||
let image = PngImage::new(ihdr, &frame.data)?;
|
let image = PngImage::new(ihdr, &frame.data)?;
|
||||||
let filtered = image.filter_image(filter, opts.optimize_alpha);
|
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
|
||||||
let max_size = Some(frame.data.len() - 1);
|
let max_size = Some(frame.data.len() - 1);
|
||||||
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) {
|
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) {
|
||||||
debug!(
|
debug!(
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,11 @@ impl PngImage {
|
||||||
|
|
||||||
/// Apply the specified filter type to all rows in the image
|
/// Apply the specified filter type to all rows in the image
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn filter_image(&self, strategy: FilterStrategy, optimize_alpha: bool) -> Vec<u8> {
|
pub fn filter_image(
|
||||||
|
&self,
|
||||||
|
strategy: FilterStrategy,
|
||||||
|
optimize_alpha: bool,
|
||||||
|
) -> (Vec<u8>, FilterStrategy) {
|
||||||
let mut filtered = Vec::with_capacity(self.data.len());
|
let mut filtered = Vec::with_capacity(self.data.len());
|
||||||
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
|
||||||
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
|
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
|
||||||
|
|
@ -399,31 +403,39 @@ impl PngImage {
|
||||||
let mut prev_line = Vec::new();
|
let mut prev_line = Vec::new();
|
||||||
let mut prev_pass: Option<u8> = None;
|
let mut prev_pass: Option<u8> = None;
|
||||||
let mut f_buf = Vec::new();
|
let mut f_buf = Vec::new();
|
||||||
for line in self.scan_lines(false) {
|
// For heuristic strategies, keep track of the actual filter used for each line
|
||||||
|
let mut filters_used = Vec::new();
|
||||||
|
for (i, line) in self.scan_lines(false).enumerate() {
|
||||||
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
if prev_pass != line.pass || line.data.len() != prev_line.len() {
|
||||||
prev_line = vec![0; line.data.len()];
|
prev_line = vec![0; line.data.len()];
|
||||||
}
|
}
|
||||||
// Alpha optimisation may alter the line data, so we need a mutable copy of it
|
// Alpha optimisation may alter the line data, so we need a mutable copy of it
|
||||||
let mut line_data = line.data.to_vec();
|
let mut line_data = line.data.to_vec();
|
||||||
|
|
||||||
if let FilterStrategy::Basic(filter) = strategy {
|
if let FilterStrategy::Basic(mut filter) = strategy {
|
||||||
// Standard filters
|
// Standard filters
|
||||||
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
|
if prev_pass != line.pass && filter > RowFilter::Sub {
|
||||||
filter
|
filter = RowFilter::None;
|
||||||
} else {
|
}
|
||||||
RowFilter::None
|
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
};
|
filtered.extend_from_slice(&f_buf);
|
||||||
|
prev_line = line_data;
|
||||||
|
} else if let FilterStrategy::Predefined(lines) = &strategy {
|
||||||
|
// Predefined filter for each line
|
||||||
|
let filter = lines.get(i).unwrap_or(&RowFilter::None);
|
||||||
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
|
||||||
filtered.extend_from_slice(&f_buf);
|
filtered.extend_from_slice(&f_buf);
|
||||||
prev_line = line_data;
|
prev_line = line_data;
|
||||||
} else {
|
} else {
|
||||||
// Heuristic filter selection strategies
|
// Heuristic filter selection strategies
|
||||||
|
|
||||||
|
let mut best_filter = RowFilter::None;
|
||||||
if line_data.iter().all(|&x| x == 0) {
|
if line_data.iter().all(|&x| x == 0) {
|
||||||
// Assume None if the line is all zeros
|
// Assume None if the line is all zeros
|
||||||
filtered.push(RowFilter::None as u8);
|
filtered.push(best_filter as u8);
|
||||||
filtered.extend_from_slice(&line_data);
|
filtered.extend_from_slice(&line_data);
|
||||||
prev_line = line_data;
|
prev_line = line_data;
|
||||||
|
filters_used.push(best_filter);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,6 +462,7 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -473,6 +486,7 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -492,6 +506,7 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -512,6 +527,7 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -537,6 +553,7 @@ impl PngImage {
|
||||||
best_size = size;
|
best_size = size;
|
||||||
std::mem::swap(&mut best_line, &mut f_buf);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
best_line_raw.clone_from(&line_data);
|
best_line_raw.clone_from(&line_data);
|
||||||
|
best_filter = *f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filtered.resize(line_start, 0);
|
filtered.resize(line_start, 0);
|
||||||
|
|
@ -545,11 +562,17 @@ impl PngImage {
|
||||||
}
|
}
|
||||||
filtered.extend_from_slice(&best_line);
|
filtered.extend_from_slice(&best_line);
|
||||||
prev_line = best_line_raw;
|
prev_line = best_line_raw;
|
||||||
|
filters_used.push(best_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
prev_pass = line.pass;
|
prev_pass = line.pass;
|
||||||
}
|
}
|
||||||
filtered
|
|
||||||
|
if filters_used.is_empty() {
|
||||||
|
(filtered, strategy)
|
||||||
|
} else {
|
||||||
|
(filtered, FilterStrategy::Predefined(filters_used))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue