diff --git a/Cargo.toml b/Cargo.toml index a46d0ba..72ea522 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,16 +4,18 @@ readme = "README.md" license = "Apache-2.0" authors = ["trivernis "] description = "An image thumbnail creation library" -version = "0.2.0" +version = "0.2.1" edition = "2018" +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.0" mime = "0.3.16" -thiserror = "1.0.30" rayon = "1.5.1" +vid2img = "0.1.0" +tempfile = "3.2.0" [dependencies.image] version = "0.23.14" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 1bf2a8d..8367325 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,20 +1,76 @@ -use std::io; +use image::ImageError; use mime::Mime; -use thiserror::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::io; +use vid2img::{CaptureError, StreamError}; pub type ThumbResult = Result; -#[derive(Debug, Error)] +#[derive(Debug)] pub enum ThumbError { - #[error("IO Error {0}")] - IO(#[from] io::Error), + IO(io::Error), - #[error("Image Error {0}")] - Image(#[from] image::error::ImageError), + Image(image::error::ImageError), - #[error("Failed to decode image")] Decode, - #[error("Unsupported media type {0}")] Unsupported(Mime), -} \ No newline at end of file + + NullVideo, + + CaptureError(vid2img::CaptureError), + + StreamError(vid2img::StreamError), +} + +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::CaptureError(c) => { + write!(f, "capture error when creating video thumbnail: {:?}", c) + } + ThumbError::StreamError(s) => { + write!(f, "stream error when creating video thumbnail: {:?}", s) + } + } + } +} + +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(), + _ => None, + } + } +} + +impl From for ThumbError { + fn from(e: io::Error) -> Self { + Self::IO(e) + } +} + +impl From for ThumbError { + fn from(e: ImageError) -> Self { + Self::Image(e) + } +} + +impl From for ThumbError { + fn from(e: CaptureError) -> Self { + Self::CaptureError(e) + } +} + +impl From for ThumbError { + fn from(s: StreamError) -> Self { + Self::StreamError(s) + } +} diff --git a/src/formats/mod.rs b/src/formats/mod.rs index 1e77a97..7418c1d 100644 --- a/src/formats/mod.rs +++ b/src/formats/mod.rs @@ -1,15 +1,18 @@ use crate::error::{ThumbError, ThumbResult}; use crate::formats::image_format::read_image; +use crate::formats::video_format::get_video_frame; use image::DynamicImage; use mime::Mime; use std::io::{BufRead, Seek}; pub mod image_format; +pub mod video_format; /// Reads the buffer content into an image that can be used for thumbnail generation pub fn get_base_image(reader: R, mime: Mime) -> ThumbResult { match mime.type_() { mime::IMAGE => read_image(reader, mime), + mime::VIDEO => get_video_frame(reader, mime), _ => Err(ThumbError::Unsupported(mime)), } } diff --git a/src/formats/video_format.rs b/src/formats/video_format.rs new file mode 100644 index 0000000..b8e6669 --- /dev/null +++ b/src/formats/video_format.rs @@ -0,0 +1,37 @@ +use crate::error::{ThumbError, ThumbResult}; +use image::png::PngDecoder; +use image::DynamicImage; +use mime::Mime; +use std::fs; +use std::io::{BufRead, Seek}; +use std::path::PathBuf; +use vid2img::FileSource; + +pub fn get_video_frame(mut reader: R, mime: Mime) -> ThumbResult { + let tempdir = tempfile::tempdir()?; + tempdir.path(); + let path = PathBuf::from(tempdir.path()) + .join("video") + .with_extension(mime.subtype().as_str()); + + let mut buf = Vec::new(); + reader.read_to_end(&mut buf)?; + fs::write(&path, buf)?; + + let img = extract_frame_from_video(&path)?; + tempdir.close()?; + + Ok(img) +} + +fn extract_frame_from_video(path: &PathBuf) -> ThumbResult { + let source = FileSource::new(path, (2000, 2000))?; + for frame in source.into_iter() { + if let Ok(Some(data)) = frame { + let decoder = PngDecoder::new(data.as_slice())?; + let img = DynamicImage::from_decoder(decoder)?; + return Ok(img); + } + } + Err(ThumbError::NullVideo) +} diff --git a/tests/assets/test.mp4 b/tests/assets/test.mp4 new file mode 100644 index 0000000..b90c4b5 Binary files /dev/null and b/tests/assets/test.mp4 differ diff --git a/tests/video_reading.rs b/tests/video_reading.rs new file mode 100644 index 0000000..4d6f225 --- /dev/null +++ b/tests/video_reading.rs @@ -0,0 +1,21 @@ +use mime::Mime; +use std::io::Cursor; +use std::str::FromStr; +use thumbnailer::{create_thumbnails, ThumbnailSize}; + +const VIDEO_BYTES: &'static [u8] = include_bytes!("assets/test.mp4"); + +#[test] +fn it_creates_thumbnails_for_mp4() { + let reader = Cursor::new(VIDEO_BYTES); + create_thumbnails( + reader, + Mime::from_str("video/mp4").unwrap(), + [ + ThumbnailSize::Small, + ThumbnailSize::Medium, + ThumbnailSize::Large, + ], + ) + .unwrap(); +}