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