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, T>(
86 &'_ self,
87 path: P,
88 output: T,
89 ) -> anyhow::Result<(), ThumbnailError<'_>>
90 where
91 P: AsRef<Path>,
92 T: AsRef<Path>,
93 {
94 let path = path.as_ref();
95 let mime = tika_magic::from_filepath(path).context("Failed to find MIME type.")?;
96 let encoding = output
99 .as_ref()
100 .extension()
101 .and_then(|ext| ext.to_ascii_uppercase().to_str().map(str::to_string))
102 .and_then(|ext| Encoding::from_str(&ext).ok())
103 .unwrap_or_else(|| {
104 log::debug!("Defaulting encoding to Jpeg");
105 Encoding::Jpeg
106 });
107
108 #[cfg(feature = "image")]
109 if mime.starts_with("image/") {
110 use crate::thumbs::image;
111
112 let img = image::create_thumbnail(path, self.width, self.height)?;
113 self.encod_and_save(img, encoding, output)?;
114 return Ok(());
115 }
116
117 #[cfg(feature = "pdf")]
118 if mime.eq("application/pdf") {
119 use crate::thumbs::pdf;
120
121 let img = pdf::create_thumbnail(path, self.width, self.height)?;
122 self.encod_and_save(img, encoding, output)?;
123 return Ok(());
124 }
125
126 #[cfg(feature = "video")]
127 if mime.starts_with("video/") {
128 use crate::thumbs::video;
129
130 let img = video::create_thumbnail(path, self.width, self.height)?;
131 self.encod_and_save(img, encoding, output)?;
132 return Ok(());
133 }
134
135 Err(ThumbnailError::UnsupportedError(mime))
136 }
137
138 fn encod_and_save<P>(
139 &'_ self,
140 img: DynamicImage,
141 encoding: Encoding,
142 output: P,
143 ) -> anyhow::Result<(), ThumbnailError<'_>>
144 where
145 P: AsRef<Path>,
146 {
147 match encoding {
148 Encoding::Jpeg => {
149 let output = File::create(output)?;
150 let encoder = JpegEncoder::new_with_quality(output, self.quality);
151 img.write_with_encoder(encoder)?;
152 }
153 Encoding::Png => {
154 img.save_with_format(&output, ImageFormat::Png)?;
155
156 oxipng::optimize(
157 &oxipng::InFile::Path(output.as_ref().to_path_buf()),
158 &oxipng::OutFile::from_path(output.as_ref().to_path_buf()),
159 &oxipng::Options::max_compression(),
160 )?;
161 }
162 Encoding::Webp => {
163 let encoder = webp::Encoder::from_image(&img)
164 .ok()
165 .context("Unimplemented")?;
166 let memory = encoder.encode(self.quality.into());
167 std::fs::write(output, &*memory)?;
168 }
169 };
170
171 Ok(())
172 }
173}