diff --git a/src/lib.rs b/src/lib.rs index d4046583..35b7489d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,10 +34,9 @@ use crate::reduction::*; use log::{debug, info, trace, warn}; use rayon::prelude::*; use std::borrow::Cow; -use std::fmt; use std::fs::{File, Metadata}; use std::io::{stdin, stdout, BufWriter, Read, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -48,6 +47,7 @@ pub use crate::error::PngError; pub use crate::filters::RowFilter; pub use crate::headers::StripChunks; pub use crate::interlace::Interlacing; +pub use crate::options::{InFile, Options, OutFile}; pub use indexmap::{indexset, IndexSet}; pub use rgb::{RGB16, RGBA8}; @@ -59,6 +59,7 @@ mod evaluate; mod filters; mod headers; mod interlace; +mod options; mod png; mod reduction; #[cfg(feature = "sanity-checks")] @@ -75,250 +76,8 @@ pub mod internal_tests { pub use crate::sanity_checks::*; } -#[derive(Clone, Debug)] -pub enum OutFile { - /// Don't actually write any output, just calculate the best results. - None, - /// Write output to a file. - /// - /// * `path`: Path to write the output file. `None` means same as input. - /// * `preserve_attrs`: Ensure the output file has the same permissions & timestamps as the input file. - Path { - path: Option, - preserve_attrs: bool, - }, - /// Write to standard output. - StdOut, -} - -impl OutFile { - /// Construct a new `OutFile` with the given path. - /// - /// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`. - pub fn from_path(path: PathBuf) -> Self { - OutFile::Path { - path: Some(path), - preserve_attrs: false, - } - } - - pub fn path(&self) -> Option<&Path> { - match *self { - OutFile::Path { - path: Some(ref p), .. - } => Some(p.as_path()), - _ => None, - } - } -} - -/// Where to read images from -#[derive(Clone, Debug)] -pub enum InFile { - Path(PathBuf), - StdIn, -} - -impl InFile { - pub fn path(&self) -> Option<&Path> { - match *self { - InFile::Path(ref p) => Some(p.as_path()), - InFile::StdIn => None, - } - } -} - -impl fmt::Display for InFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - InFile::Path(ref p) => write!(f, "{}", p.display()), - InFile::StdIn => f.write_str("stdin"), - } - } -} - -impl> From for InFile { - fn from(s: T) -> Self { - InFile::Path(s.into()) - } -} - pub type PngResult = Result; -#[derive(Clone, Debug)] -/// Options controlling the output of the `optimize` function -pub struct Options { - /// Attempt to fix errors when decoding the input file rather than returning an `Err`. - /// - /// Default: `false` - pub fix_errors: bool, - /// Write to output even if there was no improvement in compression. - /// - /// Default: `false` - pub force: bool, - /// Which RowFilters to try on the file - /// - /// Default: `None,Sub,Entropy,Bigrams` - pub filter: IndexSet, - /// Whether to change the interlacing type of the file. - /// - /// `None` will not change the current interlacing type. - /// - /// `Some(x)` will change the file to interlacing mode `x`. - /// - /// Default: `Some(Interlacing::None)` - pub interlace: Option, - /// Whether to allow transparent pixels to be altered to improve compression. - pub optimize_alpha: bool, - /// Whether to attempt bit depth reduction - /// - /// Default: `true` - pub bit_depth_reduction: bool, - /// Whether to attempt color type reduction - /// - /// Default: `true` - pub color_type_reduction: bool, - /// Whether to attempt palette reduction - /// - /// Default: `true` - pub palette_reduction: bool, - /// Whether to attempt grayscale reduction - /// - /// Default: `true` - pub grayscale_reduction: bool, - /// Whether to perform recoding of IDAT and other compressed chunks - /// - /// If any type of reduction is performed, IDAT recoding will be performed - /// regardless of this setting - /// - /// Default: `true` - pub idat_recoding: bool, - /// Whether to forcibly reduce 16-bit to 8-bit by scaling - /// - /// Default: `false` - pub scale_16: bool, - /// Which chunks to strip from the PNG file, if any - /// - /// Default: `None` - pub strip: StripChunks, - /// Which DEFLATE algorithm to use - /// - /// Default: `Libdeflater` - pub deflate: Deflaters, - /// Whether to use fast evaluation to pick the best filter - /// - /// Default: `true` - pub fast_evaluation: bool, - - /// Maximum amount of time to spend on optimizations. - /// Further potential optimizations are skipped if the timeout is exceeded. - pub timeout: Option, -} - -impl Options { - pub fn from_preset(level: u8) -> Options { - let opts = Options::default(); - match level { - 0 => opts.apply_preset_0(), - 1 => opts.apply_preset_1(), - 2 => opts.apply_preset_2(), - 3 => opts.apply_preset_3(), - 4 => opts.apply_preset_4(), - 5 => opts.apply_preset_5(), - 6 => opts.apply_preset_6(), - _ => { - warn!("Level 7 and above don't exist yet and are identical to level 6"); - opts.apply_preset_6() - } - } - } - - pub fn max_compression() -> Options { - Options::from_preset(6) - } - - // 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 { - *compression = 5; - } - self - } - - fn apply_preset_1(mut self) -> Self { - self.filter.clear(); - if let Deflaters::Libdeflater { compression } = &mut self.deflate { - *compression = 10; - } - self - } - - fn apply_preset_2(self) -> Self { - self - } - - fn apply_preset_3(mut self) -> Self { - self.fast_evaluation = false; - self.filter = indexset! { - RowFilter::None, - RowFilter::Bigrams, - RowFilter::BigEnt, - RowFilter::Brute - }; - self - } - - fn apply_preset_4(mut self) -> Self { - if let Deflaters::Libdeflater { compression } = &mut self.deflate { - *compression = 12; - } - self.apply_preset_3() - } - - fn apply_preset_5(mut self) -> Self { - self.fast_evaluation = false; - self.filter.insert(RowFilter::Up); - self.filter.insert(RowFilter::MinSum); - self.filter.insert(RowFilter::BigEnt); - self.filter.insert(RowFilter::Brute); - if let Deflaters::Libdeflater { compression } = &mut self.deflate { - *compression = 12; - } - self - } - - fn apply_preset_6(mut self) -> Self { - self.filter.insert(RowFilter::Average); - self.filter.insert(RowFilter::Paeth); - self.apply_preset_5() - } -} - -impl Default for Options { - fn default() -> Options { - // Default settings based on -o 2 from the CLI interface - Options { - fix_errors: false, - force: false, - filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, - interlace: Some(Interlacing::None), - optimize_alpha: false, - bit_depth_reduction: true, - color_type_reduction: true, - palette_reduction: true, - grayscale_reduction: true, - idat_recoding: true, - scale_16: false, - strip: StripChunks::None, - deflate: Deflaters::Libdeflater { compression: 11 }, - fast_evaluation: true, - timeout: None, - } - } -} - #[derive(Debug)] /// A raw image definition which can be used to create an optimized png pub struct RawImage { @@ -660,8 +419,13 @@ fn optimize_raw( } _ => 8, }; - // None and Bigrams work well together, especially for alpha reductions - let eval_filters = indexset! {RowFilter::None, RowFilter::Bigrams}; + // If only one filter is selected, use this for evaluations + let eval_filters = if opts.filter.len() == 1 { + opts.filter.clone() + } else { + // None and Bigrams work well together, especially for alpha reductions + indexset! {RowFilter::None, RowFilter::Bigrams} + }; // This will collect all versions of images and pick one that compresses best let eval = Evaluator::new( deadline.clone(), diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 00000000..0523f76f --- /dev/null +++ b/src/options.rs @@ -0,0 +1,252 @@ +use log::warn; +use std::fmt; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use crate::deflate::Deflaters; +use crate::filters::RowFilter; +use crate::headers::StripChunks; +use crate::interlace::Interlacing; +use indexmap::{indexset, IndexSet}; + +#[derive(Clone, Debug)] +pub enum OutFile { + /// Don't actually write any output, just calculate the best results. + None, + /// Write output to a file. + /// + /// * `path`: Path to write the output file. `None` means same as input. + /// * `preserve_attrs`: Ensure the output file has the same permissions & timestamps as the input file. + Path { + path: Option, + preserve_attrs: bool, + }, + /// Write to standard output. + StdOut, +} + +impl OutFile { + /// Construct a new `OutFile` with the given path. + /// + /// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`. + pub fn from_path(path: PathBuf) -> Self { + OutFile::Path { + path: Some(path), + preserve_attrs: false, + } + } + + pub fn path(&self) -> Option<&Path> { + match *self { + OutFile::Path { + path: Some(ref p), .. + } => Some(p.as_path()), + _ => None, + } + } +} + +/// Where to read images from +#[derive(Clone, Debug)] +pub enum InFile { + Path(PathBuf), + StdIn, +} + +impl InFile { + pub fn path(&self) -> Option<&Path> { + match *self { + InFile::Path(ref p) => Some(p.as_path()), + InFile::StdIn => None, + } + } +} + +impl fmt::Display for InFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + InFile::Path(ref p) => write!(f, "{}", p.display()), + InFile::StdIn => f.write_str("stdin"), + } + } +} + +impl> From for InFile { + fn from(s: T) -> Self { + InFile::Path(s.into()) + } +} + +#[derive(Clone, Debug)] +/// Options controlling the output of the `optimize` function +pub struct Options { + /// Attempt to fix errors when decoding the input file rather than returning an `Err`. + /// + /// Default: `false` + pub fix_errors: bool, + /// Write to output even if there was no improvement in compression. + /// + /// Default: `false` + pub force: bool, + /// Which RowFilters to try on the file + /// + /// Default: `None,Sub,Entropy,Bigrams` + pub filter: IndexSet, + /// Whether to change the interlacing type of the file. + /// + /// `None` will not change the current interlacing type. + /// + /// `Some(x)` will change the file to interlacing mode `x`. + /// + /// Default: `Some(Interlacing::None)` + pub interlace: Option, + /// Whether to allow transparent pixels to be altered to improve compression. + pub optimize_alpha: bool, + /// Whether to attempt bit depth reduction + /// + /// Default: `true` + pub bit_depth_reduction: bool, + /// Whether to attempt color type reduction + /// + /// Default: `true` + pub color_type_reduction: bool, + /// Whether to attempt palette reduction + /// + /// Default: `true` + pub palette_reduction: bool, + /// Whether to attempt grayscale reduction + /// + /// Default: `true` + pub grayscale_reduction: bool, + /// Whether to perform recoding of IDAT and other compressed chunks + /// + /// If any type of reduction is performed, IDAT recoding will be performed + /// regardless of this setting + /// + /// Default: `true` + pub idat_recoding: bool, + /// Whether to forcibly reduce 16-bit to 8-bit by scaling + /// + /// Default: `false` + pub scale_16: bool, + /// Which chunks to strip from the PNG file, if any + /// + /// Default: `None` + pub strip: StripChunks, + /// Which DEFLATE algorithm to use + /// + /// Default: `Libdeflater` + pub deflate: Deflaters, + /// Whether to use fast evaluation to pick the best filter + /// + /// Default: `true` + pub fast_evaluation: bool, + + /// Maximum amount of time to spend on optimizations. + /// Further potential optimizations are skipped if the timeout is exceeded. + pub timeout: Option, +} + +impl Options { + pub fn from_preset(level: u8) -> Options { + let opts = Options::default(); + match level { + 0 => opts.apply_preset_0(), + 1 => opts.apply_preset_1(), + 2 => opts.apply_preset_2(), + 3 => opts.apply_preset_3(), + 4 => opts.apply_preset_4(), + 5 => opts.apply_preset_5(), + 6 => opts.apply_preset_6(), + _ => { + warn!("Level 7 and above don't exist yet and are identical to level 6"); + opts.apply_preset_6() + } + } + } + + pub fn max_compression() -> Options { + Options::from_preset(6) + } + + // 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 { + *compression = 5; + } + self + } + + fn apply_preset_1(mut self) -> Self { + self.filter.clear(); + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + *compression = 10; + } + self + } + + fn apply_preset_2(self) -> Self { + self + } + + fn apply_preset_3(mut self) -> Self { + self.fast_evaluation = false; + self.filter = indexset! { + RowFilter::None, + RowFilter::Bigrams, + RowFilter::BigEnt, + RowFilter::Brute + }; + self + } + + fn apply_preset_4(mut self) -> Self { + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + *compression = 12; + } + self.apply_preset_3() + } + + fn apply_preset_5(mut self) -> Self { + self.fast_evaluation = false; + self.filter.insert(RowFilter::Up); + self.filter.insert(RowFilter::MinSum); + self.filter.insert(RowFilter::BigEnt); + self.filter.insert(RowFilter::Brute); + if let Deflaters::Libdeflater { compression } = &mut self.deflate { + *compression = 12; + } + self + } + + fn apply_preset_6(mut self) -> Self { + self.filter.insert(RowFilter::Average); + self.filter.insert(RowFilter::Paeth); + self.apply_preset_5() + } +} + +impl Default for Options { + fn default() -> Options { + // Default settings based on -o 2 from the CLI interface + Options { + fix_errors: false, + force: false, + filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, + interlace: Some(Interlacing::None), + optimize_alpha: false, + bit_depth_reduction: true, + color_type_reduction: true, + palette_reduction: true, + grayscale_reduction: true, + idat_recoding: true, + scale_16: false, + strip: StripChunks::None, + deflate: Deflaters::Libdeflater { compression: 11 }, + fast_evaluation: true, + timeout: None, + } + } +} diff --git a/tests/files/grayscale_8_should_be_grayscale_4.png b/tests/files/grayscale_8_should_be_grayscale_4.png index e5e62cb1..13537e8e 100644 Binary files a/tests/files/grayscale_8_should_be_grayscale_4.png and b/tests/files/grayscale_8_should_be_grayscale_4.png differ