1mod decode;
2pub mod frame_valid;
3mod meta;
4mod mime_resolve;
5mod thumbs;
6pub mod types;
7
8pub use decode::{DecodeError, decode_and_thumbnail, decode_image};
9pub use frame_valid::{is_effectively_blank, is_rgba_blank};
10pub use meta::{MediaMeta, probe_media_meta};
11
12use std::{fs::File, path::Path, str::FromStr};
13
14use ::image::{DynamicImage, ImageFormat, codecs::jpeg::JpegEncoder};
15use strum_macros::{AsRefStr, Display, EnumString};
16
17#[derive(thiserror::Error, Debug)]
18pub enum ThumbnailError {
19 #[error("IOError")]
20 IOError(#[from] std::io::Error),
21 #[error("ImageError")]
22 ImageError(#[from] ::image::ImageError),
23 #[error("PngError")]
24 PngError(#[from] oxipng::PngError),
25 #[error("AnyError")]
26 AnyError(#[from] anyhow::Error),
27 #[error("Unsupported MIME type:`{0}`")]
28 UnsupportedError(String),
29}
30
31#[derive(Debug, Copy, Clone, Display, EnumString, AsRefStr)]
32#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
33pub enum Encoding {
34 Jpeg,
35 Png,
36 Webp,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum PsdPreviewPolicy {
42 #[default]
44 PreferComposite,
45 IrbOnly,
47}
48
49#[derive(Clone, Copy, Debug)]
51pub enum ThumbnailSize {
52 Icon,
53 Small,
54 Medium,
55 Large,
56 Larger,
57 Custom((u32, u32)),
58}
59
60impl ThumbnailSize {
61 pub fn dimensions(&self) -> (u32, u32) {
62 match self {
63 ThumbnailSize::Icon => (64, 64),
64 ThumbnailSize::Small => (128, 128),
65 ThumbnailSize::Medium => (256, 256),
66 ThumbnailSize::Large => (512, 512),
67 ThumbnailSize::Larger => (1024, 1024),
68 ThumbnailSize::Custom(size) => *size,
69 }
70 }
71}
72
73pub fn decode_for_thumbnail(path: &Path, max_dim: u32) -> Result<DynamicImage, DecodeError> {
75 decode_for_thumbnail_with_psd_policy(path, max_dim, PsdPreviewPolicy::PreferComposite)
76}
77
78pub fn decode_for_thumbnail_with_psd_policy(
82 path: &Path,
83 max_dim: u32,
84 psd_policy: PsdPreviewPolicy,
85) -> Result<DynamicImage, DecodeError> {
86 let mime = mime_resolve::resolve_mime(path);
87 let ext = path
88 .extension()
89 .and_then(|e| e.to_str())
90 .unwrap_or("")
91 .to_ascii_lowercase();
92
93 #[cfg(feature = "svg")]
94 if mime_resolve::is_svg_mime(&mime) || ext == "svg" {
95 use crate::thumbs::svg;
96 return svg::create_thumbnail(path, max_dim).map_err(|_| DecodeError::Unsupported);
97 }
98
99 #[cfg(feature = "raw")]
100 if mime_resolve::is_raw_ext(&ext) {
101 use crate::thumbs::raw;
102 return raw::create_thumbnail(path, max_dim).map_err(|_| DecodeError::Unsupported);
103 }
104
105 #[cfg(feature = "audio")]
106 if mime_resolve::is_audio_mime(&mime) || mime_resolve::is_audio_ext(&ext) {
107 use crate::thumbs::audio;
108 return audio::extract_cover(path, max_dim).ok_or(DecodeError::Unsupported);
109 }
110
111 #[cfg(feature = "office")]
112 if mime_resolve::is_office_mime(&mime) || mime_resolve::is_office_ext(&ext) {
113 use crate::thumbs::office;
114 return office::create_thumbnail(path, max_dim).ok_or(DecodeError::Unsupported);
115 }
116
117 #[cfg(feature = "source")]
118 if crate::thumbs::source::is_source_ext(&ext) {
119 use crate::thumbs::source;
120 return source::create_thumbnail_with_psd_policy(path, max_dim, psd_policy)
121 .ok_or(DecodeError::Unsupported);
122 }
123
124 decode_and_thumbnail(path, max_dim)
125}
126
127#[cfg(feature = "office")]
129pub fn extract_office_emf_bytes(path: &Path) -> Option<Vec<u8>> {
130 crate::thumbs::office::extract_emf_bytes(path)
131}
132
133#[cfg(feature = "pdf")]
135pub fn render_pdf_pages(path: &Path, max_pages: usize) -> Option<Vec<DynamicImage>> {
136 crate::thumbs::pdf::render_pdf_pages(path, max_pages)
137}
138
139#[cfg(feature = "pdf")]
141pub fn render_pdf_pages_with_limit(
142 path: &Path,
143 max_pages: usize,
144 max_long_side: Option<u32>,
145) -> Option<Vec<DynamicImage>> {
146 crate::thumbs::pdf::render_pdf_pages_with_limit(path, max_pages, max_long_side)
147}
148
149#[cfg(feature = "pdf")]
151pub fn render_pdf_pages_for_ocr(
152 path: &Path,
153 max_pages: usize,
154 scale: f32,
155) -> Option<Vec<DynamicImage>> {
156 crate::thumbs::pdf::render_pdf_pages_for_ocr(path, max_pages, scale)
157}
158
159#[cfg(feature = "pdf")]
163pub fn extract_pdf_pages_text(path: &Path, max_pages: usize) -> Option<Vec<String>> {
164 crate::thumbs::pdf::extract_pages_text(path, max_pages)
165}
166
167#[cfg(feature = "video")]
169pub fn decode_video_sample_frames(path: &Path, max_frames: usize) -> Vec<DynamicImage> {
170 crate::decode::ffmpeg_decode::decode_video_sample_frames(path, max_frames)
171}
172
173pub struct Thumbnailer {
174 pub width: u32,
176 pub height: u32,
178 pub quality: u8,
182}
183
184impl Default for Thumbnailer {
185 fn default() -> Self {
186 Self::new(ThumbnailSize::Medium, 90)
187 }
188}
189
190impl Thumbnailer {
191 pub fn new(size: ThumbnailSize, quality: u8) -> Self {
192 let (width, height) = size.dimensions();
193 Self {
194 width,
195 height,
196 quality,
197 }
198 }
199
200 pub fn create_thumbnail<P, T>(
204 &'_ self,
205 path: P,
206 output: T,
207 ) -> anyhow::Result<(), ThumbnailError>
208 where
209 P: AsRef<Path>,
210 T: AsRef<Path>,
211 {
212 let path = path.as_ref();
213 let mime = mime_resolve::resolve_mime(path);
214 let ext = path
215 .extension()
216 .and_then(|e| e.to_str())
217 .unwrap_or("")
218 .to_ascii_lowercase();
219
220 let encoding = output
221 .as_ref()
222 .extension()
223 .and_then(|ext| ext.to_ascii_uppercase().to_str().map(str::to_string))
224 .and_then(|ext| Encoding::from_str(&ext).ok())
225 .unwrap_or_else(|| {
226 log::debug!("Defaulting encoding to Jpeg");
227 Encoding::Jpeg
228 });
229
230 let max_dim = self.width.max(self.height);
231
232 #[cfg(feature = "svg")]
233 if mime_resolve::is_svg_mime(&mime) || ext == "svg" {
234 use crate::thumbs::svg;
235 let img = svg::create_thumbnail(path, max_dim)?;
236 self.encod_and_save(img, encoding, output)?;
237 return Ok(());
238 }
239
240 #[cfg(feature = "raw")]
241 if mime_resolve::is_raw_ext(&ext) {
242 use crate::thumbs::raw;
243 let img = raw::create_thumbnail(path, max_dim)?;
244 self.encod_and_save(img, encoding, output)?;
245 return Ok(());
246 }
247
248 #[cfg(feature = "audio")]
249 if mime_resolve::is_audio_mime(&mime) || mime_resolve::is_audio_ext(&ext) {
250 use crate::thumbs::audio;
251 let img = audio::extract_cover(path, max_dim)
252 .ok_or_else(|| ThumbnailError::UnsupportedError(mime.clone()))?;
253 self.encod_and_save(img, encoding, output)?;
254 return Ok(());
255 }
256
257 #[cfg(feature = "office")]
258 if mime_resolve::is_office_mime(&mime) || mime_resolve::is_office_ext(&ext) {
259 use crate::thumbs::office;
260 let img = office::create_thumbnail(path, max_dim)
261 .ok_or_else(|| ThumbnailError::UnsupportedError(mime.clone()))?;
262 self.encod_and_save(img, encoding, output)?;
263 return Ok(());
264 }
265
266 #[cfg(feature = "source")]
267 if crate::thumbs::source::is_source_ext(&ext) {
268 use crate::thumbs::source;
269 let img = source::create_thumbnail(path, max_dim)
270 .ok_or_else(|| ThumbnailError::UnsupportedError(mime.clone()))?;
271 self.encod_and_save(img, encoding, output)?;
272 return Ok(());
273 }
274
275 #[cfg(feature = "image")]
276 if mime_resolve::is_image_mime(&mime) {
277 use crate::thumbs::image;
278
279 let img = image::create_thumbnail(path, self.width, self.height)?;
280 self.encod_and_save(img, encoding, output)?;
281 return Ok(());
282 }
283
284 #[cfg(feature = "pdf")]
285 if mime_resolve::is_pdf_mime(&mime) {
286 use crate::thumbs::pdf;
287
288 let img = pdf::create_thumbnail(path, self.width, self.height)?;
289 self.encod_and_save(img, encoding, output)?;
290 return Ok(());
291 }
292
293 #[cfg(feature = "video")]
294 if mime_resolve::is_video_mime(&mime) {
295 use crate::thumbs::video;
296
297 let img = video::create_thumbnail(path, self.width, self.height)?;
298 self.encod_and_save(img, encoding, output)?;
299 return Ok(());
300 }
301
302 Err(ThumbnailError::UnsupportedError(mime))
303 }
304
305 fn encod_and_save<P>(
306 &'_ self,
307 img: DynamicImage,
308 encoding: Encoding,
309 output: P,
310 ) -> anyhow::Result<(), ThumbnailError>
311 where
312 P: AsRef<Path>,
313 {
314 match encoding {
315 Encoding::Jpeg => {
316 let output = File::create(output)?;
317 let encoder = JpegEncoder::new_with_quality(output, self.quality);
318 img.write_with_encoder(encoder)?;
319 }
320 Encoding::Png => {
321 img.save_with_format(&output, ImageFormat::Png)?;
322
323 oxipng::optimize(
324 &oxipng::InFile::Path(output.as_ref().to_path_buf()),
325 &oxipng::OutFile::from_path(output.as_ref().to_path_buf()),
326 &oxipng::Options::max_compression(),
327 )?;
328 }
329 Encoding::Webp => {
330 let rgba = img.to_rgba8();
331 let encoder = webp::Encoder::from_rgba(rgba.as_raw(), rgba.width(), rgba.height());
332 let memory = encoder.encode(self.quality.into());
333 std::fs::write(output, &*memory)?;
334 }
335 };
336
337 Ok(())
338 }
339}