Raw API (#482)
This commit is contained in:
parent
a7be8751dc
commit
9a500941d8
9 changed files with 415 additions and 201 deletions
|
|
@ -56,7 +56,7 @@ impl ColorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn channels_per_pixel(&self) -> u8 {
|
pub(crate) fn channels_per_pixel(&self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
||||||
ColorType::GrayscaleAlpha => 2,
|
ColorType::GrayscaleAlpha => 2,
|
||||||
|
|
@ -66,12 +66,12 @@ impl ColorType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_rgb(&self) -> bool {
|
pub(crate) fn is_rgb(&self) -> bool {
|
||||||
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_alpha(&self) -> bool {
|
pub(crate) fn has_alpha(&self) -> bool {
|
||||||
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
src/error.rs
11
src/error.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::colors::{BitDepth, ColorType};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
|
@ -11,6 +12,8 @@ pub enum PngError {
|
||||||
InvalidData,
|
InvalidData,
|
||||||
TruncatedData,
|
TruncatedData,
|
||||||
ChunkMissing(&'static str),
|
ChunkMissing(&'static str),
|
||||||
|
InvalidDepthForType(BitDepth, ColorType),
|
||||||
|
IncorrectDataLength(usize, usize),
|
||||||
Other(Box<str>),
|
Other(Box<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,6 +33,14 @@ impl fmt::Display for PngError {
|
||||||
}
|
}
|
||||||
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
||||||
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
|
PngError::ChunkMissing(s) => write!(f, "Chunk {} missing or empty", s),
|
||||||
|
PngError::InvalidDepthForType(d, ref c) => {
|
||||||
|
write!(f, "Invalid bit depth {} for color type {}", d, c)
|
||||||
|
}
|
||||||
|
PngError::IncorrectDataLength(l1, l2) => write!(
|
||||||
|
f,
|
||||||
|
"Data length {} does not match the expected length {}",
|
||||||
|
l1, l2
|
||||||
|
),
|
||||||
PngError::Other(ref s) => f.write_str(s),
|
PngError::Other(ref s) => f.write_str(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,11 @@ impl Display for RowFilter {
|
||||||
|
|
||||||
impl RowFilter {
|
impl RowFilter {
|
||||||
pub const LAST: u8 = Self::Brute as u8;
|
pub const LAST: u8 = Self::Brute as u8;
|
||||||
pub const STANDARD: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
pub(crate) const STANDARD: [Self; 5] =
|
||||||
pub const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
[Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
||||||
|
pub(crate) const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
||||||
|
|
||||||
pub fn filter_line(
|
pub(crate) fn filter_line(
|
||||||
self,
|
self,
|
||||||
bpp: usize,
|
bpp: usize,
|
||||||
data: &mut [u8],
|
data: &mut [u8],
|
||||||
|
|
@ -176,7 +177,7 @@ impl RowFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfilter_line(
|
pub(crate) fn unfilter_line(
|
||||||
self,
|
self,
|
||||||
bpp: usize,
|
bpp: usize,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,6 @@ pub struct IhdrData {
|
||||||
pub color_type: ColorType,
|
pub color_type: ColorType,
|
||||||
/// The bit depth of the image
|
/// The bit depth of the image
|
||||||
pub bit_depth: BitDepth,
|
pub bit_depth: BitDepth,
|
||||||
/// The compression method used for this image (0 for DEFLATE)
|
|
||||||
pub compression: u8,
|
|
||||||
/// The filter mode used for this image (currently only 0 is valid)
|
|
||||||
pub filter: u8,
|
|
||||||
/// The interlacing mode of the image
|
/// The interlacing mode of the image
|
||||||
pub interlaced: Interlacing,
|
pub interlaced: Interlacing,
|
||||||
}
|
}
|
||||||
|
|
@ -176,8 +172,6 @@ pub fn parse_ihdr_header(
|
||||||
bit_depth: byte_data[8].try_into()?,
|
bit_depth: byte_data[8].try_into()?,
|
||||||
width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?,
|
width: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?,
|
||||||
height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?,
|
height: read_be_u32(&mut rdr).map_err(|_| PngError::TruncatedData)?,
|
||||||
compression: byte_data[10],
|
|
||||||
filter: byte_data[11],
|
|
||||||
interlaced: interlaced.try_into()?,
|
interlaced: interlaced.try_into()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
438
src/lib.rs
438
src/lib.rs
|
|
@ -27,6 +27,7 @@ mod rayon;
|
||||||
use crate::atomicmin::AtomicMin;
|
use crate::atomicmin::AtomicMin;
|
||||||
use crate::deflate::{crc32, inflate};
|
use crate::deflate::{crc32, inflate};
|
||||||
use crate::evaluate::Evaluator;
|
use crate::evaluate::Evaluator;
|
||||||
|
use crate::headers::IhdrData;
|
||||||
use crate::png::PngData;
|
use crate::png::PngData;
|
||||||
use crate::png::PngImage;
|
use crate::png::PngImage;
|
||||||
use crate::reduction::*;
|
use crate::reduction::*;
|
||||||
|
|
@ -40,12 +41,14 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub use crate::colors::{BitDepth, ColorType};
|
||||||
pub use crate::deflate::Deflaters;
|
pub use crate::deflate::Deflaters;
|
||||||
pub use crate::error::PngError;
|
pub use crate::error::PngError;
|
||||||
pub use crate::filters::RowFilter;
|
pub use crate::filters::RowFilter;
|
||||||
pub use crate::headers::Headers;
|
pub use crate::headers::Headers;
|
||||||
pub use crate::interlace::Interlacing;
|
pub use crate::interlace::Interlacing;
|
||||||
pub use indexmap::{indexset, IndexMap, IndexSet};
|
pub use indexmap::{indexset, IndexMap, IndexSet};
|
||||||
|
pub use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
mod atomicmin;
|
mod atomicmin;
|
||||||
mod colors;
|
mod colors;
|
||||||
|
|
@ -57,6 +60,8 @@ mod headers;
|
||||||
mod interlace;
|
mod interlace;
|
||||||
mod png;
|
mod png;
|
||||||
mod reduction;
|
mod reduction;
|
||||||
|
#[cfg(feature = "sanity-checks")]
|
||||||
|
mod sanity_checks;
|
||||||
|
|
||||||
/// Private to oxipng; don't use outside tests and benches
|
/// Private to oxipng; don't use outside tests and benches
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
@ -67,6 +72,8 @@ pub mod internal_tests {
|
||||||
pub use crate::headers::*;
|
pub use crate::headers::*;
|
||||||
pub use crate::png::*;
|
pub use crate::png::*;
|
||||||
pub use crate::reduction::*;
|
pub use crate::reduction::*;
|
||||||
|
#[cfg(feature = "sanity-checks")]
|
||||||
|
pub use crate::sanity_checks::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -307,6 +314,88 @@ impl Default for Options {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// A raw image definition which can be used to create an optimized png
|
||||||
|
pub struct RawImage {
|
||||||
|
png: Arc<PngImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawImage {
|
||||||
|
/// Construct a new raw image definition
|
||||||
|
///
|
||||||
|
/// * `width` - The width of the image in pixels
|
||||||
|
/// * `height` - The height of the image in pixels
|
||||||
|
/// * `color_type` - The color type of the image
|
||||||
|
/// * `bit_depth` - The bit depth of the image
|
||||||
|
/// * `data` - The raw pixel data of the image
|
||||||
|
pub fn new(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
color_type: ColorType,
|
||||||
|
bit_depth: BitDepth,
|
||||||
|
data: Vec<u8>,
|
||||||
|
) -> Result<Self, PngError> {
|
||||||
|
// Validate bit depth
|
||||||
|
let valid_depth = match color_type {
|
||||||
|
ColorType::Grayscale { .. } => true,
|
||||||
|
ColorType::Indexed { .. } => (bit_depth as u8) <= 8,
|
||||||
|
_ => (bit_depth as u8) >= 8,
|
||||||
|
};
|
||||||
|
if !valid_depth {
|
||||||
|
return Err(PngError::InvalidDepthForType(bit_depth, color_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data length
|
||||||
|
let bpp = bit_depth as usize * color_type.channels_per_pixel() as usize;
|
||||||
|
let row_bytes = (bpp * width as usize + 7) / 8;
|
||||||
|
let expected_len = row_bytes * height as usize;
|
||||||
|
if data.len() != expected_len {
|
||||||
|
return Err(PngError::IncorrectDataLength(data.len(), expected_len));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
png: Arc::new(PngImage {
|
||||||
|
ihdr: IhdrData {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color_type,
|
||||||
|
bit_depth,
|
||||||
|
interlaced: Interlacing::None,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
aux_headers: IndexMap::new(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a png chunk, such as "iTXt", to be included in the output
|
||||||
|
pub fn add_png_chunk(&mut self, chunk_type: [u8; 4], data: Vec<u8>) {
|
||||||
|
// We can guarantee this will succeed - failure indicates a bug
|
||||||
|
let png = Arc::get_mut(&mut self.png).unwrap();
|
||||||
|
png.aux_headers.insert(chunk_type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an ICC profile for the image
|
||||||
|
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
||||||
|
// Compress with default compression level
|
||||||
|
if let Ok(mut compressed) = deflate::deflate(data, 11, &AtomicMin::new(None)) {
|
||||||
|
let mut iccp = Vec::with_capacity(compressed.len() + 13);
|
||||||
|
iccp.extend(b"icc"); // Profile name - generally unused, can be anything
|
||||||
|
iccp.extend([0, 0]); // Null separator, zlib compression method
|
||||||
|
iccp.append(&mut compressed);
|
||||||
|
self.add_png_chunk(*b"iCCP", iccp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an optimized png from the raw image data using the options provided
|
||||||
|
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
|
||||||
|
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||||
|
let png = optimize_raw(Arc::clone(&self.png), opts, deadline, None)
|
||||||
|
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
||||||
|
Ok(png.output())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform optimization on the input file using the options provided
|
/// Perform optimization on the input file using the options provided
|
||||||
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
|
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
|
||||||
// Read in the file and try to decode as PNG.
|
// Read in the file and try to decode as PNG.
|
||||||
|
|
@ -474,141 +563,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();
|
||||||
|
|
@ -643,11 +601,169 @@ fn optimize_png(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sanity-checks")]
|
#[cfg(feature = "sanity-checks")]
|
||||||
debug_assert!(sanity_checks::validate_output(&output, original_data));
|
assert!(sanity_checks::validate_output(&output, original_data));
|
||||||
|
|
||||||
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],
|
||||||
|
|
@ -958,55 +1074,3 @@ fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sanity-checks")]
|
|
||||||
mod sanity_checks {
|
|
||||||
use super::*;
|
|
||||||
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
|
||||||
use log::error;
|
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
/// Validate that the output png data still matches the original image
|
|
||||||
pub(super) fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
|
||||||
let (old_png, new_png) = rayon::join(
|
|
||||||
|| load_png_image_from_memory(original_data),
|
|
||||||
|| load_png_image_from_memory(output),
|
|
||||||
);
|
|
||||||
|
|
||||||
match (new_png, old_png) {
|
|
||||||
(Err(new_err), _) => {
|
|
||||||
error!("Failed to read output image for validation: {}", new_err);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
(_, Err(old_err)) => {
|
|
||||||
// The original image might be invalid if, for example, there is a CRC error,
|
|
||||||
// and we set fix_errors to true. In that case, all we can do is check that the
|
|
||||||
// new image is decodable.
|
|
||||||
warn!("Failed to read input image for validation: {}", old_err);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
(Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads a PNG image from memory to a [DynamicImage]
|
|
||||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<DynamicImage, image::ImageError> {
|
|
||||||
let mut reader = image::io::Reader::new(Cursor::new(png_data));
|
|
||||||
reader.set_format(ImageFormat::Png);
|
|
||||||
reader.no_limits();
|
|
||||||
reader.decode()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compares images pixel by pixel for equivalent content
|
|
||||||
fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool {
|
|
||||||
let a = old_png.pixels().filter(|x| {
|
|
||||||
let p = x.2.channels();
|
|
||||||
!(p.len() == 4 && p[3] == 0)
|
|
||||||
});
|
|
||||||
let b = new_png.pixels().filter(|x| {
|
|
||||||
let p = x.2.channels();
|
|
||||||
!(p.len() == 4 && p[3] == 0)
|
|
||||||
});
|
|
||||||
a.eq(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
47
src/sanity_checks.rs
Normal file
47
src/sanity_checks.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
||||||
|
use log::{error, warn};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
/// Validate that the output png data still matches the original image
|
||||||
|
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
||||||
|
let (old_png, new_png) = rayon::join(
|
||||||
|
|| load_png_image_from_memory(original_data),
|
||||||
|
|| load_png_image_from_memory(output),
|
||||||
|
);
|
||||||
|
|
||||||
|
match (new_png, old_png) {
|
||||||
|
(Err(new_err), _) => {
|
||||||
|
error!("Failed to read output image for validation: {}", new_err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
(_, Err(old_err)) => {
|
||||||
|
// The original image might be invalid if, for example, there is a CRC error,
|
||||||
|
// and we set fix_errors to true. In that case, all we can do is check that the
|
||||||
|
// new image is decodable.
|
||||||
|
warn!("Failed to read input image for validation: {}", old_err);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
(Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a PNG image from memory to a [DynamicImage]
|
||||||
|
fn load_png_image_from_memory(png_data: &[u8]) -> Result<DynamicImage, image::ImageError> {
|
||||||
|
let mut reader = image::io::Reader::new(Cursor::new(png_data));
|
||||||
|
reader.set_format(ImageFormat::Png);
|
||||||
|
reader.no_limits();
|
||||||
|
reader.decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares images pixel by pixel for equivalent content
|
||||||
|
fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool {
|
||||||
|
let a = old_png.pixels().filter(|x| {
|
||||||
|
let p = x.2.channels();
|
||||||
|
!(p.len() == 4 && p[3] == 0)
|
||||||
|
});
|
||||||
|
let b = new_png.pixels().filter(|x| {
|
||||||
|
let p = x.2.channels();
|
||||||
|
!(p.len() == 4 && p[3] == 0)
|
||||||
|
});
|
||||||
|
a.eq(b)
|
||||||
|
}
|
||||||
BIN
tests/files/raw_api.png
Normal file
BIN
tests/files/raw_api.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
97
tests/raw.rs
Normal file
97
tests/raw.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use oxipng::internal_tests::*;
|
||||||
|
use oxipng::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn get_opts() -> Options {
|
||||||
|
Options {
|
||||||
|
force: true,
|
||||||
|
filter: indexset! { RowFilter::None },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_it_converts(input: &str) {
|
||||||
|
let input = PathBuf::from(input);
|
||||||
|
let opts = get_opts();
|
||||||
|
|
||||||
|
let original_data = PngData::read_file(&PathBuf::from(input)).unwrap();
|
||||||
|
let png = PngData::from_slice(&original_data, opts.fix_errors).unwrap();
|
||||||
|
let png = Arc::try_unwrap(png.raw).unwrap();
|
||||||
|
|
||||||
|
let num_headers = png.aux_headers.len();
|
||||||
|
assert!(num_headers > 0);
|
||||||
|
|
||||||
|
let mut raw = RawImage::new(
|
||||||
|
png.ihdr.width,
|
||||||
|
png.ihdr.height,
|
||||||
|
png.ihdr.color_type,
|
||||||
|
png.ihdr.bit_depth,
|
||||||
|
png.data,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (chunk_type, data) in png.aux_headers {
|
||||||
|
raw.add_png_chunk(chunk_type, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = raw.create_optimized_png(&opts).unwrap();
|
||||||
|
|
||||||
|
let new = PngData::from_slice(&output, opts.fix_errors).unwrap();
|
||||||
|
assert!(new.raw.aux_headers.len() == num_headers);
|
||||||
|
|
||||||
|
#[cfg(feature = "sanity-checks")]
|
||||||
|
assert!(validate_output(&output, &original_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_file() {
|
||||||
|
test_it_converts("tests/files/raw_api.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_indexed() {
|
||||||
|
let opts = get_opts();
|
||||||
|
|
||||||
|
let raw = RawImage::new(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
ColorType::Indexed {
|
||||||
|
palette: vec![
|
||||||
|
RGBA8::new(255, 255, 255, 255),
|
||||||
|
RGBA8::new(255, 0, 0, 255),
|
||||||
|
RGBA8::new(0, 255, 0, 255),
|
||||||
|
RGBA8::new(0, 0, 255, 255),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
BitDepth::Eight,
|
||||||
|
vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
raw.create_optimized_png(&opts).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_depth() {
|
||||||
|
RawImage::new(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
ColorType::RGBA,
|
||||||
|
BitDepth::Four,
|
||||||
|
vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3],
|
||||||
|
)
|
||||||
|
.expect_err("Expected invalid depth for color type");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incorrect_length() {
|
||||||
|
RawImage::new(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
ColorType::RGBA,
|
||||||
|
BitDepth::Eight,
|
||||||
|
vec![0, 0, 1, 1, 0, 0, 1, 1],
|
||||||
|
)
|
||||||
|
.expect_err("Expected incorrect data length");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue