oxipng/src/options.rs
andrews05 14532e8bf5
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).
2025-09-20 17:16:22 +02:00

268 lines
7.6 KiB
Rust

use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use indexmap::{IndexSet, indexset};
use log::warn;
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.
#[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<PathBuf>,
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 }`.
#[must_use]
pub fn from_path(path: PathBuf) -> Self {
OutFile::Path {
path: Some(path),
preserve_attrs: false,
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path {
path: Some(ref p), ..
} => Some(p.as_path()),
_ => None,
}
}
}
/// Where to read images from in [`optimize`][crate::optimize].
/// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O.
#[derive(Clone, Debug)]
pub enum InFile {
Path(PathBuf),
StdIn,
}
impl InFile {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
Self::Path(ref p) => Some(p.as_path()),
Self::StdIn => None,
}
}
}
impl fmt::Display for InFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Path(ref p) => write!(f, "{}", p.display()),
Self::StdIn => f.write_str("stdin"),
}
}
}
impl<T: Into<PathBuf>> From<T> for InFile {
fn from(s: T) -> Self {
Self::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 `FilterStrategy` to try on the file
///
/// Default: `None,Sub,Entropy,Bigrams`
pub filters: IndexSet<FilterStrategy>,
/// Whether to change the interlacing of the file.
///
/// - `None` will not change the current interlacing.
/// - `Some(x)` will turn interlacing on or off.
///
/// Default: `Some(false)`
pub interlace: Option<bool>,
/// Whether to allow transparent pixels to be altered to improve compression.
///
/// Default: `false`
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 (zlib) algorithm to use
#[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")]
///
/// Default: `Libdeflater`
pub deflater: Deflater,
/// 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.
///
/// Default: `None`
pub timeout: Option<Duration>,
}
impl Options {
#[must_use]
pub fn from_preset(level: u8) -> Self {
let opts = Self::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()
}
}
}
#[must_use]
pub fn max_compression() -> Self {
Self::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.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 5;
}
self
}
fn apply_preset_1(mut self) -> Self {
self.filters.clear();
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 10;
}
self
}
fn apply_preset_2(self) -> Self {
self
}
fn apply_preset_3(mut self) -> Self {
self.fast_evaluation = false;
self.filters = indexset! {
FilterStrategy::NONE,
FilterStrategy::Bigrams,
FilterStrategy::BigEnt,
FilterStrategy::Brute
};
self
}
fn apply_preset_4(mut self) -> Self {
if let Deflater::Libdeflater { compression } = &mut self.deflater {
*compression = 12;
}
self.apply_preset_3()
}
fn apply_preset_5(mut self) -> Self {
self.fast_evaluation = false;
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.filters.insert(FilterStrategy::AVERAGE);
self.filters.insert(FilterStrategy::PAETH);
self.apply_preset_5()
}
}
impl Default for Options {
fn default() -> Self {
// Default settings based on -o 2 from the CLI interface
Self {
fix_errors: false,
force: false,
filters: indexset! {
FilterStrategy::NONE,
FilterStrategy::SUB,
FilterStrategy::Entropy,
FilterStrategy::Bigrams
},
interlace: Some(false),
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,
deflater: Deflater::Libdeflater { compression: 11 },
fast_evaluation: true,
timeout: None,
}
}
}