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:
andrews05 2025-09-21 03:16:22 +12:00 committed by GitHub
parent 2a3aa83928
commit 14532e8bf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 256 additions and 336 deletions

View file

@ -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
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
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]

View file

@ -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 png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[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 png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -31,7 +31,7 @@ fn interlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -41,7 +41,7 @@ fn interlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -51,7 +51,7 @@ fn interlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::Adam7));
b.iter(|| png.raw.change_interlacing(true));
}
#[bench]
@ -61,7 +61,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -71,7 +71,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -81,7 +81,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -91,7 +91,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}
#[bench]
@ -101,5 +101,5 @@ fn deinterlacing_1_bits(b: &mut Bencher) {
));
let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| png.raw.change_interlacing(Interlacing::None));
b.iter(|| png.raw.change_interlacing(false));
}

View file

@ -3,20 +3,18 @@
extern crate oxipng;
extern crate test;
use std::{num::NonZeroU8, path::PathBuf};
use std::path::PathBuf;
use oxipng::{internal_tests::*, *};
use test::Bencher;
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = NonZeroU8::new(15).unwrap();
#[bench]
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 png = PngData::new(&input, &Options::default()).unwrap();
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();
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();
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();
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();
b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok();
zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
});
}

View file

@ -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");
@ -97,10 +97,10 @@ Note that this will not preserve the directory structure of the input files when
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("pretend")
Arg::new("dry-run")
.help("Do not write any files, only show compression results")
.short('P')
.long("pretend")
.short('d')
.long("dry-run")
.action(ArgAction::SetTrue),
)
.arg(
@ -163,24 +163,23 @@ transformation and may be unsuitable for some applications.")
.action(ArgAction::SetTrue),
)
.arg(
// Note: The default value is not explicitly set here, as it is dependant on the `--nx` flag.
Arg::new("interlace")
.help("Set PNG interlacing type (0, 1, keep) [default: 0]")
.help("Set PNG interlacing (off, on, keep)")
.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
1 => Apply Adam7 interlacing on all images that are processed
keep => Keep the existing interlacing type of each image
off => Remove interlacing from all images that are processed
on => Apply Adam7 interlacing on all images that are processed
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 \
believe the benefits outweigh the costs for your use case.
[default: 0]")
believe the benefits outweigh the costs for your use case.")
.short('i')
.long("interlace")
.value_name("type")
.value_parser(["0", "1", "keep"])
.value_name("mode")
.value_parser(["off", "on", "keep", "0", "1"])
.default_value("off")
.default_value_if("no-reductions", ArgPredicate::IsPresent, "keep")
.hide_possible_values(true),
)
.arg(
@ -325,7 +324,8 @@ be processed successfully.")
.long_help("\
Use the much slower but stronger Zopfli compressor for main compression trials. \
Recommended use is with '-o max' and '--fast'.")
.short('Z')
.short('z')
.short_alias('Z') // Kept for backwards compatibility
.long("zopfli")
.action(ArgAction::SetTrue),
)
@ -338,7 +338,18 @@ speed up compression for large files. This option requires '--zopfli' to be set.
.long("zi")
.value_name("iterations")
.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"),
)
.arg(

View file

@ -116,7 +116,7 @@ impl TryFrom<u8> for BitDepth {
4 => Ok(Self::Four),
8 => Ok(Self::Eight),
16 => Ok(Self::Sixteen),
_ => Err(PngError::new("Unexpected bit depth")),
_ => Err(PngError::InvalidData),
}
}
}

View file

@ -22,7 +22,7 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult<Vec<u8>> {
.zlib_decompress(data, &mut dest)
.map_err(|err| match err {
DecompressionError::BadData => PngError::InvalidData,
DecompressionError::InsufficientSpace => PngError::new("inflated data too long"),
DecompressionError::InsufficientSpace => PngError::InflatedDataTooLong(out_size),
})?;
dest.truncate(len);
Ok(dest)

View file

@ -1,19 +1,19 @@
mod deflater;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{fmt, fmt::Display};
pub use deflater::{crc32, deflate, inflate};
use crate::{PngError, PngResult};
use std::{fmt, fmt::Display};
#[cfg(feature = "zopfli")]
mod zopfli_oxipng;
#[cfg(feature = "zopfli")]
pub use zopfli::Options as ZopfliOptions;
#[cfg(feature = "zopfli")]
pub use zopfli_oxipng::deflate as zopfli_deflate;
/// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options])
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Deflaters {
pub enum Deflater {
/// Use libdeflater.
Libdeflater {
/// Which compression level to use on the file (0-12)
@ -21,20 +21,15 @@ pub enum Deflaters {
},
#[cfg(feature = "zopfli")]
/// Use the better but slower Zopfli implementation
Zopfli {
/// 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,
},
Zopfli(ZopfliOptions),
}
impl Deflaters {
impl Deflater {
pub(crate) fn deflate(self, data: &[u8], max_size: Option<usize>) -> PngResult<Vec<u8>> {
let compressed = match self {
Self::Libdeflater { compression } => deflate(data, compression, max_size)?,
#[cfg(feature = "zopfli")]
Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?,
Self::Zopfli(options) => zopfli_deflate(data, options)?,
};
if let Some(max) = max_size {
if compressed.len() > max {
@ -45,13 +40,13 @@ impl Deflaters {
}
}
impl Display for Deflaters {
impl Display for Deflater {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Libdeflater { compression } => write!(f, "zc = {compression}"),
#[cfg(feature = "zopfli")]
Self::Zopfli { iterations } => write!(f, "zopfli, zi = {iterations}"),
Self::Zopfli(options) => write!(f, "zopfli, zi = {}", options.iteration_count),
}
}
}

View file

@ -1,13 +1,7 @@
use std::num::NonZeroU8;
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 options = zopfli::Options {
iteration_count: iterations.into(),
..Default::default()
};
// 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.
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {

View file

@ -2,20 +2,22 @@ use std::{error::Error, fmt};
use crate::colors::{BitDepth, ColorType};
#[derive(Debug, Clone)]
#[derive(Debug)]
#[non_exhaustive]
pub enum PngError {
DeflatedDataTooLong(usize),
TimedOut,
NotPNG,
APNGNotSupported,
APNGOutOfOrder,
InvalidData,
TruncatedData,
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
IncorrectDataLength(usize, usize),
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>),
}
@ -26,26 +28,32 @@ impl fmt::Display for PngError {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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::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::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
}
PngError::CRCMismatch(ref c) => write!(
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!(
f,
"Data length {l1} does not match the expected length {l2}"
),
PngError::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
PngError::InflatedDataTooLong(_) => f.write_str("Inflated data too long"),
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),
}
}

View file

@ -10,7 +10,7 @@ use std::sync::{
#[cfg(feature = "parallel")]
use crossbeam_channel::{Receiver, Sender, unbounded};
use deflate::Deflaters;
use deflate::Deflater;
use indexmap::IndexSet;
use log::trace;
use rayon::prelude::*;
@ -50,7 +50,7 @@ impl Candidate {
pub(crate) struct Evaluator {
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflaters,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
nth: AtomicUsize,
@ -68,7 +68,7 @@ impl Evaluator {
pub fn new(
deadline: Arc<Deadline>,
filters: IndexSet<FilterStrategy>,
deflater: Deflaters,
deflater: Deflater,
optimize_alpha: bool,
final_round: bool,
) -> Self {

View file

@ -3,12 +3,11 @@ use log::{debug, trace, warn};
use rgb::{RGB16, RGBA8};
use crate::{
Deflaters, Options, PngResult,
Deflater, Options, PngResult,
colors::{BitDepth, ColorType},
deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS,
error::PngError,
interlace::Interlacing,
};
#[derive(Debug, Clone)]
@ -22,8 +21,8 @@ pub struct IhdrData {
pub color_type: ColorType,
/// The bit depth of the image
pub bit_depth: BitDepth,
/// The interlacing mode of the image
pub interlaced: Interlacing,
/// Whether the image is interlaced
pub interlaced: bool,
}
impl IhdrData {
@ -45,7 +44,7 @@ impl IhdrData {
(w * bpp).div_ceil(8) * h
}
if self.interlaced == Interlacing::None {
if !self.interlaced {
bitmap_size(bpp, w, h) + h
} else {
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];
if !fix_errors && crc32(chunk_bytes) != crc {
return Err(PngError::new(&format!(
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name)
)));
return Err(PngError::CRCMismatch(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,
6 => ColorType::RGBA,
_ => return Err(PngError::new("Unexpected color type in header")),
_ => return Err(PngError::InvalidData),
},
bit_depth: byte_data[8].try_into()?,
width: read_be_u32(&byte_data[0..4]),
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>>,
trns_data: Option<Vec<u8>>,
) -> 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
.chunks_exact(3)
.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
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 data = Vec::with_capacity(compressed.len() + 5);
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 {
// Try recompressing the profile
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!(
"Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(),

View file

@ -1,42 +1,6 @@
use std::{fmt, fmt::Display};
use bitvec::prelude::*;
use crate::{PngError, 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,
)
}
}
use crate::{headers::IhdrData, png::PngImage};
#[must_use]
pub fn interlace_image(png: &PngImage) -> PngImage {
@ -89,7 +53,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
data: output,
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::Adam7,
interlaced: true,
..png.ihdr
},
}
@ -103,7 +67,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
},
ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::None,
interlaced: false,
..png.ihdr
},
}

View file

@ -41,13 +41,14 @@ use log::{debug, info, trace, warn};
use rayon::prelude::*;
pub use rgb::{RGB16, RGBA8};
#[cfg(feature = "zopfli")]
pub use crate::deflate::ZopfliOptions;
pub use crate::{
colors::{BitDepth, ColorType},
deflate::Deflaters,
deflate::Deflater,
error::PngError,
filters::{FilterStrategy, RowFilter},
headers::StripChunks,
interlace::Interlacing,
options::{InFile, Options, OutFile},
};
use crate::{
@ -130,7 +131,7 @@ impl RawImage {
height,
color_type,
bit_depth,
interlaced: Interlacing::None,
interlaced: false,
},
data,
}),
@ -146,7 +147,7 @@ impl RawImage {
/// Add an ICC profile for the image
pub fn add_icc_profile(&mut self, data: &[u8]) {
// 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) {
self.aux_chunks.push(iccp);
}
@ -181,7 +182,9 @@ impl RawImage {
}
/// 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.
info!("Processing: {input}");
@ -218,7 +221,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
let mut data = Vec::new();
stdin()
.read_to_end(&mut data)
.map_err(|e| PngError::new(&format!("Error reading stdin: {e}")))?;
.map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
data
}
};
@ -230,14 +233,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
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) {
// If output path is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) =>
{
info!("{input}: Could not optimize further, no change written");
return Ok(());
return Ok((in_length, in_length));
}
_ => {
optimized_output = in_data;
@ -261,26 +264,21 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
match (output, input) {
(OutFile::None, _) => {
info!("{savings}: Running in pretend mode, no output");
info!("{savings}: Dry run, no output");
}
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
let mut buffer = BufWriter::new(stdout());
buffer
.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, .. }, _) => {
let output_path = path
.as_ref()
.map(|p| p.as_path())
.unwrap_or_else(|| input.path().unwrap());
let out_file = File::create(output_path).map_err(|err| {
PngError::new(&format!(
"Unable to write to file {}: {}",
output_path.display(),
err
))
})?;
let out_file = File::create(output_path)
.map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
if let Some(metadata_input) = &opt_metadata_preserved {
copy_permissions(metadata_input, &out_file)?;
}
@ -290,13 +288,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
.write_all(&optimized_output)
// flush BufWriter so IO errors don't get swallowed silently on close() by drop!
.and_then(|()| buffer.flush())
.map_err(|e| {
PngError::new(&format!(
"Unable to write to {}: {}",
output_path.display(),
e
))
})?;
.map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
// force drop and thereby closing of file handle before modifying any timestamp
std::mem::drop(buffer);
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());
}
}
Ok(())
Ok((in_length, optimized_output.len()))
}
/// 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!(
"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)
// 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
let compression = match opts.deflate {
Deflaters::Libdeflater { compression } => {
let compression = match opts.deflater {
Deflater::Libdeflater { compression } => {
if opts.fast_evaluation { 7 } else { 8 }.min(compression)
}
_ => 8,
};
let eval_deflater = Deflaters::Libdeflater { compression };
let eval_deflater = Deflater::Libdeflater { compression };
// If only one filter is selected, use this for evaluations
let eval_filters = if opts.filter.len() == 1 {
opts.filter.clone()
let eval_filters = if opts.filters.len() == 1 {
opts.filters.clone()
} else {
// None and Bigrams work well together, especially for alpha reductions
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
@ -440,7 +432,7 @@ fn optimize_raw(
eval_filters.clone(),
eval_deflater,
false,
opts.deflate == eval_deflater,
opts.deflater == eval_deflater,
);
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate();
@ -465,7 +457,7 @@ fn optimize_raw(
eval_filters,
eval_deflater,
);
(result?, opts.deflate)
(result?, opts.deflater)
} else {
// 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.
@ -490,9 +482,9 @@ fn perform_trials(
max_size: Option<usize>,
mut eval_result: Option<Candidate>,
eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflaters,
eval_deflater: Deflater,
) -> 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());
if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial
@ -509,7 +501,7 @@ fn perform_trials(
filters,
eval_deflater,
opts.optimize_alpha,
opts.deflate == eval_deflater,
opts.deflater == eval_deflater,
);
if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size);
@ -525,9 +517,9 @@ fn perform_trials(
if result.idat_data.is_none() {
// 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);
match opts.deflate.deflate(&data, max_size) {
match opts.deflater.deflate(&data, max_size) {
Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.idat_data = Some(idat_data);
@ -555,8 +547,8 @@ fn perform_trials(
}
}
debug!("Trying {} filters with {}", filters.len(), opts.deflate);
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha, true);
debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
if let Some(max_size) = max_size {
eval.set_best_size(max_size);
}
@ -616,9 +608,14 @@ impl Deadline {
/// Display the format of the image data
fn report_format(prefix: &str, png: &PngImage) {
let interlaced = if png.ihdr.interlaced {
"interlaced"
} else {
"non-interlaced"
};
debug!(
"{}{}-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 (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
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!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i,
@ -671,7 +668,7 @@ fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()>
.set_permissions(metadata_input.permissions())
.map_err(|err_io| {
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")]
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);
trace!("attempting to set file times: atime: {atime:?}, mtime: {mtime:?}");
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| {
trace!("attempting to set file modification time: {mtime:?}");
filetime::set_file_mtime(out_path, mtime).map_err(|err_io| {
PngError::new(&format!(
"unable to set file times on {out_path:?}: {err_io}"
"Unable to set file times on {out_path:?}: {err_io}"
))
})
}

View file

@ -17,7 +17,7 @@
mod rayon;
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::num::NonZeroU64;
use std::{
ffi::OsString, fs::DirBuilder, io::Write, path::PathBuf, process::ExitCode, time::Duration,
};
@ -26,7 +26,9 @@ use clap::ArgMatches;
mod cli;
use indexmap::IndexSet;
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 crate::cli::DISPLAY_CHUNKS;
@ -198,7 +200,7 @@ fn parse_opts_into_struct(
};
if let Some(x) = matches.get_one::<IndexSet<u8>>("filters") {
opts.filter = x
opts.filters = x
.iter()
.map(|&f| match f {
0..=4 => FilterStrategy::Basic(f.try_into().unwrap()),
@ -233,7 +235,7 @@ fn parse_opts_into_struct(
None
};
let out_file = if matches.get_flag("pretend") {
let out_file = if matches.get_flag("dry-run") {
OutFile::None
} else if matches.get_flag("stdout") {
OutFile::StdOut
@ -276,10 +278,10 @@ fn parse_opts_into_struct(
opts.idat_recoding = !matches.get_flag("no-recoding");
if let Some(x) = matches.get_one::<String>("interlace") {
opts.interlace = if x == "keep" {
None
} else {
x.parse::<u8>().unwrap().try_into().ok()
opts.interlace = match x.as_str() {
"off" | "0" => Some(false),
"on" | "1" => Some(true),
_ => None, // keep
};
}
@ -335,13 +337,18 @@ fn parse_opts_into_struct(
#[cfg(feature = "zopfli")]
if matches.get_flag("zopfli") {
let iterations = *matches.get_one::<i64>("iterations").unwrap();
opts.deflate = Deflaters::Zopfli {
iterations: NonZeroU8::new(iterations as u8).unwrap(),
};
let iteration_count = *matches.get_one::<NonZeroU64>("iterations").unwrap();
let iterations_without_improvement = *matches
.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)) =
(&mut opts.deflate, matches.get_one::<i64>("compression"))
if let (Deflater::Libdeflater { compression }, Some(x)) =
(&mut opts.deflater, matches.get_one::<i64>("compression"))
{
*compression = *x as u8;
}

View file

@ -7,9 +7,7 @@ use std::{
use indexmap::{IndexSet, indexset};
use log::warn;
use crate::{
deflate::Deflaters, filters::FilterStrategy, headers::StripChunks, interlace::Interlacing,
};
use crate::{deflate::Deflater, filters::FilterStrategy, headers::StripChunks};
/// Write destination for [`optimize`][crate::optimize].
/// 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
///
/// Default: `None,Sub,Entropy,Bigrams`
pub filter: IndexSet<FilterStrategy>,
/// Whether to change the interlacing type of the file.
pub filters: IndexSet<FilterStrategy>,
/// Whether to change the interlacing of the file.
///
/// These are the interlacing types avaliable:
/// - `None` will not change the current interlacing type.
/// - `Some(x)` will change the file to interlacing mode `x`.
/// See [`Interlacing`] for the possible interlacing types.
/// - `None` will not change the current interlacing.
/// - `Some(x)` will turn interlacing on or off.
///
/// Default: `Some(Interlacing::None)`
pub interlace: Option<Interlacing>,
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
@ -148,7 +144,7 @@ pub struct Options {
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
///
/// Default: `Libdeflater`
pub deflate: Deflaters,
pub deflater: Deflater,
/// Whether to use fast evaluation to pick the best filter
///
/// Default: `true`
@ -187,16 +183,16 @@ impl Options {
// The following methods make assumptions that they are operating
// on an `Options` struct generated by the `default` method.
fn apply_preset_0(mut self) -> Self {
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
self.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 5;
}
self
}
fn apply_preset_1(mut self) -> Self {
self.filter.clear();
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
self.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 10;
}
self
@ -208,7 +204,7 @@ impl Options {
fn apply_preset_3(mut self) -> Self {
self.fast_evaluation = false;
self.filter = indexset! {
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
@ -218,7 +214,7 @@ impl Options {
}
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;
}
self.apply_preset_3()
@ -226,19 +222,19 @@ impl Options {
fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false;
self.filter.insert(FilterStrategy::UP);
self.filter.insert(FilterStrategy::MinSum);
self.filter.insert(FilterStrategy::BigEnt);
self.filter.insert(FilterStrategy::Brute);
if let Deflaters::Libdeflater { compression } = &mut self.deflate {
self.filters.insert(FilterStrategy::UP);
self.filters.insert(FilterStrategy::MinSum);
self.filters.insert(FilterStrategy::BigEnt);
self.filters.insert(FilterStrategy::Brute);
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 12;
}
self
}
fn apply_preset_6(mut self) -> Self {
self.filter.insert(FilterStrategy::AVERAGE);
self.filter.insert(FilterStrategy::PAETH);
self.filters.insert(FilterStrategy::AVERAGE);
self.filters.insert(FilterStrategy::PAETH);
self.apply_preset_5()
}
}
@ -249,13 +245,13 @@ impl Default for Options {
Self {
fix_errors: false,
force: false,
filter: indexset! {
filters: indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::Entropy,
FilterStrategy::Bigrams
},
interlace: Some(Interlacing::None),
interlace: Some(false),
optimize_alpha: false,
bit_depth_reduction: true,
color_type_reduction: true,
@ -264,7 +260,7 @@ impl Default for Options {
idat_recoding: true,
scale_16: false,
strip: StripChunks::None,
deflate: Deflaters::Libdeflater { compression: 11 },
deflater: Deflater::Libdeflater { compression: 11 },
fast_evaluation: true,
timeout: None,
}

View file

@ -1,9 +1,4 @@
use std::{
fs::File,
io::{BufReader, Read, Write},
path::Path,
sync::Arc,
};
use std::{fs, io::Write, path::Path, sync::Arc};
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor};
@ -19,7 +14,7 @@ use crate::{
error::PngError,
filters::*,
headers::*,
interlace::{Interlacing, deinterlace_image, interlace_image},
interlace::{deinterlace_image, interlace_image},
};
pub(crate) mod scan_lines;
@ -62,28 +57,7 @@ impl PngData {
}
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
let file = match File::open(filepath) {
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)
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
}
/// Create a new `PngData` struct by reading a slice
@ -293,18 +267,17 @@ impl PngImage {
Ok(image)
}
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Enable or disable interlacing
/// Returns the new image if the interlacing was changed, None otherwise
/// Assumes that the data has already been de-filtered
#[inline]
#[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 {
return None;
}
Some(if interlace == Interlacing::Adam7 {
Some(if interlace {
// Convert progressive to interlaced data
interlace_image(self)
} else {

View file

@ -1,4 +1,4 @@
use crate::{interlace::Interlacing, png::PngImage};
use crate::png::PngImage;
/// An iterator over the scan lines of a PNG image
#[derive(Debug, Clone)]
@ -69,7 +69,7 @@ impl ScanLineRanges {
width: png.ihdr.width,
height: png.ihdr.height,
left: png.data.len(),
pass: if png.ihdr.interlaced == Interlacing::Adam7 {
pass: if png.ihdr.interlaced {
Some((1, 0))
} else {
None

View file

@ -1,6 +1,6 @@
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;
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
// This currently affects optimization presets 0-2
let cheap = match opts.deflate {
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
let cheap = match opts.deflater {
Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
_ => false,
};

View file

@ -2,7 +2,6 @@ use indexmap::IndexSet;
use rgb::RGBA8;
use crate::{
Interlacing,
colors::{BitDepth, ColorType},
headers::IhdrData,
png::{PngImage, scan_lines::ScanLine},
@ -134,7 +133,7 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
// 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;
}
let palette = match &png.ihdr.color_type {
@ -156,7 +155,7 @@ pub fn sorted_palette_mzeng(png: &PngImage) -> Option<PngImage> {
#[must_use]
pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
// 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;
}
let palette = match &png.ihdr.color_type {

View file

@ -14,7 +14,6 @@ const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
filter: indexset! {FilterStrategy::NONE},
..Default::default()
};
(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 png = PngData::new(&input, &opts).unwrap();
opts.filter = IndexSet::new();
opts.filter.insert(filter);
opts.filters = indexset! {filter};
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);

View file

@ -1,5 +1,3 @@
#[cfg(feature = "zopfli")]
use std::num::NonZeroU8;
use std::{
fs::remove_file,
path::{Path, PathBuf},
@ -16,7 +14,7 @@ fn get_opts(input: &Path) -> (OutFile, Options) {
let options = Options {
force: true,
fast_evaluation: false,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
(OutFile::from_path(input.with_extension("out.png")), options)
@ -316,7 +314,7 @@ fn strip_chunks_none() {
fn interlacing_0_to_1() {
let input = PathBuf::from("tests/files/interlacing_0_to_1.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(Interlacing::Adam7);
opts.interlace = Some(true);
test_it_converts_callbacks(
input,
@ -327,10 +325,10 @@ fn interlacing_0_to_1() {
RGB,
BitDepth::Eight,
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
assert!(!png.raw.ihdr.interlaced);
},
|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() {
let input = PathBuf::from("tests/files/interlacing_1_to_0.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(Interlacing::None);
opts.interlace = Some(false);
test_it_converts_callbacks(
input,
@ -350,10 +348,10 @@ fn interlacing_1_to_0() {
RGB,
BitDepth::Eight,
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
assert!(png.raw.ihdr.interlaced);
},
|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() {
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(Interlacing::Adam7);
opts.interlace = Some(true);
test_it_converts_callbacks(
input,
@ -373,10 +371,10 @@ fn interlacing_0_to_1_small_files() {
RGB,
BitDepth::Eight,
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
assert!(!png.raw.ihdr.interlaced);
},
|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() {
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(Interlacing::None);
opts.interlace = Some(false);
test_it_converts_callbacks(
input,
@ -396,10 +394,10 @@ fn interlacing_1_to_0_small_files() {
RGB,
BitDepth::Eight,
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
assert!(png.raw.ihdr.interlaced);
},
|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() {
let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png");
let (output, mut opts) = get_opts(&input);
opts.interlace = Some(Interlacing::Adam7);
opts.filter = indexset! {FilterStrategy::PAETH};
opts.interlace = Some(true);
opts.filters = indexset! {FilterStrategy::PAETH};
test_it_converts_callbacks(
input,
@ -420,10 +418,10 @@ fn interlaced_0_to_1_other_filter_mode() {
GRAY,
BitDepth::Sixteen,
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
assert!(!png.raw.ihdr.interlaced);
},
|png| {
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
assert!(png.raw.ihdr.interlaced);
},
);
}
@ -437,8 +435,6 @@ fn preserve_attrs() {
.metadata()
.expect("unable to get file metadata for output file");
#[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 (mut output, opts) = get_opts(&input);
@ -458,12 +454,6 @@ fn preserve_attrs() {
.metadata()
.expect("unable to get file metadata for output file");
#[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!(
&mtime_canon,
&filetime::FileTime::from_last_modification_time(&meta_output),
@ -649,9 +639,7 @@ fn scale_16() {
fn zopfli_mode() {
let input = PathBuf::from("tests/files/zopfli_mode.png");
let (output, mut opts) = get_opts(&input);
opts.deflate = Deflaters::Zopfli {
iterations: NonZeroU8::new(15).unwrap(),
};
opts.deflater = Deflater::Zopfli(ZopfliOptions::default());
test_it_converts(
input,

View file

@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
fast_evaluation: false,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
interlace: None,
..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.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) {
Ok(_) => (),

View file

@ -11,7 +11,7 @@ const INDEXED: u8 = 3;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
(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(
input: &str,
interlace: Interlacing,
interlace: bool,
color_type_in: u8,
bit_depth_in: BitDepth,
color_type_out: u8,
@ -31,14 +31,7 @@ fn test_it_converts(
opts.interlace = Some(interlace);
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.interlaced,
if interlace == Interlacing::Adam7 {
Interlacing::None
} else {
Interlacing::Adam7
}
);
assert_eq!(png.raw.ihdr.interlaced, !interlace);
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (),
@ -65,7 +58,7 @@ fn test_it_converts(
fn deinterlace_rgb_16() {
test_it_converts(
"tests/files/interlaced_rgb_16_should_be_rgb_16.png",
Interlacing::None,
false,
RGB,
BitDepth::Sixteen,
RGB,
@ -77,7 +70,7 @@ fn deinterlace_rgb_16() {
fn deinterlace_rgb_8() {
test_it_converts(
"tests/files/interlaced_rgb_8_should_be_rgb_8.png",
Interlacing::None,
false,
RGB,
BitDepth::Eight,
RGB,
@ -89,7 +82,7 @@ fn deinterlace_rgb_8() {
fn deinterlace_palette_8() {
test_it_converts(
"tests/files/interlaced_palette_8_should_be_palette_8.png",
Interlacing::None,
false,
INDEXED,
BitDepth::Eight,
INDEXED,
@ -101,7 +94,7 @@ fn deinterlace_palette_8() {
fn deinterlace_palette_4() {
test_it_converts(
"tests/files/interlaced_palette_4_should_be_palette_4.png",
Interlacing::None,
false,
INDEXED,
BitDepth::Four,
INDEXED,
@ -113,7 +106,7 @@ fn deinterlace_palette_4() {
fn deinterlace_palette_2() {
test_it_converts(
"tests/files/interlaced_palette_2_should_be_palette_2.png",
Interlacing::None,
false,
INDEXED,
BitDepth::Two,
INDEXED,
@ -125,7 +118,7 @@ fn deinterlace_palette_2() {
fn deinterlace_palette_1() {
test_it_converts(
"tests/files/interlaced_palette_1_should_be_palette_1.png",
Interlacing::None,
false,
INDEXED,
BitDepth::One,
INDEXED,
@ -137,7 +130,7 @@ fn deinterlace_palette_1() {
fn interlace_rgb_16() {
test_it_converts(
"tests/files/rgb_16_should_be_rgb_16.png",
Interlacing::Adam7,
true,
RGB,
BitDepth::Sixteen,
RGB,
@ -149,7 +142,7 @@ fn interlace_rgb_16() {
fn interlace_rgb_8() {
test_it_converts(
"tests/files/rgb_8_should_be_rgb_8.png",
Interlacing::Adam7,
true,
RGB,
BitDepth::Eight,
RGB,
@ -161,7 +154,7 @@ fn interlace_rgb_8() {
fn interlace_palette_8() {
test_it_converts(
"tests/files/palette_8_should_be_palette_8.png",
Interlacing::Adam7,
true,
INDEXED,
BitDepth::Eight,
INDEXED,
@ -173,7 +166,7 @@ fn interlace_palette_8() {
fn interlace_palette_4() {
test_it_converts(
"tests/files/palette_4_should_be_palette_4.png",
Interlacing::Adam7,
true,
INDEXED,
BitDepth::Four,
INDEXED,
@ -185,7 +178,7 @@ fn interlace_palette_4() {
fn interlace_palette_2() {
test_it_converts(
"tests/files/palette_2_should_be_palette_2.png",
Interlacing::Adam7,
true,
INDEXED,
BitDepth::Two,
INDEXED,
@ -197,7 +190,7 @@ fn interlace_palette_2() {
fn interlace_palette_1() {
test_it_converts(
"tests/files/palette_1_should_be_palette_1.png",
Interlacing::Adam7,
true,
INDEXED,
BitDepth::One,
INDEXED,

View file

@ -5,7 +5,7 @@ use oxipng::{internal_tests::*, *};
fn get_opts() -> Options {
Options {
force: true,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
..Default::default()
}
}

View file

@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
fast_evaluation: false,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
(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.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) {
Ok(_) => (),

View file

@ -14,7 +14,7 @@ const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options {
force: true,
filter: indexset! {FilterStrategy::NONE},
filters: indexset! {FilterStrategy::NONE},
..Default::default()
};
(OutFile::from_path(input.with_extension("out.png")), options)
@ -75,7 +75,7 @@ fn test_it_converts(
fn issue_42() {
let input = "tests/files/issue-42.png";
let (output, mut opts) = get_opts(Path::new(input));
opts.interlace = Some(Interlacing::Adam7);
opts.interlace = Some(true);
test_it_converts(
input,
Some((output, opts)),
@ -186,7 +186,7 @@ fn issue_175() {
fn issue_182() {
let input = "tests/files/issue-182.png";
let (output, mut opts) = get_opts(Path::new(input));
opts.interlace = Some(Interlacing::Adam7);
opts.interlace = Some(true);
test_it_converts(
input,

View file

@ -30,7 +30,7 @@ fn test_it_converts(
let (output, mut opts) = get_opts(&input);
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.bit_depth, bit_depth_in);