BC breaks for v10 (#715)
This is a collection of all the BC breaks mentioned in #714, except for 11 which I'm not including for now. Fixes #658. Fixes #660. It might be best to review each commit individually, referencing the notes in #714 and #660 (I just didn't want to create a dozen separate PRs).
This commit is contained in:
parent
2a3aa83928
commit
14532e8bf5
27 changed files with 256 additions and 336 deletions
|
|
@ -76,7 +76,7 @@ All options are case-sensitive.
|
||||||
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
|
\* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
|
||||||
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
|
are almost always better or equal to lower levels, this is not guaranteed and it is possible in
|
||||||
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
|
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
|
||||||
compression (`-Z`) is not guaranteed to always be better than without.
|
compression (`-z`) is not guaranteed to always be better than without.
|
||||||
|
|
||||||
## Git integration via [pre-commit]
|
## Git integration via [pre-commit]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ fn interlacing_16_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -21,7 +21,7 @@ fn interlacing_8_bits(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -31,7 +31,7 @@ fn interlacing_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -41,7 +41,7 @@ fn interlacing_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -51,7 +51,7 @@ fn interlacing_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
|
b.iter(|| png.raw.change_interlacing(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -61,7 +61,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -71,7 +71,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -81,7 +81,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -91,7 +91,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
|
|
@ -101,5 +101,5 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
|
||||||
));
|
));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| png.raw.change_interlacing(Interlacing::None));
|
b.iter(|| png.raw.change_interlacing(false));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,18 @@
|
||||||
extern crate oxipng;
|
extern crate oxipng;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use std::{num::NonZeroU8, path::PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use oxipng::{internal_tests::*, *};
|
use oxipng::{internal_tests::*, *};
|
||||||
use test::Bencher;
|
use test::Bencher;
|
||||||
|
|
||||||
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = NonZeroU8::new(15).unwrap();
|
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
|
fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
|
||||||
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +24,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +36,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +48,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +60,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
|
||||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
|
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
src/cli.rs
47
src/cli.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use std::path::PathBuf;
|
use std::{num::NonZeroU64, path::PathBuf};
|
||||||
|
|
||||||
use clap::{Arg, ArgAction, Command, value_parser};
|
use clap::{Arg, ArgAction, Command, builder::ArgPredicate, value_parser};
|
||||||
|
|
||||||
include!("display_chunks.rs");
|
include!("display_chunks.rs");
|
||||||
|
|
||||||
|
|
@ -97,10 +97,10 @@ Note that this will not preserve the directory structure of the input files when
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("pretend")
|
Arg::new("dry-run")
|
||||||
.help("Do not write any files, only show compression results")
|
.help("Do not write any files, only show compression results")
|
||||||
.short('P')
|
.short('d')
|
||||||
.long("pretend")
|
.long("dry-run")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
@ -163,24 +163,23 @@ transformation and may be unsuitable for some applications.")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
// Note: The default value is not explicitly set here, as it is dependant on the `--nx` flag.
|
|
||||||
Arg::new("interlace")
|
Arg::new("interlace")
|
||||||
.help("Set PNG interlacing type (0, 1, keep) [default: 0]")
|
.help("Set PNG interlacing (off, on, keep)")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Set the PNG interlacing type, where <type> is one of:
|
Set the PNG interlacing mode, where <mode> is one of:
|
||||||
|
|
||||||
0 => Remove interlacing from all images that are processed
|
off => Remove interlacing from all images that are processed
|
||||||
1 => Apply Adam7 interlacing on all images that are processed
|
on => Apply Adam7 interlacing on all images that are processed
|
||||||
keep => Keep the existing interlacing type of each image
|
keep => Keep the existing interlacing mode of each image
|
||||||
|
|
||||||
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
|
Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you \
|
||||||
believe the benefits outweigh the costs for your use case.
|
believe the benefits outweigh the costs for your use case.")
|
||||||
|
|
||||||
[default: 0]")
|
|
||||||
.short('i')
|
.short('i')
|
||||||
.long("interlace")
|
.long("interlace")
|
||||||
.value_name("type")
|
.value_name("mode")
|
||||||
.value_parser(["0", "1", "keep"])
|
.value_parser(["off", "on", "keep", "0", "1"])
|
||||||
|
.default_value("off")
|
||||||
|
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
|
||||||
.hide_possible_values(true),
|
.hide_possible_values(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
@ -325,7 +324,8 @@ be processed successfully.")
|
||||||
.long_help("\
|
.long_help("\
|
||||||
Use the much slower but stronger Zopfli compressor for main compression trials. \
|
Use the much slower but stronger Zopfli compressor for main compression trials. \
|
||||||
Recommended use is with '-o max' and '--fast'.")
|
Recommended use is with '-o max' and '--fast'.")
|
||||||
.short('Z')
|
.short('z')
|
||||||
|
.short_alias('Z') // Kept for backwards compatibility
|
||||||
.long("zopfli")
|
.long("zopfli")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
|
@ -338,7 +338,18 @@ speed up compression for large files. This option requires '--zopfli' to be set.
|
||||||
.long("zi")
|
.long("zi")
|
||||||
.value_name("iterations")
|
.value_name("iterations")
|
||||||
.default_value("15")
|
.default_value("15")
|
||||||
.value_parser(1..=255)
|
.value_parser(value_parser!(NonZeroU64))
|
||||||
|
.requires("zopfli"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("iterations-without-improvement")
|
||||||
|
.hide_short_help(true)
|
||||||
|
.long_help("\
|
||||||
|
Stop Zopfli compression after this number of iterations without improvement. Use this in \
|
||||||
|
conjunction with a high value for '--zi' to achieve better compression in reasonable time.")
|
||||||
|
.long("ziwi")
|
||||||
|
.value_name("iterations")
|
||||||
|
.value_parser(value_parser!(NonZeroU64))
|
||||||
.requires("zopfli"),
|
.requires("zopfli"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ impl TryFrom<u8> for BitDepth {
|
||||||
4 => Ok(Self::Four),
|
4 => Ok(Self::Four),
|
||||||
8 => Ok(Self::Eight),
|
8 => Ok(Self::Eight),
|
||||||
16 => Ok(Self::Sixteen),
|
16 => Ok(Self::Sixteen),
|
||||||
_ => Err(PngError::new("Unexpected bit depth")),
|
_ => Err(PngError::InvalidData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
|
||||||
.zlib_decompress(data, &mut dest)
|
.zlib_decompress(data, &mut dest)
|
||||||
.map_err(|err| match err {
|
.map_err(|err| match err {
|
||||||
DecompressionError::BadData => PngError::InvalidData,
|
DecompressionError::BadData => PngError::InvalidData,
|
||||||
DecompressionError::InsufficientSpace => PngError::new("inflated data too long"),
|
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
|
||||||
})?;
|
})?;
|
||||||
dest.truncate(len);
|
dest.truncate(len);
|
||||||
Ok(dest)
|
Ok(dest)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
mod deflater;
|
mod deflater;
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
use std::num::NonZeroU8;
|
|
||||||
use std::{fmt, fmt::Display};
|
|
||||||
|
|
||||||
pub use deflater::{crc32, deflate, inflate};
|
pub use deflater::{crc32, deflate, inflate};
|
||||||
|
|
||||||
use crate::{PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
use std::{fmt, fmt::Display};
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
mod zopfli_oxipng;
|
mod zopfli_oxipng;
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
|
pub use zopfli::Options as ZopfliOptions;
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
pub use zopfli_oxipng::deflate as zopfli_deflate;
|
||||||
|
|
||||||
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
|
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Deflaters {
|
pub enum Deflater {
|
||||||
/// Use libdeflater.
|
/// Use libdeflater.
|
||||||
Libdeflater {
|
Libdeflater {
|
||||||
/// Which compression level to use on the file (0-12)
|
/// Which compression level to use on the file (0-12)
|
||||||
|
|
@ -21,20 +21,15 @@ pub enum Deflaters {
|
||||||
},
|
},
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
/// Use the better but slower Zopfli implementation
|
/// Use the better but slower Zopfli implementation
|
||||||
Zopfli {
|
Zopfli(ZopfliOptions),
|
||||||
/// The number of compression iterations to do. 15 iterations are fine
|
|
||||||
/// for small files, but bigger files will need to be compressed with
|
|
||||||
/// less iterations, or else they will be too slow.
|
|
||||||
iterations: NonZeroU8,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deflaters {
|
impl Deflater {
|
||||||
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
|
||||||
let compressed = match self {
|
let compressed = match self {
|
||||||
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
|
Self::Zopfli(options) => zopfli_deflate(data, options)?,
|
||||||
};
|
};
|
||||||
if let Some(max) = max_size {
|
if let Some(max) = max_size {
|
||||||
if compressed.len() > max {
|
if compressed.len() > max {
|
||||||
|
|
@ -45,13 +40,13 @@ impl Deflaters {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Deflaters {
|
impl Display for Deflater {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
|
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
Self::Zopfli { iterations } => write!(f, "zopfli, zi = {iterations}"),
|
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
use std::num::NonZeroU8;
|
|
||||||
|
|
||||||
use crate::{PngError, PngResult};
|
use crate::{PngError, PngResult};
|
||||||
|
|
||||||
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> {
|
pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
|
||||||
let mut output = Vec::with_capacity(data.len());
|
let mut output = Vec::with_capacity(data.len());
|
||||||
let options = zopfli::Options {
|
|
||||||
iteration_count: iterations.into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
|
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
|
||||||
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
|
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
|
||||||
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
|
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {
|
||||||
|
|
|
||||||
56
src/error.rs
56
src/error.rs
|
|
@ -2,20 +2,22 @@ use std::{error::Error, fmt};
|
||||||
|
|
||||||
use crate::colors::{BitDepth, ColorType};
|
use crate::colors::{BitDepth, ColorType};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum PngError {
|
pub enum PngError {
|
||||||
DeflatedDataTooLong(usize),
|
|
||||||
TimedOut,
|
|
||||||
NotPNG,
|
|
||||||
APNGNotSupported,
|
|
||||||
APNGOutOfOrder,
|
APNGOutOfOrder,
|
||||||
InvalidData,
|
|
||||||
TruncatedData,
|
|
||||||
ChunkMissing(&'static str),
|
|
||||||
InvalidDepthForType(BitDepth, ColorType),
|
|
||||||
IncorrectDataLength(usize, usize),
|
|
||||||
C2PAMetadataPreventsChanges,
|
C2PAMetadataPreventsChanges,
|
||||||
|
ChunkMissing(&'static str),
|
||||||
|
CRCMismatch([u8; 4]),
|
||||||
|
DeflatedDataTooLong(usize),
|
||||||
|
IncorrectDataLength(usize, usize),
|
||||||
|
InflatedDataTooLong(usize),
|
||||||
|
InvalidData,
|
||||||
|
InvalidDepthForType(BitDepth, ColorType),
|
||||||
|
NotPNG,
|
||||||
|
ReadFailed(String, std::io::Error),
|
||||||
|
TruncatedData,
|
||||||
|
WriteFailed(String, std::io::Error),
|
||||||
Other(Box<str>),
|
Other(Box<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,26 +28,32 @@ impl fmt::Display for PngError {
|
||||||
#[cold]
|
#[cold]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
|
|
||||||
PngError::TimedOut => f.write_str("timed out"),
|
|
||||||
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
|
|
||||||
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
|
|
||||||
PngError::TruncatedData => {
|
|
||||||
f.write_str("Missing data in the file; the file is truncated")
|
|
||||||
}
|
|
||||||
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
|
||||||
PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
|
PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
|
||||||
|
PngError::C2PAMetadataPreventsChanges => f.write_str(
|
||||||
|
"The image contains C2PA manifest that would be invalidated by any file changes",
|
||||||
|
),
|
||||||
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
|
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
|
||||||
PngError::InvalidDepthForType(d, ref c) => {
|
PngError::CRCMismatch(ref c) => write!(
|
||||||
write!(f, "Invalid bit depth {d} for color type {c}")
|
f,
|
||||||
}
|
"CRC mismatch in {} chunk; May be recoverable by using --fix",
|
||||||
|
String::from_utf8_lossy(c)
|
||||||
|
),
|
||||||
|
PngError::DeflatedDataTooLong(_) => f.write_str("Deflated data too long"),
|
||||||
PngError::IncorrectDataLength(l1, l2) => write!(
|
PngError::IncorrectDataLength(l1, l2) => write!(
|
||||||
f,
|
f,
|
||||||
"Data length {l1} does not match the expected length {l2}"
|
"Data length {l1} does not match the expected length {l2}"
|
||||||
),
|
),
|
||||||
PngError::C2PAMetadataPreventsChanges => f.write_str(
|
PngError::InflatedDataTooLong(_) => f.write_str("Inflated data too long"),
|
||||||
"The image contains C2PA manifest that would be invalidated by any file changes",
|
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
|
||||||
),
|
PngError::InvalidDepthForType(d, ref c) => {
|
||||||
|
write!(f, "Invalid bit depth {d} for color type {c}")
|
||||||
|
}
|
||||||
|
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
|
||||||
|
PngError::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
|
||||||
|
PngError::TruncatedData => {
|
||||||
|
f.write_str("Missing data in the file; the file is truncated")
|
||||||
|
}
|
||||||
|
PngError::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
|
||||||
PngError::Other(ref s) => f.write_str(s),
|
PngError::Other(ref s) => f.write_str(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use std::sync::{
|
||||||
|
|
||||||
#[cfg(feature = "parallel")]
|
#[cfg(feature = "parallel")]
|
||||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||||
use deflate::Deflaters;
|
use deflate::Deflater;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
@ -50,7 +50,7 @@ impl Candidate {
|
||||||
pub(crate) struct Evaluator {
|
pub(crate) struct Evaluator {
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<FilterStrategy>,
|
filters: IndexSet<FilterStrategy>,
|
||||||
deflater: Deflaters,
|
deflater: Deflater,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
final_round: bool,
|
final_round: bool,
|
||||||
nth: AtomicUsize,
|
nth: AtomicUsize,
|
||||||
|
|
@ -68,7 +68,7 @@ impl Evaluator {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
deadline: Arc<Deadline>,
|
deadline: Arc<Deadline>,
|
||||||
filters: IndexSet<FilterStrategy>,
|
filters: IndexSet<FilterStrategy>,
|
||||||
deflater: Deflaters,
|
deflater: Deflater,
|
||||||
optimize_alpha: bool,
|
optimize_alpha: bool,
|
||||||
final_round: bool,
|
final_round: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ use log::{debug, trace, warn};
|
||||||
use rgb::{RGB16, RGBA8};
|
use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Deflaters, Options, PngResult,
|
Deflater, Options, PngResult,
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate::{crc32, inflate},
|
deflate::{crc32, inflate},
|
||||||
display_chunks::DISPLAY_CHUNKS,
|
display_chunks::DISPLAY_CHUNKS,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
interlace::Interlacing,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -22,8 +21,8 @@ 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 interlacing mode of the image
|
/// Whether the image is interlaced
|
||||||
pub interlaced: Interlacing,
|
pub interlaced: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IhdrData {
|
impl IhdrData {
|
||||||
|
|
@ -45,7 +44,7 @@ impl IhdrData {
|
||||||
(w * bpp).div_ceil(8) * h
|
(w * bpp).div_ceil(8) * h
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.interlaced == Interlacing::None {
|
if !self.interlaced {
|
||||||
bitmap_size(bpp, w, h) + h
|
bitmap_size(bpp, w, h) + h
|
||||||
} else {
|
} else {
|
||||||
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
|
||||||
|
|
@ -173,10 +172,7 @@ pub fn parse_next_chunk<'a>(
|
||||||
|
|
||||||
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
|
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
|
||||||
if !fix_errors && crc32(chunk_bytes) != crc {
|
if !fix_errors && crc32(chunk_bytes) != crc {
|
||||||
return Err(PngError::new(&format!(
|
return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
|
||||||
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
|
|
||||||
String::from_utf8_lossy(chunk_name)
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let name: [u8; 4] = chunk_name.try_into().unwrap();
|
let name: [u8; 4] = chunk_name.try_into().unwrap();
|
||||||
|
|
@ -209,12 +205,16 @@ pub fn parse_ihdr_chunk(
|
||||||
},
|
},
|
||||||
4 => ColorType::GrayscaleAlpha,
|
4 => ColorType::GrayscaleAlpha,
|
||||||
6 => ColorType::RGBA,
|
6 => ColorType::RGBA,
|
||||||
_ => return Err(PngError::new("Unexpected color type in header")),
|
_ => return Err(PngError::InvalidData),
|
||||||
},
|
},
|
||||||
bit_depth: byte_data[8].try_into()?,
|
bit_depth: byte_data[8].try_into()?,
|
||||||
width: read_be_u32(&byte_data[0..4]),
|
width: read_be_u32(&byte_data[0..4]),
|
||||||
height: read_be_u32(&byte_data[4..8]),
|
height: read_be_u32(&byte_data[4..8]),
|
||||||
interlaced: interlaced.try_into()?,
|
interlaced: match interlaced {
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => return Err(PngError::InvalidData),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ fn palette_to_rgba(
|
||||||
palette_data: Option<Vec<u8>>,
|
palette_data: Option<Vec<u8>>,
|
||||||
trns_data: Option<Vec<u8>>,
|
trns_data: Option<Vec<u8>>,
|
||||||
) -> Result<Vec<RGBA8>, PngError> {
|
) -> Result<Vec<RGBA8>, PngError> {
|
||||||
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?;
|
let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
|
||||||
let mut palette: Vec<_> = palette_data
|
let mut palette: Vec<_> = palette_data
|
||||||
.chunks_exact(3)
|
.chunks_exact(3)
|
||||||
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
||||||
|
|
@ -276,7 +276,7 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make an iCCP chunk by compressing the ICC profile
|
/// Make an iCCP chunk by compressing the ICC profile
|
||||||
pub fn make_iccp(icc: &[u8], deflater: Deflaters, max_size: Option<usize>) -> PngResult<Chunk> {
|
pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
|
||||||
let mut compressed = deflater.deflate(icc, max_size)?;
|
let mut compressed = deflater.deflate(icc, max_size)?;
|
||||||
let mut data = Vec::with_capacity(compressed.len() + 5);
|
let mut data = Vec::with_capacity(compressed.len() + 5);
|
||||||
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
data.extend(b"icc"); // Profile name - generally unused, can be anything
|
||||||
|
|
@ -348,7 +348,7 @@ pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
|
||||||
} else if opts.idat_recoding {
|
} else if opts.idat_recoding {
|
||||||
// Try recompressing the profile
|
// Try recompressing the profile
|
||||||
let cur_len = aux_chunks[iccp_idx].data.len();
|
let cur_len = aux_chunks[iccp_idx].data.len();
|
||||||
if let Ok(iccp) = make_iccp(&icc, opts.deflate, Some(cur_len - 1)) {
|
if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
|
||||||
debug!(
|
debug!(
|
||||||
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
"Recompressed iCCP chunk: {} ({} bytes decrease)",
|
||||||
iccp.data.len(),
|
iccp.data.len(),
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,6 @@
|
||||||
use std::{fmt, fmt::Display};
|
|
||||||
|
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
|
|
||||||
use crate::{PngError, headers::IhdrData, png::PngImage};
|
use crate::{headers::IhdrData, png::PngImage};
|
||||||
|
|
||||||
/// Whether to enable progressive rendering. See [`Options`][crate::Options])
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum Interlacing {
|
|
||||||
/// Makes images load top to bottom.
|
|
||||||
None,
|
|
||||||
/// Makes it possible to render partially-loaded images at lower resolution. Usually increases file sizes.
|
|
||||||
Adam7,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u8> for Interlacing {
|
|
||||||
type Error = PngError;
|
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(Self::None),
|
|
||||||
1 => Ok(Self::Adam7),
|
|
||||||
_ => Err(PngError::new("Unexpected interlacing in header")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Interlacing {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
Display::fmt(
|
|
||||||
match self {
|
|
||||||
Self::None => "non-interlaced",
|
|
||||||
Self::Adam7 => "interlaced",
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn interlace_image(png: &PngImage) -> PngImage {
|
pub fn interlace_image(png: &PngImage) -> PngImage {
|
||||||
|
|
@ -89,7 +53,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
|
||||||
data: output,
|
data: output,
|
||||||
ihdr: IhdrData {
|
ihdr: IhdrData {
|
||||||
color_type: png.ihdr.color_type.clone(),
|
color_type: png.ihdr.color_type.clone(),
|
||||||
interlaced: Interlacing::Adam7,
|
interlaced: true,
|
||||||
..png.ihdr
|
..png.ihdr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +67,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
|
||||||
},
|
},
|
||||||
ihdr: IhdrData {
|
ihdr: IhdrData {
|
||||||
color_type: png.ihdr.color_type.clone(),
|
color_type: png.ihdr.color_type.clone(),
|
||||||
interlaced: Interlacing::None,
|
interlaced: false,
|
||||||
..png.ihdr
|
..png.ihdr
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
src/lib.rs
90
src/lib.rs
|
|
@ -41,13 +41,14 @@ use log::{debug, info, trace, warn};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
pub use rgb::{RGB16, RGBA8};
|
pub use rgb::{RGB16, RGBA8};
|
||||||
|
|
||||||
|
#[cfg(feature = "zopfli")]
|
||||||
|
pub use crate::deflate::ZopfliOptions;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate::Deflaters,
|
deflate::Deflater,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
filters::{FilterStrategy, RowFilter},
|
filters::{FilterStrategy, RowFilter},
|
||||||
headers::StripChunks,
|
headers::StripChunks,
|
||||||
interlace::Interlacing,
|
|
||||||
options::{InFile, Options, OutFile},
|
options::{InFile, Options, OutFile},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -130,7 +131,7 @@ impl RawImage {
|
||||||
height,
|
height,
|
||||||
color_type,
|
color_type,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
interlaced: Interlacing::None,
|
interlaced: false,
|
||||||
},
|
},
|
||||||
data,
|
data,
|
||||||
}),
|
}),
|
||||||
|
|
@ -146,7 +147,7 @@ impl RawImage {
|
||||||
/// Add an ICC profile for the image
|
/// Add an ICC profile for the image
|
||||||
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
pub fn add_icc_profile(&mut self, data: &[u8]) {
|
||||||
// Compress with fastest compression level - will be recompressed during optimization
|
// Compress with fastest compression level - will be recompressed during optimization
|
||||||
let deflater = Deflaters::Libdeflater { compression: 1 };
|
let deflater = Deflater::Libdeflater { compression: 1 };
|
||||||
if let Ok(iccp) = make_iccp(data, deflater, None) {
|
if let Ok(iccp) = make_iccp(data, deflater, None) {
|
||||||
self.aux_chunks.push(iccp);
|
self.aux_chunks.push(iccp);
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +182,9 @@ impl RawImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<()> {
|
///
|
||||||
|
/// Returns the original and optimized file sizes
|
||||||
|
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> {
|
||||||
// Read in the file and try to decode as PNG.
|
// Read in the file and try to decode as PNG.
|
||||||
info!("Processing: {input}");
|
info!("Processing: {input}");
|
||||||
|
|
||||||
|
|
@ -218,7 +221,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
stdin()
|
stdin()
|
||||||
.read_to_end(&mut data)
|
.read_to_end(&mut data)
|
||||||
.map_err(|e| PngError::new(&format!("Error reading stdin: {e}")))?;
|
.map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -230,14 +233,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
|
|
||||||
let in_length = in_data.len();
|
let in_length = in_data.len();
|
||||||
|
|
||||||
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) {
|
if is_fully_optimized(in_length, optimized_output.len(), opts) {
|
||||||
match (output, input) {
|
match (output, input) {
|
||||||
// If output path is None, it also means same as the input path
|
// If output path is None, it also means same as the input path
|
||||||
(OutFile::Path { path, .. }, InFile::Path(input_path))
|
(OutFile::Path { path, .. }, InFile::Path(input_path))
|
||||||
if path.as_ref().is_none_or(|p| p == input_path) =>
|
if path.as_ref().is_none_or(|p| p == input_path) =>
|
||||||
{
|
{
|
||||||
info!("{input}: Could not optimize further, no change written");
|
info!("{input}: Could not optimize further, no change written");
|
||||||
return Ok(());
|
return Ok((in_length, in_length));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
optimized_output = in_data;
|
optimized_output = in_data;
|
||||||
|
|
@ -261,26 +264,21 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
|
|
||||||
match (output, input) {
|
match (output, input) {
|
||||||
(OutFile::None, _) => {
|
(OutFile::None, _) => {
|
||||||
info!("{savings}: Running in pretend mode, no output");
|
info!("{savings}: Dry run, no output");
|
||||||
}
|
}
|
||||||
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
|
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
|
||||||
let mut buffer = BufWriter::new(stdout());
|
let mut buffer = BufWriter::new(stdout());
|
||||||
buffer
|
buffer
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
.map_err(|e| PngError::new(&format!("Unable to write to stdout: {e}")))?;
|
.map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
|
||||||
}
|
}
|
||||||
(OutFile::Path { path, .. }, _) => {
|
(OutFile::Path { path, .. }, _) => {
|
||||||
let output_path = path
|
let output_path = path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.as_path())
|
.map(|p| p.as_path())
|
||||||
.unwrap_or_else(|| input.path().unwrap());
|
.unwrap_or_else(|| input.path().unwrap());
|
||||||
let out_file = File::create(output_path).map_err(|err| {
|
let out_file = File::create(output_path)
|
||||||
PngError::new(&format!(
|
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
|
||||||
"Unable to write to file {}: {}",
|
|
||||||
output_path.display(),
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
if let Some(metadata_input) = &opt_metadata_preserved {
|
||||||
copy_permissions(metadata_input, &out_file)?;
|
copy_permissions(metadata_input, &out_file)?;
|
||||||
}
|
}
|
||||||
|
|
@ -290,13 +288,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
.write_all(&optimized_output)
|
.write_all(&optimized_output)
|
||||||
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
|
||||||
.and_then(|()| buffer.flush())
|
.and_then(|()| buffer.flush())
|
||||||
.map_err(|e| {
|
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
|
||||||
PngError::new(&format!(
|
|
||||||
"Unable to write to {}: {}",
|
|
||||||
output_path.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
// force drop and thereby closing of file handle before modifying any timestamp
|
// force drop and thereby closing of file handle before modifying any timestamp
|
||||||
std::mem::drop(buffer);
|
std::mem::drop(buffer);
|
||||||
if let Some(metadata_input) = &opt_metadata_preserved {
|
if let Some(metadata_input) = &opt_metadata_preserved {
|
||||||
|
|
@ -305,7 +297,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
|
||||||
info!("{}: {}", savings, output_path.display());
|
info!("{}: {}", savings, output_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok((in_length, optimized_output.len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform optimization on the input file using the options provided, where the file is already
|
/// Perform optimization on the input file using the options provided, where the file is already
|
||||||
|
|
@ -395,7 +387,7 @@ fn optimize_png(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.interlace == Some(Interlacing::Adam7) && png.raw.ihdr.interlaced != Interlacing::Adam7 {
|
if opts.interlace == Some(true) && !png.raw.ihdr.interlaced {
|
||||||
warn!(
|
warn!(
|
||||||
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
|
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
|
||||||
);
|
);
|
||||||
|
|
@ -420,16 +412,16 @@ fn optimize_raw(
|
||||||
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
|
||||||
// 9 is not appreciably better than 8
|
// 9 is not appreciably better than 8
|
||||||
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
|
||||||
let compression = match opts.deflate {
|
let compression = match opts.deflater {
|
||||||
Deflaters::Libdeflater { compression } => {
|
Deflater::Libdeflater { compression } => {
|
||||||
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
|
||||||
}
|
}
|
||||||
_ => 8,
|
_ => 8,
|
||||||
};
|
};
|
||||||
let eval_deflater = Deflaters::Libdeflater { compression };
|
let eval_deflater = Deflater::Libdeflater { compression };
|
||||||
// If only one filter is selected, use this for evaluations
|
// If only one filter is selected, use this for evaluations
|
||||||
let eval_filters = if opts.filter.len() == 1 {
|
let eval_filters = if opts.filters.len() == 1 {
|
||||||
opts.filter.clone()
|
opts.filters.clone()
|
||||||
} else {
|
} else {
|
||||||
// None and Bigrams work well together, especially for alpha reductions
|
// None and Bigrams work well together, especially for alpha reductions
|
||||||
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
|
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
|
||||||
|
|
@ -440,7 +432,7 @@ fn optimize_raw(
|
||||||
eval_filters.clone(),
|
eval_filters.clone(),
|
||||||
eval_deflater,
|
eval_deflater,
|
||||||
false,
|
false,
|
||||||
opts.deflate == eval_deflater,
|
opts.deflater == eval_deflater,
|
||||||
);
|
);
|
||||||
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
|
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
|
||||||
let eval_result = eval.get_best_candidate();
|
let eval_result = eval.get_best_candidate();
|
||||||
|
|
@ -465,7 +457,7 @@ fn optimize_raw(
|
||||||
eval_filters,
|
eval_filters,
|
||||||
eval_deflater,
|
eval_deflater,
|
||||||
);
|
);
|
||||||
(result?, opts.deflate)
|
(result?, opts.deflater)
|
||||||
} else {
|
} else {
|
||||||
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
// 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.
|
// we should still check if the evaluator compressed the baseline smaller than the original.
|
||||||
|
|
@ -490,9 +482,9 @@ fn perform_trials(
|
||||||
max_size: Option<usize>,
|
max_size: Option<usize>,
|
||||||
mut eval_result: Option<Candidate>,
|
mut eval_result: Option<Candidate>,
|
||||||
eval_filters: IndexSet<FilterStrategy>,
|
eval_filters: IndexSet<FilterStrategy>,
|
||||||
eval_deflater: Deflaters,
|
eval_deflater: Deflater,
|
||||||
) -> Option<Candidate> {
|
) -> Option<Candidate> {
|
||||||
let mut filters = opts.filter.clone();
|
let mut filters = opts.filters.clone();
|
||||||
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
|
||||||
if fast_eval {
|
if fast_eval {
|
||||||
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
// Perform a fast evaluation of selected filters followed by a single main compression trial
|
||||||
|
|
@ -509,7 +501,7 @@ fn perform_trials(
|
||||||
filters,
|
filters,
|
||||||
eval_deflater,
|
eval_deflater,
|
||||||
opts.optimize_alpha,
|
opts.optimize_alpha,
|
||||||
opts.deflate == eval_deflater,
|
opts.deflater == eval_deflater,
|
||||||
);
|
);
|
||||||
if let Some(result) = &eval_result {
|
if let Some(result) = &eval_result {
|
||||||
eval.set_best_size(result.estimated_output_size);
|
eval.set_best_size(result.estimated_output_size);
|
||||||
|
|
@ -525,9 +517,9 @@ fn perform_trials(
|
||||||
|
|
||||||
if result.idat_data.is_none() {
|
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.deflater);
|
||||||
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
|
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
|
||||||
match opts.deflate.deflate(&data, max_size) {
|
match opts.deflater.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.idat_data = Some(idat_data);
|
result.idat_data = Some(idat_data);
|
||||||
|
|
@ -555,8 +547,8 @@ fn perform_trials(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Trying {} filters with {}", filters.len(), opts.deflate);
|
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
|
||||||
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha, true);
|
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
|
||||||
if let Some(max_size) = max_size {
|
if let Some(max_size) = max_size {
|
||||||
eval.set_best_size(max_size);
|
eval.set_best_size(max_size);
|
||||||
}
|
}
|
||||||
|
|
@ -616,9 +608,14 @@ impl Deadline {
|
||||||
|
|
||||||
/// Display the format of the image data
|
/// Display the format of the image data
|
||||||
fn report_format(prefix: &str, png: &PngImage) {
|
fn report_format(prefix: &str, png: &PngImage) {
|
||||||
|
let interlaced = if png.ihdr.interlaced {
|
||||||
|
"interlaced"
|
||||||
|
} else {
|
||||||
|
"non-interlaced"
|
||||||
|
};
|
||||||
debug!(
|
debug!(
|
||||||
"{}{}-bit {}, {}",
|
"{}{}-bit {}, {}",
|
||||||
prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced
|
prefix, png.ihdr.bit_depth, png.ihdr.color_type, interlaced
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -648,7 +645,7 @@ fn recompress_frames(
|
||||||
let image = PngImage::new(ihdr, &frame.data)?;
|
let image = PngImage::new(ihdr, &frame.data)?;
|
||||||
let (filtered, _) = image.filter_image(filter.clone(), 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.deflater.deflate(&filtered, max_size) {
|
||||||
debug!(
|
debug!(
|
||||||
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
||||||
i,
|
i,
|
||||||
|
|
@ -671,7 +668,7 @@ fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()>
|
||||||
.set_permissions(metadata_input.permissions())
|
.set_permissions(metadata_input.permissions())
|
||||||
.map_err(|err_io| {
|
.map_err(|err_io| {
|
||||||
PngError::new(&format!(
|
PngError::new(&format!(
|
||||||
"unable to set permissions for output file: {err_io}"
|
"Unable to set permissions for output file: {err_io}"
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -683,12 +680,11 @@ fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
|
||||||
|
|
||||||
#[cfg(feature = "filetime")]
|
#[cfg(feature = "filetime")]
|
||||||
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
|
||||||
let atime = filetime::FileTime::from_last_access_time(input_path_meta);
|
|
||||||
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
|
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
|
||||||
trace!("attempting to set file times: atime: {atime:?}, mtime: {mtime:?}");
|
trace!("attempting to set file modification time: {mtime:?}");
|
||||||
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| {
|
filetime::set_file_mtime(out_path, mtime).map_err(|err_io| {
|
||||||
PngError::new(&format!(
|
PngError::new(&format!(
|
||||||
"unable to set file times on {out_path:?}: {err_io}"
|
"Unable to set file times on {out_path:?}: {err_io}"
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -17,7 +17,7 @@
|
||||||
mod rayon;
|
mod rayon;
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
use std::num::NonZeroU8;
|
use std::num::NonZeroU64;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::ExitCode, time::Duration,
|
ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::ExitCode, time::Duration,
|
||||||
};
|
};
|
||||||
|
|
@ -26,7 +26,9 @@ use clap::ArgMatches;
|
||||||
mod cli;
|
mod cli;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::{Level, LevelFilter, error, warn};
|
use log::{Level, LevelFilter, error, warn};
|
||||||
use oxipng::{Deflaters, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks};
|
#[cfg(feature = "zopfli")]
|
||||||
|
use oxipng::ZopfliOptions;
|
||||||
|
use oxipng::{Deflater, FilterStrategy, InFile, Options, OutFile, PngError, StripChunks};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::cli::DISPLAY_CHUNKS;
|
use crate::cli::DISPLAY_CHUNKS;
|
||||||
|
|
@ -198,7 +200,7 @@ fn parse_opts_into_struct(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
|
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
|
||||||
opts.filter = x
|
opts.filters = x
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&f| match f {
|
.map(|&f| match f {
|
||||||
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
|
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
|
||||||
|
|
@ -233,7 +235,7 @@ fn parse_opts_into_struct(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let out_file = if matches.get_flag("pretend") {
|
let out_file = if matches.get_flag("dry-run") {
|
||||||
OutFile::None
|
OutFile::None
|
||||||
} else if matches.get_flag("stdout") {
|
} else if matches.get_flag("stdout") {
|
||||||
OutFile::StdOut
|
OutFile::StdOut
|
||||||
|
|
@ -276,10 +278,10 @@ fn parse_opts_into_struct(
|
||||||
opts.idat_recoding = !matches.get_flag("no-recoding");
|
opts.idat_recoding = !matches.get_flag("no-recoding");
|
||||||
|
|
||||||
if let Some(x) = matches.get_one::<String>("interlace") {
|
if let Some(x) = matches.get_one::<String>("interlace") {
|
||||||
opts.interlace = if x == "keep" {
|
opts.interlace = match x.as_str() {
|
||||||
None
|
"off" | "0" => Some(false),
|
||||||
} else {
|
"on" | "1" => Some(true),
|
||||||
x.parse::<u8>().unwrap().try_into().ok()
|
_ => None, // keep
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,13 +337,18 @@ fn parse_opts_into_struct(
|
||||||
|
|
||||||
#[cfg(feature = "zopfli")]
|
#[cfg(feature = "zopfli")]
|
||||||
if matches.get_flag("zopfli") {
|
if matches.get_flag("zopfli") {
|
||||||
let iterations = *matches.get_one::<i64>("iterations").unwrap();
|
let iteration_count = *matches.get_one::<NonZeroU64>("iterations").unwrap();
|
||||||
opts.deflate = Deflaters::Zopfli {
|
let iterations_without_improvement = *matches
|
||||||
iterations: NonZeroU8::new(iterations as u8).unwrap(),
|
.get_one::<NonZeroU64>("iterations-without-improvement")
|
||||||
};
|
.unwrap_or(&NonZeroU64::MAX);
|
||||||
|
opts.deflater = Deflater::Zopfli(ZopfliOptions {
|
||||||
|
iteration_count,
|
||||||
|
iterations_without_improvement,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let (Deflaters::Libdeflater { compression }, Some(x)) =
|
if let (Deflater::Libdeflater { compression }, Some(x)) =
|
||||||
(&mut opts.deflate, matches.get_one::<i64>("compression"))
|
(&mut opts.deflater, matches.get_one::<i64>("compression"))
|
||||||
{
|
{
|
||||||
*compression = *x as u8;
|
*compression = *x as u8;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ use std::{
|
||||||
use indexmap::{IndexSet, indexset};
|
use indexmap::{IndexSet, indexset};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
|
||||||
deflate::Deflaters, filters::FilterStrategy, headers::StripChunks, interlace::Interlacing,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Write destination for [`optimize`][crate::optimize].
|
/// Write destination for [`optimize`][crate::optimize].
|
||||||
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
|
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
|
||||||
|
|
@ -99,16 +97,14 @@ pub struct Options {
|
||||||
/// Which `FilterStrategy` to try on the file
|
/// Which `FilterStrategy` to try on the file
|
||||||
///
|
///
|
||||||
/// Default: `None,Sub,Entropy,Bigrams`
|
/// Default: `None,Sub,Entropy,Bigrams`
|
||||||
pub filter: IndexSet<FilterStrategy>,
|
pub filters: IndexSet<FilterStrategy>,
|
||||||
/// Whether to change the interlacing type of the file.
|
/// Whether to change the interlacing of the file.
|
||||||
///
|
///
|
||||||
/// These are the interlacing types avaliable:
|
/// - `None` will not change the current interlacing.
|
||||||
/// - `None` will not change the current interlacing type.
|
/// - `Some(x)` will turn interlacing on or off.
|
||||||
/// - `Some(x)` will change the file to interlacing mode `x`.
|
|
||||||
/// See [`Interlacing`] for the possible interlacing types.
|
|
||||||
///
|
///
|
||||||
/// Default: `Some(Interlacing::None)`
|
/// Default: `Some(false)`
|
||||||
pub interlace: Option<Interlacing>,
|
pub interlace: Option<bool>,
|
||||||
/// Whether to allow transparent pixels to be altered to improve compression.
|
/// Whether to allow transparent pixels to be altered to improve compression.
|
||||||
///
|
///
|
||||||
/// Default: `false`
|
/// Default: `false`
|
||||||
|
|
@ -148,7 +144,7 @@ pub struct Options {
|
||||||
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
|
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
|
||||||
///
|
///
|
||||||
/// Default: `Libdeflater`
|
/// Default: `Libdeflater`
|
||||||
pub deflate: Deflaters,
|
pub deflater: Deflater,
|
||||||
/// Whether to use fast evaluation to pick the best filter
|
/// Whether to use fast evaluation to pick the best filter
|
||||||
///
|
///
|
||||||
/// Default: `true`
|
/// Default: `true`
|
||||||
|
|
@ -187,16 +183,16 @@ impl Options {
|
||||||
// The following methods make assumptions that they are operating
|
// The following methods make assumptions that they are operating
|
||||||
// on an `Options` struct generated by the `default` method.
|
// on an `Options` struct generated by the `default` method.
|
||||||
fn apply_preset_0(mut self) -> Self {
|
fn apply_preset_0(mut self) -> Self {
|
||||||
self.filter.clear();
|
self.filters.clear();
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
if let Deflater::Libdeflater { compression } = &mut self.deflater {
|
||||||
*compression = 5;
|
*compression = 5;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_1(mut self) -> Self {
|
fn apply_preset_1(mut self) -> Self {
|
||||||
self.filter.clear();
|
self.filters.clear();
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
if let Deflater::Libdeflater { compression } = &mut self.deflater {
|
||||||
*compression = 10;
|
*compression = 10;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
|
|
@ -208,7 +204,7 @@ impl Options {
|
||||||
|
|
||||||
fn apply_preset_3(mut self) -> Self {
|
fn apply_preset_3(mut self) -> Self {
|
||||||
self.fast_evaluation = false;
|
self.fast_evaluation = false;
|
||||||
self.filter = indexset! {
|
self.filters = indexset! {
|
||||||
FilterStrategy::NONE,
|
FilterStrategy::NONE,
|
||||||
FilterStrategy::Bigrams,
|
FilterStrategy::Bigrams,
|
||||||
FilterStrategy::BigEnt,
|
FilterStrategy::BigEnt,
|
||||||
|
|
@ -218,7 +214,7 @@ impl Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_4(mut self) -> Self {
|
fn apply_preset_4(mut self) -> Self {
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
if let Deflater::Libdeflater { compression } = &mut self.deflater {
|
||||||
*compression = 12;
|
*compression = 12;
|
||||||
}
|
}
|
||||||
self.apply_preset_3()
|
self.apply_preset_3()
|
||||||
|
|
@ -226,19 +222,19 @@ impl Options {
|
||||||
|
|
||||||
fn apply_preset_5(mut self) -> Self {
|
fn apply_preset_5(mut self) -> Self {
|
||||||
self.fast_evaluation = false;
|
self.fast_evaluation = false;
|
||||||
self.filter.insert(FilterStrategy::UP);
|
self.filters.insert(FilterStrategy::UP);
|
||||||
self.filter.insert(FilterStrategy::MinSum);
|
self.filters.insert(FilterStrategy::MinSum);
|
||||||
self.filter.insert(FilterStrategy::BigEnt);
|
self.filters.insert(FilterStrategy::BigEnt);
|
||||||
self.filter.insert(FilterStrategy::Brute);
|
self.filters.insert(FilterStrategy::Brute);
|
||||||
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
|
if let Deflater::Libdeflater { compression } = &mut self.deflater {
|
||||||
*compression = 12;
|
*compression = 12;
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_6(mut self) -> Self {
|
fn apply_preset_6(mut self) -> Self {
|
||||||
self.filter.insert(FilterStrategy::AVERAGE);
|
self.filters.insert(FilterStrategy::AVERAGE);
|
||||||
self.filter.insert(FilterStrategy::PAETH);
|
self.filters.insert(FilterStrategy::PAETH);
|
||||||
self.apply_preset_5()
|
self.apply_preset_5()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -249,13 +245,13 @@ impl Default for Options {
|
||||||
Self {
|
Self {
|
||||||
fix_errors: false,
|
fix_errors: false,
|
||||||
force: false,
|
force: false,
|
||||||
filter: indexset! {
|
filters: indexset! {
|
||||||
FilterStrategy::NONE,
|
FilterStrategy::NONE,
|
||||||
FilterStrategy::SUB,
|
FilterStrategy::SUB,
|
||||||
FilterStrategy::Entropy,
|
FilterStrategy::Entropy,
|
||||||
FilterStrategy::Bigrams
|
FilterStrategy::Bigrams
|
||||||
},
|
},
|
||||||
interlace: Some(Interlacing::None),
|
interlace: Some(false),
|
||||||
optimize_alpha: false,
|
optimize_alpha: false,
|
||||||
bit_depth_reduction: true,
|
bit_depth_reduction: true,
|
||||||
color_type_reduction: true,
|
color_type_reduction: true,
|
||||||
|
|
@ -264,7 +260,7 @@ impl Default for Options {
|
||||||
idat_recoding: true,
|
idat_recoding: true,
|
||||||
scale_16: false,
|
scale_16: false,
|
||||||
strip: StripChunks::None,
|
strip: StripChunks::None,
|
||||||
deflate: Deflaters::Libdeflater { compression: 11 },
|
deflater: Deflater::Libdeflater { compression: 11 },
|
||||||
fast_evaluation: true,
|
fast_evaluation: true,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
use std::{
|
use std::{fs, io::Write, path::Path, sync::Arc};
|
||||||
fs::File,
|
|
||||||
io::{BufReader, Read, Write},
|
|
||||||
path::Path,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitvec::bitarr;
|
use bitvec::bitarr;
|
||||||
use libdeflater::{CompressionLvl, Compressor};
|
use libdeflater::{CompressionLvl, Compressor};
|
||||||
|
|
@ -19,7 +14,7 @@ use crate::{
|
||||||
error::PngError,
|
error::PngError,
|
||||||
filters::*,
|
filters::*,
|
||||||
headers::*,
|
headers::*,
|
||||||
interlace::{Interlacing, deinterlace_image, interlace_image},
|
interlace::{deinterlace_image, interlace_image},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod scan_lines;
|
pub(crate) mod scan_lines;
|
||||||
|
|
@ -62,28 +57,7 @@ impl PngData {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
||||||
let file = match File::open(filepath) {
|
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
|
||||||
Ok(f) => f,
|
|
||||||
Err(_) => return Err(PngError::new("Failed to open file for reading")),
|
|
||||||
};
|
|
||||||
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
// Check file for PNG header
|
|
||||||
let mut header = [0; 8];
|
|
||||||
if reader.read_exact(&mut header).is_err() {
|
|
||||||
return Err(PngError::new("Not a PNG file: too small"));
|
|
||||||
}
|
|
||||||
if !file_header_is_valid(&header) {
|
|
||||||
return Err(PngError::new("Invalid PNG header detected"));
|
|
||||||
}
|
|
||||||
// Read raw png data into memory
|
|
||||||
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
|
|
||||||
byte_data.extend_from_slice(&header);
|
|
||||||
match reader.read_to_end(&mut byte_data) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => return Err(PngError::new("Failed to read from file")),
|
|
||||||
}
|
|
||||||
Ok(byte_data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `PngData` struct by reading a slice
|
/// Create a new `PngData` struct by reading a slice
|
||||||
|
|
@ -293,18 +267,17 @@ impl PngImage {
|
||||||
Ok(image)
|
Ok(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the image to the specified interlacing type
|
/// Enable or disable interlacing
|
||||||
/// Returns true if the interlacing was changed, false otherwise
|
/// Returns the new image if the interlacing was changed, None otherwise
|
||||||
/// The `interlace` parameter specifies the *new* interlacing mode
|
|
||||||
/// Assumes that the data has already been de-filtered
|
/// Assumes that the data has already been de-filtered
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<Self> {
|
pub fn change_interlacing(&self, interlace: bool) -> Option<Self> {
|
||||||
if interlace == self.ihdr.interlaced {
|
if interlace == self.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(if interlace == Interlacing::Adam7 {
|
Some(if interlace {
|
||||||
// Convert progressive to interlaced data
|
// Convert progressive to interlaced data
|
||||||
interlace_image(self)
|
interlace_image(self)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{interlace::Interlacing, png::PngImage};
|
use crate::png::PngImage;
|
||||||
|
|
||||||
/// An iterator over the scan lines of a PNG image
|
/// An iterator over the scan lines of a PNG image
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -69,7 +69,7 @@ impl ScanLineRanges {
|
||||||
width: png.ihdr.width,
|
width: png.ihdr.width,
|
||||||
height: png.ihdr.height,
|
height: png.ihdr.height,
|
||||||
left: png.data.len(),
|
left: png.data.len(),
|
||||||
pass: if png.ihdr.interlaced == Interlacing::Adam7 {
|
pass: if png.ihdr.interlaced {
|
||||||
Some((1, 0))
|
Some((1, 0))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{ColorType, Deadline, Deflaters, Options, evaluate::Evaluator, png::PngImage};
|
use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
|
||||||
|
|
||||||
pub mod alpha;
|
pub mod alpha;
|
||||||
use crate::alpha::*;
|
use crate::alpha::*;
|
||||||
|
|
@ -21,8 +21,8 @@ pub(crate) fn perform_reductions(
|
||||||
|
|
||||||
// At low compression levels, skip some transformations which are less likely to be effective
|
// At low compression levels, skip some transformations which are less likely to be effective
|
||||||
// This currently affects optimization presets 0-2
|
// This currently affects optimization presets 0-2
|
||||||
let cheap = match opts.deflate {
|
let cheap = match opts.deflater {
|
||||||
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ use indexmap::IndexSet;
|
||||||
use rgb::RGBA8;
|
use rgb::RGBA8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Interlacing,
|
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
headers::IhdrData,
|
headers::IhdrData,
|
||||||
png::{PngImage, scan_lines::ScanLine},
|
png::{PngImage, scan_lines::ScanLine},
|
||||||
|
|
@ -134,7 +133,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
||||||
// Interlacing not currently supported
|
// Interlacing not currently supported
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
|
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let palette = match &png.ihdr.color_type {
|
||||||
|
|
@ -156,7 +155,7 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
||||||
// Interlacing not currently supported
|
// Interlacing not currently supported
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced != Interlacing::None {
|
if png.ihdr.bit_depth != BitDepth::Eight || png.ihdr.interlaced {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let palette = match &png.ihdr.color_type {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ const RGBA: u8 = 6;
|
||||||
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
let options = oxipng::Options {
|
let options = oxipng::Options {
|
||||||
force: true,
|
force: true,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
||||||
|
|
@ -32,8 +31,7 @@ fn test_it_converts(
|
||||||
|
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
let png = PngData::new(&input, &opts).unwrap();
|
let png = PngData::new(&input, &opts).unwrap();
|
||||||
opts.filter = IndexSet::new();
|
opts.filters = indexset! {filter};
|
||||||
opts.filter.insert(filter);
|
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
#[cfg(feature = "zopfli")]
|
|
||||||
use std::num::NonZeroU8;
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::remove_file,
|
fs::remove_file,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
|
@ -16,7 +14,7 @@ fn get_opts(input: &Path) -> (OutFile, Options) {
|
||||||
let options = Options {
|
let options = Options {
|
||||||
force: true,
|
force: true,
|
||||||
fast_evaluation: false,
|
fast_evaluation: false,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
||||||
|
|
@ -316,7 +314,7 @@ fn strip_chunks_none() {
|
||||||
fn interlacing_0_to_1() {
|
fn interlacing_0_to_1() {
|
||||||
let input = PathBuf::from("tests/files/interlacing_0_to_1.png");
|
let input = PathBuf::from("tests/files/interlacing_0_to_1.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.interlace = Some(Interlacing::Adam7);
|
opts.interlace = Some(true);
|
||||||
|
|
||||||
test_it_converts_callbacks(
|
test_it_converts_callbacks(
|
||||||
input,
|
input,
|
||||||
|
|
@ -327,10 +325,10 @@ fn interlacing_0_to_1() {
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +337,7 @@ fn interlacing_0_to_1() {
|
||||||
fn interlacing_1_to_0() {
|
fn interlacing_1_to_0() {
|
||||||
let input = PathBuf::from("tests/files/interlacing_1_to_0.png");
|
let input = PathBuf::from("tests/files/interlacing_1_to_0.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.interlace = Some(Interlacing::None);
|
opts.interlace = Some(false);
|
||||||
|
|
||||||
test_it_converts_callbacks(
|
test_it_converts_callbacks(
|
||||||
input,
|
input,
|
||||||
|
|
@ -350,10 +348,10 @@ fn interlacing_1_to_0() {
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -362,7 +360,7 @@ fn interlacing_1_to_0() {
|
||||||
fn interlacing_0_to_1_small_files() {
|
fn interlacing_0_to_1_small_files() {
|
||||||
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
|
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.interlace = Some(Interlacing::Adam7);
|
opts.interlace = Some(true);
|
||||||
|
|
||||||
test_it_converts_callbacks(
|
test_it_converts_callbacks(
|
||||||
input,
|
input,
|
||||||
|
|
@ -373,10 +371,10 @@ fn interlacing_0_to_1_small_files() {
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +383,7 @@ fn interlacing_0_to_1_small_files() {
|
||||||
fn interlacing_1_to_0_small_files() {
|
fn interlacing_1_to_0_small_files() {
|
||||||
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
|
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.interlace = Some(Interlacing::None);
|
opts.interlace = Some(false);
|
||||||
|
|
||||||
test_it_converts_callbacks(
|
test_it_converts_callbacks(
|
||||||
input,
|
input,
|
||||||
|
|
@ -396,10 +394,10 @@ fn interlacing_1_to_0_small_files() {
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -408,8 +406,8 @@ fn interlacing_1_to_0_small_files() {
|
||||||
fn interlaced_0_to_1_other_filter_mode() {
|
fn interlaced_0_to_1_other_filter_mode() {
|
||||||
let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png");
|
let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.interlace = Some(Interlacing::Adam7);
|
opts.interlace = Some(true);
|
||||||
opts.filter = indexset! {FilterStrategy::PAETH};
|
opts.filters = indexset! {FilterStrategy::PAETH};
|
||||||
|
|
||||||
test_it_converts_callbacks(
|
test_it_converts_callbacks(
|
||||||
input,
|
input,
|
||||||
|
|
@ -420,10 +418,10 @@ fn interlaced_0_to_1_other_filter_mode() {
|
||||||
GRAY,
|
GRAY,
|
||||||
BitDepth::Sixteen,
|
BitDepth::Sixteen,
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
|png| {
|
|png| {
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -437,8 +435,6 @@ fn preserve_attrs() {
|
||||||
.metadata()
|
.metadata()
|
||||||
.expect("unable to get file metadata for output file");
|
.expect("unable to get file metadata for output file");
|
||||||
#[cfg(feature = "filetime")]
|
#[cfg(feature = "filetime")]
|
||||||
let atime_canon = filetime::FileTime::from_last_access_time(&meta_input);
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
|
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
|
||||||
|
|
||||||
let (mut output, opts) = get_opts(&input);
|
let (mut output, opts) = get_opts(&input);
|
||||||
|
|
@ -458,12 +454,6 @@ fn preserve_attrs() {
|
||||||
.metadata()
|
.metadata()
|
||||||
.expect("unable to get file metadata for output file");
|
.expect("unable to get file metadata for output file");
|
||||||
#[cfg(feature = "filetime")]
|
#[cfg(feature = "filetime")]
|
||||||
assert_eq!(
|
|
||||||
&atime_canon,
|
|
||||||
&filetime::FileTime::from_last_access_time(&meta_output),
|
|
||||||
"expected access time to be identical to that of input",
|
|
||||||
);
|
|
||||||
#[cfg(feature = "filetime")]
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&mtime_canon,
|
&mtime_canon,
|
||||||
&filetime::FileTime::from_last_modification_time(&meta_output),
|
&filetime::FileTime::from_last_modification_time(&meta_output),
|
||||||
|
|
@ -649,9 +639,7 @@ fn scale_16() {
|
||||||
fn zopfli_mode() {
|
fn zopfli_mode() {
|
||||||
let input = PathBuf::from("tests/files/zopfli_mode.png");
|
let input = PathBuf::from("tests/files/zopfli_mode.png");
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
opts.deflate = Deflaters::Zopfli {
|
opts.deflater = Deflater::Zopfli(ZopfliOptions::default());
|
||||||
iterations: NonZeroU8::new(15).unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
input,
|
input,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
let options = oxipng::Options {
|
let options = oxipng::Options {
|
||||||
force: true,
|
force: true,
|
||||||
fast_evaluation: false,
|
fast_evaluation: false,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
interlace: None,
|
interlace: None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -35,7 +35,7 @@ fn test_it_converts(
|
||||||
|
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
assert!(png.raw.ihdr.interlaced);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const INDEXED: u8 = 3;
|
||||||
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
let options = oxipng::Options {
|
let options = oxipng::Options {
|
||||||
force: true,
|
force: true,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
||||||
|
|
@ -19,7 +19,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
|
|
||||||
fn test_it_converts(
|
fn test_it_converts(
|
||||||
input: &str,
|
input: &str,
|
||||||
interlace: Interlacing,
|
interlace: bool,
|
||||||
color_type_in: u8,
|
color_type_in: u8,
|
||||||
bit_depth_in: BitDepth,
|
bit_depth_in: BitDepth,
|
||||||
color_type_out: u8,
|
color_type_out: u8,
|
||||||
|
|
@ -31,14 +31,7 @@ fn test_it_converts(
|
||||||
opts.interlace = Some(interlace);
|
opts.interlace = Some(interlace);
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
||||||
assert_eq!(
|
assert_eq!(png.raw.ihdr.interlaced, !interlace);
|
||||||
png.raw.ihdr.interlaced,
|
|
||||||
if interlace == Interlacing::Adam7 {
|
|
||||||
Interlacing::None
|
|
||||||
} else {
|
|
||||||
Interlacing::Adam7
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
@ -65,7 +58,7 @@ fn test_it_converts(
|
||||||
fn deinterlace_rgb_16() {
|
fn deinterlace_rgb_16() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
|
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Sixteen,
|
BitDepth::Sixteen,
|
||||||
RGB,
|
RGB,
|
||||||
|
|
@ -77,7 +70,7 @@ fn deinterlace_rgb_16() {
|
||||||
fn deinterlace_rgb_8() {
|
fn deinterlace_rgb_8() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
|
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
RGB,
|
RGB,
|
||||||
|
|
@ -89,7 +82,7 @@ fn deinterlace_rgb_8() {
|
||||||
fn deinterlace_palette_8() {
|
fn deinterlace_palette_8() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_palette_8_should_be_palette_8.png",
|
"tests/files/interlaced_palette_8_should_be_palette_8.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -101,7 +94,7 @@ fn deinterlace_palette_8() {
|
||||||
fn deinterlace_palette_4() {
|
fn deinterlace_palette_4() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_palette_4_should_be_palette_4.png",
|
"tests/files/interlaced_palette_4_should_be_palette_4.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Four,
|
BitDepth::Four,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -113,7 +106,7 @@ fn deinterlace_palette_4() {
|
||||||
fn deinterlace_palette_2() {
|
fn deinterlace_palette_2() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_palette_2_should_be_palette_2.png",
|
"tests/files/interlaced_palette_2_should_be_palette_2.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Two,
|
BitDepth::Two,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -125,7 +118,7 @@ fn deinterlace_palette_2() {
|
||||||
fn deinterlace_palette_1() {
|
fn deinterlace_palette_1() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/interlaced_palette_1_should_be_palette_1.png",
|
"tests/files/interlaced_palette_1_should_be_palette_1.png",
|
||||||
Interlacing::None,
|
false,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::One,
|
BitDepth::One,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -137,7 +130,7 @@ fn deinterlace_palette_1() {
|
||||||
fn interlace_rgb_16() {
|
fn interlace_rgb_16() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/rgb_16_should_be_rgb_16.png",
|
"tests/files/rgb_16_should_be_rgb_16.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Sixteen,
|
BitDepth::Sixteen,
|
||||||
RGB,
|
RGB,
|
||||||
|
|
@ -149,7 +142,7 @@ fn interlace_rgb_16() {
|
||||||
fn interlace_rgb_8() {
|
fn interlace_rgb_8() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/rgb_8_should_be_rgb_8.png",
|
"tests/files/rgb_8_should_be_rgb_8.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
RGB,
|
RGB,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
RGB,
|
RGB,
|
||||||
|
|
@ -161,7 +154,7 @@ fn interlace_rgb_8() {
|
||||||
fn interlace_palette_8() {
|
fn interlace_palette_8() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/palette_8_should_be_palette_8.png",
|
"tests/files/palette_8_should_be_palette_8.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Eight,
|
BitDepth::Eight,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -173,7 +166,7 @@ fn interlace_palette_8() {
|
||||||
fn interlace_palette_4() {
|
fn interlace_palette_4() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/palette_4_should_be_palette_4.png",
|
"tests/files/palette_4_should_be_palette_4.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Four,
|
BitDepth::Four,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -185,7 +178,7 @@ fn interlace_palette_4() {
|
||||||
fn interlace_palette_2() {
|
fn interlace_palette_2() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/palette_2_should_be_palette_2.png",
|
"tests/files/palette_2_should_be_palette_2.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::Two,
|
BitDepth::Two,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
@ -197,7 +190,7 @@ fn interlace_palette_2() {
|
||||||
fn interlace_palette_1() {
|
fn interlace_palette_1() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
"tests/files/palette_1_should_be_palette_1.png",
|
"tests/files/palette_1_should_be_palette_1.png",
|
||||||
Interlacing::Adam7,
|
true,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
BitDepth::One,
|
BitDepth::One,
|
||||||
INDEXED,
|
INDEXED,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use oxipng::{internal_tests::*, *};
|
||||||
fn get_opts() -> Options {
|
fn get_opts() -> Options {
|
||||||
Options {
|
Options {
|
||||||
force: true,
|
force: true,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
let options = oxipng::Options {
|
let options = oxipng::Options {
|
||||||
force: true,
|
force: true,
|
||||||
fast_evaluation: false,
|
fast_evaluation: false,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
||||||
|
|
@ -36,7 +36,7 @@ fn test_it_converts(
|
||||||
|
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
|
||||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
assert!(!png.raw.ihdr.interlaced);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const RGBA: u8 = 6;
|
||||||
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
|
||||||
let options = oxipng::Options {
|
let options = oxipng::Options {
|
||||||
force: true,
|
force: true,
|
||||||
filter: indexset! {FilterStrategy::NONE},
|
filters: indexset! {FilterStrategy::NONE},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(OutFile::from_path(input.with_extension("out.png")), options)
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
||||||
|
|
@ -75,7 +75,7 @@ fn test_it_converts(
|
||||||
fn issue_42() {
|
fn issue_42() {
|
||||||
let input = "tests/files/issue-42.png";
|
let input = "tests/files/issue-42.png";
|
||||||
let (output, mut opts) = get_opts(Path::new(input));
|
let (output, mut opts) = get_opts(Path::new(input));
|
||||||
opts.interlace = Some(Interlacing::Adam7);
|
opts.interlace = Some(true);
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
input,
|
input,
|
||||||
Some((output, opts)),
|
Some((output, opts)),
|
||||||
|
|
@ -186,7 +186,7 @@ fn issue_175() {
|
||||||
fn issue_182() {
|
fn issue_182() {
|
||||||
let input = "tests/files/issue-182.png";
|
let input = "tests/files/issue-182.png";
|
||||||
let (output, mut opts) = get_opts(Path::new(input));
|
let (output, mut opts) = get_opts(Path::new(input));
|
||||||
opts.interlace = Some(Interlacing::Adam7);
|
opts.interlace = Some(true);
|
||||||
|
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
input,
|
input,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ fn test_it_converts(
|
||||||
|
|
||||||
let (output, mut opts) = get_opts(&input);
|
let (output, mut opts) = get_opts(&input);
|
||||||
let png = PngData::new(&input, &opts).unwrap();
|
let png = PngData::new(&input, &opts).unwrap();
|
||||||
opts.filter = indexset! {filter};
|
opts.filters = indexset! {filter};
|
||||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
||||||
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue