diff --git a/Cargo.toml b/Cargo.toml index e47c74e..6dbe107 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,15 @@ license = "Apache-2.0" authors = ["trivernis "] description = "An image thumbnail creation library" version = "0.5.1" -edition = "2018" +edition = "2021" repository = "https://github.com/Trivernis/thumbnailer" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -webp = "0.2.1" -mime = "0.3.16" -rayon = "1.5.1" -tempfile = "3.3.0" -image= "0.24.0" +webp = "0.2.7" +mime = "0.3.17" +rayon = "1.10.0" +tempfile = "3.10.1" +image= "0.25.1" lazy_static = "1.4.0" diff --git a/src/error.rs b/src/error.rs index 9e0fc11..705d882 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,12 +23,12 @@ pub enum ThumbError { impl Display for ThumbError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - ThumbError::IO(_) => write!(f, "an io error occurred"), - ThumbError::Image(e) => write!(f, "an image error occurred {}", e), - ThumbError::Decode => write!(f, "failed to decode image"), - ThumbError::Unsupported(mime) => write!(f, "Unsupported media type {}", mime), - ThumbError::NullVideo => write!(f, "no video data found in file"), - ThumbError::FFMPEG(e) => write!(f, "ffmpeg error: {}", e), + Self::IO(_) => write!(f, "an io error occurred"), + Self::Image(e) => write!(f, "an image error occurred {e}"), + Self::Decode => write!(f, "failed to decode image"), + Self::Unsupported(mime) => write!(f, "Unsupported media type {mime}"), + Self::NullVideo => write!(f, "no video data found in file"), + Self::FFMPEG(e) => write!(f, "ffmpeg error: {e}"), } } } @@ -36,8 +36,8 @@ impl Display for ThumbError { impl std::error::Error for ThumbError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - ThumbError::IO(e) => e.source(), - ThumbError::Image(i) => i.source(), + Self::IO(e) => e.source(), + Self::Image(i) => i.source(), _ => None, } } diff --git a/src/formats/image_format.rs b/src/formats/image_format.rs index 47cb672..8790802 100644 --- a/src/formats/image_format.rs +++ b/src/formats/image_format.rs @@ -1,9 +1,9 @@ -use std::io::{BufRead, Read, Seek}; +use crate::error::{ThumbError, ThumbResult}; +use image::io::Reader as ImageReader; use image::{DynamicImage, ImageFormat}; use mime::Mime; -use image::io::Reader as ImageReader; +use std::io::{BufRead, Read, Seek}; use webp::Decoder as WebpDecoder; -use crate::error::{ThumbError, ThumbResult}; const IMAGE_WEBP_MIME: &str = "image/webp"; @@ -19,13 +19,18 @@ pub fn read_image(reader: R, mime: Mime) -> ThumbResult(mut reader: R) -> ThumbResult { let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; - let webp_image = WebpDecoder::new(&buf).decode().ok_or_else(|| ThumbError::Decode)?; + let webp_image = WebpDecoder::new(&buf) + .decode() + .ok_or_else(|| ThumbError::Decode)?; Ok(webp_image.to_image()) } /// Reads a generic image -fn read_generic_image(reader: R, format: Option) -> ThumbResult { +fn read_generic_image( + reader: R, + format: Option, +) -> ThumbResult { let reader = if let Some(format) = format { ImageReader::with_format(reader, format) } else { @@ -36,6 +41,7 @@ fn read_generic_image(reader: R, format: Option) Ok(image) } +#[allow(clippy::needless_pass_by_value)] fn mime_to_image_format(mime: Mime) -> Option { match mime.subtype().as_str() { "png" => Some(ImageFormat::Png), @@ -44,4 +50,4 @@ fn mime_to_image_format(mime: Mime) -> Option { "gif" => Some(ImageFormat::Gif), _ => None, } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 40b48fd..1bf0410 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,27 +9,24 @@ //! use std::io::BufReader; //! use std::io::Cursor; //! -//! fn main() { -//! let file = File::open("tests/assets/test.png").unwrap(); -//! let reader = BufReader::new(file); -//! let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small, ThumbnailSize::Medium]).unwrap(); -//! -//! let thumbnail = thumbnails.pop().unwrap(); -//! let mut buf = Cursor::new(Vec::new()); -//! thumbnail.write_png(&mut buf).unwrap(); -//! } +//! let file = File::open("tests/assets/test.png").unwrap(); +//! let reader = BufReader::new(file); +//! let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small, ThumbnailSize::Medium]).unwrap(); +//! +//! let thumbnail = thumbnails.pop().unwrap(); +//! let mut buf = Cursor::new(Vec::new()); +//! thumbnail.write_png(&mut buf).unwrap(); //! ``` use crate::error::ThumbResult; -use image; -use image::imageops::FilterType; -use image::{DynamicImage, GenericImageView, ImageOutputFormat}; +use image::{DynamicImage, GenericImageView, ImageFormat}; use mime::Mime; use rayon::prelude::*; use std::io::{BufRead, Seek, Write}; use crate::formats::get_base_image; pub use size::ThumbnailSize; +use std::convert::From; pub mod error; mod formats; @@ -41,11 +38,38 @@ pub struct Thumbnail { inner: DynamicImage, } +#[derive(Clone, Debug)] +pub enum FilterType { + Nearest, + Triangle, + CatmullRom, + Gaussian, + Lanczos3, +} + +impl FilterType { + const fn translate_filter(&self) -> image::imageops::FilterType { + match self { + Self::Nearest => image::imageops::FilterType::Nearest, + Self::Triangle => image::imageops::FilterType::Triangle, + Self::CatmullRom => image::imageops::FilterType::CatmullRom, + Self::Gaussian => image::imageops::FilterType::Gaussian, + Self::Lanczos3 => image::imageops::FilterType::Lanczos3, + } + } +} + +impl From for image::imageops::FilterType { + fn from(filter_type: FilterType) -> Self { + filter_type.translate_filter() + } +} + impl Thumbnail { /// Writes the bytes of the image in a png format pub fn write_png(self, writer: &mut W) -> ThumbResult<()> { let image = DynamicImage::ImageRgba8(self.inner.into_rgba8()); - image.write_to(writer, ImageOutputFormat::Png)?; + image.write_to(writer, ImageFormat::Png)?; Ok(()) } @@ -53,7 +77,8 @@ impl Thumbnail { /// Writes the bytes of the image in a jpeg format pub fn write_jpeg(self, writer: &mut W, quality: u8) -> ThumbResult<()> { let image = DynamicImage::ImageRgb8(self.inner.into_rgb8()); - image.write_to(writer, ImageOutputFormat::Jpeg(quality))?; + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(writer, quality); + encoder.encode_image(&image)?; Ok(()) } @@ -64,6 +89,24 @@ impl Thumbnail { } } +/// Creates thumbnails of the requested sizes for the given reader providing the content as bytes and +/// the mime describing the contents type +pub fn create_thumbnails_samplefilter>( + reader: R, + mime: Mime, + sizes: I, + filter: FilterType, +) -> ThumbResult> { + let image = get_base_image(reader, mime)?; + let sizes: Vec = sizes.into_iter().collect(); + let thumbnails = resize_images(image, &sizes, filter) + .into_iter() + .map(|image| Thumbnail { inner: image }) + .collect(); + + Ok(thumbnails) +} + /// Creates thumbnails of the requested sizes for the given reader providing the content as bytes and /// the mime describing the contents type pub fn create_thumbnails>( @@ -73,7 +116,7 @@ pub fn create_thumbnails ThumbResult> { let image = get_base_image(reader, mime)?; let sizes: Vec = sizes.into_iter().collect(); - let thumbnails = resize_images(image, &sizes) + let thumbnails = resize_images(image, &sizes, FilterType::Lanczos3) .into_iter() .map(|image| Thumbnail { inner: image }) .collect(); @@ -81,12 +124,20 @@ pub fn create_thumbnails Vec { +fn resize_images( + image: DynamicImage, + sizes: &[ThumbnailSize], + filter_type: crate::FilterType, +) -> Vec { sizes .into_par_iter() .map(|size| { let (width, height) = size.dimensions(); - image.resize(width, height, FilterType::Lanczos3) + image.resize( + width, + height, + image::imageops::FilterType::from(filter_type.clone()), + ) }) .collect() } diff --git a/src/size.rs b/src/size.rs index 09cae04..37d3e91 100644 --- a/src/size.rs +++ b/src/size.rs @@ -1,4 +1,3 @@ - /// Represents fixed sizes of a thumbnail #[derive(Clone, Copy, Debug)] pub enum ThumbnailSize { @@ -7,18 +6,18 @@ pub enum ThumbnailSize { Medium, Large, Larger, - Custom((u32, u32)) + Custom((u32, u32)), } impl ThumbnailSize { - pub fn dimensions(&self) -> (u32, u32) { + pub const fn dimensions(&self) -> (u32, u32) { match self { - ThumbnailSize::Icon => (64, 64), - ThumbnailSize::Small => (128, 128), - ThumbnailSize::Medium => (256, 256), - ThumbnailSize::Large => (512, 512), - ThumbnailSize::Larger => (1024, 1024), - ThumbnailSize::Custom(size) => *size, + Self::Icon => (64, 64), + Self::Small => (128, 128), + Self::Medium => (256, 256), + Self::Large => (512, 512), + Self::Larger => (1024, 1024), + Self::Custom(size) => *size, } } -} \ No newline at end of file +} diff --git a/src/utils/ffmpeg_cli.rs b/src/utils/ffmpeg_cli.rs index 1d82361..5fcec55 100644 --- a/src/utils/ffmpeg_cli.rs +++ b/src/utils/ffmpeg_cli.rs @@ -14,7 +14,7 @@ pub fn get_png_frame(video_file: &str, index: usize) -> ThumbResult> { "-i", video_file, "-vf", - format!("select=eq(n\\,{})", index).as_str(), + format!("select=eq(n\\,{index})").as_str(), "-vframes", "1", "-c:v", @@ -35,7 +35,7 @@ fn ffmpeg, S: AsRef>(args: I) -> ThumbResult 0 { + if output.status.success() && !output.stdout.is_empty() { Ok(output.stdout) } else { Err(ThumbError::FFMPEG( @@ -47,12 +47,6 @@ fn ffmpeg, S: AsRef>(args: I) -> ThumbResult bool { match Command::new("ffmpeg").args(["-loglevel", "quiet"]).spawn() { Ok(_) => true, - Err(e) => { - if let ErrorKind::NotFound = e.kind() { - false - } else { - true - } - } + Err(e) => !matches!(e.kind(), ErrorKind::NotFound), } } diff --git a/tests/image_reading.rs b/tests/image_reading.rs index bcd4fd4..eef40a9 100644 --- a/tests/image_reading.rs +++ b/tests/image_reading.rs @@ -1,6 +1,6 @@ -const PNG_BYTES: &'static [u8] = include_bytes!("assets/test.png"); -const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg"); -const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp"); +const PNG_BYTES: &[u8] = include_bytes!("assets/test.png"); +const JPG_BYTES: &[u8] = include_bytes!("assets/test.jpg"); +const WEBP_BYTES: &[u8] = include_bytes!("assets/test.webp"); use crate::ImageType::{Jpeg, Png, Webp}; use mime::Mime; diff --git a/tests/image_writing.rs b/tests/image_writing.rs index 7335870..e618598 100644 --- a/tests/image_writing.rs +++ b/tests/image_writing.rs @@ -2,11 +2,11 @@ use mime::Mime; use std::io::Cursor; use std::str::FromStr; use thumbnailer::error::ThumbResult; -use thumbnailer::{create_thumbnails, ThumbnailSize}; +use thumbnailer::{create_thumbnails, create_thumbnails_samplefilter, FilterType, ThumbnailSize}; -const PNG_BYTES: &'static [u8] = include_bytes!("assets/test.png"); -const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg"); -const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp"); +const PNG_BYTES: &[u8] = include_bytes!("assets/test.png"); +const JPG_BYTES: &[u8] = include_bytes!("assets/test.jpg"); +const WEBP_BYTES: &[u8] = include_bytes!("assets/test.webp"); enum SourceFormat { Png, @@ -22,31 +22,91 @@ enum TargetFormat { #[test] fn it_converts_png_thumbnails_for_png() { write_thumbnail(SourceFormat::Png, TargetFormat::Png).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Png, TargetFormat::Png, filter).unwrap(); + } } #[test] fn it_converts_jpeg_thumbnails_for_png() { write_thumbnail(SourceFormat::Png, TargetFormat::Jpeg).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Png, TargetFormat::Jpeg, filter).unwrap(); + } } #[test] fn it_converts_png_thumbnails_for_jpeg() { write_thumbnail(SourceFormat::Jpeg, TargetFormat::Png).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Jpeg, TargetFormat::Png, filter).unwrap(); + } } #[test] fn it_converts_jpeg_thumbnails_for_jpeg() { write_thumbnail(SourceFormat::Jpeg, TargetFormat::Jpeg).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Jpeg, TargetFormat::Jpeg, filter).unwrap(); + } } #[test] fn it_converts_png_thumbnails_for_webp() { write_thumbnail(SourceFormat::Webp, TargetFormat::Png).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Webp, TargetFormat::Png, filter).unwrap(); + } } #[test] fn it_converts_jpeg_thumbnails_for_webp() { write_thumbnail(SourceFormat::Webp, TargetFormat::Jpeg).unwrap(); + + for filter in [ + FilterType::Nearest, + FilterType::Triangle, + FilterType::CatmullRom, + FilterType::Gaussian, + FilterType::Lanczos3, + ] { + write_thumbnail_samplefilter(SourceFormat::Webp, TargetFormat::Jpeg, filter).unwrap(); + } } fn write_thumbnail( @@ -79,3 +139,43 @@ fn write_thumbnail( Ok(buf.into_inner()) } + +fn write_thumbnail_samplefilter( + source_format: SourceFormat, + target_format: TargetFormat, + filter: thumbnailer::FilterType, +) -> ThumbResult> { + let thumb = match source_format { + SourceFormat::Png => { + let reader = Cursor::new(PNG_BYTES); + create_thumbnails_samplefilter(reader, mime::IMAGE_PNG, [ThumbnailSize::Medium], filter) + .unwrap() + } + SourceFormat::Jpeg => { + let reader = Cursor::new(JPG_BYTES); + create_thumbnails_samplefilter( + reader, + mime::IMAGE_JPEG, + [ThumbnailSize::Medium], + filter, + ) + .unwrap() + } + SourceFormat::Webp => { + let reader = Cursor::new(WEBP_BYTES); + let webp_mime = Mime::from_str("image/webp").unwrap(); + create_thumbnails_samplefilter(reader, webp_mime, [ThumbnailSize::Medium], filter) + .unwrap() + } + } + .pop() + .unwrap(); + + let mut buf = Cursor::new(Vec::new()); + match target_format { + TargetFormat::Png => thumb.write_png(&mut buf)?, + TargetFormat::Jpeg => thumb.write_jpeg(&mut buf, 8)?, + } + + Ok(buf.into_inner()) +} diff --git a/tests/video_reading.rs b/tests/video_reading.rs index f5f648e..7d8dc21 100644 --- a/tests/video_reading.rs +++ b/tests/video_reading.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use thumbnailer::error::ThumbError; use thumbnailer::{create_thumbnails, ThumbnailSize}; -const VIDEO_BYTES: &'static [u8] = include_bytes!("assets/test.mp4"); +const VIDEO_BYTES: &[u8] = include_bytes!("assets/test.mp4"); #[test] fn it_creates_thumbnails_for_mp4() { @@ -23,14 +23,14 @@ fn it_creates_thumbnails_for_mp4() { match result { Ok(_) => { - assert!(true) + assert!(true); } Err(e) => match e { ThumbError::Unsupported(_) => { assert!(true, "ffmpeg is not installed"); } e => { - panic!("failed to create thumbnails {}", e); + panic!("failed to create thumbnails {e}"); } }, }