auto_thumbnail/
lib.rs

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