From 6e258b5b446e0a70de29f8f80aba5288ce5d36b4 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 24 Sep 2024 15:24:15 +0100 Subject: [PATCH 01/37] Introduce advanced compression settings --- src/common.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/encoder.rs | 63 +++++++++++++++++++----------------------- 2 files changed, 103 insertions(+), 35 deletions(-) diff --git a/src/common.rs b/src/common.rs index 4475153e..20e246c8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,8 @@ //! Common types shared between the encoder and decoder use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk}; use crate::{chunk, encoder}; +#[allow(unused_imports)] // used by doc comments only +use crate::{AdaptiveFilterType, FilterType}; use io::Write; use std::{borrow::Cow, convert::TryFrom, fmt, io}; @@ -333,6 +335,67 @@ impl Default for Compression { } } +/// Advanced compression settings with more customization options than [Compression]. +/// +/// Note that this setting only affects DEFLATE compression. +/// Another setting that influences the compression ratio and lets you choose +/// between encoding speed and compression ratio is the *filter*, +/// See [FilterType] and [AdaptiveFilterType]. +#[non_exhaustive] +#[derive(Debug, Clone, Copy)] +pub enum AdvancedCompression { + /// Do not compress the data at all. + /// + /// Useful for incompressible images such as photographs, + /// or when speed is paramount and you don't care about size at all. + /// + /// This mode also disables filters, forcing [FilterType::NoFilter]. + NoCompression, + + /// Excellent for creating lightly compressed PNG images very quickly. + /// + /// Uses the [fdeflate](https://crates.io/crates/fdeflate) crate under the hood + /// to achieve speeds far exceeding what libpng is capable of + /// while still providing a decent compression ratio. + /// + /// Images encoded in this mode can also be decoded by the `png` crate extremely quickly, + /// much faster than what is typical for PNG images. + /// Other decoders (e.g. libpng) do not get a decoding speed boost from this mode. + FdeflateUltraFast, + + /// Uses [flate2](https://crates.io/crates/flate2) crate with the specified [compression level](flate2::Compression::new). + /// + /// Flate2 has several backends that make different trade-offs. + /// See the flate2 documentation for the available backends for more information. + Flate2(u32), + // TODO: Zopfli? +} + +impl AdvancedCompression { + pub(crate) fn from_simple(value: Compression) -> Self { + #[allow(deprecated)] + match value { + Compression::Default => Self::Flate2(flate2::Compression::default().level()), + Compression::Fast => Self::FdeflateUltraFast, + Compression::Best => Self::Flate2(flate2::Compression::best().level()), + // These two options are deprecated, and no longer directly supported. + // They used to map to flate2 level 0, which was meant to map to "no compression", + // but miniz_oxide doesn't understand level 0 and uses its default level instead. + // So we just keep mapping these to the default compression level to preserve that behavior. + Compression::Huffman => Self::Flate2(flate2::Compression::default().level()), + Compression::Rle => Self::Flate2(flate2::Compression::default().level()), + } + } + + pub(crate) fn closest_flate2_level(&self) -> flate2::Compression { + match self { + AdvancedCompression::NoCompression => flate2::Compression::none(), + AdvancedCompression::FdeflateUltraFast => flate2::Compression::new(1), + AdvancedCompression::Flate2(level) => flate2::Compression::new(*level), + } + } +} + /// An unsigned integer scaled version of a floating point value, /// equivalent to an integer quotient with fixed denominator (100_000)). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -495,6 +558,8 @@ pub struct Info<'a> { pub frame_control: Option, pub animation_control: Option, pub compression: Compression, + /// Advanced compression settings. Overrides the `compression` field, if set. + pub compression_advanced: Option, /// Gamma of the source system. /// Set by both `gAMA` as well as to a replacement by `sRGB` chunk. pub source_gamma: Option, @@ -533,6 +598,7 @@ impl Default for Info<'_> { // Default to `deflate::Compression::Fast` and `filter::FilterType::Sub` // to maintain backward compatible output. compression: Compression::Fast, + compression_advanced: None, source_gamma: None, source_chromaticities: None, srgb: None, @@ -684,6 +750,15 @@ impl Info<'_> { Ok(()) } + + /// Computes the low-level compression settings from [Self::compression] and [Self::compression_advanced] + pub(crate) fn compression(&self) -> AdvancedCompression { + if let Some(options) = self.compression_advanced { + options + } else { + AdvancedCompression::from_simple(self.compression) + } + } } impl BytesPerPixel { diff --git a/src/encoder.rs b/src/encoder.rs index 15050ec0..8e736d3f 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -16,6 +16,7 @@ use crate::text_metadata::{ EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk, }; use crate::traits::WriteBytesExt; +use crate::AdvancedCompression; pub type Result = result::Result; @@ -295,11 +296,16 @@ impl<'a, W: Write> Encoder<'a, W> { } /// Set compression parameters. - /// - /// Accepts a `Compression` or any type that can transform into a `Compression`. Notably `deflate::Compression` and - /// `deflate::CompressionOptions` which "just work". pub fn set_compression(&mut self, compression: Compression) { self.info.compression = compression; + self.info.compression_advanced = None; + } + + /// Provides in-depth customization of DEFLATE compression options. + /// + /// For a simpler selection of compression options see [Self::set_compression]. + pub fn set_compression_advanced(&mut self, compression: AdvancedCompression) { + self.info.compression_advanced = Some(compression); } /// Set the used filter type. @@ -495,7 +501,7 @@ struct PartialInfo { color_type: ColorType, frame_control: Option, animation_control: Option, - compression: Compression, + compression: AdvancedCompression, has_palette: bool, } @@ -508,7 +514,7 @@ impl PartialInfo { color_type: info.color_type, frame_control: info.frame_control, animation_control: info.animation_control, - compression: info.compression, + compression: info.compression(), has_palette: info.palette.is_some(), } } @@ -538,7 +544,7 @@ impl PartialInfo { color_type: self.color_type, frame_control: self.frame_control, animation_control: self.animation_control, - compression: self.compression, + compression_advanced: Some(self.compression), ..Default::default() } } @@ -694,7 +700,16 @@ impl Writer { let adaptive_method = self.options.adaptive_filter; let zlib_encoded = match self.info.compression { - Compression::Fast => { + AdvancedCompression::NoCompression => { + let mut compressor = + fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(Vec::new()))?; + for line in data.chunks(in_len) { + compressor.write_data(&[0])?; + compressor.write_data(line)?; + } + compressor.finish()?.into_inner() + } + AdvancedCompression::FdeflateUltraFast => { let mut compressor = fdeflate::Compressor::new(std::io::Cursor::new(Vec::new()))?; let mut current = vec![0; in_len + 1]; @@ -720,10 +735,7 @@ impl Writer { // Write uncompressed data since the result from fast compression would take // more space than that. // - // We always use FilterType::NoFilter here regardless of the filter method - // requested by the user. Doing filtering again would only add performance - // cost for both encoding and subsequent decoding, without improving the - // compression ratio. + // This is essentially a fallback to NoCompression. let mut compressor = fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(Vec::new()))?; for line in data.chunks(in_len) { @@ -735,10 +747,10 @@ impl Writer { compressed } } - _ => { + AdvancedCompression::Flate2(level) => { let mut current = vec![0; in_len]; - let mut zlib = ZlibEncoder::new(Vec::new(), self.info.compression.to_options()); + let mut zlib = ZlibEncoder::new(Vec::new(), flate2::Compression::new(level)); for line in data.chunks(in_len) { let filter_type = filter( filter_method, @@ -1322,7 +1334,7 @@ pub struct StreamWriter<'a, W: Write> { filter: FilterType, adaptive_filter: AdaptiveFilterType, fctl: Option, - compression: Compression, + compression: AdvancedCompression, } impl<'a, W: Write> StreamWriter<'a, W> { @@ -1345,7 +1357,7 @@ impl<'a, W: Write> StreamWriter<'a, W> { let mut chunk_writer = ChunkWriter::new(writer, buf_len); let (line_len, to_write) = chunk_writer.next_frame_info(); chunk_writer.write_header()?; - let zlib = ZlibEncoder::new(chunk_writer, compression.to_options()); + let zlib = ZlibEncoder::new(chunk_writer, compression.closest_flate2_level()); Ok(StreamWriter { writer: Wrapper::Zlib(zlib), @@ -1595,7 +1607,7 @@ impl<'a, W: Write> StreamWriter<'a, W> { // now it can be taken because the next statements cannot cause any errors match self.writer.take() { Wrapper::Chunk(wrt) => { - let encoder = ZlibEncoder::new(wrt, self.compression.to_options()); + let encoder = ZlibEncoder::new(wrt, self.compression.closest_flate2_level()); self.writer = Wrapper::Zlib(encoder); } _ => unreachable!(), @@ -1691,25 +1703,6 @@ impl Drop for StreamWriter<'_, W> { } } -/// Mod to encapsulate the converters depending on the `deflate` crate. -/// -/// Since this only contains trait impls, there is no need to make this public, they are simply -/// available when the mod is compiled as well. -impl Compression { - fn to_options(self) -> flate2::Compression { - #[allow(deprecated)] - match self { - Compression::Default => flate2::Compression::default(), - Compression::Fast => flate2::Compression::fast(), - Compression::Best => flate2::Compression::best(), - #[allow(deprecated)] - Compression::Huffman => flate2::Compression::none(), - #[allow(deprecated)] - Compression::Rle => flate2::Compression::none(), - } - } -} - #[cfg(test)] mod tests { use super::*; From b431bbf17d4234063a5276af62c2e93b0c80954e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 24 Sep 2024 15:34:35 +0100 Subject: [PATCH 02/37] Make roundtrip tests try various compression modes --- src/encoder.rs | 110 ++++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/src/encoder.rs b/src/encoder.rs index 8e736d3f..47c92a25 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -1731,30 +1731,34 @@ mod tests { let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); - // Encode decoded image - let mut out = Vec::new(); - { - let mut wrapper = RandomChunkWriter { - rng: thread_rng(), - w: &mut out, - }; - - let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); - encoder.set_color(info.color_type); - encoder.set_depth(info.bit_depth); - if let Some(palette) = &reader.info().palette { - encoder.set_palette(palette.clone()); + use AdvancedCompression::*; + for compression in [NoCompression, FdeflateUltraFast, Flate2(4)] { + // Encode decoded image + let mut out = Vec::new(); + { + let mut wrapper = RandomChunkWriter { + rng: thread_rng(), + w: &mut out, + }; + + let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); + encoder.set_color(info.color_type); + encoder.set_depth(info.bit_depth); + encoder.set_compression_advanced(compression); + if let Some(palette) = &reader.info().palette { + encoder.set_palette(palette.clone()); + } + let mut encoder = encoder.write_header().unwrap(); + encoder.write_image_data(&buf).unwrap(); } - let mut encoder = encoder.write_header().unwrap(); - encoder.write_image_data(&buf).unwrap(); + // Decode encoded decoded image + let decoder = Decoder::new(&*out); + let mut reader = decoder.read_info().unwrap(); + let mut buf2 = vec![0; reader.output_buffer_size()]; + reader.next_frame(&mut buf2).unwrap(); + // check if the encoded image is ok: + assert_eq!(buf, buf2); } - // Decode encoded decoded image - let decoder = Decoder::new(&*out); - let mut reader = decoder.read_info().unwrap(); - let mut buf2 = vec![0; reader.output_buffer_size()]; - reader.next_frame(&mut buf2).unwrap(); - // check if the encoded image is ok: - assert_eq!(buf, buf2); } } } @@ -1776,37 +1780,41 @@ mod tests { let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); - // Encode decoded image - let mut out = Vec::new(); - { - let mut wrapper = RandomChunkWriter { - rng: thread_rng(), - w: &mut out, - }; - - let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); - encoder.set_color(info.color_type); - encoder.set_depth(info.bit_depth); - if let Some(palette) = &reader.info().palette { - encoder.set_palette(palette.clone()); + use AdvancedCompression::*; + for compression in [NoCompression, FdeflateUltraFast, Flate2(4)] { + // Encode decoded image + let mut out = Vec::new(); + { + let mut wrapper = RandomChunkWriter { + rng: thread_rng(), + w: &mut out, + }; + + let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); + encoder.set_color(info.color_type); + encoder.set_depth(info.bit_depth); + encoder.set_compression_advanced(compression); + if let Some(palette) = &reader.info().palette { + encoder.set_palette(palette.clone()); + } + let mut encoder = encoder.write_header().unwrap(); + let mut stream_writer = encoder.stream_writer().unwrap(); + + let mut outer_wrapper = RandomChunkWriter { + rng: thread_rng(), + w: &mut stream_writer, + }; + + outer_wrapper.write_all(&buf).unwrap(); } - let mut encoder = encoder.write_header().unwrap(); - let mut stream_writer = encoder.stream_writer().unwrap(); - - let mut outer_wrapper = RandomChunkWriter { - rng: thread_rng(), - w: &mut stream_writer, - }; - - outer_wrapper.write_all(&buf).unwrap(); + // Decode encoded decoded image + let decoder = Decoder::new(&*out); + let mut reader = decoder.read_info().unwrap(); + let mut buf2 = vec![0; reader.output_buffer_size()]; + reader.next_frame(&mut buf2).unwrap(); + // check if the encoded image is ok: + assert_eq!(buf, buf2); } - // Decode encoded decoded image - let decoder = Decoder::new(&*out); - let mut reader = decoder.read_info().unwrap(); - let mut buf2 = vec![0; reader.output_buffer_size()]; - reader.next_frame(&mut buf2).unwrap(); - // check if the encoded image is ok: - assert_eq!(buf, buf2); } } } From 72df36885913bc9fde7baec89fd26ea97534378b Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 24 Sep 2024 15:57:39 +0100 Subject: [PATCH 03/37] Rename AdvancedCompression to DeflateCompression, so that Compression can be evolved to set both --- src/common.rs | 20 ++++++++++---------- src/encoder.rs | 28 ++++++++++++++-------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/common.rs b/src/common.rs index 20e246c8..d1f14826 100644 --- a/src/common.rs +++ b/src/common.rs @@ -343,7 +343,7 @@ impl Default for Compression { /// See [FilterType] and [AdaptiveFilterType]. #[non_exhaustive] #[derive(Debug, Clone, Copy)] -pub enum AdvancedCompression { +pub enum DeflateCompression { /// Do not compress the data at all. /// /// Useful for incompressible images such as photographs, @@ -371,7 +371,7 @@ pub enum AdvancedCompression { // TODO: Zopfli? } -impl AdvancedCompression { +impl DeflateCompression { pub(crate) fn from_simple(value: Compression) -> Self { #[allow(deprecated)] match value { @@ -389,9 +389,9 @@ impl AdvancedCompression { pub(crate) fn closest_flate2_level(&self) -> flate2::Compression { match self { - AdvancedCompression::NoCompression => flate2::Compression::none(), - AdvancedCompression::FdeflateUltraFast => flate2::Compression::new(1), - AdvancedCompression::Flate2(level) => flate2::Compression::new(*level), + DeflateCompression::NoCompression => flate2::Compression::none(), + DeflateCompression::FdeflateUltraFast => flate2::Compression::new(1), + DeflateCompression::Flate2(level) => flate2::Compression::new(*level), } } } @@ -559,7 +559,7 @@ pub struct Info<'a> { pub animation_control: Option, pub compression: Compression, /// Advanced compression settings. Overrides the `compression` field, if set. - pub compression_advanced: Option, + pub compression_deflate: Option, /// Gamma of the source system. /// Set by both `gAMA` as well as to a replacement by `sRGB` chunk. pub source_gamma: Option, @@ -598,7 +598,7 @@ impl Default for Info<'_> { // Default to `deflate::Compression::Fast` and `filter::FilterType::Sub` // to maintain backward compatible output. compression: Compression::Fast, - compression_advanced: None, + compression_deflate: None, source_gamma: None, source_chromaticities: None, srgb: None, @@ -752,11 +752,11 @@ impl Info<'_> { } /// Computes the low-level compression settings from [Self::compression] and [Self::compression_advanced] - pub(crate) fn compression(&self) -> AdvancedCompression { - if let Some(options) = self.compression_advanced { + pub(crate) fn compression(&self) -> DeflateCompression { + if let Some(options) = self.compression_deflate { options } else { - AdvancedCompression::from_simple(self.compression) + DeflateCompression::from_simple(self.compression) } } } diff --git a/src/encoder.rs b/src/encoder.rs index 47c92a25..5f71ddcf 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -16,7 +16,7 @@ use crate::text_metadata::{ EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk, }; use crate::traits::WriteBytesExt; -use crate::AdvancedCompression; +use crate::DeflateCompression; pub type Result = result::Result; @@ -298,14 +298,14 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set compression parameters. pub fn set_compression(&mut self, compression: Compression) { self.info.compression = compression; - self.info.compression_advanced = None; + self.info.compression_deflate = None; } /// Provides in-depth customization of DEFLATE compression options. /// /// For a simpler selection of compression options see [Self::set_compression]. - pub fn set_compression_advanced(&mut self, compression: AdvancedCompression) { - self.info.compression_advanced = Some(compression); + pub fn set_deflate_compression(&mut self, compression: DeflateCompression) { + self.info.compression_deflate = Some(compression); } /// Set the used filter type. @@ -501,7 +501,7 @@ struct PartialInfo { color_type: ColorType, frame_control: Option, animation_control: Option, - compression: AdvancedCompression, + compression: DeflateCompression, has_palette: bool, } @@ -544,7 +544,7 @@ impl PartialInfo { color_type: self.color_type, frame_control: self.frame_control, animation_control: self.animation_control, - compression_advanced: Some(self.compression), + compression_deflate: Some(self.compression), ..Default::default() } } @@ -700,7 +700,7 @@ impl Writer { let adaptive_method = self.options.adaptive_filter; let zlib_encoded = match self.info.compression { - AdvancedCompression::NoCompression => { + DeflateCompression::NoCompression => { let mut compressor = fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(Vec::new()))?; for line in data.chunks(in_len) { @@ -709,7 +709,7 @@ impl Writer { } compressor.finish()?.into_inner() } - AdvancedCompression::FdeflateUltraFast => { + DeflateCompression::FdeflateUltraFast => { let mut compressor = fdeflate::Compressor::new(std::io::Cursor::new(Vec::new()))?; let mut current = vec![0; in_len + 1]; @@ -747,7 +747,7 @@ impl Writer { compressed } } - AdvancedCompression::Flate2(level) => { + DeflateCompression::Flate2(level) => { let mut current = vec![0; in_len]; let mut zlib = ZlibEncoder::new(Vec::new(), flate2::Compression::new(level)); @@ -1334,7 +1334,7 @@ pub struct StreamWriter<'a, W: Write> { filter: FilterType, adaptive_filter: AdaptiveFilterType, fctl: Option, - compression: AdvancedCompression, + compression: DeflateCompression, } impl<'a, W: Write> StreamWriter<'a, W> { @@ -1731,7 +1731,7 @@ mod tests { let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); - use AdvancedCompression::*; + use DeflateCompression::*; for compression in [NoCompression, FdeflateUltraFast, Flate2(4)] { // Encode decoded image let mut out = Vec::new(); @@ -1744,7 +1744,7 @@ mod tests { let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); encoder.set_color(info.color_type); encoder.set_depth(info.bit_depth); - encoder.set_compression_advanced(compression); + encoder.set_deflate_compression(compression); if let Some(palette) = &reader.info().palette { encoder.set_palette(palette.clone()); } @@ -1780,7 +1780,7 @@ mod tests { let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); - use AdvancedCompression::*; + use DeflateCompression::*; for compression in [NoCompression, FdeflateUltraFast, Flate2(4)] { // Encode decoded image let mut out = Vec::new(); @@ -1793,7 +1793,7 @@ mod tests { let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); encoder.set_color(info.color_type); encoder.set_depth(info.bit_depth); - encoder.set_compression_advanced(compression); + encoder.set_deflate_compression(compression); if let Some(palette) = &reader.info().palette { encoder.set_palette(palette.clone()); } From 54b89cf17dd6c835282dd08d3ed1b4760be349f5 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 25 Sep 2024 16:30:37 +0100 Subject: [PATCH 04/37] Refactor the high-level API for setting the compression level. Semver-breaking. --- examples/corpus-bench.rs | 2 +- src/common.rs | 73 ++++++++++++++++------------------------ src/encoder.rs | 10 +++--- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/examples/corpus-bench.rs b/examples/corpus-bench.rs index 07307fb3..9ddcd599 100644 --- a/examples/corpus-bench.rs +++ b/examples/corpus-bench.rs @@ -44,7 +44,7 @@ fn run_encode( encoder.set_compression(match args.speed { Speed::Fast => png::Compression::Fast, Speed::Default => png::Compression::Default, - Speed::Best => png::Compression::Best, + Speed::Best => png::Compression::High, }); encoder.set_filter(match args.filter { Filter::None => png::FilterType::NoFilter, diff --git a/src/common.rs b/src/common.rs index d1f14826..204e7c78 100644 --- a/src/common.rs +++ b/src/common.rs @@ -305,28 +305,26 @@ impl AnimationControl { } /// The type and strength of applied compression. +/// +/// This is a simple, high-level interface that will automatically choose +/// the appropriate DEFLATE compression mode and PNG filter. +/// +/// If you need more control over the encoding paramters, +/// you can set the [DeflateCompression], [FilterType] and [AdaptiveFilterType] manually. #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum Compression { - /// Default level - Default, - /// Fast minimal compression - Fast, - /// Higher compression level + /// No compression whatsoever. Fastest, but results in large files. + None, + /// Extremely fast compression with a decent compression ratio. /// - /// Best in this context isn't actually the highest possible level - /// the encoder can do, but is meant to emulate the `Best` setting in the `Flate2` - /// library. - Best, - #[deprecated( - since = "0.17.6", - note = "use one of the other compression levels instead, such as 'fast'" - )] - Huffman, - #[deprecated( - since = "0.17.6", - note = "use one of the other compression levels instead, such as 'fast'" - )] - Rle, + /// Significantly outperforms libpng and other popular encoders + /// by using a [specialized DEFLATE implementation tuned for PNG](https://crates.io/crates/fdeflate). + Fast, + /// Balances encoding speed and compression ratio + Default, + /// Spend more time to produce a slightly smaller file than with `Default` + High, } impl Default for Compression { @@ -371,19 +369,19 @@ pub enum DeflateCompression { // TODO: Zopfli? } +impl Default for DeflateCompression { + fn default() -> Self { + Self::from_simple(Compression::Default) + } +} + impl DeflateCompression { pub(crate) fn from_simple(value: Compression) -> Self { - #[allow(deprecated)] match value { - Compression::Default => Self::Flate2(flate2::Compression::default().level()), + Compression::None => Self::NoCompression, Compression::Fast => Self::FdeflateUltraFast, - Compression::Best => Self::Flate2(flate2::Compression::best().level()), - // These two options are deprecated, and no longer directly supported. - // They used to map to flate2 level 0, which was meant to map to "no compression", - // but miniz_oxide doesn't understand level 0 and uses its default level instead. - // So we just keep mapping these to the default compression level to preserve that behavior. - Compression::Huffman => Self::Flate2(flate2::Compression::default().level()), - Compression::Rle => Self::Flate2(flate2::Compression::default().level()), + Compression::Default => Self::Flate2(flate2::Compression::default().level()), + Compression::High => Self::Flate2(flate2::Compression::best().level()), } } @@ -557,9 +555,8 @@ pub struct Info<'a> { pub frame_control: Option, pub animation_control: Option, - pub compression: Compression, - /// Advanced compression settings. Overrides the `compression` field, if set. - pub compression_deflate: Option, + /// Controls the DEFLATE compression options. Influences the trade-off between compression speed and ratio, along with filters. + pub compression_deflate: DeflateCompression, /// Gamma of the source system. /// Set by both `gAMA` as well as to a replacement by `sRGB` chunk. pub source_gamma: Option, @@ -595,10 +592,7 @@ impl Default for Info<'_> { pixel_dims: None, frame_control: None, animation_control: None, - // Default to `deflate::Compression::Fast` and `filter::FilterType::Sub` - // to maintain backward compatible output. - compression: Compression::Fast, - compression_deflate: None, + compression_deflate: DeflateCompression::default(), source_gamma: None, source_chromaticities: None, srgb: None, @@ -750,15 +744,6 @@ impl Info<'_> { Ok(()) } - - /// Computes the low-level compression settings from [Self::compression] and [Self::compression_advanced] - pub(crate) fn compression(&self) -> DeflateCompression { - if let Some(options) = self.compression_deflate { - options - } else { - DeflateCompression::from_simple(self.compression) - } - } } impl BytesPerPixel { diff --git a/src/encoder.rs b/src/encoder.rs index 5f71ddcf..afc6b48a 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -297,15 +297,15 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set compression parameters. pub fn set_compression(&mut self, compression: Compression) { - self.info.compression = compression; - self.info.compression_deflate = None; + self.info.compression_deflate = DeflateCompression::from_simple(compression); + // TODO: also set filters } /// Provides in-depth customization of DEFLATE compression options. /// /// For a simpler selection of compression options see [Self::set_compression]. pub fn set_deflate_compression(&mut self, compression: DeflateCompression) { - self.info.compression_deflate = Some(compression); + self.info.compression_deflate = compression; } /// Set the used filter type. @@ -514,7 +514,7 @@ impl PartialInfo { color_type: info.color_type, frame_control: info.frame_control, animation_control: info.animation_control, - compression: info.compression(), + compression: info.compression_deflate, has_palette: info.palette.is_some(), } } @@ -544,7 +544,7 @@ impl PartialInfo { color_type: self.color_type, frame_control: self.frame_control, animation_control: self.animation_control, - compression_deflate: Some(self.compression), + compression_deflate: self.compression, ..Default::default() } } From c7674389ed4cd4a8afb4b3f353e447bde59289c8 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 25 Sep 2024 16:48:36 +0100 Subject: [PATCH 05/37] Make the Compression setting also influence the filter choice --- src/encoder.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/encoder.rs b/src/encoder.rs index afc6b48a..98b8a57d 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -298,7 +298,19 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set compression parameters. pub fn set_compression(&mut self, compression: Compression) { self.info.compression_deflate = DeflateCompression::from_simple(compression); - // TODO: also set filters + // choose the filter based on the requested compression + match compression { + Compression::None => { + self.set_filter(FilterType::NoFilter); // with no DEFLATE filtering would only waste time + self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); + } + Compression::Fast => { + self.set_filter(FilterType::Up); // fast and avoids long backreferences in DEFLATE stream + self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); + } + Compression::Default => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), + Compression::High => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), + } } /// Provides in-depth customization of DEFLATE compression options. From e7dd3dc1a6d320b6aaf4a24fb0c67517afe8b73d Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 25 Sep 2024 17:07:52 +0100 Subject: [PATCH 06/37] Fix compilation of the roundtrip fuzzer --- fuzz/fuzz_targets/roundtrip.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fuzz/fuzz_targets/roundtrip.rs b/fuzz/fuzz_targets/roundtrip.rs index e428444d..4996d747 100644 --- a/fuzz/fuzz_targets/roundtrip.rs +++ b/fuzz/fuzz_targets/roundtrip.rs @@ -31,9 +31,8 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi let compression = match compression { 0 => png::Compression::Default, 1 => png::Compression::Fast, - 2 => png::Compression::Best, - 3 => png::Compression::Huffman, - 4 => png::Compression::Rle, + 2 => png::Compression::High, + 3 => png::Compression::None, _ => return None, }; From 6b06728b70401e5f8e180ab5539a250044d84484 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 26 Sep 2024 23:47:43 +0100 Subject: [PATCH 07/37] Disable fuzzing Compression::None to see if it's the culprit for non-reproducible fuzzer errors --- fuzz/fuzz_targets/roundtrip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/roundtrip.rs b/fuzz/fuzz_targets/roundtrip.rs index 4996d747..1233805d 100644 --- a/fuzz/fuzz_targets/roundtrip.rs +++ b/fuzz/fuzz_targets/roundtrip.rs @@ -32,7 +32,7 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi 0 => png::Compression::Default, 1 => png::Compression::Fast, 2 => png::Compression::High, - 3 => png::Compression::None, + // 3 => png::Compression::None, // TODO: re-enable _ => return None, }; From b702a949d88a2a3bab0a697de12fe74bb04a3974 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 26 Sep 2024 23:54:24 +0100 Subject: [PATCH 08/37] Compression::None was not the impostor. This reverts commit 6b06728b70401e5f8e180ab5539a250044d84484. --- fuzz/fuzz_targets/roundtrip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/roundtrip.rs b/fuzz/fuzz_targets/roundtrip.rs index 1233805d..4996d747 100644 --- a/fuzz/fuzz_targets/roundtrip.rs +++ b/fuzz/fuzz_targets/roundtrip.rs @@ -32,7 +32,7 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi 0 => png::Compression::Default, 1 => png::Compression::Fast, 2 => png::Compression::High, - // 3 => png::Compression::None, // TODO: re-enable + 3 => png::Compression::None, _ => return None, }; From aed727ac0a6e82314d988d980e03a0a0627db6ee Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 14:42:20 +0100 Subject: [PATCH 09/37] Rename None to NoCompression --- src/common.rs | 4 ++-- src/encoder.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common.rs b/src/common.rs index 204e7c78..2c8ec5ba 100644 --- a/src/common.rs +++ b/src/common.rs @@ -315,7 +315,7 @@ impl AnimationControl { #[non_exhaustive] pub enum Compression { /// No compression whatsoever. Fastest, but results in large files. - None, + NoCompression, /// Extremely fast compression with a decent compression ratio. /// /// Significantly outperforms libpng and other popular encoders @@ -378,7 +378,7 @@ impl Default for DeflateCompression { impl DeflateCompression { pub(crate) fn from_simple(value: Compression) -> Self { match value { - Compression::None => Self::NoCompression, + Compression::NoCompression => Self::NoCompression, Compression::Fast => Self::FdeflateUltraFast, Compression::Default => Self::Flate2(flate2::Compression::default().level()), Compression::High => Self::Flate2(flate2::Compression::best().level()), diff --git a/src/encoder.rs b/src/encoder.rs index c92a347b..14c978f5 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -300,7 +300,7 @@ impl<'a, W: Write> Encoder<'a, W> { self.info.compression_deflate = DeflateCompression::from_simple(compression); // choose the filter based on the requested compression match compression { - Compression::None => { + Compression::NoCompression => { self.set_filter(FilterType::NoFilter); // with no DEFLATE filtering would only waste time self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } From dd547b0484dc12b1f088c610db8f178735f4a8fc Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 14:43:05 +0100 Subject: [PATCH 10/37] Rename Default to Balanced --- examples/corpus-bench.rs | 2 +- src/common.rs | 8 ++++---- src/encoder.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/corpus-bench.rs b/examples/corpus-bench.rs index 9ddcd599..edf8e637 100644 --- a/examples/corpus-bench.rs +++ b/examples/corpus-bench.rs @@ -43,7 +43,7 @@ fn run_encode( encoder.set_depth(bit_depth); encoder.set_compression(match args.speed { Speed::Fast => png::Compression::Fast, - Speed::Default => png::Compression::Default, + Speed::Default => png::Compression::Balanced, Speed::Best => png::Compression::High, }); encoder.set_filter(match args.filter { diff --git a/src/common.rs b/src/common.rs index 2c8ec5ba..f95dc2af 100644 --- a/src/common.rs +++ b/src/common.rs @@ -322,14 +322,14 @@ pub enum Compression { /// by using a [specialized DEFLATE implementation tuned for PNG](https://crates.io/crates/fdeflate). Fast, /// Balances encoding speed and compression ratio - Default, + Balanced, /// Spend more time to produce a slightly smaller file than with `Default` High, } impl Default for Compression { fn default() -> Self { - Self::Default + Self::Balanced } } @@ -371,7 +371,7 @@ pub enum DeflateCompression { impl Default for DeflateCompression { fn default() -> Self { - Self::from_simple(Compression::Default) + Self::from_simple(Compression::Balanced) } } @@ -380,7 +380,7 @@ impl DeflateCompression { match value { Compression::NoCompression => Self::NoCompression, Compression::Fast => Self::FdeflateUltraFast, - Compression::Default => Self::Flate2(flate2::Compression::default().level()), + Compression::Balanced => Self::Flate2(flate2::Compression::default().level()), Compression::High => Self::Flate2(flate2::Compression::best().level()), } } diff --git a/src/encoder.rs b/src/encoder.rs index 14c978f5..400d5e46 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -308,7 +308,7 @@ impl<'a, W: Write> Encoder<'a, W> { self.set_filter(FilterType::Up); // fast and avoids long backreferences in DEFLATE stream self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } - Compression::Default => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), + Compression::Balanced => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), Compression::High => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), } } From 17e7229675db114c7a766722eebac9eae1355777 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 14:44:04 +0100 Subject: [PATCH 11/37] Drop misleading mentions of photos --- src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index f95dc2af..e71a140e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -344,7 +344,7 @@ impl Default for Compression { pub enum DeflateCompression { /// Do not compress the data at all. /// - /// Useful for incompressible images such as photographs, + /// Useful for incompressible images, /// or when speed is paramount and you don't care about size at all. /// /// This mode also disables filters, forcing [FilterType::NoFilter]. From 31944746d8c95ed7fc1cb06537e16bfbce7ecc5e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 14:45:54 +0100 Subject: [PATCH 12/37] Correct the description of ultrafast mode --- src/common.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index e71a140e..bc5382ca 100644 --- a/src/common.rs +++ b/src/common.rs @@ -356,8 +356,7 @@ pub enum DeflateCompression { /// to achieve speeds far exceeding what libpng is capable of /// while still providing a decent compression ratio. /// - /// Images encoded in this mode can also be decoded by the `png` crate extremely quickly, - /// much faster than what is typical for PNG images. + /// Images encoded in this mode can also be decoded by the `png` crate slightly faster than usual. /// Other decoders (e.g. libpng) do not get a decoding speed boost from this mode. FdeflateUltraFast, From cc8956a95be506c54e571066ca4ac37357f9a79c Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 15:01:14 +0100 Subject: [PATCH 13/37] Switch the default filter over to adaptive --- src/encoder.rs | 4 ++-- src/filter.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/encoder.rs b/src/encoder.rs index dffe0ef9..6cd09344 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -334,7 +334,7 @@ impl<'a, W: Write> Encoder<'a, W> { /// Adaptive filtering attempts to select the best filter for each line /// based on heuristics which minimize the file size for compression rather /// than use a single filter for the entire image. The default method is - /// [`AdaptiveFilterType::NonAdaptive`]. + /// [`AdaptiveFilterType::Adaptive`]. pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { self.options.adaptive_filter = adaptive_filter; @@ -1385,7 +1385,7 @@ impl<'a, W: Write> StreamWriter<'a, W> { /// based on heuristics which minimize the file size for compression rather /// than use a single filter for the entire image. /// - /// The default method is [`AdaptiveFilterType::NonAdaptive`]. + /// The default method is [`AdaptiveFilterType::Adaptive`]. pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { self.adaptive_filter = adaptive_filter; } diff --git a/src/filter.rs b/src/filter.rs index 9290a040..e4a7be92 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -306,7 +306,7 @@ impl FilterType { /// Filtering is quite cheap compared to other parts of encoding, but can contribute /// to the compression ratio significantly. /// -/// `NonAdaptive` filtering is the default. +/// `Adaptive` filtering is the default. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum AdaptiveFilterType { @@ -316,7 +316,7 @@ pub enum AdaptiveFilterType { impl Default for AdaptiveFilterType { fn default() -> Self { - AdaptiveFilterType::NonAdaptive + AdaptiveFilterType::Adaptive } } From 104a04822f41f2e44e2d534544743b254ddce947 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 15:06:55 +0100 Subject: [PATCH 14/37] Add Fastest mode for Fdeflate+Up, switch Fast mode to Fdeflate+Adaptive --- src/common.rs | 6 +++++- src/encoder.rs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index bc5382ca..8915f518 100644 --- a/src/common.rs +++ b/src/common.rs @@ -316,10 +316,13 @@ impl AnimationControl { pub enum Compression { /// No compression whatsoever. Fastest, but results in large files. NoCompression, + /// Extremely fast but light compression. + Fastest, /// Extremely fast compression with a decent compression ratio. /// /// Significantly outperforms libpng and other popular encoders - /// by using a [specialized DEFLATE implementation tuned for PNG](https://crates.io/crates/fdeflate). + /// by using a [specialized DEFLATE implementation tuned for PNG](https://crates.io/crates/fdeflate), + /// while still providing better compression ratio than the fastest modes of other encoders. Fast, /// Balances encoding speed and compression ratio Balanced, @@ -378,6 +381,7 @@ impl DeflateCompression { pub(crate) fn from_simple(value: Compression) -> Self { match value { Compression::NoCompression => Self::NoCompression, + Compression::Fastest => Self::FdeflateUltraFast, Compression::Fast => Self::FdeflateUltraFast, Compression::Balanced => Self::Flate2(flate2::Compression::default().level()), Compression::High => Self::Flate2(flate2::Compression::best().level()), diff --git a/src/encoder.rs b/src/encoder.rs index 6cd09344..02ab164c 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -304,10 +304,13 @@ impl<'a, W: Write> Encoder<'a, W> { self.set_filter(FilterType::NoFilter); // with no DEFLATE filtering would only waste time self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } - Compression::Fast => { + Compression::Fastest => { self.set_filter(FilterType::Up); // fast and avoids long backreferences in DEFLATE stream self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } + Compression::Fast => { + self.set_adaptive_filter(AdaptiveFilterType::Adaptive); + } Compression::Balanced => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), Compression::High => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), } From c8b19c9544259d77d7a64f44cc13c1b3cb2ff77d Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 15:51:30 +0100 Subject: [PATCH 15/37] Refactor FilterType and AdaptiveFilterType into a single public enum --- src/decoder/mod.rs | 4 +- src/encoder.rs | 20 +------- src/filter.rs | 121 +++++++++++++++++++++++++++++---------------- 3 files changed, 83 insertions(+), 62 deletions(-) diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8a76200c..688f21cf 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -15,7 +15,7 @@ use crate::chunk; use crate::common::{ BitDepth, BytesPerPixel, ColorType, Info, ParameterErrorKind, Transformations, }; -use crate::filter::{unfilter, FilterType}; +use crate::filter::{unfilter, RowFilter}; pub use interlace_info::InterlaceInfo; use interlace_info::InterlaceInfoIter; @@ -723,7 +723,7 @@ impl Reader { let (prev, row) = self.data_stream.split_at_mut(self.current_start); // Unfilter the row. - let filter = FilterType::from_u8(row[0]).ok_or(DecodingError::Format( + let filter = RowFilter::from_u8(row[0]).ok_or(DecodingError::Format( FormatErrorInner::UnknownFilterMethod(row[0]).into(), ))?; unfilter( diff --git a/src/encoder.rs b/src/encoder.rs index 02ab164c..53760ea1 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -699,7 +699,6 @@ impl Writer { let bpp = self.info.bpp_in_prediction(); let filter_method = self.options.filter; - let adaptive_method = self.options.adaptive_filter; let zlib_encoded = match self.info.compression { DeflateCompression::NoCompression => { @@ -716,14 +715,7 @@ impl Writer { let mut current = vec![0; in_len + 1]; for line in data.chunks(in_len) { - let filter_type = filter( - filter_method, - adaptive_method, - bpp, - prev, - line, - &mut current[1..], - ); + let filter_type = filter(filter_method, bpp, prev, line, &mut current[1..]); current[0] = filter_type as u8; compressor.write_data(¤t)?; @@ -754,14 +746,7 @@ impl Writer { let mut zlib = ZlibEncoder::new(Vec::new(), flate2::Compression::new(level)); for line in data.chunks(in_len) { - let filter_type = filter( - filter_method, - adaptive_method, - bpp, - prev, - line, - &mut current, - ); + let filter_type = filter(filter_method, bpp, prev, line, &mut current); zlib.write_all(&[filter_type as u8])?; zlib.write_all(¤t)?; @@ -1642,7 +1627,6 @@ impl<'a, W: Write> Write for StreamWriter<'a, W> { let mut filtered = vec![0; self.curr_buf.len()]; let filter_type = filter( self.filter, - self.adaptive_filter, self.bpp, &self.prev_buf, &self.curr_buf, diff --git a/src/filter.rs b/src/filter.rs index e4a7be92..80be48c4 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -270,8 +270,37 @@ mod simd { /// /// Details on how each filter works can be found in the [PNG Book](http://www.libpng.org/pub/png/book/chapter09.html). #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] pub enum FilterType { + NoFilter, + Sub, + Up, + Avg, + Paeth, + Adaptive, +} + +impl Default for FilterType { + fn default() -> Self { + FilterType::Up + } +} + +impl From for FilterType { + fn from(value: RowFilter) -> Self { + match value { + RowFilter::NoFilter => FilterType::NoFilter, + RowFilter::Sub => FilterType::Sub, + RowFilter::Up => FilterType::Up, + RowFilter::Avg => FilterType::Avg, + RowFilter::Paeth => FilterType::Paeth, + } + } +} + +/// Unlike the public [FilterType], does not include the "Adaptive" option +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub(crate) enum RowFilter { NoFilter = 0, Sub = 1, Up = 2, @@ -279,24 +308,34 @@ pub enum FilterType { Paeth = 4, } -impl Default for FilterType { +impl Default for RowFilter { fn default() -> Self { - FilterType::Sub + RowFilter::Up } } -impl FilterType { - /// u8 -> Self. Temporary solution until Rust provides a canonical one. - pub fn from_u8(n: u8) -> Option { +impl RowFilter { + pub fn from_u8(n: u8) -> Option { match n { - 0 => Some(FilterType::NoFilter), - 1 => Some(FilterType::Sub), - 2 => Some(FilterType::Up), - 3 => Some(FilterType::Avg), - 4 => Some(FilterType::Paeth), + 0 => Some(Self::NoFilter), + 1 => Some(Self::Sub), + 2 => Some(Self::Up), + 3 => Some(Self::Avg), + 4 => Some(Self::Paeth), _ => None, } } + + pub fn from_method(strat: FilterType) -> Option { + match strat { + FilterType::NoFilter => Some(Self::NoFilter), + FilterType::Sub => Some(Self::Sub), + FilterType::Up => Some(Self::Up), + FilterType::Avg => Some(Self::Avg), + FilterType::Paeth => Some(Self::Paeth), + FilterType::Adaptive => None, + } + } } /// Adaptive filtering tries every possible filter for each row and uses a heuristic to select the best one. @@ -382,12 +421,12 @@ fn filter_paeth(a: u8, b: u8, c: u8) -> u8 { } pub(crate) fn unfilter( - mut filter: FilterType, + mut filter: RowFilter, tbpp: BytesPerPixel, previous: &[u8], current: &mut [u8], ) { - use self::FilterType::*; + use self::RowFilter::*; // If the previous row is empty, then treat it as if it were filled with zeros. if previous.is_empty() { @@ -894,14 +933,14 @@ pub(crate) fn unfilter( } fn filter_internal( - method: FilterType, + method: RowFilter, bpp: usize, len: usize, previous: &[u8], current: &[u8], output: &mut [u8], -) -> FilterType { - use self::FilterType::*; +) -> RowFilter { + use self::RowFilter::*; // This value was chosen experimentally based on what achieved the best performance. The // Rust compiler does auto-vectorization, and 32-bytes per loop iteration seems to enable @@ -1034,23 +1073,19 @@ fn filter_internal( pub(crate) fn filter( method: FilterType, - adaptive: AdaptiveFilterType, bpp: BytesPerPixel, previous: &[u8], current: &[u8], output: &mut [u8], -) -> FilterType { - use FilterType::*; +) -> RowFilter { + use RowFilter::*; let bpp = bpp.into_usize(); let len = current.len(); - match adaptive { - AdaptiveFilterType::NonAdaptive => { - filter_internal(method, bpp, len, previous, current, output) - } - AdaptiveFilterType::Adaptive => { + match method { + FilterType::Adaptive => { let mut min_sum: u64 = u64::MAX; - let mut filter_choice = FilterType::NoFilter; + let mut filter_choice = RowFilter::NoFilter; for &filter in [Sub, Up, Avg, Paeth].iter() { filter_internal(filter, bpp, len, previous, current, output); let sum = sum_buffer(output); @@ -1065,6 +1100,10 @@ pub(crate) fn filter( } filter_choice } + _ => { + let filter = RowFilter::from_method(method).unwrap(); + filter_internal(filter, bpp, len, previous, current, output) + } } } @@ -1094,7 +1133,7 @@ fn sum_buffer(buf: &[u8]) -> u64 { #[cfg(test)] mod test { - use super::{filter, unfilter, AdaptiveFilterType, BytesPerPixel, FilterType}; + use super::{filter, unfilter, BytesPerPixel, RowFilter}; use core::iter; #[test] @@ -1104,11 +1143,10 @@ mod test { let previous: Vec<_> = iter::repeat(1).take(LEN.into()).collect(); let current: Vec<_> = (0..LEN).collect(); let expected = current.clone(); - let adaptive = AdaptiveFilterType::NonAdaptive; - let roundtrip = |kind, bpp: BytesPerPixel| { + let roundtrip = |kind: RowFilter, bpp: BytesPerPixel| { let mut output = vec![0; LEN.into()]; - filter(kind, adaptive, bpp, &previous, ¤t, &mut output); + filter(kind.into(), bpp, &previous, ¤t, &mut output); unfilter(kind, bpp, &previous, &mut output); assert_eq!( output, expected, @@ -1118,11 +1156,11 @@ mod test { }; let filters = [ - FilterType::NoFilter, - FilterType::Sub, - FilterType::Up, - FilterType::Avg, - FilterType::Paeth, + RowFilter::NoFilter, + RowFilter::Sub, + RowFilter::Up, + RowFilter::Avg, + RowFilter::Paeth, ]; let bpps = [ @@ -1148,11 +1186,10 @@ mod test { let previous: Vec<_> = (0..LEN).collect(); let current: Vec<_> = (0..LEN).collect(); let expected = current.clone(); - let adaptive = AdaptiveFilterType::NonAdaptive; - let roundtrip = |kind, bpp: BytesPerPixel| { + let roundtrip = |kind: RowFilter, bpp: BytesPerPixel| { let mut output = vec![0; LEN.into()]; - filter(kind, adaptive, bpp, &previous, ¤t, &mut output); + filter(kind.into(), bpp, &previous, ¤t, &mut output); unfilter(kind, bpp, &previous, &mut output); assert_eq!( output, expected, @@ -1162,11 +1199,11 @@ mod test { }; let filters = [ - FilterType::NoFilter, - FilterType::Sub, - FilterType::Up, - FilterType::Avg, - FilterType::Paeth, + RowFilter::NoFilter, + RowFilter::Sub, + RowFilter::Up, + RowFilter::Avg, + RowFilter::Paeth, ]; let bpps = [ From 30192da932dd7d43621edb857d74c8b7329d5f92 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 15:57:05 +0100 Subject: [PATCH 16/37] Drop vestigial AdaptiveFilterType enum --- src/common.rs | 4 ++-- src/encoder.rs | 48 ++++-------------------------------------------- src/filter.rs | 21 --------------------- src/lib.rs | 2 +- 4 files changed, 7 insertions(+), 68 deletions(-) diff --git a/src/common.rs b/src/common.rs index 8915f518..a52b93d9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,8 +1,8 @@ //! Common types shared between the encoder and decoder use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk}; -use crate::{chunk, encoder}; #[allow(unused_imports)] // used by doc comments only -use crate::{AdaptiveFilterType, FilterType}; +use crate::FilterType; +use crate::{chunk, encoder}; use io::Write; use std::{borrow::Cow, convert::TryFrom, fmt, io}; diff --git a/src/encoder.rs b/src/encoder.rs index 53760ea1..a8c32ace 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -11,7 +11,7 @@ use crate::common::{ AnimationControl, BitDepth, BlendOp, BytesPerPixel, ColorType, Compression, DisposeOp, FrameControl, Info, ParameterError, ParameterErrorKind, PixelDimensions, ScaledFloat, }; -use crate::filter::{filter, AdaptiveFilterType, FilterType}; +use crate::filter::{filter, FilterType}; use crate::text_metadata::{ EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk, }; @@ -156,7 +156,6 @@ pub struct Encoder<'a, W: Write> { #[derive(Default)] struct Options { filter: FilterType, - adaptive_filter: AdaptiveFilterType, sep_def_img: bool, validate_sequence: bool, } @@ -302,17 +301,13 @@ impl<'a, W: Write> Encoder<'a, W> { match compression { Compression::NoCompression => { self.set_filter(FilterType::NoFilter); // with no DEFLATE filtering would only waste time - self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } Compression::Fastest => { self.set_filter(FilterType::Up); // fast and avoids long backreferences in DEFLATE stream - self.set_adaptive_filter(AdaptiveFilterType::NonAdaptive); } - Compression::Fast => { - self.set_adaptive_filter(AdaptiveFilterType::Adaptive); - } - Compression::Balanced => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), - Compression::High => self.set_adaptive_filter(AdaptiveFilterType::Adaptive), + Compression::Fast => self.set_filter(FilterType::Adaptive), + Compression::Balanced => self.set_filter(FilterType::Adaptive), + Compression::High => self.set_filter(FilterType::Adaptive), } } @@ -332,17 +327,6 @@ impl<'a, W: Write> Encoder<'a, W> { self.options.filter = filter; } - /// Set the adaptive filter type. - /// - /// Adaptive filtering attempts to select the best filter for each line - /// based on heuristics which minimize the file size for compression rather - /// than use a single filter for the entire image. The default method is - /// [`AdaptiveFilterType::Adaptive`]. - - pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { - self.options.adaptive_filter = adaptive_filter; - } - /// Set the fraction of time every frame is going to be displayed, in seconds. /// /// *Note that this parameter can be set for each individual frame after @@ -825,16 +809,6 @@ impl Writer { self.options.filter = filter; } - /// Set the adaptive filter type for the following frames. - /// - /// Adaptive filtering attempts to select the best filter for each line - /// based on heuristics which minimize the file size for compression rather - /// than use a single filter for the entire image. The default method is - /// [`AdaptiveFilterType::NonAdaptive`]. - pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { - self.options.adaptive_filter = adaptive_filter; - } - /// Set the fraction of time the following frames are going to be displayed, /// in seconds /// @@ -1311,7 +1285,6 @@ pub struct StreamWriter<'a, W: Write> { bpp: BytesPerPixel, filter: FilterType, - adaptive_filter: AdaptiveFilterType, fctl: Option, compression: DeflateCompression, } @@ -1329,7 +1302,6 @@ impl<'a, W: Write> StreamWriter<'a, W> { let bpp = writer.info.bpp_in_prediction(); let in_len = writer.info.raw_row_length() - 1; let filter = writer.options.filter; - let adaptive_filter = writer.options.adaptive_filter; let prev_buf = vec![0; in_len]; let curr_buf = vec![0; in_len]; @@ -1347,7 +1319,6 @@ impl<'a, W: Write> StreamWriter<'a, W> { filter, width, height, - adaptive_filter, line_len, to_write, fctl, @@ -1367,17 +1338,6 @@ impl<'a, W: Write> StreamWriter<'a, W> { self.filter = filter; } - /// Set the adaptive filter type for the next frame. - /// - /// Adaptive filtering attempts to select the best filter for each line - /// based on heuristics which minimize the file size for compression rather - /// than use a single filter for the entire image. - /// - /// The default method is [`AdaptiveFilterType::Adaptive`]. - pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { - self.adaptive_filter = adaptive_filter; - } - /// Set the fraction of time the following frames are going to be displayed, /// in seconds /// diff --git a/src/filter.rs b/src/filter.rs index 80be48c4..037c6100 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -338,27 +338,6 @@ impl RowFilter { } } -/// Adaptive filtering tries every possible filter for each row and uses a heuristic to select the best one. -/// This improves compression ratio, but makes encoding slightly slower. -/// -/// It is recommended to use `Adaptive` whenever you care about compression ratio. -/// Filtering is quite cheap compared to other parts of encoding, but can contribute -/// to the compression ratio significantly. -/// -/// `Adaptive` filtering is the default. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum AdaptiveFilterType { - Adaptive, - NonAdaptive, -} - -impl Default for AdaptiveFilterType { - fn default() -> Self { - AdaptiveFilterType::Adaptive - } -} - fn filter_paeth_decode(a: u8, b: u8, c: u8) -> u8 { // Decoding seems to optimize better with this algorithm let pa = (i16::from(b) - i16::from(c)).abs(); diff --git a/src/lib.rs b/src/lib.rs index 9e8a2113..9777afa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub use crate::decoder::{ OutputInfo, Reader, StreamingDecoder, }; pub use crate::encoder::{Encoder, EncodingError, StreamWriter, Writer}; -pub use crate::filter::{AdaptiveFilterType, FilterType}; +pub use crate::filter::FilterType; #[cfg(test)] pub(crate) mod test_utils; From f07106bd8406d0e8b907ab1eb53ab352ed21f4c4 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 15:57:37 +0100 Subject: [PATCH 17/37] Rename FilterType to Filter --- benches/unfilter.rs | 11 +++-------- examples/corpus-bench.rs | 12 ++++++------ src/common.rs | 2 +- src/encoder.rs | 38 +++++++++++++++++++------------------- src/filter.rs | 36 ++++++++++++++++++------------------ src/lib.rs | 2 +- 6 files changed, 48 insertions(+), 53 deletions(-) diff --git a/benches/unfilter.rs b/benches/unfilter.rs index 4ff5daa8..6cc9d5f5 100644 --- a/benches/unfilter.rs +++ b/benches/unfilter.rs @@ -9,17 +9,12 @@ use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use png::benchable_apis::unfilter; -use png::FilterType; +use png::Filter; use rand::Rng; fn unfilter_all(c: &mut Criterion) { let bpps = [1, 2, 3, 4, 6, 8]; - let filters = [ - FilterType::Sub, - FilterType::Up, - FilterType::Avg, - FilterType::Paeth, - ]; + let filters = [Filter::Sub, Filter::Up, Filter::Avg, Filter::Paeth]; for &filter in filters.iter() { for &bpp in bpps.iter() { bench_unfilter(c, filter, bpp); @@ -30,7 +25,7 @@ fn unfilter_all(c: &mut Criterion) { criterion_group!(benches, unfilter_all); criterion_main!(benches); -fn bench_unfilter(c: &mut Criterion, filter: FilterType, bpp: u8) { +fn bench_unfilter(c: &mut Criterion, filter: Filter, bpp: u8) { let mut group = c.benchmark_group("unfilter"); fn get_random_bytes(rng: &mut R, n: usize) -> Vec { diff --git a/examples/corpus-bench.rs b/examples/corpus-bench.rs index edf8e637..b87527cd 100644 --- a/examples/corpus-bench.rs +++ b/examples/corpus-bench.rs @@ -47,12 +47,12 @@ fn run_encode( Speed::Best => png::Compression::High, }); encoder.set_filter(match args.filter { - Filter::None => png::FilterType::NoFilter, - Filter::Sub => png::FilterType::Sub, - Filter::Up => png::FilterType::Up, - Filter::Average => png::FilterType::Avg, - Filter::Paeth => png::FilterType::Paeth, - Filter::Adaptive => png::FilterType::Paeth, + Filter::None => png::Filter::NoFilter, + Filter::Sub => png::Filter::Sub, + Filter::Up => png::Filter::Up, + Filter::Average => png::Filter::Avg, + Filter::Paeth => png::Filter::Paeth, + Filter::Adaptive => png::Filter::Paeth, }); encoder.set_adaptive_filter(match args.filter { Filter::Adaptive => png::AdaptiveFilterType::Adaptive, diff --git a/src/common.rs b/src/common.rs index a52b93d9..3f2fed74 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ //! Common types shared between the encoder and decoder use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk}; #[allow(unused_imports)] // used by doc comments only -use crate::FilterType; +use crate::Filter; use crate::{chunk, encoder}; use io::Write; use std::{borrow::Cow, convert::TryFrom, fmt, io}; diff --git a/src/encoder.rs b/src/encoder.rs index a8c32ace..6c863958 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -11,7 +11,7 @@ use crate::common::{ AnimationControl, BitDepth, BlendOp, BytesPerPixel, ColorType, Compression, DisposeOp, FrameControl, Info, ParameterError, ParameterErrorKind, PixelDimensions, ScaledFloat, }; -use crate::filter::{filter, FilterType}; +use crate::filter::{filter, Filter}; use crate::text_metadata::{ EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk, }; @@ -155,7 +155,7 @@ pub struct Encoder<'a, W: Write> { /// Decoding options, internal type, forwarded to the Writer. #[derive(Default)] struct Options { - filter: FilterType, + filter: Filter, sep_def_img: bool, validate_sequence: bool, } @@ -300,14 +300,14 @@ impl<'a, W: Write> Encoder<'a, W> { // choose the filter based on the requested compression match compression { Compression::NoCompression => { - self.set_filter(FilterType::NoFilter); // with no DEFLATE filtering would only waste time + self.set_filter(Filter::NoFilter); // with no DEFLATE filtering would only waste time } Compression::Fastest => { - self.set_filter(FilterType::Up); // fast and avoids long backreferences in DEFLATE stream + self.set_filter(Filter::Up); // fast and avoids long backreferences in DEFLATE stream } - Compression::Fast => self.set_filter(FilterType::Adaptive), - Compression::Balanced => self.set_filter(FilterType::Adaptive), - Compression::High => self.set_filter(FilterType::Adaptive), + Compression::Fast => self.set_filter(Filter::Adaptive), + Compression::Balanced => self.set_filter(Filter::Adaptive), + Compression::High => self.set_filter(Filter::Adaptive), } } @@ -323,7 +323,7 @@ impl<'a, W: Write> Encoder<'a, W> { /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the /// cost of more complex processing, try out [`FilterType::Paeth`]. - pub fn set_filter(&mut self, filter: FilterType) { + pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -805,7 +805,7 @@ impl Writer { /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the /// cost of more complex processing, try out [`FilterType::Paeth`]. - pub fn set_filter(&mut self, filter: FilterType) { + pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -1284,7 +1284,7 @@ pub struct StreamWriter<'a, W: Write> { height: u32, bpp: BytesPerPixel, - filter: FilterType, + filter: Filter, fctl: Option, compression: DeflateCompression, } @@ -1334,7 +1334,7 @@ impl<'a, W: Write> StreamWriter<'a, W> { /// For optimal compression ratio you should enable adaptive filtering /// instead of setting a single filter for the entire image, see /// [set_adaptive_filter](Self::set_adaptive_filter). - pub fn set_filter(&mut self, filter: FilterType) { + pub fn set_filter(&mut self, filter: Filter) { self.filter = filter; } @@ -1972,7 +1972,7 @@ mod tests { fn all_filters_roundtrip() -> io::Result<()> { let pixel: Vec<_> = (0..48).collect(); - let roundtrip = |filter: FilterType| -> io::Result<()> { + let roundtrip = |filter: Filter| -> io::Result<()> { let mut buffer = vec![]; let mut encoder = Encoder::new(&mut buffer, 4, 4); encoder.set_depth(BitDepth::Eight); @@ -1992,11 +1992,11 @@ mod tests { Ok(()) }; - roundtrip(FilterType::NoFilter)?; - roundtrip(FilterType::Sub)?; - roundtrip(FilterType::Up)?; - roundtrip(FilterType::Avg)?; - roundtrip(FilterType::Paeth)?; + roundtrip(Filter::NoFilter)?; + roundtrip(Filter::Sub)?; + roundtrip(Filter::Up)?; + roundtrip(Filter::Avg)?; + roundtrip(Filter::Paeth)?; Ok(()) } @@ -2010,7 +2010,7 @@ mod tests { let mut encoder = Encoder::new(&mut buffer, 4, 4); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgb); - encoder.set_filter(FilterType::Avg); + encoder.set_filter(Filter::Avg); if let Some(gamma) = gamma { encoder.set_source_gamma(gamma); } @@ -2234,7 +2234,7 @@ mod tests { let mut encoder = Encoder::new(&mut cursor, 8, 8); encoder.set_color(ColorType::Rgba); - encoder.set_filter(FilterType::Paeth); + encoder.set_filter(Filter::Paeth); let mut writer = encoder.write_header()?; let mut stream = writer.stream_writer()?; diff --git a/src/filter.rs b/src/filter.rs index 037c6100..37d55e66 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -270,7 +270,7 @@ mod simd { /// /// Details on how each filter works can be found in the [PNG Book](http://www.libpng.org/pub/png/book/chapter09.html). #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FilterType { +pub enum Filter { NoFilter, Sub, Up, @@ -279,20 +279,20 @@ pub enum FilterType { Adaptive, } -impl Default for FilterType { +impl Default for Filter { fn default() -> Self { - FilterType::Up + Filter::Up } } -impl From for FilterType { +impl From for Filter { fn from(value: RowFilter) -> Self { match value { - RowFilter::NoFilter => FilterType::NoFilter, - RowFilter::Sub => FilterType::Sub, - RowFilter::Up => FilterType::Up, - RowFilter::Avg => FilterType::Avg, - RowFilter::Paeth => FilterType::Paeth, + RowFilter::NoFilter => Filter::NoFilter, + RowFilter::Sub => Filter::Sub, + RowFilter::Up => Filter::Up, + RowFilter::Avg => Filter::Avg, + RowFilter::Paeth => Filter::Paeth, } } } @@ -326,14 +326,14 @@ impl RowFilter { } } - pub fn from_method(strat: FilterType) -> Option { + pub fn from_method(strat: Filter) -> Option { match strat { - FilterType::NoFilter => Some(Self::NoFilter), - FilterType::Sub => Some(Self::Sub), - FilterType::Up => Some(Self::Up), - FilterType::Avg => Some(Self::Avg), - FilterType::Paeth => Some(Self::Paeth), - FilterType::Adaptive => None, + Filter::NoFilter => Some(Self::NoFilter), + Filter::Sub => Some(Self::Sub), + Filter::Up => Some(Self::Up), + Filter::Avg => Some(Self::Avg), + Filter::Paeth => Some(Self::Paeth), + Filter::Adaptive => None, } } } @@ -1051,7 +1051,7 @@ fn filter_internal( } pub(crate) fn filter( - method: FilterType, + method: Filter, bpp: BytesPerPixel, previous: &[u8], current: &[u8], @@ -1062,7 +1062,7 @@ pub(crate) fn filter( let len = current.len(); match method { - FilterType::Adaptive => { + Filter::Adaptive => { let mut min_sum: u64 = u64::MAX; let mut filter_choice = RowFilter::NoFilter; for &filter in [Sub, Up, Avg, Paeth].iter() { diff --git a/src/lib.rs b/src/lib.rs index 9777afa2..89034a00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub use crate::decoder::{ OutputInfo, Reader, StreamingDecoder, }; pub use crate::encoder::{Encoder, EncodingError, StreamWriter, Writer}; -pub use crate::filter::FilterType; +pub use crate::filter::Filter; #[cfg(test)] pub(crate) mod test_utils; From 085fba9cd41e2fe333dee3c53e861b616c802400 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:02:23 +0100 Subject: [PATCH 18/37] Refactor set_compression: split the conversion from Compression to Filter now that it's possible --- src/encoder.rs | 15 ++------------- src/filter.rs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/encoder.rs b/src/encoder.rs index 6c863958..b00c79af 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -296,19 +296,8 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set compression parameters. pub fn set_compression(&mut self, compression: Compression) { - self.info.compression_deflate = DeflateCompression::from_simple(compression); - // choose the filter based on the requested compression - match compression { - Compression::NoCompression => { - self.set_filter(Filter::NoFilter); // with no DEFLATE filtering would only waste time - } - Compression::Fastest => { - self.set_filter(Filter::Up); // fast and avoids long backreferences in DEFLATE stream - } - Compression::Fast => self.set_filter(Filter::Adaptive), - Compression::Balanced => self.set_filter(Filter::Adaptive), - Compression::High => self.set_filter(Filter::Adaptive), - } + self.set_deflate_compression(DeflateCompression::from_simple(compression)); + self.set_filter(Filter::from_simple(compression)); } /// Provides in-depth customization of DEFLATE compression options. diff --git a/src/filter.rs b/src/filter.rs index 37d55e66..7e660142 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,6 +1,6 @@ use core::convert::TryInto; -use crate::common::BytesPerPixel; +use crate::{common::BytesPerPixel, Compression}; /// SIMD helpers for `fn unfilter` /// @@ -297,6 +297,18 @@ impl From for Filter { } } +impl Filter { + pub(crate) fn from_simple(compression: Compression) -> Self { + match compression { + Compression::NoCompression => Filter::NoFilter, // with no DEFLATE filtering would only waste time + Compression::Fastest => Filter::Up, // fast and avoids long backreferences in DEFLATE stream + Compression::Fast => Filter::Adaptive, + Compression::Balanced => Filter::Adaptive, + Compression::High => Filter::Adaptive, + } + } +} + /// Unlike the public [FilterType], does not include the "Adaptive" option #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] From 571ca493f780b3378ebf7b9fe309959eabbbb0e5 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:04:32 +0100 Subject: [PATCH 19/37] Make adaptive filtering the default in one more place --- src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter.rs b/src/filter.rs index 7e660142..0b333c65 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -281,7 +281,7 @@ pub enum Filter { impl Default for Filter { fn default() -> Self { - Filter::Up + Filter::Adaptive } } From eecf56c94ba68e5936e4d82f03ab952c7346a418 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:07:37 +0100 Subject: [PATCH 20/37] Fix compilation of benchmakring example --- examples/corpus-bench.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/corpus-bench.rs b/examples/corpus-bench.rs index b87527cd..c088d71c 100644 --- a/examples/corpus-bench.rs +++ b/examples/corpus-bench.rs @@ -52,11 +52,7 @@ fn run_encode( Filter::Up => png::Filter::Up, Filter::Average => png::Filter::Avg, Filter::Paeth => png::Filter::Paeth, - Filter::Adaptive => png::Filter::Paeth, - }); - encoder.set_adaptive_filter(match args.filter { - Filter::Adaptive => png::AdaptiveFilterType::Adaptive, - _ => png::AdaptiveFilterType::NonAdaptive, + Filter::Adaptive => png::Filter::Adaptive, }); let mut encoder = encoder.write_header().unwrap(); encoder.write_image_data(image).unwrap(); From f7adc08295add13d5b2a724af078c24f0908bd4f Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:09:08 +0100 Subject: [PATCH 21/37] Fix doclinks --- fuzz/fuzz_targets/roundtrip.rs | 4 ++-- src/benchable_apis.rs | 4 ++-- src/common.rs | 6 +++--- src/encoder.rs | 10 +++++----- src/filter.rs | 12 ++++++------ 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/fuzz/fuzz_targets/roundtrip.rs b/fuzz/fuzz_targets/roundtrip.rs index 4996d747..a9354b89 100644 --- a/fuzz/fuzz_targets/roundtrip.rs +++ b/fuzz/fuzz_targets/roundtrip.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use png::{FilterType, ColorType, BitDepth}; +use png::{Filter, ColorType, BitDepth}; fuzz_target!(|data: (u8, u8, u8, u8, u8, Vec, Vec)| { if let Some((raw, encoded)) = encode_png(data.0, data.1, data.2, data.3, data.4, &data.5, &data.6) { @@ -16,7 +16,7 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi // Convert untyped bytes to the correct types and validate them: let width = width as u32; if width == 0 { return None }; - let filter = FilterType::from_u8(filter)?; + let filter = Filter::from_u8(filter)?; let bit_depth = BitDepth::from_u8(raw_bit_depth)?; let max_palette_length = 3 * u32::pow(2, raw_bit_depth as u32) as usize; let mut palette = raw_palette; diff --git a/src/benchable_apis.rs b/src/benchable_apis.rs index 17b0b0d6..a7aef22a 100644 --- a/src/benchable_apis.rs +++ b/src/benchable_apis.rs @@ -2,12 +2,12 @@ //! This module is gated behind the "benchmarks" feature. use crate::common::BytesPerPixel; -use crate::filter::FilterType; +use crate::filter::Filter; use crate::{BitDepth, ColorType, Info}; /// Re-exporting `unfilter` to make it easier to benchmark, despite some items being only /// `pub(crate)`: `fn unfilter`, `enum BytesPerPixel`. -pub fn unfilter(filter: FilterType, tbpp: u8, previous: &[u8], current: &mut [u8]) { +pub fn unfilter(filter: Filter, tbpp: u8, previous: &[u8], current: &mut [u8]) { let tbpp = BytesPerPixel::from_usize(tbpp as usize); crate::filter::unfilter(filter, tbpp, previous, current) } diff --git a/src/common.rs b/src/common.rs index 3f2fed74..209642e3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -310,7 +310,7 @@ impl AnimationControl { /// the appropriate DEFLATE compression mode and PNG filter. /// /// If you need more control over the encoding paramters, -/// you can set the [DeflateCompression], [FilterType] and [AdaptiveFilterType] manually. +/// you can set the [DeflateCompression] and [Filter] manually. #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum Compression { @@ -341,7 +341,7 @@ impl Default for Compression { /// Note that this setting only affects DEFLATE compression. /// Another setting that influences the compression ratio and lets you choose /// between encoding speed and compression ratio is the *filter*, -/// See [FilterType] and [AdaptiveFilterType]. +/// See [Filter] and [AdaptiveFilter]. #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum DeflateCompression { @@ -350,7 +350,7 @@ pub enum DeflateCompression { /// Useful for incompressible images, /// or when speed is paramount and you don't care about size at all. /// - /// This mode also disables filters, forcing [FilterType::NoFilter]. + /// This mode also disables filters, forcing [Filter::NoFilter]. NoCompression, /// Excellent for creating lightly compressed PNG images very quickly. diff --git a/src/encoder.rs b/src/encoder.rs index b00c79af..f2d76c7c 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -309,9 +309,9 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set the used filter type. /// - /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for + /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the - /// cost of more complex processing, try out [`FilterType::Paeth`]. + /// cost of more complex processing, try out [`Filter::Paeth`]. pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -791,9 +791,9 @@ impl Writer { /// Set the used filter type for the following frames. /// - /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for + /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the - /// cost of more complex processing, try out [`FilterType::Paeth`]. + /// cost of more complex processing, try out [`Filter::Paeth`]. pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -1317,7 +1317,7 @@ impl<'a, W: Write> StreamWriter<'a, W> { /// Set the used filter type for the next frame. /// - /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for + /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. /// /// For optimal compression ratio you should enable adaptive filtering diff --git a/src/filter.rs b/src/filter.rs index 0b333c65..a76ea797 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -103,7 +103,7 @@ mod simd { .select(a, smallest.simd_eq(pb).select(b, c)) } - /// Memory of previous pixels (as needed to unfilter `FilterType::Paeth`). + /// Memory of previous pixels (as needed to unfilter `Filter::Paeth`). /// See also https://www.w3.org/TR/png/#filter-byte-positions #[derive(Default)] struct PaethState @@ -118,7 +118,7 @@ mod simd { a: Simd, } - /// Mutates `x` as needed to unfilter `FilterType::Paeth`. + /// Mutates `x` as needed to unfilter `Filter::Paeth`. /// /// `b` is the current pixel in the previous row. `x` is the current pixel in the current row. /// See also https://www.w3.org/TR/png/#filter-byte-positions @@ -167,7 +167,7 @@ mod simd { dest[0..3].copy_from_slice(&src.to_array()[0..3]) } - /// Undoes `FilterType::Paeth` for `BytesPerPixel::Three`. + /// Undoes `Filter::Paeth` for `BytesPerPixel::Three`. pub fn unfilter_paeth3(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 3, 0); @@ -198,7 +198,7 @@ mod simd { store3(x, curr_row); } - /// Undoes `FilterType::Paeth` for `BytesPerPixel::Four` and `BytesPerPixel::Eight`. + /// Undoes `Filter::Paeth` for `BytesPerPixel::Four` and `BytesPerPixel::Eight`. /// /// This function calculates the Paeth predictor entirely in `Simd` /// without converting to an intermediate `Simd`. Doing so avoids @@ -230,7 +230,7 @@ mod simd { dest[0..6].copy_from_slice(&src.to_array()[0..6]) } - /// Undoes `FilterType::Paeth` for `BytesPerPixel::Six`. + /// Undoes `Filter::Paeth` for `BytesPerPixel::Six`. pub fn unfilter_paeth6(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 6, 0); @@ -309,7 +309,7 @@ impl Filter { } } -/// Unlike the public [FilterType], does not include the "Adaptive" option +/// Unlike the public [Filter], does not include the "Adaptive" option #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub(crate) enum RowFilter { From 2eaf58846d0aa02a72cb76b1546441dd8a37ecc9 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:10:46 +0100 Subject: [PATCH 22/37] Fix one more doclink --- src/common.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index 209642e3..932f23dd 100644 --- a/src/common.rs +++ b/src/common.rs @@ -340,8 +340,7 @@ impl Default for Compression { /// /// Note that this setting only affects DEFLATE compression. /// Another setting that influences the compression ratio and lets you choose -/// between encoding speed and compression ratio is the *filter*, -/// See [Filter] and [AdaptiveFilter]. +/// between encoding speed and compression ratio is the [Filter]. #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum DeflateCompression { From d157df100904069367b01c7a67fe00014a6e8286 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:15:53 +0100 Subject: [PATCH 23/37] Update doc comments on the oddly numerous set_filter --- src/encoder.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/encoder.rs b/src/encoder.rs index f2d76c7c..91615864 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -294,7 +294,7 @@ impl<'a, W: Write> Encoder<'a, W> { self.info.bit_depth = depth; } - /// Set compression parameters. + /// Set compression parameters, see [Compression] for the available options. pub fn set_compression(&mut self, compression: Compression) { self.set_deflate_compression(DeflateCompression::from_simple(compression)); self.set_filter(Filter::from_simple(compression)); @@ -309,9 +309,11 @@ impl<'a, W: Write> Encoder<'a, W> { /// Set the used filter type. /// - /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for - /// sample values based on the previous. For a potentially better compression ratio, at the - /// cost of more complex processing, try out [`Filter::Paeth`]. + /// The default filter is [`Filter::Adaptive`] which automatically selects the best filter + /// for each row of the image. + /// + /// You should only change this if you are after very fast compression, + /// and either don't care about compression ratio or know exactly what works best for your images. pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -789,11 +791,13 @@ impl Writer { Ok(()) } - /// Set the used filter type for the following frames. + /// Set the used filter type. /// - /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for - /// sample values based on the previous. For a potentially better compression ratio, at the - /// cost of more complex processing, try out [`Filter::Paeth`]. + /// The default filter is [`Filter::Adaptive`] which automatically selects the best filter + /// for each row of the image. + /// + /// You should only change this if you are after very fast compression, + /// and either don't care about compression ratio or know exactly what works best for your images. pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; } @@ -1315,14 +1319,13 @@ impl<'a, W: Write> StreamWriter<'a, W> { }) } - /// Set the used filter type for the next frame. + /// Set the used filter type. /// - /// The default filter is [`Filter::Sub`] which provides a basic prediction algorithm for - /// sample values based on the previous. + /// The default filter is [`Filter::Adaptive`] which automatically selects the best filter + /// for each row of the image. /// - /// For optimal compression ratio you should enable adaptive filtering - /// instead of setting a single filter for the entire image, see - /// [set_adaptive_filter](Self::set_adaptive_filter). + /// You should only change this if you are after very fast compression, + /// and either don't care about compression ratio or know exactly what works best for your images. pub fn set_filter(&mut self, filter: Filter) { self.filter = filter; } From 98f0c2936c8dde37c3dae302d0b96e3e84fe1b71 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:18:37 +0100 Subject: [PATCH 24/37] Document the default on the Filter struct too --- src/filter.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/filter.rs b/src/filter.rs index a76ea797..ce30bc8f 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -269,6 +269,8 @@ mod simd { /// this does not operate on pixels but on raw bytes of a scanline. /// /// Details on how each filter works can be found in the [PNG Book](http://www.libpng.org/pub/png/book/chapter09.html). +/// +/// The default filter is `Adaptive`, which uses heuristics to select the best filter for every row. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Filter { NoFilter, From a96a3a234310e964c50f172c2ec06b86652869dd Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:28:14 +0100 Subject: [PATCH 25/37] Fix compilation of benchmark reexport module --- src/benchable_apis.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/benchable_apis.rs b/src/benchable_apis.rs index a7aef22a..10599037 100644 --- a/src/benchable_apis.rs +++ b/src/benchable_apis.rs @@ -2,12 +2,13 @@ //! This module is gated behind the "benchmarks" feature. use crate::common::BytesPerPixel; -use crate::filter::Filter; +use crate::filter::{Filter, RowFilter}; use crate::{BitDepth, ColorType, Info}; /// Re-exporting `unfilter` to make it easier to benchmark, despite some items being only /// `pub(crate)`: `fn unfilter`, `enum BytesPerPixel`. pub fn unfilter(filter: Filter, tbpp: u8, previous: &[u8], current: &mut [u8]) { + let filter = RowFilter::from_method(filter).unwrap(); // RowFilter type is private let tbpp = BytesPerPixel::from_usize(tbpp as usize); crate::filter::unfilter(filter, tbpp, previous, current) } From 111f7c26ec43dfcdab9990d71fd326f28e4260a8 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 16:49:55 +0100 Subject: [PATCH 26/37] Make Filter enum non-exhaustive to facilitate further experimentation with filtering heuristics --- src/filter.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/filter.rs b/src/filter.rs index ce30bc8f..fa15d62f 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -272,6 +272,7 @@ mod simd { /// /// The default filter is `Adaptive`, which uses heuristics to select the best filter for every row. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum Filter { NoFilter, Sub, From 7ba0fdaf6ad5a77a0d21bb3a2a555797e785f69f Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 27 Sep 2024 17:02:50 +0100 Subject: [PATCH 27/37] Document stability guarantees for in-depth compression controls --- src/common.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/common.rs b/src/common.rs index 932f23dd..c227bc3f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -341,6 +341,15 @@ impl Default for Compression { /// Note that this setting only affects DEFLATE compression. /// Another setting that influences the compression ratio and lets you choose /// between encoding speed and compression ratio is the [Filter]. +/// +/// ### Stability guarantees +/// +/// The implementation details of DEFLATE compression may evolve over time, +/// even without a semver-breaking change to the version of `png` crate. +/// +/// If a certain compression setting is superseded by other options, +/// it may be marked deprecated and remapped to a different option. +/// You will see a deprecation notice when compiling code relying on such options. #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum DeflateCompression { From 4c336ebc39b930525a5b83e3f26a31c8f82f4cb9 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 5 Dec 2024 12:01:40 +0000 Subject: [PATCH 28/37] Use `Filter::None` for `Compression::Fastest` preset --- src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter.rs b/src/filter.rs index fa15d62f..57b63810 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -304,7 +304,7 @@ impl Filter { pub(crate) fn from_simple(compression: Compression) -> Self { match compression { Compression::NoCompression => Filter::NoFilter, // with no DEFLATE filtering would only waste time - Compression::Fastest => Filter::Up, // fast and avoids long backreferences in DEFLATE stream + Compression::Fastest => Filter::None, Compression::Fast => Filter::Adaptive, Compression::Balanced => Filter::Adaptive, Compression::High => Filter::Adaptive, From df73363a3f57e60dbace74040274844e9f1ae120 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 5 Dec 2024 21:54:55 +0000 Subject: [PATCH 29/37] Revert "Use `Filter::None` for `Compression::Fastest` preset" This reverts commit 4c336ebc39b930525a5b83e3f26a31c8f82f4cb9. --- src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter.rs b/src/filter.rs index 57b63810..fa15d62f 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -304,7 +304,7 @@ impl Filter { pub(crate) fn from_simple(compression: Compression) -> Self { match compression { Compression::NoCompression => Filter::NoFilter, // with no DEFLATE filtering would only waste time - Compression::Fastest => Filter::None, + Compression::Fastest => Filter::Up, // fast and avoids long backreferences in DEFLATE stream Compression::Fast => Filter::Adaptive, Compression::Balanced => Filter::Adaptive, Compression::High => Filter::Adaptive, From 76b219a5f9d1bf94b27bbdc579bc45baeb43cd03 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 5 Dec 2024 21:59:10 +0000 Subject: [PATCH 30/37] Record the rationale for Filter::Up in a comment --- src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter.rs b/src/filter.rs index fa15d62f..8532fec0 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -304,7 +304,7 @@ impl Filter { pub(crate) fn from_simple(compression: Compression) -> Self { match compression { Compression::NoCompression => Filter::NoFilter, // with no DEFLATE filtering would only waste time - Compression::Fastest => Filter::Up, // fast and avoids long backreferences in DEFLATE stream + Compression::Fastest => Filter::Up, // pairs well with FdeflateUltraFast, producing much smaller files while being very fast Compression::Fast => Filter::Adaptive, Compression::Balanced => Filter::Adaptive, Compression::High => Filter::Adaptive, From a10a26650b3338667bdec6ff355b7cd35d3b6f4d Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 5 Dec 2024 23:10:18 +0000 Subject: [PATCH 31/37] Clarify comment --- src/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index c227bc3f..b151b766 100644 --- a/src/common.rs +++ b/src/common.rs @@ -376,7 +376,8 @@ pub enum DeflateCompression { /// Flate2 has several backends that make different trade-offs. /// See the flate2 documentation for the available backends for more information. Flate2(u32), - // TODO: Zopfli? + + // Other variants can be added in the future } impl Default for DeflateCompression { From 43489af12419895f3e71b3d846477f51fecf5cee Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 5 Dec 2024 23:13:13 +0000 Subject: [PATCH 32/37] cargo fmt --- src/common.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index b151b766..ffc4e4c9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -376,7 +376,6 @@ pub enum DeflateCompression { /// Flate2 has several backends that make different trade-offs. /// See the flate2 documentation for the available backends for more information. Flate2(u32), - // Other variants can be added in the future } From 5af8a95aa284cf72b5a788edd0485f49ca67849e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sun, 22 Dec 2024 22:07:07 +0000 Subject: [PATCH 33/37] Switch flate2 mode from using u32 compression level to u8, for a hopefully less confusing API --- src/common.rs | 8 ++++---- src/encoder.rs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common.rs b/src/common.rs index ffc4e4c9..0f1130f0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -375,7 +375,7 @@ pub enum DeflateCompression { /// /// Flate2 has several backends that make different trade-offs. /// See the flate2 documentation for the available backends for more information. - Flate2(u32), + Flate2(u8), // Other variants can be added in the future } @@ -391,8 +391,8 @@ impl DeflateCompression { Compression::NoCompression => Self::NoCompression, Compression::Fastest => Self::FdeflateUltraFast, Compression::Fast => Self::FdeflateUltraFast, - Compression::Balanced => Self::Flate2(flate2::Compression::default().level()), - Compression::High => Self::Flate2(flate2::Compression::best().level()), + Compression::Balanced => Self::Flate2(flate2::Compression::default().level() as u8), + Compression::High => Self::Flate2(flate2::Compression::best().level() as u8), } } @@ -400,7 +400,7 @@ impl DeflateCompression { match self { DeflateCompression::NoCompression => flate2::Compression::none(), DeflateCompression::FdeflateUltraFast => flate2::Compression::new(1), - DeflateCompression::Flate2(level) => flate2::Compression::new(*level), + DeflateCompression::Flate2(level) => flate2::Compression::new(u32::from(*level)), } } } diff --git a/src/encoder.rs b/src/encoder.rs index 91615864..8b2b8b9e 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -719,7 +719,8 @@ impl Writer { DeflateCompression::Flate2(level) => { let mut current = vec![0; in_len]; - let mut zlib = ZlibEncoder::new(Vec::new(), flate2::Compression::new(level)); + let mut zlib = + ZlibEncoder::new(Vec::new(), flate2::Compression::new(u32::from(level))); for line in data.chunks(in_len) { let filter_type = filter(filter_method, bpp, prev, line, &mut current); From ca24d5501dd522027a0c2b0f846707d16a2f6564 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Mon, 23 Dec 2024 10:44:17 +0000 Subject: [PATCH 34/37] Correct comment --- src/encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoder.rs b/src/encoder.rs index 8b2b8b9e..1b5908c9 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -152,7 +152,7 @@ pub struct Encoder<'a, W: Write> { options: Options, } -/// Decoding options, internal type, forwarded to the Writer. +/// Encoding options, internal type, forwarded to the Writer. #[derive(Default)] struct Options { filter: Filter, From a5990f37f8a64878fddcd094275aec4fcbc03dbd Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sun, 29 Dec 2024 18:27:33 +0000 Subject: [PATCH 35/37] cargo fmt --- src/encoder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/encoder.rs b/src/encoder.rs index f39dd834..ac23a68e 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -331,7 +331,6 @@ impl<'a, W: Write> Encoder<'a, W> { /// and either don't care about compression ratio or know exactly what works best for your images. pub fn set_filter(&mut self, filter: Filter) { self.options.filter = filter; - } /// Set the fraction of time every frame is going to be displayed, in seconds. From 9a398c217a91035e4bee2fab6c0258b84829df7f Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sun, 29 Dec 2024 18:38:29 +0000 Subject: [PATCH 36/37] Update roundtrip target to the new encoding API. improves encoding parameter coverage --- fuzz/fuzz_targets/roundtrip.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/fuzz/fuzz_targets/roundtrip.rs b/fuzz/fuzz_targets/roundtrip.rs index a9354b89..904612ee 100644 --- a/fuzz/fuzz_targets/roundtrip.rs +++ b/fuzz/fuzz_targets/roundtrip.rs @@ -16,7 +16,7 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi // Convert untyped bytes to the correct types and validate them: let width = width as u32; if width == 0 { return None }; - let filter = Filter::from_u8(filter)?; + let filter = filter_from_u8(filter); let bit_depth = BitDepth::from_u8(raw_bit_depth)?; let max_palette_length = 3 * u32::pow(2, raw_bit_depth as u32) as usize; let mut palette = raw_palette; @@ -29,10 +29,9 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi } // compression let compression = match compression { - 0 => png::Compression::Default, - 1 => png::Compression::Fast, - 2 => png::Compression::High, - 3 => png::Compression::None, + 0 => png::DeflateCompression::NoCompression, + level @ 1..=9 => png::DeflateCompression::Flate2(level), + 10 => png::DeflateCompression::FdeflateUltraFast, _ => return None, }; @@ -51,7 +50,7 @@ fn encode_png<'a>(width: u8, filter: u8, compression: u8, color_type: u8, raw_bi encoder.set_depth(bit_depth); encoder.set_color(color_type); encoder.set_filter(filter); - encoder.set_compression(compression); + encoder.set_deflate_compression(compression); if let ColorType::Indexed = color_type { encoder.set_palette(palette) } @@ -74,6 +73,18 @@ fn decode_png(data: &[u8]) -> (png::OutputInfo, Vec) { (info, img_data) } +/// Filter::from() doesn't cover the Filter::Adaptive variant, so we roll our own +fn filter_from_u8(input: u8) -> Filter { + match input { + 0 => Filter::NoFilter, + 1 => Filter::Sub, + 2 => Filter::Up, + 3 => Filter::Avg, + 4 => Filter::Paeth, + _ => Filter::Adaptive, + } +} + // copied from the `png` codebase because it's pub(crate) fn raw_row_length_from_width(depth: BitDepth, color: ColorType, width: u32) -> usize { let samples = width as usize * color.samples(); From f9d27becfe73bf5a0887f09fe0be92ca8339e96e Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Sun, 29 Dec 2024 18:43:42 +0000 Subject: [PATCH 37/37] drop unused imports --- src/common.rs | 2 +- src/decoder/mod.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index 90fd950f..081cbc47 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,5 @@ //! Common types shared between the encoder and decoder -use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk}; +use crate::text_metadata::{ITXtChunk, TEXtChunk, ZTXtChunk}; #[allow(unused_imports)] // used by doc comments only use crate::Filter; use crate::{chunk, encoder}; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index afbe55a7..45aa30b7 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -17,7 +17,6 @@ use crate::adam7::{self, Adam7Info}; use crate::common::{ BitDepth, BytesPerPixel, ColorType, Info, ParameterErrorKind, Transformations, }; -use crate::filter::{unfilter, RowFilter}; use crate::FrameControl; pub use interlace_info::InterlaceInfo;