use latest crates, add samplefilter

pull/7/head
Joerg 7 months ago
parent 2631a72a4f
commit 626d847834

@ -5,15 +5,15 @@ license = "Apache-2.0"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
description = "An image thumbnail creation library" description = "An image thumbnail creation library"
version = "0.5.1" version = "0.5.1"
edition = "2018" edition = "2021"
repository = "https://github.com/Trivernis/thumbnailer" repository = "https://github.com/Trivernis/thumbnailer"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
webp = "0.2.1" webp = "0.2.7"
mime = "0.3.16" mime = "0.3.17"
rayon = "1.5.1" rayon = "1.10.0"
tempfile = "3.3.0" tempfile = "3.10.1"
image= "0.24.0" image= "0.25.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"

@ -23,12 +23,12 @@ pub enum ThumbError {
impl Display for ThumbError { impl Display for ThumbError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
ThumbError::IO(_) => write!(f, "an io error occurred"), Self::IO(_) => write!(f, "an io error occurred"),
ThumbError::Image(e) => write!(f, "an image error occurred {}", e), Self::Image(e) => write!(f, "an image error occurred {e}"),
ThumbError::Decode => write!(f, "failed to decode image"), Self::Decode => write!(f, "failed to decode image"),
ThumbError::Unsupported(mime) => write!(f, "Unsupported media type {}", mime), Self::Unsupported(mime) => write!(f, "Unsupported media type {mime}"),
ThumbError::NullVideo => write!(f, "no video data found in file"), Self::NullVideo => write!(f, "no video data found in file"),
ThumbError::FFMPEG(e) => write!(f, "ffmpeg error: {}", e), Self::FFMPEG(e) => write!(f, "ffmpeg error: {e}"),
} }
} }
} }
@ -36,8 +36,8 @@ impl Display for ThumbError {
impl std::error::Error for ThumbError { impl std::error::Error for ThumbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
ThumbError::IO(e) => e.source(), Self::IO(e) => e.source(),
ThumbError::Image(i) => i.source(), Self::Image(i) => i.source(),
_ => None, _ => None,
} }
} }

