Skip to main content

auto_thumbnail/
lib.rs

1mod decode;
2pub mod frame_valid;
3mod mime_resolve;
4mod thumbs;
5pub mod types;
6
7pub use decode::{decode_and_thumbnail, decode_image, DecodeError};
8pub use frame_valid::{is_effectively_blank, is_rgba_blank};
9
10use std::{fs::File, path::Path, str::FromStr};
11
12use ::image::{DynamicImage, ImageFormat, codecs::jpeg::JpegEncoder};
13use strum_macros::{AsRefStr, Display, EnumString};
14
15#[derive(thiserror::Error, Debug)]
16pub enum ThumbnailError {
17    #[error("IOError")]
18    IOError(#[from] std::io::Error),
19    #[error("ImageError")]
20    ImageError(#[from] ::image::ImageError),
21    #[error("PngError")]
22    PngError(#[from] oxipng::PngError),
23    #[error("AnyError")]
24    AnyError(#[from] anyhow::Error),
25    #[error("Unsupported MIME type:`{0}`")]
26    UnsupportedError(String),
27}
28
29#[derive(Debug, Copy, Clone, Display, EnumString, AsRefStr)]
30#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
31pub enum Encoding {
32    Jpeg,
33    Png,
34    Webp,
35}
36
37/// Represents fixed sizes of a thumbnail
38#[derive(Clone, Copy, Debug)]
39pub enum ThumbnailSize {
40    Icon,
41    Small,
42    Medium,
43    Large,
44    Larger,
45    Custom((u32, u32)),
46}
47
48impl ThumbnailSize {
49    pub fn dimensions(&self) -> (u32, u32) {
50        match self {
51            ThumbnailSize::Icon => (64, 64),
52            ThumbnailSize::Small => (128, 128),
53            ThumbnailSize::Medium => (256, 256),
54            ThumbnailSize::Large => (512, 512),
55            ThumbnailSize::Larger => (1024, 1024),
56            ThumbnailSize::Custom(size) => *size,
57        }
58    }
59}
60
61/// 按 MIME/扩展名路由解码并缩放到 max_dim 边长内(svg/raw/audio/office 优先于通用 image)
62pub fn decode_for_thumbnail(path: &Path, max_dim: u32) -> Result<DynamicImage, DecodeError> {
63    let mime = mime_resolve::resolve_mime(path);
64    let ext = path
65        .extension()
66        .and_then(|e| e.to_str())
67        .unwrap_or("")
68        .to_ascii_lowercase();
69
70    #[cfg(feature = "svg")]
71    if mime_resolve::is_svg_mime(&mime) || ext == "svg" {
72        use crate::thumbs::svg;
73        return svg::create_thumbnail(path, max_dim).map_err(|_| DecodeError::Unsupported);
74    }
75
76    #[cfg(feature = "raw")]
77    if mime_resolve::is_raw_ext(&ext) {
78        use crate::thumbs::raw;
79        return raw::create_thumbnail(path, max_dim).map_err(|_| DecodeError::Unsupported);
80    }
81
82    #[cfg(feature = "audio")]
83    if mime_resolve::is_audio_mime(&mime) || mime_resolve::is_audio_ext(&ext) {
84        use crate::thumbs::audio;
85        return audio::extract_cover(path, max_dim).ok_or(DecodeError::Unsupported);
86    }
87
88    #[cfg(feature = "office")]
89    if mime_resolve::is_office_mime(&mime) || mime_resolve::is_office_ext(&ext) {
90        use crate::thumbs::office;
91        return office::create_thumbnail(path, max_dim).ok_or(DecodeError::Unsupported);
92    }
93
94    decode_and_thumbnail(path, max_dim)
95}
96
97/// 读取 Office ZIP 内 EMF 缩略图原始字节(供应用层 GDI 栅格化)
98#[cfg(feature = "office")]
99pub fn extract_office_emf_bytes(path: &Path) -> Option<Vec<u8>> {
100    crate::thumbs::office::extract_emf_bytes(path)
101}
102
103pub struct Thumbnailer {
104    /// The maximum output width.
105    pub width: u32,
106    /// The maximum output height.
107    pub height: u32,
108    /// Encode the image with the given quality.
109    /// Only support Jpeg and Webp.
110    /// The image quality must be between 1 and 100 inclusive for minimal and maximal quality respectively.
111    pub quality: u8,
112}
113
114impl Default for Thumbnailer {
115    fn default() -> Self {
116        Self::new(ThumbnailSize::Medium, 90)
117    }
118}
119
120impl Thumbnailer {
121    pub fn new(size: ThumbnailSize, quality: u8) -> Self {
122        let (width, height) = size.dimensions();
123        Self {
124            width,
125            height,
126            quality,
127        }
128    }
129
130    /// create thumbnail image.
131    /// path: source file path.
132    /// output: thumbnail image path.
133    pub fn create_thumbnail<P, T>(
134        &'_ self,
135        path: P,
136        output: T,
137    ) -> anyhow::Result<(), ThumbnailError>
138    where
139        P: AsRef<Path>,
140        T: AsRef<Path>,
141    {
142        let path = path.as_ref();
143        let mime = mime_resolve::resolve_mime(path);
144        let ext = path
145            .extension()
146            .and_then(|e| e.to_str())
147            .unwrap_or("")
148            .to_ascii_lowercase();
149
150        let encoding = output
151            .as_ref()
152            .extension()
153            .and_then(|ext| ext.to_ascii_uppercase().to_str().map(str::to_string))
154            .and_then(|ext| Encoding::from_str(&ext).ok())
155            .unwrap_or_else(|| {
156                log::debug!("Defaulting encoding to Jpeg");
157                Encoding::Jpeg
158            });
159
160        let max_dim = self.width.max(self.height);
161
162        #[cfg(feature = "svg")]
163        if mime_resolve::is_svg_mime(&mime) || ext == "svg" {
164            use crate::thumbs::svg;
165            let img = svg::create_thumbnail(path, max_dim)?;
166            self.encod_and_save(img, encoding, output)?;
167            return Ok(());
168        }
169
170        #[cfg(feature = "raw")]
171        if mime_resolve::is_raw_ext(&ext) {
172            use crate::thumbs::raw;
173            let img = raw::create_thumbnail(path, max_dim)?;
174            self.encod_and_save(img, encoding, output)?;
175            return Ok(());
176        }
177
178        #[cfg(feature = "audio")]
179        if mime_resolve::is_audio_mime(&mime) || mime_resolve::is_audio_ext(&ext) {
180            use crate::thumbs::audio;
181            let img = audio::extract_cover(path, max_dim)
182                .ok_or_else(|| ThumbnailError::UnsupportedError(mime.clone()))?;
183            self.encod_and_save(img, encoding, output)?;
184            return Ok(());
185        }
186
187        #[cfg(feature = "office")]
188        if mime_resolve::is_office_mime(&mime) || mime_resolve::is_office_ext(&ext) {
189            use crate::thumbs::office;
190            let img = office::create_thumbnail(path, max_dim)
191                .ok_or_else(|| ThumbnailError::UnsupportedError(mime.clone()))?;
192            self.encod_and_save(img, encoding, output)?;
193            return Ok(());
194        }
195
196        #[cfg(feature = "image")]
197        if mime_resolve::is_image_mime(&mime) {
198            use crate::thumbs::image;
199
200            let img = image::create_thumbnail(path, self.width, self.height)?;
201            self.encod_and_save(img, encoding, output)?;
202            return Ok(());
203        }
204
205        #[cfg(feature = "pdf")]
206        if mime_resolve::is_pdf_mime(&mime) {
207            use crate::thumbs::pdf;
208
209            let img = pdf::create_thumbnail(path, self.width, self.height)?;
210            self.encod_and_save(img, encoding, output)?;
211            return Ok(());
212        }
213
214        #[cfg(feature = "video")]
215        if mime_resolve::is_video_mime(&mime) {
216            use crate::thumbs::video;
217
218            let img = video::create_thumbnail(path, self.width, self.height)?;
219            self.encod_and_save(img, encoding, output)?;
220            return Ok(());
221        }
222
223        Err(ThumbnailError::UnsupportedError(mime))
224    }
225
226    fn encod_and_save<P>(
227        &'_ self,
228        img: DynamicImage,
229        encoding: Encoding,
230        output: P,
231    ) -> anyhow::Result<(), ThumbnailError>
232    where
233        P: AsRef<Path>,
234    {
235        match encoding {
236            Encoding::Jpeg => {
237                let output = File::create(output)?;
238                let encoder = JpegEncoder::new_with_quality(output, self.quality);
239                img.write_with_encoder(encoder)?;
240            }
241            Encoding::Png => {
242                img.save_with_format(&output, ImageFormat::Png)?;
243
244                oxipng::optimize(
245                    &oxipng::InFile::Path(output.as_ref().to_path_buf()),
246                    &oxipng::OutFile::from_path(output.as_ref().to_path_buf()),
247                    &oxipng::Options::max_compression(),
248                )?;
249            }
250            Encoding::Webp => {
251                let rgba = img.to_rgba8();
252                let encoder = webp::Encoder::from_rgba(
253                    rgba.as_raw(),
254                    rgba.width(),
255                    rgba.height(),
256                );
257                let memory = encoder.encode(self.quality.into());
258                std::fs::write(output, &*memory)?;
259            }
260        };
261
262        Ok(())
263    }
264}