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]
|
||||
pub fn channels_per_pixel(&self) -> u8 {
|
||||
pub(crate) fn channels_per_pixel(&self) -> u8 {
|
||||
match self {
|
||||
ColorType::Grayscale { .. } | ColorType::Indexed { .. } => 1,
|
||||
ColorType::GrayscaleAlpha => 2,
|
||||
|
|
@ -66,12 +66,12 @@ impl ColorType {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_rgb(&self) -> bool {
|
||||
pub(crate) fn is_rgb(&self) -> bool {
|
||||
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_alpha(&self) -> bool {
|
||||
pub(crate) fn has_alpha(&self) -> bool {
|
||||
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::fmt;
|
||||
|
||||
|
|
@ -11,6 +12,8 @@ pub enum PngError {
|
|||
InvalidData,
|
||||
TruncatedData,
|
||||
ChunkMissing(&'static str),
|
||||
InvalidDepthForType(BitDepth, ColorType),
|
||||
IncorrectDataLength(usize, usize),
|
||||
Other(Box<str>),
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +33,14 @@ impl fmt::Display for PngError {
|
|||
}
|
||||
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ impl Evaluator {
|
|||
}
|
||||
|
||||
/// 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")]
|
||||
pub fn get_best_candidate(self) -> Option<Candidate> {
|
||||
let (eval_send, eval_recv) = self.eval_channel;
|
||||
|
|
|
|||
|
|
@ -53,10 +53,11 @@ impl Display for RowFilter {
|
|||
|
||||
impl RowFilter {
|
||||
pub const LAST: u8 = Self::Brute as u8;
|
||||
pub const STANDARD: [Self; 5] = [Self::None, Self::Sub, Self::Up, Self::Average, Self::Paeth];
|
||||
pub const SINGLE_LINE: [Self; 2] = [Self::None, Self::Sub];
|
||||
pub(crate) const STANDARD: [Self; 5] =
|
||||
[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,
|
||||
bpp: usize,
|
||||
data: &mut [u8],
|
||||
|
|
@ -176,7 +177,7 @@ impl RowFilter {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn unfilter_line(
|
||||
pub(crate) fn unfilter_line(
|
||||
self,
|
||||
bpp: usize,
|
||||
data: &[u8],
|
||||
|
|
|
|||
|
|
@ -19,10 +19,6 @@ pub struct IhdrData {
|
|||
pub color_type: ColorType,
|
||||
/// The bit depth of the image
|
||||
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
|
||||
pub interlaced: Interlacing,
|
||||
}
|
||||
|
|
@ -176,8 +172,6 @@ pub fn parse_ihdr_header(
|
|||
bit_depth: byte_data[8].try_into()?,
|
||||
width: 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()?,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
438
src/lib.rs
438
src/lib.rs
|
|
@ -27,6 +27,7 @@ mod rayon;
|
|||
use crate::atomicmin::AtomicMin;
|
||||
use crate::deflate::{crc32, inflate};
|
||||
use crate::evaluate::Evaluator;
|
||||
use crate::headers::IhdrData;
|
||||
use crate::png::PngData;
|
||||
use crate::png::PngImage;
|
||||
use crate::reduction::*;
|
||||
|
|
@ -40,12 +41,14 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub use crate::colors::{BitDepth, ColorType};
|
||||
pub use crate::deflate::Deflaters;
|
||||
pub use crate::error::PngError;
|
||||
pub use crate::filters::RowFilter;
|
||||
pub use crate::headers::Headers;
|
||||
pub use crate::interlace::Interlacing;
|
||||
pub use indexmap::{indexset, IndexMap, IndexSet};
|
||||
pub use rgb::{RGB16, RGBA8};
|
||||
|
||||
mod atomicmin;
|
||||
mod colors;
|
||||
|
|
@ -57,6 +60,8 @@ mod headers;
|
|||
mod interlace;
|
||||
mod png;
|
||||
mod reduction;
|
||||
#[cfg(feature = "sanity-checks")]
|
||||
mod sanity_checks;
|
||||
|
||||
/// Private to oxipng; don't use outside tests and benches
|
||||
#[doc(hidden)]
|
||||
|
|
@ -67,6 +72,8 @@ pub mod internal_tests {
|
|||
pub use crate::headers::*;
|
||||
pub use crate::png::*;
|
||||
pub use crate::reduction::*;
|
||||
#[cfg(feature = "sanity-checks")]
|
||||
pub use crate::sanity_checks::*;
|
||||
}
|
||||
|
||||
#[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
|
||||
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> {
|
||||
// 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
|
||||
perform_strip(png, opts);
|
||||
let stripped_png = png.clone();
|
||||
|
||||
// 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.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;
|
||||
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) {
|
||||
png.raw = new_png.raw;
|
||||
png.idat_data = new_png.idat_data;
|
||||
}
|
||||
|
||||
let output = png.output();
|
||||
|
|
@ -643,11 +601,169 @@ fn optimize_png(
|
|||
}
|
||||
|
||||
#[cfg(feature = "sanity-checks")]
|
||||
debug_assert!(sanity_checks::validate_output(&output, original_data));
|
||||
assert!(sanity_checks::validate_output(&output, original_data));
|
||||
|
||||
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
|
||||
fn perform_trial(
|
||||
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