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#[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 pub width: u32,
58 pub height: u32,
60 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 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 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}