diff --git a/src/codecs/avif/encoder.rs b/src/codecs/avif/encoder.rs index 17cb77ceff..57aae8e7cb 100644 --- a/src/codecs/avif/encoder.rs +++ b/src/codecs/avif/encoder.rs @@ -5,14 +5,14 @@ /// [AVIF]: https://aomediacodec.github.io/av1-avif/ use std::borrow::Cow; use std::cmp::min; -use std::io::Write; +use std::io::{Seek, Write}; use std::mem::size_of; use crate::color::{FromColor, Luma, LumaA, Rgb, Rgba}; use crate::error::{ EncodingError, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind, }; -use crate::{ExtendedColorType, ImageBuffer, ImageEncoder, ImageFormat, Pixel}; +use crate::{EncoderOptions, ExtendedColorType, ImageBuffer, ImageEncoder, ImageFormat, Pixel}; use crate::{ImageError, ImageResult}; use bytemuck::{try_cast_slice, try_cast_slice_mut, Pod, PodCastError}; @@ -52,6 +52,53 @@ enum RgbColor<'buf> { Rgba8(Img<&'buf [RGBA8]>), } +/// Encoding options for the AVIF format. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct AvifOptions { + /// The quality of the AVIF encoding, from 1 to 100. Higher is better quality and larger file size. + /// + /// Defaults to **80**. + pub quality: u8, + /// The speed of the AVIF encoding, from 1 to 10. Higher is faster but lower quality. + /// + /// Defaults to **4**. + pub speed: u8, + /// The color space to encode with. + /// + /// If `None`, the color space will be chosen dynamically for each image. No particular choice + /// is guaranteed and the chosen color space may change without warning between versions of the + /// library. + /// + /// Defaults to [`ColorSpace::Bt709`]. + pub color_space: ColorSpace, + /// The number of threads to use for encoding. + /// + /// If `None`, the encoder will use all available threads. + /// + /// Defaults to `None`. + // TODO: Using `usize` is weird. Also None == all seems weird too. Why not usize::MAX == all? + pub num_threads: Option, +} +impl Default for AvifOptions { + fn default() -> Self { + Self { + quality: 80, + speed: 4, + color_space: ColorSpace::Bt709, + num_threads: None, + } + } +} +impl EncoderOptions for AvifOptions { + fn build(self, w: W) -> ImageResult { + let encoder = AvifEncoder::new_with_speed_quality(w, self.speed, self.quality) + .with_colorspace(self.color_space) + .with_num_threads(self.num_threads); + Ok(encoder) + } +} + impl AvifEncoder { /// Create a new encoder that writes its output to `w`. pub fn new(w: W) -> Self { diff --git a/src/codecs/avif/mod.rs b/src/codecs/avif/mod.rs index 3059b82fe3..58ad14f24d 100644 --- a/src/codecs/avif/mod.rs +++ b/src/codecs/avif/mod.rs @@ -6,7 +6,7 @@ #[cfg(feature = "avif-native")] pub use self::decoder::AvifDecoder; #[cfg(feature = "avif")] -pub use self::encoder::{AvifEncoder, ColorSpace}; +pub use self::encoder::{AvifEncoder, AvifOptions, ColorSpace}; #[cfg(feature = "avif-native")] mod decoder; diff --git a/src/codecs/jpeg/encoder.rs b/src/codecs/jpeg/encoder.rs index 30dab266ef..f6cb6bf5a9 100644 --- a/src/codecs/jpeg/encoder.rs +++ b/src/codecs/jpeg/encoder.rs @@ -1,11 +1,13 @@ #![allow(clippy::too_many_arguments)] -use std::io::Write; +use std::io::{Seek, Write}; use std::{error, fmt}; use crate::error::{ EncodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, }; -use crate::{ColorType, DynamicImage, ExtendedColorType, ImageEncoder, ImageFormat}; +use crate::{ + ColorType, DynamicImage, EncoderOptions, ExtendedColorType, ImageEncoder, ImageFormat, +}; use jpeg_encoder::Encoder; @@ -41,7 +43,7 @@ pub enum ChromaSubsampling { S422, /// **4:2:0** The resolution of color information is reduced by a factor of 2 both horizontally and vertically. /// - /// Results in a smaller file size. Well suited for photographs where it incurs no visial quality loss. + /// Results in a smaller file size. Well suited for photographs where it incurs no visual quality loss. S420, } @@ -134,6 +136,54 @@ impl From for ImageError { impl error::Error for EncoderError {} +/// Encoding options for the JPEG format. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct JpegOptions { + /// The quality of the JPEG encoding, from 1 to 100. Higher is better quality and larger file size. + /// + /// Defaults to **75**. + pub quality: u8, + /// The chroma subsampling mode. See [ChromaSubsampling] for details. + pub chroma_subsampling: ChromaSubsampling, + /// Spend extra time optimizing Huffman tables. Slightly reduces file size at the cost of encoding speed. + /// + /// Defaults to **false**. + pub optimize_huffman_tables: bool, + /// Progressive files allow showing a low-resolution view of the entire image before it's fully downloaded. + /// Useful for large images that will be displayed on the web. + /// + /// Defaults to **false**. + pub progress: bool, + /// The pixel density of the images the encoder will encode. + /// If this method is not called, then a default pixel aspect ratio of 1x1 will be applied, + /// and no DPI information will be stored in the image. + pub pixel_density: Option, +} +impl Default for JpegOptions { + fn default() -> Self { + JpegOptions { + quality: 75, + chroma_subsampling: ChromaSubsampling::S420, + optimize_huffman_tables: false, + progress: false, + pixel_density: None, + } + } +} +impl EncoderOptions for JpegOptions { + fn build(self, w: W) -> ImageResult { + let mut encoder = JpegEncoder::new_with_quality(w, self.quality); + encoder.set_chroma_subsampling(self.chroma_subsampling); + encoder.set_optimize_huffman_tables(self.optimize_huffman_tables); + encoder.set_progressive(self.progress); + if let Some(pixel_density) = self.pixel_density { + encoder.set_pixel_density(pixel_density); + } + Ok(encoder) + } +} + /// The representation of a JPEG encoder pub struct JpegEncoder { encoder: Encoder, diff --git a/src/codecs/jpeg/mod.rs b/src/codecs/jpeg/mod.rs index 74625bcf9e..97d0d2aa39 100644 --- a/src/codecs/jpeg/mod.rs +++ b/src/codecs/jpeg/mod.rs @@ -7,7 +7,9 @@ //! * - The JPEG specification pub use self::decoder::JpegDecoder; -pub use self::encoder::{ChromaSubsampling, JpegEncoder, PixelDensity, PixelDensityUnit}; +pub use self::encoder::{ + ChromaSubsampling, JpegEncoder, JpegOptions, PixelDensity, PixelDensityUnit, +}; mod decoder; mod encoder; diff --git a/src/codecs/png.rs b/src/codecs/png.rs index 3c544e00ad..a6bc37d342 100644 --- a/src/codecs/png.rs +++ b/src/codecs/png.rs @@ -25,8 +25,8 @@ use crate::math::Rect; use crate::metadata::LoopCount; use crate::utils::vec_try_with_capacity; use crate::{ - DynamicImage, GenericImage, GenericImageView, ImageDecoder, ImageEncoder, ImageFormat, - ImageLayout, Limits, Luma, LumaA, Rgb, Rgba, + DynamicImage, EncoderOptions, GenericImage, GenericImageView, ImageDecoder, ImageEncoder, + ImageFormat, ImageLayout, Limits, Luma, LumaA, Rgb, Rgba, }; // http://www.w3.org/TR/PNG-Structure.html @@ -691,9 +691,8 @@ pub struct PngEncoder { } /// DEFLATE compression level of a PNG encoder. The default setting is `Fast`. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] #[non_exhaustive] -#[derive(Default)] pub enum CompressionType { /// No compression whatsoever Uncompressed, @@ -711,9 +710,8 @@ pub enum CompressionType { /// Filter algorithms used to process image data to improve compression. /// /// The default filter is `Adaptive`. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] #[non_exhaustive] -#[derive(Default)] pub enum FilterType { /// No processing done, best used for low bit depth grayscale or data with a /// low color count @@ -732,6 +730,39 @@ pub enum FilterType { Adaptive, } +/// Encoding options for the PNG format. +/// +/// It is best to view the options as a _hint_ to the implementation on the smallest or fastest +/// option for encoding a particular image. That is, using options that map directly to a PNG +/// image parameter will use this parameter where possible. But variants that have no direct +/// mapping may be interpreted differently in minor versions. The exact output is expressly +/// __not__ part of the SemVer stability guarantee. +#[derive(Default, Debug, Clone)] +#[non_exhaustive] +pub struct PngOptions { + /// DEFLATE compression level of a PNG encoder. + /// + /// Defaults to [`CompressionType::Fast`]. + pub compression: CompressionType, + /// Filter algorithms used to process image data. + /// + /// Note that it is not optimal to use a single filter type, so an adaptive + /// filter type is selected as the default. The filter which best minimizes + /// file size may change with the type of compression used. + /// + /// Defaults to [`FilterType::Adaptive`]. + pub filter: FilterType, +} +impl EncoderOptions for PngOptions { + fn build(self, w: W) -> ImageResult { + Ok(PngEncoder::new_with_quality( + w, + self.compression, + self.filter, + )) + } +} + impl PngEncoder { /// Create a new encoder that writes its output to ```w``` pub fn new(w: W) -> PngEncoder { diff --git a/src/codecs/pnm/encoder.rs b/src/codecs/pnm/encoder.rs index 1172ef0c60..0b9e1eb3a4 100644 --- a/src/codecs/pnm/encoder.rs +++ b/src/codecs/pnm/encoder.rs @@ -1,7 +1,9 @@ //! Encoding of PNM Images use crate::utils::vec_try_with_capacity; +use crate::EncoderOptions; use std::fmt; use std::io; +use std::io::Seek; use std::io::Write; use super::AutoBreak; @@ -29,6 +31,29 @@ pub enum FlatSamples<'a> { U16(&'a [u16]), } +/// Encoding options for the PNM format. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct PnmOptions { + /// The specific PNM subtype to encode to. + /// + /// If `None`, the subtype will be chosen dynamically for each image. No + /// particular choice is guaranteed and the chosen subtype may change + /// without warning between versions of the library. + /// + /// Defaults to `None`. + pub subtype: Option, +} +impl EncoderOptions for PnmOptions { + fn build(self, w: W) -> ImageResult { + let mut encoder = PnmEncoder::new(w); + if let Some(subtype) = self.subtype { + encoder = encoder.with_subtype(subtype); + } + Ok(encoder) + } +} + /// Encodes images to any of the `pnm` image formats. pub struct PnmEncoder { writer: W, diff --git a/src/codecs/pnm/mod.rs b/src/codecs/pnm/mod.rs index 2e0817e597..e3a7c0e853 100644 --- a/src/codecs/pnm/mod.rs +++ b/src/codecs/pnm/mod.rs @@ -6,7 +6,7 @@ //! interpretation as an image and will be rejected. use self::autobreak::AutoBreak; pub use self::decoder::PnmDecoder; -pub use self::encoder::PnmEncoder; +pub use self::encoder::{PnmEncoder, PnmOptions}; use self::header::HeaderRecord; pub use self::header::{ ArbitraryHeader, ArbitraryTuplType, BitmapHeader, GraymapHeader, PixmapHeader, diff --git a/src/codecs/tga/encoder.rs b/src/codecs/tga/encoder.rs index 6a65d164cb..6fef61cfe1 100644 --- a/src/codecs/tga/encoder.rs +++ b/src/codecs/tga/encoder.rs @@ -1,6 +1,10 @@ use super::header::Header; use crate::{codecs::tga::header::ImageType, error::EncodingError, utils::vec_try_with_capacity}; -use crate::{DynamicImage, ExtendedColorType, ImageEncoder, ImageError, ImageFormat, ImageResult}; +use crate::{ + DynamicImage, EncoderOptions, ExtendedColorType, ImageEncoder, ImageError, ImageFormat, + ImageResult, +}; +use std::io::Seek; use std::{error, fmt, io::Write}; /// Errors that can occur during encoding and saving of a TGA image. @@ -34,6 +38,30 @@ impl From for ImageError { impl error::Error for EncoderError {} +/// Encoding options for the TGA format. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct TgaOptions { + /// Whether to use run-length encoding (RLE) for the image data. + /// + /// Defaults to `true`. + pub use_rle: bool, +} +impl Default for TgaOptions { + fn default() -> Self { + Self { use_rle: true } + } +} +impl EncoderOptions for TgaOptions { + fn build(self, w: W) -> ImageResult { + let mut encoder = TgaEncoder::new(w); + if !self.use_rle { + encoder = encoder.disable_rle(); + } + Ok(encoder) + } +} + /// TGA encoder. pub struct TgaEncoder { writer: W, diff --git a/src/codecs/tga/mod.rs b/src/codecs/tga/mod.rs index bdb6ac6fd1..e309bd02e9 100644 --- a/src/codecs/tga/mod.rs +++ b/src/codecs/tga/mod.rs @@ -5,7 +5,7 @@ pub use self::decoder::TgaDecoder; -pub use self::encoder::TgaEncoder; +pub use self::encoder::{TgaEncoder, TgaOptions}; mod decoder; mod encoder; diff --git a/src/images/buffer.rs b/src/images/buffer.rs index 24a6631989..e21d08b3e1 100644 --- a/src/images/buffer.rs +++ b/src/images/buffer.rs @@ -18,7 +18,10 @@ use crate::{ metadata::{Cicp, CicpColorPrimaries, CicpTransferCharacteristics, CicpTransform}, save_buffer, save_buffer_with_format, write_buffer_with_format, ImageError, }; -use crate::{DynamicImage, GenericImage, GenericImageView, ImageEncoder, ImageFormat, Primitive}; +use crate::{ + save_buffer_with_options, DynamicImage, EncoderOptions, GenericImage, GenericImageView, + ImageEncoder, ImageFormat, Primitive, +}; /// Iterate over rows of an image /// @@ -1161,6 +1164,24 @@ where ) } + /// Saves the buffer to a file at the specified path with the given options. + /// + /// See [`save_buffer_with_options`](crate::save_buffer_with_options) for + /// supported types. + pub fn save_with_options(&self, path: Q, options: impl EncoderOptions) -> ImageResult<()> + where + Q: AsRef, + { + save_buffer_with_options( + path, + self.subpixels().as_bytes(), + self.width(), + self.height(), + P::COLOR_TYPE, + options, + ) + } + /// Writes the buffer to a writer in the specified format. /// /// Assumes the writer is buffered. In most cases, you should wrap your writer in a `BufWriter` diff --git a/src/images/dynimage.rs b/src/images/dynimage.rs index 862b999550..2a1347352a 100644 --- a/src/images/dynimage.rs +++ b/src/images/dynimage.rs @@ -17,6 +17,7 @@ use crate::io::{DecodedImageAttributes, DecoderPreparedImage}; use crate::math::{resize_dimensions, Rect}; use crate::metadata::Orientation; use crate::traits::Pixel; +use crate::EncoderOptions; use crate::{ imageops, metadata::{Cicp, CicpColorPrimaries, CicpTransferCharacteristics}, @@ -1666,6 +1667,39 @@ impl DynamicImage { let encoder = encoder_for_format(format, file)?; self.write_with_encoder_impl(encoder) } + + /// Saves the buffer to a file with the specified options. + /// + /// The format is derived from the options. + /// + /// ## Color Conversion + /// + /// Unlike other encoding methods in this crate, methods on `DynamicImage` try to automatically + /// convert the image to some color type supported by the encoder. This may result in a loss of + /// precision or the removal of the alpha channel. + /// + /// ## Example + /// + /// ```no_run + /// # #[cfg(feature = "png")] { + /// # use image::{DynamicImage, ColorType}; + /// use image::codecs::png; + /// let image = DynamicImage::new(32, 32, ColorType::Rgba8); + /// + /// let mut options = png::PngOptions::default(); + /// options.compression = png::CompressionType::Best; + /// image.save_with_options("file.png", options)?; + /// # } + /// # image::ImageResult::Ok(()) + /// ``` + pub fn save_with_options(&self, path: Q, options: impl EncoderOptions) -> ImageResult<()> + where + Q: AsRef, + { + let file = BufWriter::new(File::create(path)?); + let encoder = options.build(file)?; + self.write_with_encoder_impl(Box::new(encoder)) + } } impl From for DynamicImage { diff --git a/src/io/encoder.rs b/src/io/encoder.rs index f520f7293e..cd574d8f23 100644 --- a/src/io/encoder.rs +++ b/src/io/encoder.rs @@ -1,3 +1,5 @@ +use std::io::{Seek, Write}; + use crate::error::{ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind}; use crate::{ColorType, DynamicImage, ExtendedColorType}; @@ -115,6 +117,12 @@ pub trait ImageEncoder { } } +/// Encoding options for a specific format. +pub trait EncoderOptions { + /// Creates the encoder for the options. + fn build(self, w: W) -> ImageResult; +} + pub(crate) trait ImageEncoderBoxed: ImageEncoder { fn write_image( self: Box, diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index 5215e3f7e6..8d4fdeed62 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -5,7 +5,7 @@ use std::{iter, mem::size_of}; use crate::io::encoder::ImageEncoderBoxed; use crate::io::DecodedImageAttributes; -use crate::{codecs::*, ExtendedColorType, ImageReaderOptions}; +use crate::{codecs::*, EncoderOptions, ExtendedColorType, ImageEncoder, ImageReaderOptions}; use crate::error::{ ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, UnsupportedError, @@ -58,6 +58,23 @@ pub fn save_buffer_with_format( encoder.write_image(buf, width, height, color.into()) } +/// Saves the supplied buffer to a file given the path. The format is derived from the given options. +/// +/// The buffer is assumed to have the correct format according to the specified color type. This +/// will lead to corrupted files if the buffer contains malformed data. +pub fn save_buffer_with_options( + path: impl AsRef, + buf: &[u8], + width: u32, + height: u32, + color: impl Into, + options: impl EncoderOptions, +) -> ImageResult<()> { + let buffered_file_write = BufWriter::new(File::create(path)?); // always seekable + let encoder = options.build(buffered_file_write)?; + encoder.write_image(buf, width, height, color.into()) +} + pub(crate) fn encoder_for_format<'a, W: Write + Seek>( format: ImageFormat, buffered_write: &'a mut W, diff --git a/src/lib.rs b/src/lib.rs index 8d5a3430de..4db753dfbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,11 +150,13 @@ pub use crate::images::dynimage::{ write_buffer_with_format, }; -pub use crate::io::free_functions::{guess_format, load, save_buffer, save_buffer_with_format}; +pub use crate::io::free_functions::{ + guess_format, load, save_buffer, save_buffer_with_format, save_buffer_with_options, +}; pub use crate::io::{ decoder::ImageDecoder, - encoder::ImageEncoder, + encoder::{EncoderOptions, ImageEncoder}, format::ImageFormat, image_reader_type::{ImageReader, ImageReaderOptions, SpecCompliance}, limits::{LimitSupport, Limits},