diff --git a/src/common.rs b/src/common.rs index 4475153e..d1f14826 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 DeflateCompression { + /// 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 DeflateCompression { + 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 { + DeflateCompression::NoCompression => flate2::Compression::none(), + DeflateCompression::FdeflateUltraFast => flate2::Compression::new(1), + DeflateCompression::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_deflate: 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_deflate: 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) -> 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 27c1432c..0126d2f3 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::DeflateCompression; 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_deflate = None; + } + + /// 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); } /// Set the used filter type. @@ -482,7 +488,7 @@ struct PartialInfo { color_type: ColorType, frame_control: Option, animation_control: Option, - compression: Compression, + compression: DeflateCompression, has_palette: bool, } @@ -495,7 +501,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(), } } @@ -525,7 +531,7 @@ impl PartialInfo { color_type: self.color_type, frame_control: self.frame_control, animation_control: self.animation_control, - compression: self.compression, + compression_deflate: Some(self.compression), ..Default::default() } } @@ -681,7 +687,16 @@ impl Writer { let adaptive_method = self.options.adaptive_filter; let zlib_encoded = match self.info.compression { - Compression::Fast => { + DeflateCompression::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() + } + DeflateCompression::FdeflateUltraFast => { let mut compressor = fdeflate::Compressor::new(std::io::Cursor::new(Vec::new()))?; let mut current = vec![0; in_len + 1]; @@ -707,10 +722,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) { @@ -722,10 +734,10 @@ impl Writer { compressed } } - _ => { + DeflateCompression::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, @@ -1301,7 +1313,7 @@ pub struct StreamWriter<'a, W: Write> { filter: FilterType, adaptive_filter: AdaptiveFilterType, fctl: Option, - compression: Compression, + compression: DeflateCompression, } impl<'a, W: Write> StreamWriter<'a, W> { @@ -1324,7 +1336,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), @@ -1563,7 +1575,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!(), @@ -1659,25 +1671,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::*; @@ -1706,30 +1699,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 DeflateCompression::*; + 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_deflate_compression(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); } } } @@ -1751,37 +1748,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 DeflateCompression::*; + 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_deflate_compression(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); } } }