Make dependency on image optional (#498)
* Make dependency on `image` optional After PR https://github.com/shssoichiro/oxipng/pull/481 was merged, the `image` dependency became unused when building with debug assertions disabled, as it is only used to implement output sanity checks when such assertions are enabled. The `image` crate transitively pulls a significant amount of dependencies, so it's useful for OxiPNG users to get rid of them when not needed. [Cargo does not allow specifying dependencies that are only pulled when debug assertions are enabled](https://github.com/rust-lang/cargo/issues/7634), so the next best way to give users some flexibility is to gate those debug assertions behind a feature flag. These changes add a `sanity-checks` feature flag that controls whether the `image` crate and the related sanity checks are compiled in. This feature is enabled by default to keep debug builds useful to catch problems during development. * Fix Clippy lints * Run tests with new sanity-checks feature enabled
This commit is contained in:
parent
110eae7c19
commit
be19ed592d
6 changed files with 68 additions and 52 deletions
2
.github/workflows/oxipng.yml
vendored
2
.github/workflows/oxipng.yml
vendored
|
|
@ -78,7 +78,7 @@ jobs:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: -- -D warnings
|
args: -- -D warnings
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test
|
run: cargo test --features sanity-checks
|
||||||
- name: Build benchmarks
|
- name: Build benchmarks
|
||||||
if: matrix.toolchain == 'nightly'
|
if: matrix.toolchain == 'nightly'
|
||||||
run: cargo bench --no-run
|
run: cargo bench --no-run
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ name = "oxipng"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
required-features = ["binary"]
|
required-features = ["binary"]
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "zopfli"
|
||||||
|
required-features = ["zopfli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zopfli = { version = "0.7.1", optional = true }
|
zopfli = { version = "0.7.1", optional = true }
|
||||||
rgb = "0.8.33"
|
rgb = "0.8.33"
|
||||||
|
|
@ -53,6 +57,7 @@ optional = true
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
||||||
[dependencies.image]
|
[dependencies.image]
|
||||||
|
optional = true
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["png"]
|
features = ["png"]
|
||||||
version = "0.24.3"
|
version = "0.24.3"
|
||||||
|
|
@ -65,6 +70,7 @@ binary = ["clap", "wild", "stderrlog"]
|
||||||
default = ["binary", "filetime", "parallel", "zopfli"]
|
default = ["binary", "filetime", "parallel", "zopfli"]
|
||||||
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
|
parallel = ["rayon", "indexmap/rayon", "crossbeam-channel"]
|
||||||
freestanding = ["libdeflater/freestanding"]
|
freestanding = ["libdeflater/freestanding"]
|
||||||
|
sanity-checks = ["image"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "oxipng"
|
name = "oxipng"
|
||||||
|
|
|
||||||
94
src/lib.rs
94
src/lib.rs
|
|
@ -31,12 +31,11 @@ use crate::evaluate::Evaluator;
|
||||||
use crate::png::PngData;
|
use crate::png::PngData;
|
||||||
use crate::png::PngImage;
|
use crate::png::PngImage;
|
||||||
use crate::reduction::*;
|
use crate::reduction::*;
|
||||||
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
use log::{debug, info, warn};
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::{copy, File, Metadata};
|
use std::fs::{copy, File, Metadata};
|
||||||
use std::io::{stdin, stdout, BufWriter, Cursor, Read, Write};
|
use std::io::{stdin, stdout, BufWriter, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -659,7 +658,8 @@ fn optimize_png(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_assert!(validate_output(&output, original_data));
|
#[cfg(feature = "sanity-checks")]
|
||||||
|
debug_assert!(sanity_checks::validate_output(&output, original_data));
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
@ -1044,46 +1044,54 @@ fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that the output png data still matches the original image
|
#[cfg(feature = "sanity-checks")]
|
||||||
fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
mod sanity_checks {
|
||||||
let (old_png, new_png) = rayon::join(
|
use super::*;
|
||||||
|| load_png_image_from_memory(original_data),
|
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
||||||
|| load_png_image_from_memory(output),
|
use log::error;
|
||||||
);
|
use std::io::Cursor;
|
||||||
|
|
||||||
match (new_png, old_png) {
|
/// Validate that the output png data still matches the original image
|
||||||
(Err(new_err), _) => {
|
pub(super) fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
||||||
error!("Failed to read output image for validation: {}", new_err);
|
let (old_png, new_png) = rayon::join(
|
||||||
false
|
|| 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),
|
||||||
}
|
}
|
||||||
(_, 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
|
/// Loads a PNG image from memory to a [DynamicImage]
|
||||||
// new image is decodable.
|
fn load_png_image_from_memory(png_data: &[u8]) -> Result<DynamicImage, image::ImageError> {
|
||||||
warn!("Failed to read input image for validation: {}", old_err);
|
let mut reader = image::io::Reader::new(Cursor::new(png_data));
|
||||||
true
|
reader.set_format(ImageFormat::Png);
|
||||||
}
|
reader.no_limits();
|
||||||
(Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png),
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ where
|
||||||
|
|
||||||
impl<I: Iterator> ParallelIterator for I {}
|
impl<I: Iterator> ParallelIterator for I {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
|
||||||
(a(), b())
|
(a(), b())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add callback to allow checks before the output file is deleted again
|
/// Add callback to allow checks before the output file is deleted again
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn test_it_converts_callbacks<CBPRE, CBPOST>(
|
fn test_it_converts_callbacks<CBPRE, CBPOST>(
|
||||||
input: PathBuf,
|
input: PathBuf,
|
||||||
output: &OutFile,
|
output: &OutFile,
|
||||||
|
|
@ -38,8 +39,8 @@ fn test_it_converts_callbacks<CBPRE, CBPOST>(
|
||||||
mut callback_pre: CBPRE,
|
mut callback_pre: CBPRE,
|
||||||
mut callback_post: CBPOST,
|
mut callback_post: CBPOST,
|
||||||
) where
|
) where
|
||||||
CBPOST: FnMut(&Path) -> (),
|
CBPOST: FnMut(&Path),
|
||||||
CBPRE: FnMut(&Path) -> (),
|
CBPRE: FnMut(&Path),
|
||||||
{
|
{
|
||||||
let png = PngData::new(&input, opts.fix_errors).unwrap();
|
let png = PngData::new(&input, opts.fix_errors).unwrap();
|
||||||
|
|
||||||
|
|
@ -48,14 +49,14 @@ fn test_it_converts_callbacks<CBPRE, CBPOST>(
|
||||||
|
|
||||||
callback_pre(&input);
|
callback_pre(&input);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), output, opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(x) => panic!("{}", x),
|
Err(x) => panic!("{}", x),
|
||||||
};
|
};
|
||||||
let output = output.path().unwrap();
|
let output = output.path().unwrap();
|
||||||
assert!(output.exists());
|
assert!(output.exists());
|
||||||
|
|
||||||
callback_post(&output);
|
callback_post(output);
|
||||||
|
|
||||||
let png = match PngData::new(output, opts.fix_errors) {
|
let png = match PngData::new(output, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
|
|
|
||||||
|
|
@ -834,7 +834,7 @@ fn small_files() {
|
||||||
let png = match PngData::new(output, opts.fix_errors) {
|
let png = match PngData::new(output, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
remove_file(&output).ok();
|
remove_file(output).ok();
|
||||||
panic!("{}", x)
|
panic!("{}", x)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -866,7 +866,7 @@ fn palette_should_be_reduced_with_dupes() {
|
||||||
let png = match PngData::new(output, opts.fix_errors) {
|
let png = match PngData::new(output, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
remove_file(&output).ok();
|
remove_file(output).ok();
|
||||||
panic!("{}", x)
|
panic!("{}", x)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -899,7 +899,7 @@ fn palette_should_be_reduced_with_unused() {
|
||||||
let png = match PngData::new(output, opts.fix_errors) {
|
let png = match PngData::new(output, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
remove_file(&output).ok();
|
remove_file(output).ok();
|
||||||
panic!("{}", x)
|
panic!("{}", x)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -932,7 +932,7 @@ fn palette_should_be_reduced_with_both() {
|
||||||
let png = match PngData::new(output, opts.fix_errors) {
|
let png = match PngData::new(output, opts.fix_errors) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => {
|
Err(x) => {
|
||||||
remove_file(&output).ok();
|
remove_file(output).ok();
|
||||||
panic!("{}", x)
|
panic!("{}", x)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue