diff --git a/README.md b/README.md index 9c41e644..7e3a213f 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/benches/interlacing.rs b/benches/interlacing.rs index 055734bc..fbe6467d 100644 --- a/benches/interlacing.rs +++ b/benches/interlacing.rs @@ -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)); } diff --git a/benches/zopfli.rs b/benches/zopfli.rs index 345a71ab..a189a35f 100644 --- a/benches/zopfli.rs +++ b/benches/zopfli.rs @@ -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(); }); } diff --git a/src/cli.rs b/src/cli.rs index 800da204..8977cf99 100644 --- a/src/cli.rs +++ b/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"); @@ -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 is one of: +Set the PNG interlacing mode, where 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( diff --git a/src/colors.rs b/src/colors.rs index a5365d2c..e1c49724 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -116,7 +116,7 @@ impl TryFrom for BitDepth { 4 => Ok(Self::Four), 8 => Ok(Self::Eight), 16 => Ok(Self::Sixteen), - _ => Err(PngError::new("Unexpected bit depth")), + _ => Err(PngError::InvalidData), } } } diff --git a/src/deflate/deflater.rs b/src/deflate/deflater.rs index 9ba19f59..1133a44c 100644 --- a/src/deflate/deflater.rs +++ b/src/deflate/deflater.rs @@ -22,7 +22,7 @@ pub fn inflate(data: &[u8], out_size: usize) -> PngResult> { .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) diff --git a/src/deflate/mod.rs b/src/deflate/mod.rs index b8b4ceab..1ae49bcb 100644 --- a/src/deflate/mod.rs +++ b/src/deflate/mod.rs @@ -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) -> PngResult> { 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), } } } diff --git a/src/deflate/zopfli_oxipng.rs b/src/deflate/zopfli_oxipng.rs index b77e3164..c66a0c65 100644 --- a/src/deflate/zopfli_oxipng.rs +++ b/src/deflate/zopfli_oxipng.rs @@ -1,13 +1,7 @@ -use std::num::NonZeroU8; - use crate::{PngError, PngResult}; -pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult> { +pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult> { 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) { diff --git a/src/error.rs b/src/error.rs index 2c7d88a9..0d022812 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } @@ -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), } } diff --git a/src/evaluate.rs b/src/evaluate.rs index 9463cf6b..d5c38e85 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -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, filters: IndexSet, - deflater: Deflaters, + deflater: Deflater, optimize_alpha: bool, final_round: bool, nth: AtomicUsize, @@ -68,7 +68,7 @@ impl Evaluator { pub fn new( deadline: Arc, filters: IndexSet, - deflater: Deflaters, + deflater: Deflater, optimize_alpha: bool, final_round: bool, ) -> Self { diff --git a/src/headers.rs b/src/headers.rs index db48f15b..3e3f6a99 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -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>, trns_data: Option>, ) -> Result, 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> { } /// Make an iCCP chunk by compressing the ICC profile -pub fn make_iccp(icc: &[u8], deflater: Deflaters, max_size: Option) -> PngResult { +pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option) -> PngResult { 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, 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(), diff --git a/src/interlace.rs b/src/interlace.rs index 7b4f2f5e..23f0dd27 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -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 for Interlacing { - type Error = PngError; - - fn try_from(value: u8) -> Result { - 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 }, } diff --git a/src/lib.rs b/src/lib.rs index d11c3ae3..edbf33db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, mut eval_result: Option, eval_filters: IndexSet, - eval_deflater: Deflaters, + eval_deflater: Deflater, ) -> Option { - 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}" )) }) } diff --git a/src/main.rs b/src/main.rs index 17909628..f2ee9e3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::>("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::("interlace") { - opts.interlace = if x == "keep" { - None - } else { - x.parse::().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::("iterations").unwrap(); - opts.deflate = Deflaters::Zopfli { - iterations: NonZeroU8::new(iterations as u8).unwrap(), - }; + let iteration_count = *matches.get_one::("iterations").unwrap(); + let iterations_without_improvement = *matches + .get_one::("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::("compression")) + if let (Deflater::Libdeflater { compression }, Some(x)) = + (&mut opts.deflater, matches.get_one::("compression")) { *compression = *x as u8; } diff --git a/src/options.rs b/src/options.rs index 48f30141..a712c5dd 100644 --- a/src/options.rs +++ b/src/options.rs @@ -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, - /// Whether to change the interlacing type of the file. + pub filters: IndexSet, + /// 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, + /// Default: `Some(false)` + pub interlace: Option, /// 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, } diff --git a/src/png/mod.rs b/src/png/mod.rs index c13c761e..e0f0acae 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -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, 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 = 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 { + pub fn change_interlacing(&self, interlace: bool) -> Option { if interlace == self.ihdr.interlaced { return None; } - Some(if interlace == Interlacing::Adam7 { + Some(if interlace { // Convert progressive to interlaced data interlace_image(self) } else { diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index e91133a5..e19bafc8 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -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 diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 132ae802..c8444fff 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -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, }; diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index 0d07d986..dca9b643 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -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 { #[must_use] pub fn sorted_palette_mzeng(png: &PngImage) -> Option { // 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 { #[must_use] pub fn sorted_palette_battiato(png: &PngImage) -> Option { // 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 { diff --git a/tests/filters.rs b/tests/filters.rs index 3d696360..4f6f0025 100644 --- a/tests/filters.rs +++ b/tests/filters.rs @@ -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); diff --git a/tests/flags.rs b/tests/flags.rs index 4baa619b..de6afbde 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -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, diff --git a/tests/interlaced.rs b/tests/interlaced.rs index b9007d9b..9fac6038 100644 --- a/tests/interlaced.rs +++ b/tests/interlaced.rs @@ -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(_) => (), diff --git a/tests/interlacing.rs b/tests/interlacing.rs index ea7fd5a9..83bfcd7f 100644 --- a/tests/interlacing.rs +++ b/tests/interlacing.rs @@ -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, diff --git a/tests/raw.rs b/tests/raw.rs index d50a58e1..155f415a 100644 --- a/tests/raw.rs +++ b/tests/raw.rs @@ -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() } } diff --git a/tests/reduction.rs b/tests/reduction.rs index 6ba3236d..bd362dda 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -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(_) => (), diff --git a/tests/regression.rs b/tests/regression.rs index 89756c29..16b0992e 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -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, diff --git a/tests/strategies.rs b/tests/strategies.rs index 264e6983..de4db2c3 100644 --- a/tests/strategies.rs +++ b/tests/strategies.rs @@ -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);