From 6e258b5b446e0a70de29f8f80aba5288ce5d36b4 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Tue, 24 Sep 2024 15:24:15 +0100 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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()); }