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>(
86        &'_ self,
87        path: P,
88        output: P,
89    ) -> anyhow::Result<(), ThumbnailError<'_>>
90    where
91        P: AsRef<Path>,
92    {
93        let path = path.as_ref();
94        let mime = tika_magic::from_filepath(path).context("Failed to find MIME type.")?;
95        // println!("mime: {}", mime);
96
97        let encoding = output
98            .as_ref()
99            .extension()
100            .and_then(|ext| ext.to_ascii_uppercase().to_str().map(str::to_string))
101            .and_then(|ext| Encoding::from_str(&ext).ok())
102            .unwrap_or_else(|| {
103                log::debug!("Defaulting encoding to Jpeg");
104                Encoding::Jpeg
105            });
106
107        #[cfg(feature = "image")]
108        if mime.starts_with("image/") {
109            use crate::thumbs::image;
110
111            let img = image::create_thumbnail(path, self.width, self.height)?;
112            self.encod_and_save(img, encoding, output)?;
113            return Ok(());
114        }
115
116        #[cfg(feature = "pdf")]
117        if mime.eq("application/pdf") {
118            use crate::thumbs::pdf;
119
120            let img = pdf::create_thumbnail(path, self.width, self.height)?;
121            self.encod_and_save(img, encoding, output)?;
122            return Ok(());
123        }
124
125        #[cfg(feature = "video")]
126        if mime.starts_with("video/") {
127            use crate::thumbs::video;
128
129            let img = video::create_thumbnail(path, self.width, self.height)?;
130            self.encod_and_save(img, encoding, output)?;
131            return Ok(());
132        }
133
134        Err(ThumbnailError::UnsupportedError(mime))
135    }
136
137    fn encod_and_save<P>(
138        &'_ self,
139        img: DynamicImage,
140        encoding: Encoding,
141        output: P,
142    ) -> anyhow::Result<(), ThumbnailError<'_>>
143    where
144        P: AsRef<Path>,
145    {
146        match encoding {
147            Encoding::Jpeg => {
148                let output = File::create(output)?;
149                let encoder = JpegEncoder::new_with_quality(output, self.quality);
150                img.write_with_encoder(encoder)?;
151            }
152            Encoding::Png => {
153                img.save_with_format(&output, ImageFormat::Png)?;
154
155                oxipng::optimize(
156                    &oxipng::InFile::Path(output.as_ref().to_path_buf()),
157                    &oxipng::OutFile::from_path(output.as_ref().to_path_buf()),
158                    &oxipng::Options::max_compression(),
159                )?;
160            }
161            Encoding::Webp => {
162                let encoder = webp::Encoder::from_image(&img)
163                    .ok()
164                    .context("Unimplemented")?;
165                let memory = encoder.encode(self.quality.into());
166                std::fs::write(output, &*memory)?;
167            }
168        };
169
170        Ok(())
171    }
172}