1use crate::core::tools::read::{
12 ProcessImageResult as ReadProcessImageResult, convert_image_bytes_to_png,
13 detect_supported_image_mime_type, process_image_bytes,
14};
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq)]
18pub enum ImageMime {
19 Jpeg,
21 Png,
23 Gif,
25 Webp,
27 Bmp,
29}
30
31impl ImageMime {
32 #[must_use]
34 pub const fn mime(self) -> &'static str {
35 match self {
36 Self::Jpeg => "image/jpeg",
37 Self::Png => "image/png",
38 Self::Gif => "image/gif",
39 Self::Webp => "image/webp",
40 Self::Bmp => "image/bmp",
41 }
42 }
43
44 #[must_use]
46 pub fn from_canonical(mime: &str) -> Option<Self> {
47 Some(match mime {
48 "image/jpeg" | "image/jpg" => Self::Jpeg,
49 "image/png" => Self::Png,
50 "image/gif" => Self::Gif,
51 "image/webp" => Self::Webp,
52 "image/bmp" => Self::Bmp,
53 _ => return None,
54 })
55 }
56}
57
58#[must_use]
64pub fn extension_for_image_mime(mime: &str) -> Option<&'static str> {
65 let base = base_mime(mime);
66 match base.as_str() {
67 "image/png" => Some("png"),
68 "image/jpeg" | "image/jpg" => Some("jpg"),
69 "image/webp" => Some("webp"),
70 "image/gif" => Some("gif"),
71 "image/bmp" => Some("bmp"),
72 _ => None,
73 }
74}
75
76#[must_use]
82pub fn detect_supported_image_mime(bytes: &[u8]) -> Option<ImageMime> {
83 detect_supported_image_mime_type(bytes).and_then(|mime| ImageMime::from_canonical(&mime))
84}
85
86#[must_use]
91pub fn convert_to_png(bytes: &[u8]) -> Option<Vec<u8>> {
92 convert_image_bytes_to_png(bytes)
93}
94
95#[derive(Clone, Debug)]
97pub enum ProcessImageResult {
98 Ok {
100 data: String,
102 mime: String,
104 hints: Vec<String>,
106 },
107 Omitted(String),
109}
110
111#[must_use]
118pub fn process_image(bytes: &[u8], mime: &str, auto_resize: bool) -> ProcessImageResult {
119 match process_image_bytes(bytes, mime, auto_resize) {
120 ReadProcessImageResult::Ok(processed) => ProcessImageResult::Ok {
121 data: processed.data,
122 mime: processed.mime_type,
123 hints: processed.hints,
124 },
125 ReadProcessImageResult::Failed(error) => ProcessImageResult::Omitted(error.message),
126 }
127}
128
129fn base_mime(mime: &str) -> String {
131 mime.split(';')
132 .next()
133 .unwrap_or(mime)
134 .trim()
135 .to_ascii_lowercase()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use image::{GenericImageView, ImageFormat};
142 use std::io::Cursor;
143 type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
144
145 fn solid_png(width: u32, height: u32) -> TestResult<Vec<u8>> {
146 let img = image::RgbaImage::from_pixel(width, height, image::Rgba([10, 20, 30, 255]));
147 let mut buf = Vec::new();
148 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)?;
149 Ok(buf)
150 }
151
152 fn solid_jpeg(width: u32, height: u32) -> TestResult<Vec<u8>> {
153 let img = image::RgbImage::from_pixel(width, height, image::Rgb([10, 20, 30]));
154 let mut buf = Vec::new();
155 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 90);
156 encoder.encode_image(&image::DynamicImage::ImageRgb8(img))?;
157 Ok(buf)
158 }
159
160 #[test]
161 fn detects_png_jpeg_gif_webp_signatures() -> TestResult {
162 let png = solid_png(2, 2)?;
163 assert_eq!(detect_supported_image_mime(&png), Some(ImageMime::Png));
164 let jpeg = solid_jpeg(2, 2)?;
165 assert_eq!(detect_supported_image_mime(&jpeg), Some(ImageMime::Jpeg));
166 let gif = b"GIF89a...";
167 assert_eq!(detect_supported_image_mime(gif), Some(ImageMime::Gif));
168 let mut webp = b"RIFF\x00\x00\x00\x00WEBP".to_vec();
169 webp.extend_from_slice(b"VP8 ");
170 assert_eq!(detect_supported_image_mime(&webp), Some(ImageMime::Webp));
171 Ok(())
172 }
173
174 #[test]
175 fn rejects_jpeg_hico_marker() {
176 assert_eq!(detect_supported_image_mime(&[0xFF, 0xD8, 0xFF, 0xF7]), None);
178 }
179
180 #[test]
181 fn extension_for_mime_matches_ts() {
182 assert_eq!(extension_for_image_mime("image/png"), Some("png"));
183 assert_eq!(extension_for_image_mime("image/jpeg"), Some("jpg"));
184 assert_eq!(
185 extension_for_image_mime("image/jpeg; charset=binary"),
186 Some("jpg")
187 );
188 assert_eq!(extension_for_image_mime("image/x-foo"), None);
189 }
190
191 #[test]
192 fn convert_to_png_roundtrips_dimensions() -> TestResult {
193 let png = solid_png(3, 5)?;
194 let out = convert_to_png(&png)
195 .ok_or_else(|| std::io::Error::other("PNG conversion was omitted"))?;
196 let img = image::load_from_memory(&out)?;
197 assert_eq!(img.dimensions(), (3, 5));
198 Ok(())
199 }
200
201 #[test]
202 fn process_image_normalizes_bmp_to_png() -> TestResult {
203 let mut bmp_buf = Vec::new();
204 let img = image::RgbImage::from_pixel(4, 4, image::Rgb([5, 6, 7]));
205 image::DynamicImage::ImageRgb8(img)
206 .write_to(&mut Cursor::new(&mut bmp_buf), ImageFormat::Bmp)?;
207 match process_image(&bmp_buf, "image/bmp", false) {
208 ProcessImageResult::Ok { data, mime, hints } => {
209 assert_eq!(mime, "image/png");
210 assert!(!data.is_empty());
211 assert!(
212 hints
213 .iter()
214 .any(|h| h.contains("converted from image/bmp to image/png"))
215 );
216 Ok(())
217 }
218 ProcessImageResult::Omitted(reason) => {
219 Err(std::io::Error::other(format!("expected Ok, got Omitted({reason})")).into())
220 }
221 }
222 }
223
224 #[test]
225 fn process_image_keeps_supported_png() -> TestResult {
226 let png = solid_png(2, 2)?;
227 match process_image(&png, "image/png", false) {
228 ProcessImageResult::Ok { mime, .. } => {
229 assert_eq!(mime, "image/png");
230 Ok(())
231 }
232 ProcessImageResult::Omitted(reason) => {
233 Err(std::io::Error::other(format!("expected Ok, got Omitted({reason})")).into())
234 }
235 }
236 }
237}