@ -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 image::{DynamicImage, ImageFormat};
use mime::Mime; use mime::Mime;
use image::io::Reader as ImageReader; use std::io::{BufRead, Read, Seek};
use webp::Decoder as WebpDecoder; use webp::Decoder as WebpDecoder;
use crate::error::{ThumbError, ThumbResult};
const IMAGE_WEBP_MIME: &str = "image/webp"; const IMAGE_WEBP_MIME: &str = "image/webp";
@ -19,13 +19,18 @@ pub fn read_image<R: BufRead + Seek>(reader: R, mime: Mime) -> ThumbResult<Dynam
fn read_webp_image<R: Read>(mut reader: R) -> ThumbResult<DynamicImage> { fn read_webp_image<R: Read>(mut reader: R) -> ThumbResult<DynamicImage> {
let mut buf = Vec::new(); let mut buf = Vec::new();
reader.read_to_end(&mut buf)?; 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()) Ok(webp_image.to_image())
} }
/// Reads a generic image /// Reads a generic image
fn read_generic_image<R: BufRead + Seek>(reader: R, format: Option<ImageFormat>) -> ThumbResult<DynamicImage> { fn read_generic_image<R: BufRead + Seek>(
reader: R,
format: Option<ImageFormat>,
) -> ThumbResult<DynamicImage> {
let reader = if let Some(format) = format { let reader = if let Some(format) = format {
ImageReader::with_format(reader, format) ImageReader::with_format(reader, format)
} else { } else {
@ -36,6 +41,7 @@ fn read_generic_image<R: BufRead + Seek>(reader: R, format: Option<ImageFormat>)
Ok(image) Ok(image)
} }
#[allow(clippy::needless_pass_by_value)]
fn mime_to_image_format(mime: Mime) -> Option<ImageFormat> { fn mime_to_image_format(mime: Mime) -> Option<ImageFormat> {
match mime.subtype().as_str() { match mime.subtype().as_str() {
"png" => Some(ImageFormat::Png), "png" => Some(ImageFormat::Png),

@ -9,7 +9,6 @@
//! use std::io::BufReader; //! use std::io::BufReader;
//! use std::io::Cursor; //! use std::io::Cursor;
//! //!
//! fn main() {
//! let file = File::open("tests/assets/test.png").unwrap(); //! let file = File::open("tests/assets/test.png").unwrap();
//! let reader = BufReader::new(file); //! let reader = BufReader::new(file);
//! let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small, ThumbnailSize::Medium]).unwrap(); //! let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small, ThumbnailSize::Medium]).unwrap();
@ -17,19 +16,17 @@
//! let thumbnail = thumbnails.pop().unwrap(); //! let thumbnail = thumbnails.pop().unwrap();
//! let mut buf = Cursor::new(Vec::new()); //! let mut buf = Cursor::new(Vec::new());
//! thumbnail.write_png(&mut buf).unwrap(); //! thumbnail.write_png(&mut buf).unwrap();
//! }
//! ``` //! ```
use crate::error::ThumbResult; use crate::error::ThumbResult;
use image; use image::{DynamicImage, GenericImageView, ImageFormat};
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, ImageOutputFormat};
use mime::Mime; use mime::Mime;
use rayon::prelude::*; use rayon::prelude::*;
use std::io::{BufRead, Seek, Write}; use std::io::{BufRead, Seek, Write};
use crate::formats::get_base_image; use crate::formats::get_base_image;
pub use size::ThumbnailSize; pub use size::ThumbnailSize;
use std::convert::From;
pub mod error; pub mod error;
mod formats; mod formats;
@ -41,11 +38,38 @@ pub struct Thumbnail {
inner: DynamicImage, 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<FilterType> for image::imageops::FilterType {
fn from(filter_type: FilterType) -> Self {
filter_type.translate_filter()
}
}
impl Thumbnail { impl Thumbnail {
/// Writes the bytes of the image in a png format /// Writes the bytes of the image in a png format
pub fn write_png<W: Write + Seek>(self, writer: &mut W) -> ThumbResult<()> { pub fn write_png<W: Write + Seek>(self, writer: &mut W) -> ThumbResult<()> {
let image = DynamicImage::ImageRgba8(self.inner.into_rgba8()); let image = DynamicImage::ImageRgba8(self.inner.into_rgba8());
image.write_to(writer, ImageOutputFormat::Png)?; image.write_to(writer, ImageFormat::Png)?;
Ok(()) Ok(())
} }
@ -53,7 +77,8 @@ impl Thumbnail {
/// Writes the bytes of the image in a jpeg format /// Writes the bytes of the image in a jpeg format
pub fn write_jpeg<W: Write + Seek>(self, writer: &mut W, quality: u8) -> ThumbResult<()> { pub fn write_jpeg<W: Write + Seek>(self, writer: &mut W, quality: u8) -> ThumbResult<()> {
let image = DynamicImage::ImageRgb8(self.inner.into_rgb8()); 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(()) 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<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize>>(
reader: R,
mime: Mime,
sizes: I,
filter: FilterType,
) -> ThumbResult<Vec<Thumbnail>> {
let image = get_base_image(reader, mime)?;
let sizes: Vec<ThumbnailSize> = 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 /// Creates thumbnails of the requested sizes for the given reader providing the content as bytes and
/// the mime describing the contents type /// the mime describing the contents type
pub fn create_thumbnails<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize>>( pub fn create_thumbnails<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize>>(
@ -73,7 +116,7 @@ pub fn create_thumbnails<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize
) -> ThumbResult<Vec<Thumbnail>> { ) -> ThumbResult<Vec<Thumbnail>> {
let image = get_base_image(reader, mime)?; let image = get_base_image(reader, mime)?;
let sizes: Vec<ThumbnailSize> = sizes.into_iter().collect(); let sizes: Vec<ThumbnailSize> = sizes.into_iter().collect();
let thumbnails = resize_images(image, &sizes) let thumbnails = resize_images(image, &sizes, FilterType::Lanczos3)
.into_iter() .into_iter()
.map(|image| Thumbnail { inner: image }) .map(|image| Thumbnail { inner: image })
.collect(); .collect();
@ -81,12 +124,20 @@ pub fn create_thumbnails<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize
Ok(thumbnails) Ok(thumbnails)
} }
fn resize_images(image: DynamicImage, sizes: &[ThumbnailSize]) -> Vec<DynamicImage> { fn resize_images(
image: DynamicImage,
sizes: &[ThumbnailSize],
filter_type: crate::FilterType,
) -> Vec<DynamicImage> {
sizes sizes
.into_par_iter() .into_par_iter()
.map(|size| { .map(|size| {
let (width, height) = size.dimensions(); let (width, height) = size.dimensions();
image.resize(width, height, FilterType::Lanczos3) image.resize(
width,
height,
image::imageops::FilterType::from(filter_type.clone()),
)
}) })
.collect() .collect()
} }

@ -1,4 +1,3 @@
/// Represents fixed sizes of a thumbnail /// Represents fixed sizes of a thumbnail
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum ThumbnailSize { pub enum ThumbnailSize {
@ -7,18 +6,18 @@ pub enum ThumbnailSize {
Medium, Medium,
Large, Large,
Larger, Larger,
Custom((u32, u32)) Custom((u32, u32)),
} }
impl ThumbnailSize { impl ThumbnailSize {
pub fn dimensions(&self) -> (u32, u32) { pub const fn dimensions(&self) -> (u32, u32) {
match self { match self {
ThumbnailSize::Icon => (64, 64), Self::Icon => (64, 64),
ThumbnailSize::Small => (128, 128), Self::Small => (128, 128),
ThumbnailSize::Medium => (256, 256), Self::Medium => (256, 256),
ThumbnailSize::Large => (512, 512), Self::Large => (512, 512),
ThumbnailSize::Larger => (1024, 1024), Self::Larger => (1024, 1024),
ThumbnailSize::Custom(size) => *size, Self::Custom(size) => *size,
} }
} }
} }

@ -14,7 +14,7 @@ pub fn get_png_frame(video_file: &str, index: usize) -> ThumbResult<Vec<u8>> {
"-i", "-i",
video_file, video_file,
"-vf", "-vf",
format!("select=eq(n\\,{})", index).as_str(), format!("select=eq(n\\,{index})").as_str(),
"-vframes", "-vframes",
"1", "1",
"-c:v", "-c:v",
@ -35,7 +35,7 @@ fn ffmpeg<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(args: I) -> ThumbResult<Ve
.spawn()?; .spawn()?;
let output = child.wait_with_output()?; let output = child.wait_with_output()?;
if output.status.success() && output.stdout.len() > 0 { if output.status.success() && !output.stdout.is_empty() {
Ok(output.stdout) Ok(output.stdout)
} else { } else {
Err(ThumbError::FFMPEG( Err(ThumbError::FFMPEG(
@ -47,12 +47,6 @@ fn ffmpeg<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(args: I) -> ThumbResult<Ve
pub fn is_ffmpeg_installed() -> bool { pub fn is_ffmpeg_installed() -> bool {
match Command::new("ffmpeg").args(["-loglevel", "quiet"]).spawn() { match Command::new("ffmpeg").args(["-loglevel", "quiet"]).spawn() {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => !matches!(e.kind(), ErrorKind::NotFound),
if let ErrorKind::NotFound = e.kind() {
false
} else {
true
}
}
} }
} }

@ -1,6 +1,6 @@
const PNG_BYTES: &'static [u8] = include_bytes!("assets/test.png"); const PNG_BYTES: &[u8] = include_bytes!("assets/test.png");
const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg"); const JPG_BYTES: &[u8] = include_bytes!("assets/test.jpg");
const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp"); const WEBP_BYTES: &[u8] = include_bytes!("assets/test.webp");
use crate::ImageType::{Jpeg, Png, Webp}; use crate::ImageType::{Jpeg, Png, Webp};
use mime::Mime; use mime::Mime;

@ -2,11 +2,11 @@ use mime::Mime;
use std::io::Cursor; use std::io::Cursor;
use std::str::FromStr; use std::str::FromStr;
use thumbnailer::error::ThumbResult; 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 PNG_BYTES: &[u8] = include_bytes!("assets/test.png");
const JPG_BYTES: &'static [u8] = include_bytes!("assets/test.jpg"); const JPG_BYTES: &[u8] = include_bytes!("assets/test.jpg");
const WEBP_BYTES: &'static [u8] = include_bytes!("assets/test.webp"); const WEBP_BYTES: &[u8] = include_bytes!("assets/test.webp");
enum SourceFormat { enum SourceFormat {
Png, Png,
@ -22,31 +22,91 @@ enum TargetFormat {
#[test] #[test]
fn it_converts_png_thumbnails_for_png() { fn it_converts_png_thumbnails_for_png() {
write_thumbnail(SourceFormat::Png, TargetFormat::Png).unwrap(); 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] #[test]
fn it_converts_jpeg_thumbnails_for_png() { fn it_converts_jpeg_thumbnails_for_png() {
write_thumbnail(SourceFormat::Png, TargetFormat::Jpeg).unwrap(); 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] #[test]
fn it_converts_png_thumbnails_for_jpeg() { fn it_converts_png_thumbnails_for_jpeg() {
write_thumbnail(SourceFormat::Jpeg, TargetFormat::Png).unwrap(); 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] #[test]
fn it_converts_jpeg_thumbnails_for_jpeg() { fn it_converts_jpeg_thumbnails_for_jpeg() {
write_thumbnail(SourceFormat::Jpeg, TargetFormat::Jpeg).unwrap(); 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] #[test]
fn it_converts_png_thumbnails_for_webp() { fn it_converts_png_thumbnails_for_webp() {
write_thumbnail(SourceFormat::Webp, TargetFormat::Png).unwrap(); 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] #[test]
fn it_converts_jpeg_thumbnails_for_webp() { fn it_converts_jpeg_thumbnails_for_webp() {
write_thumbnail(SourceFormat::Webp, TargetFormat::Jpeg).unwrap(); 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( fn write_thumbnail(
@ -79,3 +139,43 @@ fn write_thumbnail(
Ok(buf.into_inner()) Ok(buf.into_inner())
} }
fn write_thumbnail_samplefilter(
source_format: SourceFormat,
target_format: TargetFormat,
filter: thumbnailer::FilterType,
) -> ThumbResult<Vec<u8>> {
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())
}

@ -6,7 +6,7 @@ use std::str::FromStr;
use thumbnailer::error::ThumbError; use thumbnailer::error::ThumbError;
use thumbnailer::{create_thumbnails, ThumbnailSize}; 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] #[test]
fn it_creates_thumbnails_for_mp4() { fn it_creates_thumbnails_for_mp4() {
@ -23,14 +23,14 @@ fn it_creates_thumbnails_for_mp4() {
match result { match result {
Ok(_) => { Ok(_) => {
assert!(true) assert!(true);
} }
Err(e) => match e { Err(e) => match e {
ThumbError::Unsupported(_) => { ThumbError::Unsupported(_) => {
assert!(true, "ffmpeg is not installed"); assert!(true, "ffmpeg is not installed");
} }
e => { e => {
panic!("failed to create thumbnails {}", e); panic!("failed to create thumbnails {e}");
} }
}, },
} }

Loading…
Cancel
Save