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#[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
61pub 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#[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 pub width: u32,
106 pub height: u32,
108 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 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}