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>"]
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"

@ -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,
}
}

@ -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<R: BufRead + Seek>(reader: R, mime: Mime) -> ThumbResult<Dynam
fn read_webp_image<R: Read>(mut reader: R) -> ThumbResult<DynamicImage> {
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<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 {
ImageReader::with_format(reader, format)
} else {
@ -36,6 +41,7 @@ fn read_generic_image<R: BufRead + Seek>(reader: R, format: Option<ImageFormat>)
Ok(image)
}
#[allow(clippy::needless_pass_by_value)]
fn mime_to_image_format(mime: Mime) -> Option<ImageFormat> {
match mime.subtype().as_str() {
"png" => Some(ImageFormat::Png),
@ -44,4 +50,4 @@ fn mime_to_image_format(mime: Mime) -> Option<ImageFormat> {
"gif" => Some(ImageFormat::Gif),
_ => None,
}
}
}

@ -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<FilterType> 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<W: Write + Seek>(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<W: Write + Seek>(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<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
/// the mime describing the contents type
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>> {
let image = get_base_image(reader, mime)?;
let sizes: Vec<ThumbnailSize> = 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<R: BufRead + Seek, I: IntoIterator<Item = ThumbnailSize
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
.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()
}

@ -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,
}
}
}
}

@ -14,7 +14,7 @@ pub fn get_png_frame(video_file: &str, index: usize) -> ThumbResult<Vec<u8>> {
"-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<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(args: I) -> ThumbResult<Ve
.spawn()?;
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)
} else {
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 {
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),
}
}

@ -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;

@ -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<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::{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}");
}
},
}

Loading…
Cancel
Save