auto_thumbnail/
lib.rs

1mod thumbs;
2pub mod types;
3
4use std::{fs::File, path::Path, str::FromStr};
5
6use ::image::{DynamicImage, ImageFormat, codecs::jpeg::JpegEncoder};
7use anyhow::Context;
8use strum_macros::{AsRefStr, Display, EnumString};
9
10use crate::types::{IMAGE_MIME_TYPES, PDF_MIME_TYPES, VIDEO_MIME_TYPES};
11
12#[derive(thiserror::Error, Debug)]
13pub enum ThumbnailError<'a> {
14    #[error("IOError")]
15    IOError(#[from] std::io::Error),
16    #[error("ImageError")]
17    ImageError(#[from] ::image::ImageError),
18    #[error("PngError")]
19    PngError(#[from] oxipng::PngError),
20    #[error("AnyError")]
21    AnyError(#[from] anyhow::Error),
22    #[error("Unsupported MIME type:`{0}`")]
23    UnsupportedError(&'a str),
24}
25
26#[derive(Debug, Copy, Clone, Display, EnumString, AsRefStr)]
27#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
28pub enum Encoding {
29    Jpeg,
30    Png,
31    Webp,
32}
33
34/// Represents fixed sizes of a thumbnail
35#[derive(Clone, Copy, Debug)]
36pub enum ThumbnailSize {
37    Icon,
38    Small,
39    Medium,
40    Large,
41    Larger,
42    Custom((u32, u32)),
43}
44
45impl ThumbnailSize {
46    pub fn dimensions(&self) -> (u32, u32) {
47        match self {
48            ThumbnailSize::Icon => (64, 64),
49            ThumbnailSize::Small => (128, 128),
50            ThumbnailSize::Medium => (256, 256),
51            ThumbnailSize::Large => (512, 512),
52            ThumbnailSize::Larger => (1024, 1024),
53            ThumbnailSize::Custom(size) => *size,
54        }
55    }
56}
57
58pub struct Thumbnailer {
59    /// The maximum output width.
60    pub width: u32,
61    /// The maximum output height.
62    pub height: u32,
63    /// Encode the image with the given quality.
64    /// Only support Jpeg and Webp.
65    /// The image quality must be between 1 and 100 inclusive for minimal and maximal quality respectively.
66    pub quality: u8,
67}
68
69impl Default for Thumbnailer {
70    fn default() -> Self {
71        Self::new(ThumbnailSize::Medium, 90)
72    }
73}
74
75impl Thumbnailer {
76    pub fn new(size: ThumbnailSize, quality: u8) -> Self {
77        let (width, height) = size.dimensions();
78        Self {
79            width,
80            height,
81            quality,
82        }
83    }
84
85    /// create thumbnail image.
86    /// path: source file path.
87    /// output: thumbnail image path.
88    pub fn create_thumbnail<P, T>(
89        &'_ self,
90        path: P,
91        output: T,
92    ) -> anyhow::Result<(), ThumbnailError<'_>>
93    where
94        P: AsRef<Path>,
95        T: AsRef<Path>,
96    {
97        let path = path.as_ref();
98        let mime = tika_magic::from_filepath(path).context("Failed to find MIME type.")?;
99        // log::debug!("path: {:?}, mime: {}", path, mime);
100
101        let encoding = output
102            .as_ref()
103            .extension()
104            .and_then(|ext| ext.to_ascii_uppercase().to_str().map(str::to_string))
105            .and_then(|ext| Encoding::from_str(&ext).ok())
106            .unwrap_or_else(|| {
107                log::debug!("Defaulting encoding to Jpeg");
108                Encoding::Jpeg
109            });
110
111        #[cfg(feature = "image")]
112        if IMAGE_MIME_TYPES.contains(&mime) {
113            use crate::thumbs::image;
114
115            let img = image::create_thumbnail(path, self.width, self.height)?;
116            self.encod_and_save(img, encoding, output)?;
117            return Ok(());
118        }
119
120        #[cfg(feature = "pdf")]
121        if PDF_MIME_TYPES.contains(&mime) {
122            use crate::thumbs::pdf;
123
124            let img = pdf::create_thumbnail(path, self.width, self.height)?;
125            self.encod_and_save(img, encoding, output)?;
126            return Ok(());
127        }
128
129        #[cfg(feature = "video")]
130        if VIDEO_MIME_TYPES.contains(&mime) {
131            use crate::thumbs::video;
132
133            let img = video::create_thumbnail(path, self.width, self.height)?;
134            self.encod_and_save(img, encoding, output)?;
135            return Ok(());
136        }
137
138        Err(ThumbnailError::UnsupportedError(mime))
139    }
140
141    fn encod_and_save<P>(
142        &'_ self,
143        img: DynamicImage,
144        encoding: Encoding,
145        output: P,
146    ) -> anyhow::Result<(), ThumbnailError<'_>>
147    where
148        P: AsRef<Path>,
149    {
150        match encoding {
151            Encoding::Jpeg => {
152                let output = File::create(output)?;
153                let encoder = JpegEncoder::new_with_quality(output, self.quality);
154                img.write_with_encoder(encoder)?;
155            }
156            Encoding::Png => {
157                img.save_with_format(&output, ImageFormat::Png)?;
158
159                oxipng::optimize(
160                    &oxipng::InFile::Path(output.as_ref().to_path_buf()),
161                    &oxipng::OutFile::from_path(output.as_ref().to_path_buf()),
162                    &oxipng::Options::max_compression(),
163                )?;
164            }
165            Encoding::Webp => {
166                let encoder = webp::Encoder::from_image(&img)
167                    .ok()
168                    .context("Unimplemented")?;
169                let memory = encoder.encode(self.quality.into());
170                std::fs::write(output, &*memory)?;
171            }
172        };
173
174        Ok(())
175    }
176}