1use serde::Serialize;
2use std::path::Path;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
6#[serde(rename_all = "lowercase")]
7pub enum ImageFormat {
8 Jpeg,
9 Png,
10 WebP,
11 Avif,
12 Tiff,
13 Gif,
14 Bmp,
15 Qoi,
16 Jxl,
17 Svg,
18 Pdf,
19 Heic,
20}
21
22impl ImageFormat {
23 pub fn from_bytes(data: &[u8]) -> Option<Self> {
25 if data.len() < 4 {
26 return None;
27 }
28
29 if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
31 return Some(Self::Jpeg);
32 }
33 if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
35 return Some(Self::Png);
36 }
37 if data.starts_with(b"GIF8") {
39 return Some(Self::Gif);
40 }
41 if data.starts_with(b"BM") {
43 return Some(Self::Bmp);
44 }
45 if data.starts_with(&[0x49, 0x49, 0x2A, 0x00])
47 || data.starts_with(&[0x4D, 0x4D, 0x00, 0x2A])
48 {
49 return Some(Self::Tiff);
50 }
51 if data.len() >= 12 && data.starts_with(b"RIFF") && &data[8..12] == b"WEBP" {
53 return Some(Self::WebP);
54 }
55 if data.starts_with(b"qoif") {
57 return Some(Self::Qoi);
58 }
59 if data.len() >= 12 && &data[4..8] == b"ftyp" {
61 let brand = &data[8..12];
62 if brand == b"avif" || brand == b"avis" {
63 return Some(Self::Avif);
64 }
65 if brand == b"heic" || brand == b"heix" || brand == b"hevc" || brand == b"hevx" {
66 return Some(Self::Heic);
67 }
68 if brand == b"mif1" {
70 let box_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
72 let box_end = box_size.min(data.len());
73 let mut offset = 16;
75 while offset + 4 <= box_end {
76 let compat = &data[offset..offset + 4];
77 if compat == b"avif" || compat == b"avis" {
78 return Some(Self::Avif);
79 }
80 if compat == b"heic"
81 || compat == b"heix"
82 || compat == b"hevc"
83 || compat == b"hevx"
84 {
85 return Some(Self::Heic);
86 }
87 offset += 4;
88 }
89 return Some(Self::Avif);
91 }
92 }
93 if data.starts_with(&[0xFF, 0x0A]) {
95 return Some(Self::Jxl);
96 }
97 if data.len() >= 8 && data.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20]) {
98 return Some(Self::Jxl);
99 }
100 if data.starts_with(b"<") || data.starts_with(b"\xEF\xBB\xBF<") {
102 let text = std::str::from_utf8(&data[..data.len().min(1024)]).unwrap_or("");
103 if text.contains("<svg") {
104 return Some(Self::Svg);
105 }
106 }
107 if data.starts_with(b"%PDF") {
109 return Some(Self::Pdf);
110 }
111
112 None
113 }
114
115 pub fn from_extension(ext: &str) -> Option<Self> {
117 match ext.to_ascii_lowercase().as_str() {
118 "jpg" | "jpeg" => Some(Self::Jpeg),
119 "png" => Some(Self::Png),
120 "webp" => Some(Self::WebP),
121 "avif" => Some(Self::Avif),
122 "tif" | "tiff" => Some(Self::Tiff),
123 "gif" => Some(Self::Gif),
124 "bmp" => Some(Self::Bmp),
125 "qoi" => Some(Self::Qoi),
126 "jxl" => Some(Self::Jxl),
127 "svg" => Some(Self::Svg),
128 "pdf" => Some(Self::Pdf),
129 "heic" | "heif" => Some(Self::Heic),
130 _ => None,
131 }
132 }
133
134 pub fn from_path(path: &Path) -> Option<Self> {
136 if let Ok(data) = std::fs::read(path) {
138 if let Some(fmt) = Self::from_bytes(&data) {
139 return Some(fmt);
140 }
141 }
142 path.extension()
144 .and_then(|e| e.to_str())
145 .and_then(Self::from_extension)
146 }
147
148 pub fn from_path_extension(path: &Path) -> Option<Self> {
150 path.extension()
151 .and_then(|e| e.to_str())
152 .and_then(Self::from_extension)
153 }
154
155 pub fn extension(&self) -> &'static str {
157 match self {
158 Self::Jpeg => "jpg",
159 Self::Png => "png",
160 Self::WebP => "webp",
161 Self::Avif => "avif",
162 Self::Tiff => "tiff",
163 Self::Gif => "gif",
164 Self::Bmp => "bmp",
165 Self::Qoi => "qoi",
166 Self::Jxl => "jxl",
167 Self::Svg => "svg",
168 Self::Pdf => "pdf",
169 Self::Heic => "heic",
170 }
171 }
172
173 pub fn mime_type(&self) -> &'static str {
175 match self {
176 Self::Jpeg => "image/jpeg",
177 Self::Png => "image/png",
178 Self::WebP => "image/webp",
179 Self::Avif => "image/avif",
180 Self::Tiff => "image/tiff",
181 Self::Gif => "image/gif",
182 Self::Bmp => "image/bmp",
183 Self::Qoi => "image/qoi",
184 Self::Jxl => "image/jxl",
185 Self::Svg => "image/svg+xml",
186 Self::Pdf => "application/pdf",
187 Self::Heic => "image/heic",
188 }
189 }
190
191 pub fn to_image_format(&self) -> Option<image::ImageFormat> {
193 match self {
194 Self::Jpeg => Some(image::ImageFormat::Jpeg),
195 Self::Png => Some(image::ImageFormat::Png),
196 Self::WebP => Some(image::ImageFormat::WebP),
197 Self::Avif => Some(image::ImageFormat::Avif),
198 Self::Tiff => Some(image::ImageFormat::Tiff),
199 Self::Gif => Some(image::ImageFormat::Gif),
200 Self::Bmp => Some(image::ImageFormat::Bmp),
201 Self::Qoi => Some(image::ImageFormat::Qoi),
202 Self::Heic => None,
203 _ => None,
204 }
205 }
206
207 pub fn all() -> &'static [Self] {
209 &[
210 Self::Jpeg,
211 Self::Png,
212 Self::WebP,
213 Self::Avif,
214 Self::Tiff,
215 Self::Gif,
216 Self::Bmp,
217 Self::Qoi,
218 Self::Jxl,
219 Self::Svg,
220 Self::Pdf,
221 Self::Heic,
222 ]
223 }
224
225 pub fn can_encode(&self) -> bool {
227 match self {
228 Self::Jpeg
229 | Self::Png
230 | Self::WebP
231 | Self::Bmp
232 | Self::Gif
233 | Self::Tiff
234 | Self::Qoi => true,
235 Self::Avif => cfg!(feature = "avif"),
236 Self::Jxl | Self::Svg | Self::Pdf | Self::Heic => false,
237 }
238 }
239
240 pub fn can_decode(&self) -> bool {
242 match self {
243 Self::Jpeg
244 | Self::Png
245 | Self::WebP
246 | Self::Bmp
247 | Self::Gif
248 | Self::Tiff
249 | Self::Qoi
250 | Self::Avif => true,
251 Self::Jxl => cfg!(feature = "jxl"),
252 Self::Svg => cfg!(feature = "svg"),
253 Self::Pdf => cfg!(feature = "pdf"),
254 Self::Heic => cfg!(all(feature = "heic", target_vendor = "apple")),
255 }
256 }
257}
258
259impl std::fmt::Display for ImageFormat {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Self::Jpeg => write!(f, "JPEG"),
263 Self::Png => write!(f, "PNG"),
264 Self::WebP => write!(f, "WebP"),
265 Self::Avif => write!(f, "AVIF"),
266 Self::Tiff => write!(f, "TIFF"),
267 Self::Gif => write!(f, "GIF"),
268 Self::Bmp => write!(f, "BMP"),
269 Self::Qoi => write!(f, "QOI"),
270 Self::Jxl => write!(f, "JPEG XL"),
271 Self::Svg => write!(f, "SVG"),
272 Self::Pdf => write!(f, "PDF"),
273 Self::Heic => write!(f, "HEIC"),
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn detect_jpeg_magic() {
284 let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
285 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Jpeg));
286 }
287
288 #[test]
289 fn detect_png_magic() {
290 let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
291 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Png));
292 }
293
294 #[test]
295 fn detect_webp_magic() {
296 let mut data = vec![0u8; 12];
297 data[..4].copy_from_slice(b"RIFF");
298 data[8..12].copy_from_slice(b"WEBP");
299 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::WebP));
300 }
301
302 #[test]
303 fn detect_gif_magic() {
304 assert_eq!(
305 ImageFormat::from_bytes(b"GIF89a\x01\x00"),
306 Some(ImageFormat::Gif)
307 );
308 }
309
310 #[test]
311 fn detect_bmp_magic() {
312 assert_eq!(
313 ImageFormat::from_bytes(b"BM\x00\x00\x00\x00"),
314 Some(ImageFormat::Bmp)
315 );
316 }
317
318 #[test]
319 fn detect_heic_magic() {
320 let mut data = vec![0u8; 12];
322 data[4..8].copy_from_slice(b"ftyp");
323 data[8..12].copy_from_slice(b"heic");
324 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Heic));
325
326 data[8..12].copy_from_slice(b"heix");
328 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Heic));
329
330 data[8..12].copy_from_slice(b"hevc");
332 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Heic));
333
334 data[8..12].copy_from_slice(b"hevx");
336 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Heic));
337 }
338
339 #[test]
340 fn detect_heic_from_extension() {
341 assert_eq!(ImageFormat::from_extension("heic"), Some(ImageFormat::Heic));
342 assert_eq!(ImageFormat::from_extension("heif"), Some(ImageFormat::Heic));
343 assert_eq!(ImageFormat::from_extension("HEIC"), Some(ImageFormat::Heic));
344 }
345
346 #[test]
347 fn detect_from_extension() {
348 assert_eq!(ImageFormat::from_extension("jpg"), Some(ImageFormat::Jpeg));
349 assert_eq!(ImageFormat::from_extension("JPEG"), Some(ImageFormat::Jpeg));
350 assert_eq!(ImageFormat::from_extension("png"), Some(ImageFormat::Png));
351 assert_eq!(ImageFormat::from_extension("webp"), Some(ImageFormat::WebP));
352 assert_eq!(ImageFormat::from_extension("xyz"), None);
353 }
354
355 #[test]
356 fn detect_pdf_magic() {
357 assert_eq!(
358 ImageFormat::from_bytes(b"%PDF-1.4 some content"),
359 Some(ImageFormat::Pdf)
360 );
361 }
362
363 #[test]
364 fn detect_pdf_extension() {
365 assert_eq!(ImageFormat::from_extension("pdf"), Some(ImageFormat::Pdf));
366 assert_eq!(ImageFormat::from_extension("PDF"), Some(ImageFormat::Pdf));
367 }
368
369 #[test]
370 fn pdf_can_encode_false() {
371 assert!(!ImageFormat::Pdf.can_encode());
372 }
373
374 #[test]
375 fn pdf_can_decode_depends_on_feature() {
376 let can_decode = ImageFormat::Pdf.can_decode();
379 if cfg!(feature = "pdf") {
380 assert!(can_decode);
381 } else {
382 assert!(!can_decode);
383 }
384 }
385
386 #[test]
387 fn pdf_format_properties() {
388 assert_eq!(ImageFormat::Pdf.extension(), "pdf");
389 assert_eq!(ImageFormat::Pdf.mime_type(), "application/pdf");
390 assert_eq!(ImageFormat::Pdf.to_image_format(), None);
391 assert_eq!(ImageFormat::Pdf.to_string(), "PDF");
392 }
393
394 #[test]
395 fn detect_mif1_with_avif_compat() {
396 let mut data = vec![0u8; 24];
398 data[0..4].copy_from_slice(&24u32.to_be_bytes());
400 data[4..8].copy_from_slice(b"ftyp");
401 data[8..12].copy_from_slice(b"mif1");
402 data[16..20].copy_from_slice(b"avif");
404 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Avif));
405 }
406
407 #[test]
408 fn detect_mif1_with_heic_compat() {
409 let mut data = vec![0u8; 24];
411 data[0..4].copy_from_slice(&24u32.to_be_bytes());
412 data[4..8].copy_from_slice(b"ftyp");
413 data[8..12].copy_from_slice(b"mif1");
414 data[16..20].copy_from_slice(b"heic");
415 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Heic));
416 }
417
418 #[test]
419 fn detect_mif1_default_avif() {
420 let mut data = vec![0u8; 20];
422 data[0..4].copy_from_slice(&20u32.to_be_bytes());
423 data[4..8].copy_from_slice(b"ftyp");
424 data[8..12].copy_from_slice(b"mif1");
425 data[16..20].copy_from_slice(b"miaf");
426 assert_eq!(ImageFormat::from_bytes(&data), Some(ImageFormat::Avif));
427 }
428
429 #[test]
430 fn unknown_bytes_returns_none() {
431 assert_eq!(ImageFormat::from_bytes(&[0x00, 0x01, 0x02, 0x03]), None);
432 }
433}