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 \* Note that oxipng is not a brute-force optimizer. This means that while higher optimization levels
are almost always better or equal to lower levels, this is not guaranteed and it is possible in are almost always better or equal to lower levels, this is not guaranteed and it is possible in
rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli rare circumstances that a lower level may give a marginally smaller output. Similarly, using Zopfli
compression (`-Z`) is not guaranteed to always be better than without. compression (`-z`) is not guaranteed to always be better than without.
## Git integration via [pre-commit] ## Git integration via [pre-commit]

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

View file

@ -3,20 +3,18 @@
extern crate oxipng; extern crate oxipng;
extern crate test; extern crate test;
use std::{num::NonZeroU8, path::PathBuf}; use std::path::PathBuf;
use oxipng::{internal_tests::*, *}; use oxipng::{internal_tests::*, *};
use test::Bencher; use test::Bencher;
const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = NonZeroU8::new(15).unwrap();
#[bench] #[bench]
fn zopfli_16_bits_strategy_0(b: &mut Bencher) { fn zopfli_16_bits_strategy_0(b: &mut Bencher) {
let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"));
let png = PngData::new(&input, &Options::default()).unwrap(); let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| { b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
}); });
} }
@ -26,7 +24,7 @@ fn zopfli_8_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap(); let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| { b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
}); });
} }
@ -38,7 +36,7 @@ fn zopfli_4_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap(); let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| { b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
}); });
} }
@ -50,7 +48,7 @@ fn zopfli_2_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap(); let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| { b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
}); });
} }
@ -62,6 +60,6 @@ fn zopfli_1_bits_strategy_0(b: &mut Bencher) {
let png = PngData::new(&input, &Options::default()).unwrap(); let png = PngData::new(&input, &Options::default()).unwrap();
b.iter(|| { b.iter(|| {
zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); zopfli_deflate(png.raw.data.as_ref(), ZopfliOptions::default()).ok();
}); });
} }

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,7 @@
use std::num::NonZeroU8;
use crate::{PngError, PngResult}; use crate::{PngError, PngResult};
pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult<Vec<u8>> { pub fn deflate(data: &[u8], options: zopfli::Options) -> PngResult<Vec<u8>> {
let mut output = Vec::with_capacity(data.len()); let mut output = Vec::with_capacity(data.len());
let options = zopfli::Options {
iteration_count: iterations.into(),
..Default::default()
};
// Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size // Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size
// for some files. Wrapping the slice in another Read implementer such as Box fixes it for now. // for some files. Wrapping the slice in another Read implementer such as Box fixes it for now.
match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) { match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) {

View file

@ -2,20 +2,22 @@ use std::{error::Error, fmt};
use crate::colors::{BitDepth, ColorType}; use crate::colors::{BitDepth, ColorType};
#[derive(Debug, Clone)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum PngError { pub enum PngError {
DeflatedDataTooLong(usize),
TimedOut,
NotPNG,
APNGNotSupported,
APNGOutOfOrder, APNGOutOfOrder,
InvalidData,
TruncatedData,
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
IncorrectDataLength(usize, usize),
C2PAMetadataPreventsChanges, C2PAMetadataPreventsChanges,
ChunkMissing(&'static str),
CRCMismatch([u8; 4]),
DeflatedDataTooLong(usize),
IncorrectDataLength(usize, usize),
InflatedDataTooLong(usize),
InvalidData,
InvalidDepthForType(BitDepth, ColorType),
NotPNG,
ReadFailed(String, std::io::Error),
TruncatedData,
WriteFailed(String, std::io::Error),
Other(Box<str>), Other(Box<str>),
} }
@ -26,26 +28,32 @@ impl fmt::Display for PngError {
#[cold] #[cold]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match *self {
PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"),
PngError::TimedOut => f.write_str("timed out"),
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
PngError::TruncatedData => {
f.write_str("Missing data in the file; the file is truncated")
}
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"), PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
PngError::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"), PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
PngError::InvalidDepthForType(d, ref c) => { PngError::CRCMismatch(ref c) => write!(
write!(f, "Invalid bit depth {d} for color type {c}") f,
} "CRC mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(c)
),
PngError::DeflatedDataTooLong(_) => f.write_str("Deflated data too long"),
PngError::IncorrectDataLength(l1, l2) => write!( PngError::IncorrectDataLength(l1, l2) => write!(
f, f,
"Data length {l1} does not match the expected length {l2}" "Data length {l1} does not match the expected length {l2}"
), ),
PngError::C2PAMetadataPreventsChanges => f.write_str( PngError::InflatedDataTooLong(_) => f.write_str("Inflated data too long"),
"The image contains C2PA manifest that would be invalidated by any file changes", PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"),
), PngError::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")
}
PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"),
PngError::ReadFailed(ref s, ref e) => write!(f, "Failed to read from {s}: {e}"),
PngError::TruncatedData => {
f.write_str("Missing data in the file; the file is truncated")
}
PngError::WriteFailed(ref s, ref e) => write!(f, "Failed to write to {s}: {e}"),
PngError::Other(ref s) => f.write_str(s), PngError::Other(ref s) => f.write_str(s),
} }
} }

View file

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

View file

@ -3,12 +3,11 @@ use log::{debug, trace, warn};
use rgb::{RGB16, RGBA8}; use rgb::{RGB16, RGBA8};
use crate::{ use crate::{
Deflaters, Options, PngResult, Deflater, Options, PngResult,
colors::{BitDepth, ColorType}, colors::{BitDepth, ColorType},
deflate::{crc32, inflate}, deflate::{crc32, inflate},
display_chunks::DISPLAY_CHUNKS, display_chunks::DISPLAY_CHUNKS,
error::PngError, error::PngError,
interlace::Interlacing,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -22,8 +21,8 @@ pub struct IhdrData {
pub color_type: ColorType, pub color_type: ColorType,
/// The bit depth of the image /// The bit depth of the image
pub bit_depth: BitDepth, pub bit_depth: BitDepth,
/// The interlacing mode of the image /// Whether the image is interlaced
pub interlaced: Interlacing, pub interlaced: bool,
} }
impl IhdrData { impl IhdrData {
@ -45,7 +44,7 @@ impl IhdrData {
(w * bpp).div_ceil(8) * h (w * bpp).div_ceil(8) * h
} }
if self.interlaced == Interlacing::None { if !self.interlaced {
bitmap_size(bpp, w, h) + h bitmap_size(bpp, w, h) + h
} else { } else {
let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3); let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3);
@ -173,10 +172,7 @@ pub fn parse_next_chunk<'a>(
let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize]; let chunk_bytes = &byte_data[chunk_start..chunk_start + 4 + length as usize];
if !fix_errors && crc32(chunk_bytes) != crc { if !fix_errors && crc32(chunk_bytes) != crc {
return Err(PngError::new(&format!( return Err(PngError::CRCMismatch(chunk_name.try_into().unwrap()));
"CRC Mismatch in {} chunk; May be recoverable by using --fix",
String::from_utf8_lossy(chunk_name)
)));
} }
let name: [u8; 4] = chunk_name.try_into().unwrap(); let name: [u8; 4] = chunk_name.try_into().unwrap();
@ -209,12 +205,16 @@ pub fn parse_ihdr_chunk(
}, },
4 => ColorType::GrayscaleAlpha, 4 => ColorType::GrayscaleAlpha,
6 => ColorType::RGBA, 6 => ColorType::RGBA,
_ => return Err(PngError::new("Unexpected color type in header")), _ => return Err(PngError::InvalidData),
}, },
bit_depth: byte_data[8].try_into()?, bit_depth: byte_data[8].try_into()?,
width: read_be_u32(&byte_data[0..4]), width: read_be_u32(&byte_data[0..4]),
height: read_be_u32(&byte_data[4..8]), height: read_be_u32(&byte_data[4..8]),
interlaced: interlaced.try_into()?, interlaced: match interlaced {
0 => false,
1 => true,
_ => return Err(PngError::InvalidData),
},
}) })
} }
@ -223,7 +223,7 @@ fn palette_to_rgba(
palette_data: Option<Vec<u8>>, palette_data: Option<Vec<u8>>,
trns_data: Option<Vec<u8>>, trns_data: Option<Vec<u8>>,
) -> Result<Vec<RGBA8>, PngError> { ) -> Result<Vec<RGBA8>, PngError> {
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?; let palette_data = palette_data.ok_or(PngError::ChunkMissing("PLTE"))?;
let mut palette: Vec<_> = palette_data let mut palette: Vec<_> = palette_data
.chunks_exact(3) .chunks_exact(3)
.map(|color| RGBA8::new(color[0], color[1], color[2], 255)) .map(|color| RGBA8::new(color[0], color[1], color[2], 255))
@ -276,7 +276,7 @@ pub fn extract_icc(iccp: &Chunk) -> Option<Vec<u8>> {
} }
/// Make an iCCP chunk by compressing the ICC profile /// Make an iCCP chunk by compressing the ICC profile
pub fn make_iccp(icc: &[u8], deflater: Deflaters, max_size: Option<usize>) -> PngResult<Chunk> { pub fn make_iccp(icc: &[u8], deflater: Deflater, max_size: Option<usize>) -> PngResult<Chunk> {
let mut compressed = deflater.deflate(icc, max_size)?; let mut compressed = deflater.deflate(icc, max_size)?;
let mut data = Vec::with_capacity(compressed.len() + 5); let mut data = Vec::with_capacity(compressed.len() + 5);
data.extend(b"icc"); // Profile name - generally unused, can be anything data.extend(b"icc"); // Profile name - generally unused, can be anything
@ -348,7 +348,7 @@ pub fn preprocess_chunks(aux_chunks: &mut Vec<Chunk>, opts: &mut Options) {
} else if opts.idat_recoding { } else if opts.idat_recoding {
// Try recompressing the profile // Try recompressing the profile
let cur_len = aux_chunks[iccp_idx].data.len(); let cur_len = aux_chunks[iccp_idx].data.len();
if let Ok(iccp) = make_iccp(&icc, opts.deflate, Some(cur_len - 1)) { if let Ok(iccp) = make_iccp(&icc, opts.deflater, Some(cur_len - 1)) {
debug!( debug!(
"Recompressed iCCP chunk: {} ({} bytes decrease)", "Recompressed iCCP chunk: {} ({} bytes decrease)",
iccp.data.len(), iccp.data.len(),

View file

@ -1,42 +1,6 @@
use std::{fmt, fmt::Display};
use bitvec::prelude::*; use bitvec::prelude::*;
use crate::{PngError, headers::IhdrData, png::PngImage}; use crate::{headers::IhdrData, png::PngImage};
/// Whether to enable progressive rendering. See [`Options`][crate::Options])
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Interlacing {
/// Makes images load top to bottom.
None,
/// Makes it possible to render partially-loaded images at lower resolution. Usually increases file sizes.
Adam7,
}
impl TryFrom<u8> for Interlacing {
type Error = PngError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Adam7),
_ => Err(PngError::new("Unexpected interlacing in header")),
}
}
}
impl Display for Interlacing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(
match self {
Self::None => "non-interlaced",
Self::Adam7 => "interlaced",
},
f,
)
}
}
#[must_use] #[must_use]
pub fn interlace_image(png: &PngImage) -> PngImage { pub fn interlace_image(png: &PngImage) -> PngImage {
@ -89,7 +53,7 @@ pub fn interlace_image(png: &PngImage) -> PngImage {
data: output, data: output,
ihdr: IhdrData { ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(), color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::Adam7, interlaced: true,
..png.ihdr ..png.ihdr
}, },
} }
@ -103,7 +67,7 @@ pub fn deinterlace_image(png: &PngImage) -> PngImage {
}, },
ihdr: IhdrData { ihdr: IhdrData {
color_type: png.ihdr.color_type.clone(), color_type: png.ihdr.color_type.clone(),
interlaced: Interlacing::None, interlaced: false,
..png.ihdr ..png.ihdr
}, },
} }

