1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ImageFormat {
7 Jpeg,
8 Png,
9 WebP,
11}
12
13const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
17
18impl ImageFormat {
19 pub fn from_magic_bytes(data: &[u8]) -> Option<Self> {
21 if data.len() < 4 {
22 return None;
23 }
24 if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
25 return Some(Self::Jpeg);
26 }
27 if data.len() >= 8 && data[..8] == PNG_SIGNATURE {
28 return Some(Self::Png);
29 }
30 if data.len() >= 12 && &data[..4] == b"RIFF" && &data[8..12] == b"WEBP" {
31 return Some(Self::WebP);
32 }
33 None
34 }
35
36 pub fn from_extension(filename: &str) -> Option<Self> {
38 let lower = filename.to_lowercase();
39 if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
40 Some(Self::Jpeg)
41 } else if lower.ends_with(".png") {
42 Some(Self::Png)
43 } else if lower.ends_with(".webp") {
44 Some(Self::WebP)
45 } else {
46 None
47 }
48 }
49
50 pub fn detect(data: &[u8], filename: &str) -> Option<Self> {
52 Self::from_magic_bytes(data).or_else(|| Self::from_extension(filename))
53 }
54
55 pub fn mime_type(&self) -> &'static str {
57 match self {
58 Self::Jpeg => "image/jpeg",
59 Self::Png => "image/png",
60 Self::WebP => "image/webp",
61 }
62 }
63
64 pub fn extension(&self) -> &'static str {
66 match self {
67 Self::Jpeg => "jpg",
68 Self::Png => "png",
69 Self::WebP => "webp",
70 }
71 }
72}
73
74#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
83 fn test_detect_jpeg_from_magic_bytes() {
84 let jpeg_data = include_bytes!("../../../../test-fixtures/images/small.jpg");
85 let format = ImageFormat::from_magic_bytes(jpeg_data);
86 assert_eq!(format, Some(ImageFormat::Jpeg));
87 }
88
89 #[test]
90 fn test_detect_png_from_magic_bytes() {
91 let png_data = include_bytes!("../../../../test-fixtures/images/small.png");
92 let format = ImageFormat::from_magic_bytes(png_data);
93 assert_eq!(format, Some(ImageFormat::Png));
94 }
95
96 #[test]
97 fn test_detect_webp_from_magic_bytes() {
98 let webp_data = include_bytes!("../../../../test-fixtures/images/small.webp");
99 let format = ImageFormat::from_magic_bytes(webp_data);
100 assert_eq!(format, Some(ImageFormat::WebP));
101 }
102
103 #[test]
104 fn test_magic_bytes_returns_none_for_unknown_data() {
105 let unknown_data = b"Hello, I am not an image!";
106 let format = ImageFormat::from_magic_bytes(unknown_data);
107 assert_eq!(format, None);
108 }
109
110 #[test]
111 fn test_magic_bytes_returns_none_for_too_short_data() {
112 let short_data = b"Hi";
113 let format = ImageFormat::from_magic_bytes(short_data);
114 assert_eq!(format, None);
115 }
116
117 #[test]
118 fn test_magic_bytes_returns_none_for_empty_data() {
119 let empty: &[u8] = b"";
120 let format = ImageFormat::from_magic_bytes(empty);
121 assert_eq!(format, None);
122 }
123
124 #[test]
127 fn test_detect_jpeg_from_extension_jpg() {
128 assert_eq!(
129 ImageFormat::from_extension("photo.jpg"),
130 Some(ImageFormat::Jpeg)
131 );
132 }
133
134 #[test]
135 fn test_detect_jpeg_from_extension_jpeg() {
136 assert_eq!(
137 ImageFormat::from_extension("photo.jpeg"),
138 Some(ImageFormat::Jpeg)
139 );
140 }
141
142 #[test]
143 fn test_detect_jpeg_case_insensitive() {
144 assert_eq!(
145 ImageFormat::from_extension("PHOTO.JPG"),
146 Some(ImageFormat::Jpeg)
147 );
148 assert_eq!(
149 ImageFormat::from_extension("Photo.Jpeg"),
150 Some(ImageFormat::Jpeg)
151 );
152 }
153
154 #[test]
155 fn test_detect_png_from_extension() {
156 assert_eq!(
157 ImageFormat::from_extension("screenshot.png"),
158 Some(ImageFormat::Png)
159 );
160 }
161
162 #[test]
163 fn test_detect_webp_from_extension() {
164 assert_eq!(
165 ImageFormat::from_extension("image.webp"),
166 Some(ImageFormat::WebP)
167 );
168 }
169
170 #[test]
171 fn test_extension_returns_none_for_unsupported() {
172 assert_eq!(ImageFormat::from_extension("image.bmp"), None);
173 assert_eq!(ImageFormat::from_extension("image.gif"), None);
174 assert_eq!(ImageFormat::from_extension("image.tiff"), None);
175 assert_eq!(ImageFormat::from_extension("document.pdf"), None);
176 }
177
178 #[test]
179 fn test_extension_returns_none_for_no_extension() {
180 assert_eq!(ImageFormat::from_extension("noextension"), None);
181 }
182
183 #[test]
186 fn test_detect_uses_magic_bytes_first() {
187 let jpeg_data = include_bytes!("../../../../test-fixtures/images/small.jpg");
188 let format = ImageFormat::detect(jpeg_data, "misleading.png");
189 assert_eq!(format, Some(ImageFormat::Jpeg));
190 }
191
192 #[test]
193 fn test_detect_falls_back_to_extension() {
194 let unknown_data = b"not a real image but trust the name";
195 let format = ImageFormat::detect(unknown_data, "photo.jpg");
196 assert_eq!(format, Some(ImageFormat::Jpeg));
197 }
198
199 #[test]
200 fn test_detect_returns_none_when_both_fail() {
201 let unknown_data = b"not a real image";
202 let format = ImageFormat::detect(unknown_data, "mystery_file");
203 assert_eq!(format, None);
204 }
205
206 #[test]
209 fn test_mime_types() {
210 assert_eq!(ImageFormat::Jpeg.mime_type(), "image/jpeg");
211 assert_eq!(ImageFormat::Png.mime_type(), "image/png");
212 assert_eq!(ImageFormat::WebP.mime_type(), "image/webp");
213 }
214
215 #[test]
216 fn test_extensions() {
217 assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
218 assert_eq!(ImageFormat::Png.extension(), "png");
219 assert_eq!(ImageFormat::WebP.extension(), "webp");
220 }
221
222 #[test]
225 fn test_magic_bytes_single_byte_returns_none() {
226 assert_eq!(ImageFormat::from_magic_bytes(&[0xFF]), None);
227 }
228
229 #[test]
230 fn test_magic_bytes_two_bytes_returns_none() {
231 assert_eq!(ImageFormat::from_magic_bytes(&[0xFF, 0xD8]), None);
232 }
233
234 #[test]
235 fn test_magic_bytes_three_bytes_returns_none() {
236 assert_eq!(ImageFormat::from_magic_bytes(&[0xFF, 0xD8, 0xFF]), None);
237 }
238
239 #[test]
240 fn test_magic_bytes_exactly_4_bytes_jpeg_detected() {
241 let data = [0xFF, 0xD8, 0xFF, 0xE0];
242 assert_eq!(
243 ImageFormat::from_magic_bytes(&data),
244 Some(ImageFormat::Jpeg)
245 );
246 }
247
248 #[test]
249 fn test_magic_bytes_jpeg_like_but_third_byte_not_ff() {
250 let data = [0xFF, 0xD8, 0x00, 0x00];
251 assert_eq!(ImageFormat::from_magic_bytes(&data), None);
252 }
253
254 #[test]
255 fn test_magic_bytes_jpeg_header_only_no_image_data() {
256 let data = [0xFF, 0xD8, 0xFF, 0xE1, 0x00, 0x00, 0x00, 0x00];
257 assert_eq!(
258 ImageFormat::from_magic_bytes(&data),
259 Some(ImageFormat::Jpeg)
260 );
261 }
262
263 #[test]
264 fn test_magic_bytes_7_bytes_partial_png_returns_none() {
265 let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A];
266 assert_eq!(ImageFormat::from_magic_bytes(&data), None);
267 }
268
269 #[test]
270 fn test_magic_bytes_exactly_8_bytes_png_detected() {
271 let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
272 assert_eq!(ImageFormat::from_magic_bytes(&data), Some(ImageFormat::Png));
273 }
274
275 #[test]
276 fn test_magic_bytes_png_with_wrong_final_byte() {
277 let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x00];
278 assert_eq!(ImageFormat::from_magic_bytes(&data), None);
279 }
280
281 #[test]
282 fn test_magic_bytes_11_bytes_partial_webp_returns_none() {
283 let data = [
284 b'R', b'I', b'F', b'F', 0x00, 0x00, 0x00, 0x00, b'W', b'E', b'B',
285 ];
286 assert_eq!(ImageFormat::from_magic_bytes(&data), None);
287 }
288
289 #[test]
290 fn test_magic_bytes_exactly_12_bytes_webp_detected() {
291 let data = [
292 b'R', b'I', b'F', b'F', 0x00, 0x00, 0x00, 0x00, b'W', b'E', b'B', b'P',
293 ];
294 assert_eq!(
295 ImageFormat::from_magic_bytes(&data),
296 Some(ImageFormat::WebP)
297 );
298 }
299
300 #[test]
301 fn test_magic_bytes_riff_but_not_webp() {
302 let data = [
303 b'R', b'I', b'F', b'F', 0x00, 0x00, 0x00, 0x00, b'A', b'V', b'I', b' ',
304 ];
305 assert_eq!(ImageFormat::from_magic_bytes(&data), None);
306 }
307
308 #[test]
309 fn test_detect_zero_bytes_with_jpg_extension_uses_extension() {
310 let format = ImageFormat::detect(b"", "empty.jpg");
311 assert_eq!(format, Some(ImageFormat::Jpeg));
312 }
313
314 #[test]
315 fn test_detect_zero_bytes_no_extension_returns_none() {
316 let format = ImageFormat::detect(b"", "unknown_file");
317 assert_eq!(format, None);
318 }
319
320 #[test]
321 fn test_detect_single_byte_with_png_extension_uses_extension() {
322 let format = ImageFormat::detect(&[0x42], "tiny.png");
323 assert_eq!(format, Some(ImageFormat::Png));
324 }
325
326 #[test]
327 fn test_detect_4_bytes_jpeg_ignores_wrong_extension() {
328 let data = [0xFF, 0xD8, 0xFF, 0xE0];
329 let format = ImageFormat::detect(&data, "lies.png");
330 assert_eq!(format, Some(ImageFormat::Jpeg));
331 }
332}