View file

@ -41,13 +41,14 @@ use log::{debug, info, trace, warn};
use rayon::prelude::*; use rayon::prelude::*;
pub use rgb::{RGB16, RGBA8}; pub use rgb::{RGB16, RGBA8};
#[cfg(feature = "zopfli")]
pub use crate::deflate::ZopfliOptions;
pub use crate::{ pub use crate::{
colors::{BitDepth, ColorType}, colors::{BitDepth, ColorType},
deflate::Deflaters, deflate::Deflater,
error::PngError, error::PngError,
filters::{FilterStrategy, RowFilter}, filters::{FilterStrategy, RowFilter},
headers::StripChunks, headers::StripChunks,
interlace::Interlacing,
options::{InFile, Options, OutFile}, options::{InFile, Options, OutFile},
}; };
use crate::{ use crate::{
@ -130,7 +131,7 @@ impl RawImage {
height, height,
color_type, color_type,
bit_depth, bit_depth,
interlaced: Interlacing::None, interlaced: false,
}, },
data, data,
}), }),
@ -146,7 +147,7 @@ impl RawImage {
/// Add an ICC profile for the image /// Add an ICC profile for the image
pub fn add_icc_profile(&mut self, data: &[u8]) { pub fn add_icc_profile(&mut self, data: &[u8]) {
// Compress with fastest compression level - will be recompressed during optimization // Compress with fastest compression level - will be recompressed during optimization
let deflater = Deflaters::Libdeflater { compression: 1 }; let deflater = Deflater::Libdeflater { compression: 1 };
if let Ok(iccp) = make_iccp(data, deflater, None) { if let Ok(iccp) = make_iccp(data, deflater, None) {
self.aux_chunks.push(iccp); self.aux_chunks.push(iccp);
} }
@ -181,7 +182,9 @@ impl RawImage {
} }
/// Perform optimization on the input file using the options provided /// Perform optimization on the input file using the options provided
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<()> { ///
/// Returns the original and optimized file sizes
pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(usize, usize)> {
// Read in the file and try to decode as PNG. // Read in the file and try to decode as PNG.
info!("Processing: {input}"); info!("Processing: {input}");
@ -218,7 +221,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
let mut data = Vec::new(); let mut data = Vec::new();
stdin() stdin()
.read_to_end(&mut data) .read_to_end(&mut data)
.map_err(|e| PngError::new(&format!("Error reading stdin: {e}")))?; .map_err(|e| PngError::ReadFailed("stdin".into(), e))?;
data data
} }
}; };
@ -230,14 +233,14 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
let in_length = in_data.len(); let in_length = in_data.len();
if is_fully_optimized(in_data.len(), optimized_output.len(), opts) { if is_fully_optimized(in_length, optimized_output.len(), opts) {
match (output, input) { match (output, input) {
// If output path is None, it also means same as the input path // If output path is None, it also means same as the input path
(OutFile::Path { path, .. }, InFile::Path(input_path)) (OutFile::Path { path, .. }, InFile::Path(input_path))
if path.as_ref().is_none_or(|p| p == input_path) => if path.as_ref().is_none_or(|p| p == input_path) =>
{ {
info!("{input}: Could not optimize further, no change written"); info!("{input}: Could not optimize further, no change written");
return Ok(()); return Ok((in_length, in_length));
} }
_ => { _ => {
optimized_output = in_data; optimized_output = in_data;
@ -261,26 +264,21 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
match (output, input) { match (output, input) {
(OutFile::None, _) => { (OutFile::None, _) => {
info!("{savings}: Running in pretend mode, no output"); info!("{savings}: Dry run, no output");
} }
(&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => { (&OutFile::StdOut, _) | (&OutFile::Path { path: None, .. }, &InFile::StdIn) => {
let mut buffer = BufWriter::new(stdout()); let mut buffer = BufWriter::new(stdout());
buffer buffer
.write_all(&optimized_output) .write_all(&optimized_output)
.map_err(|e| PngError::new(&format!("Unable to write to stdout: {e}")))?; .map_err(|e| PngError::WriteFailed("stdout".into(), e))?;
} }
(OutFile::Path { path, .. }, _) => { (OutFile::Path { path, .. }, _) => {
let output_path = path let output_path = path
.as_ref() .as_ref()
.map(|p| p.as_path()) .map(|p| p.as_path())
.unwrap_or_else(|| input.path().unwrap()); .unwrap_or_else(|| input.path().unwrap());
let out_file = File::create(output_path).map_err(|err| { let out_file = File::create(output_path)
PngError::new(&format!( .map_err(|err| PngError::WriteFailed(output_path.display().to_string(), err))?;
"Unable to write to file {}: {}",
output_path.display(),
err
))
})?;
if let Some(metadata_input) = &opt_metadata_preserved { if let Some(metadata_input) = &opt_metadata_preserved {
copy_permissions(metadata_input, &out_file)?; copy_permissions(metadata_input, &out_file)?;
} }
@ -290,13 +288,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
.write_all(&optimized_output) .write_all(&optimized_output)
// flush BufWriter so IO errors don't get swallowed silently on close() by drop! // flush BufWriter so IO errors don't get swallowed silently on close() by drop!
.and_then(|()| buffer.flush()) .and_then(|()| buffer.flush())
.map_err(|e| { .map_err(|e| PngError::WriteFailed(output_path.display().to_string(), e))?;
PngError::new(&format!(
"Unable to write to {}: {}",
output_path.display(),
e
))
})?;
// force drop and thereby closing of file handle before modifying any timestamp // force drop and thereby closing of file handle before modifying any timestamp
std::mem::drop(buffer); std::mem::drop(buffer);
if let Some(metadata_input) = &opt_metadata_preserved { if let Some(metadata_input) = &opt_metadata_preserved {
@ -305,7 +297,7 @@ pub fn optimize(input: &InFile, output: &OutFile, opts: &Options) -> PngResult<(
info!("{}: {}", savings, output_path.display()); info!("{}: {}", savings, output_path.display());
} }
} }
Ok(()) Ok((in_length, optimized_output.len()))
} }
/// Perform optimization on the input file using the options provided, where the file is already /// Perform optimization on the input file using the options provided, where the file is already
@ -395,7 +387,7 @@ fn optimize_png(
); );
} }
if opts.interlace == Some(Interlacing::Adam7) && png.raw.ihdr.interlaced != Interlacing::Adam7 { if opts.interlace == Some(true) && !png.raw.ihdr.interlaced {
warn!( warn!(
"Interlacing was not enabled as it would result in a larger file. To override this, use `--force`." "Interlacing was not enabled as it would result in a larger file. To override this, use `--force`."
); );
@ -420,16 +412,16 @@ fn optimize_raw(
// 8 is a little slower but not noticeably when used only for reductions (o3 and higher) // 8 is a little slower but not noticeably when used only for reductions (o3 and higher)
// 9 is not appreciably better than 8 // 9 is not appreciably better than 8
// 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level // 10 and higher are quite slow - good for filters but only good for reductions if matching the main zc level
let compression = match opts.deflate { let compression = match opts.deflater {
Deflaters::Libdeflater { compression } => { Deflater::Libdeflater { compression } => {
if opts.fast_evaluation { 7 } else { 8 }.min(compression) if opts.fast_evaluation { 7 } else { 8 }.min(compression)
} }
_ => 8, _ => 8,
}; };
let eval_deflater = Deflaters::Libdeflater { compression }; let eval_deflater = Deflater::Libdeflater { compression };
// If only one filter is selected, use this for evaluations // If only one filter is selected, use this for evaluations
let eval_filters = if opts.filter.len() == 1 { let eval_filters = if opts.filters.len() == 1 {
opts.filter.clone() opts.filters.clone()
} else { } else {
// None and Bigrams work well together, especially for alpha reductions // None and Bigrams work well together, especially for alpha reductions
indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams} indexset! {FilterStrategy::NONE, FilterStrategy::Bigrams}
@ -440,7 +432,7 @@ fn optimize_raw(
eval_filters.clone(), eval_filters.clone(),
eval_deflater, eval_deflater,
false, false,
opts.deflate == eval_deflater, opts.deflater == eval_deflater,
); );
let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval); let mut new_image = perform_reductions(image.clone(), opts, &deadline, &eval);
let eval_result = eval.get_best_candidate(); let eval_result = eval.get_best_candidate();
@ -465,7 +457,7 @@ fn optimize_raw(
eval_filters, eval_filters,
eval_deflater, eval_deflater,
); );
(result?, opts.deflate) (result?, opts.deflater)
} else { } else {
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline, // If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
// we should still check if the evaluator compressed the baseline smaller than the original. // we should still check if the evaluator compressed the baseline smaller than the original.
@ -490,9 +482,9 @@ fn perform_trials(
max_size: Option<usize>, max_size: Option<usize>,
mut eval_result: Option<Candidate>, mut eval_result: Option<Candidate>,
eval_filters: IndexSet<FilterStrategy>, eval_filters: IndexSet<FilterStrategy>,
eval_deflater: Deflaters, eval_deflater: Deflater,
) -> Option<Candidate> { ) -> Option<Candidate> {
let mut filters = opts.filter.clone(); let mut filters = opts.filters.clone();
let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some()); let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_result.is_some());
if fast_eval { if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial // Perform a fast evaluation of selected filters followed by a single main compression trial
@ -509,7 +501,7 @@ fn perform_trials(
filters, filters,
eval_deflater, eval_deflater,
opts.optimize_alpha, opts.optimize_alpha,
opts.deflate == eval_deflater, opts.deflater == eval_deflater,
); );
if let Some(result) = &eval_result { if let Some(result) = &eval_result {
eval.set_best_size(result.estimated_output_size); eval.set_best_size(result.estimated_output_size);
@ -525,9 +517,9 @@ fn perform_trials(
if result.idat_data.is_none() { if result.idat_data.is_none() {
// Compress with the main deflater // Compress with the main deflater
debug!("Trying filter {} with {}", result.filter, opts.deflate); debug!("Trying filter {} with {}", result.filter, opts.deflater);
let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha); let (data, _) = image.filter_image(result.filter_used.clone(), opts.optimize_alpha);
match opts.deflate.deflate(&data, max_size) { match opts.deflater.deflate(&data, max_size) {
Ok(idat_data) => { Ok(idat_data) => {
result.estimated_output_size = result.image.estimated_output_size(&idat_data); result.estimated_output_size = result.image.estimated_output_size(&idat_data);
result.idat_data = Some(idat_data); result.idat_data = Some(idat_data);
@ -555,8 +547,8 @@ fn perform_trials(
} }
} }
debug!("Trying {} filters with {}", filters.len(), opts.deflate); debug!("Trying {} filters with {}", filters.len(), opts.deflater);
let eval = Evaluator::new(deadline, filters, opts.deflate, opts.optimize_alpha, true); let eval = Evaluator::new(deadline, filters, opts.deflater, opts.optimize_alpha, true);
if let Some(max_size) = max_size { if let Some(max_size) = max_size {
eval.set_best_size(max_size); eval.set_best_size(max_size);
} }
@ -616,9 +608,14 @@ impl Deadline {
/// Display the format of the image data /// Display the format of the image data
fn report_format(prefix: &str, png: &PngImage) { fn report_format(prefix: &str, png: &PngImage) {
let interlaced = if png.ihdr.interlaced {
"interlaced"
} else {
"non-interlaced"
};
debug!( debug!(
"{}{}-bit {}, {}", "{}{}-bit {}, {}",
prefix, png.ihdr.bit_depth, png.ihdr.color_type, png.ihdr.interlaced prefix, png.ihdr.bit_depth, png.ihdr.color_type, interlaced
); );
} }
@ -648,7 +645,7 @@ fn recompress_frames(
let image = PngImage::new(ihdr, &frame.data)?; let image = PngImage::new(ihdr, &frame.data)?;
let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha); let (filtered, _) = image.filter_image(filter.clone(), opts.optimize_alpha);
let max_size = Some(frame.data.len() - 1); let max_size = Some(frame.data.len() - 1);
if let Ok(data) = opts.deflate.deflate(&filtered, max_size) { if let Ok(data) = opts.deflater.deflate(&filtered, max_size) {
debug!( debug!(
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)", "Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
i, i,
@ -671,7 +668,7 @@ fn copy_permissions(metadata_input: &Metadata, out_file: &File) -> PngResult<()>
.set_permissions(metadata_input.permissions()) .set_permissions(metadata_input.permissions())
.map_err(|err_io| { .map_err(|err_io| {
PngError::new(&format!( PngError::new(&format!(
"unable to set permissions for output file: {err_io}" "Unable to set permissions for output file: {err_io}"
)) ))
}) })
} }
@ -683,12 +680,11 @@ fn copy_times(_: &Metadata, _: &Path) -> PngResult<()> {
#[cfg(feature = "filetime")] #[cfg(feature = "filetime")]
fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> { fn copy_times(input_path_meta: &Metadata, out_path: &Path) -> PngResult<()> {
let atime = filetime::FileTime::from_last_access_time(input_path_meta);
let mtime = filetime::FileTime::from_last_modification_time(input_path_meta); let mtime = filetime::FileTime::from_last_modification_time(input_path_meta);
trace!("attempting to set file times: atime: {atime:?}, mtime: {mtime:?}"); trace!("attempting to set file modification time: {mtime:?}");
filetime::set_file_times(out_path, atime, mtime).map_err(|err_io| { filetime::set_file_mtime(out_path, mtime).map_err(|err_io| {
PngError::new(&format!( PngError::new(&format!(
"unable to set file times on {out_path:?}: {err_io}" "Unable to set file times on {out_path:?}: {err_io}"
)) ))
}) })
} }

View file

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

View file

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

View file

@ -1,9 +1,4 @@
use std::{ use std::{fs, io::Write, path::Path, sync::Arc};
fs::File,
io::{BufReader, Read, Write},
path::Path,
sync::Arc,
};
use bitvec::bitarr; use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor}; use libdeflater::{CompressionLvl, Compressor};
@ -19,7 +14,7 @@ use crate::{
error::PngError, error::PngError,
filters::*, filters::*,
headers::*, headers::*,
interlace::{Interlacing, deinterlace_image, interlace_image}, interlace::{deinterlace_image, interlace_image},
}; };
pub(crate) mod scan_lines; pub(crate) mod scan_lines;
@ -62,28 +57,7 @@ impl PngData {
} }
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> { pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
let file = match File::open(filepath) { fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
Ok(f) => f,
Err(_) => return Err(PngError::new("Failed to open file for reading")),
};
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
let mut reader = BufReader::new(file);
// Check file for PNG header
let mut header = [0; 8];
if reader.read_exact(&mut header).is_err() {
return Err(PngError::new("Not a PNG file: too small"));
}
if !file_header_is_valid(&header) {
return Err(PngError::new("Invalid PNG header detected"));
}
// Read raw png data into memory
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
byte_data.extend_from_slice(&header);
match reader.read_to_end(&mut byte_data) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to read from file")),
}
Ok(byte_data)
} }
/// Create a new `PngData` struct by reading a slice /// Create a new `PngData` struct by reading a slice
@ -293,18 +267,17 @@ impl PngImage {
Ok(image) Ok(image)
} }
/// Convert the image to the specified interlacing type /// Enable or disable interlacing
/// Returns true if the interlacing was changed, false otherwise /// Returns the new image if the interlacing was changed, None otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered /// Assumes that the data has already been de-filtered
#[inline] #[inline]
#[must_use] #[must_use]
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<Self> { pub fn change_interlacing(&self, interlace: bool) -> Option<Self> {
if interlace == self.ihdr.interlaced { if interlace == self.ihdr.interlaced {
return None; return None;
} }
Some(if interlace == Interlacing::Adam7 { Some(if interlace {
// Convert progressive to interlaced data // Convert progressive to interlaced data
interlace_image(self) interlace_image(self)
} else { } else {

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

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ColorType, Deadline, Deflaters, Options, evaluate::Evaluator, png::PngImage}; use crate::{ColorType, Deadline, Deflater, Options, evaluate::Evaluator, png::PngImage};
pub mod alpha; pub mod alpha;
use crate::alpha::*; use crate::alpha::*;
@ -21,8 +21,8 @@ pub(crate) fn perform_reductions(
// At low compression levels, skip some transformations which are less likely to be effective // At low compression levels, skip some transformations which are less likely to be effective
// This currently affects optimization presets 0-2 // This currently affects optimization presets 0-2
let cheap = match opts.deflate { let cheap = match opts.deflater {
Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation, Deflater::Libdeflater { compression } => compression < 12 && opts.fast_evaluation,
_ => false, _ => false,
}; };

View file

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

View file

@ -14,7 +14,6 @@ const RGBA: u8 = 6;
fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options { let options = oxipng::Options {
force: true, force: true,
filter: indexset! {FilterStrategy::NONE},
..Default::default() ..Default::default()
}; };
(OutFile::from_path(input.with_extension("out.png")), options) (OutFile::from_path(input.with_extension("out.png")), options)
@ -32,8 +31,7 @@ fn test_it_converts(
let (output, mut opts) = get_opts(&input); let (output, mut opts) = get_opts(&input);
let png = PngData::new(&input, &opts).unwrap(); let png = PngData::new(&input, &opts).unwrap();
opts.filter = IndexSet::new(); opts.filters = indexset! {filter};
opts.filter.insert(filter);
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);

View file

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

View file

@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options { let options = oxipng::Options {
force: true, force: true,
fast_evaluation: false, fast_evaluation: false,
filter: indexset! {FilterStrategy::NONE}, filters: indexset! {FilterStrategy::NONE},
interlace: None, interlace: None,
..Default::default() ..Default::default()
}; };
@ -35,7 +35,7 @@ fn test_it_converts(
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7); assert!(png.raw.ihdr.interlaced);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (), Ok(_) => (),

View file

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

View file

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

View file

@ -15,7 +15,7 @@ fn get_opts(input: &Path) -> (OutFile, oxipng::Options) {
let options = oxipng::Options { let options = oxipng::Options {
force: true, force: true,
fast_evaluation: false, fast_evaluation: false,
filter: indexset! {FilterStrategy::NONE}, filters: indexset! {FilterStrategy::NONE},
..Default::default() ..Default::default()
}; };
(OutFile::from_path(input.with_extension("out.png")), options) (OutFile::from_path(input.with_extension("out.png")), options)
@ -36,7 +36,7 @@ fn test_it_converts(
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken");
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None); assert!(!png.raw.ihdr.interlaced);
match oxipng::optimize(&InFile::Path(input), &output, &opts) { match oxipng::optimize(&InFile::Path(input), &output, &opts) {
Ok(_) => (), Ok(_) => (),

View file

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

View file

@ -30,7 +30,7 @@ fn test_it_converts(
let (output, mut opts) = get_opts(&input); let (output, mut opts) = get_opts(&input);
let png = PngData::new(&input, &opts).unwrap(); let png = PngData::new(&input, &opts).unwrap();
opts.filter = indexset! {filter}; opts.filters = indexset! {filter};
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);