1use std::path::Path;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum Format {
6 Jpeg,
7 Png,
8 WebP,
9 Avif,
10 Jxl,
11 Qoi,
12}
13
14impl Format {
15 pub fn from_extension(path: &Path) -> Option<Self> {
17 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
18 match ext.as_str() {
19 "jpg" | "jpeg" => Some(Self::Jpeg),
20 "png" => Some(Self::Png),
21 "webp" => Some(Self::WebP),
22 "avif" => Some(Self::Avif),
23 "jxl" => Some(Self::Jxl),
24 "qoi" => Some(Self::Qoi),
25 _ => None,
26 }
27 }
28
29 pub fn from_magic_bytes(data: &[u8]) -> Option<Self> {
31 if data.len() >= 3 && data[..3] == [0xFF, 0xD8, 0xFF] {
32 return Some(Self::Jpeg);
33 }
34 if data.len() >= 4 && data[..4] == [0x89, 0x50, 0x4E, 0x47] {
35 return Some(Self::Png);
36 }
37 if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
38 return Some(Self::WebP);
39 }
40 if data.len() >= 12 && &data[4..8] == b"ftyp" {
41 let brand = &data[8..12];
42 if brand.starts_with(b"avif") || brand.starts_with(b"avis") {
43 return Some(Self::Avif);
44 }
45 }
46 if data.len() >= 2 && data[..2] == [0xFF, 0x0A] {
48 return Some(Self::Jxl);
49 }
50 if data.len() >= 8 && data[..4] == [0x00, 0x00, 0x00, 0x0C] && &data[4..8] == b"JXL " {
52 return Some(Self::Jxl);
53 }
54 if data.len() >= 4 && &data[..4] == b"qoif" {
55 return Some(Self::Qoi);
56 }
57 None
58 }
59
60 pub fn extension(&self) -> &'static str {
62 match self {
63 Self::Jpeg => "jpg",
64 Self::Png => "png",
65 Self::WebP => "webp",
66 Self::Avif => "avif",
67 Self::Jxl => "jxl",
68 Self::Qoi => "qoi",
69 }
70 }
71
72 pub fn can_encode(&self) -> bool {
77 !matches!(self, Self::Jxl)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::path::PathBuf;
85
86 #[test]
89 fn extension_jpg() {
90 assert_eq!(
91 Format::from_extension(Path::new("photo.jpg")),
92 Some(Format::Jpeg)
93 );
94 }
95
96 #[test]
97 fn extension_jpeg() {
98 assert_eq!(
99 Format::from_extension(Path::new("photo.jpeg")),
100 Some(Format::Jpeg)
101 );
102 }
103
104 #[test]
105 fn extension_jpg_uppercase() {
106 assert_eq!(
107 Format::from_extension(Path::new("photo.JPG")),
108 Some(Format::Jpeg)
109 );
110 }
111
112 #[test]
113 fn extension_jpeg_mixed_case() {
114 assert_eq!(
115 Format::from_extension(Path::new("photo.JpEg")),
116 Some(Format::Jpeg)
117 );
118 }
119
120 #[test]
121 fn extension_png() {
122 assert_eq!(
123 Format::from_extension(Path::new("image.png")),
124 Some(Format::Png)
125 );
126 }
127
128 #[test]
129 fn extension_png_uppercase() {
130 assert_eq!(
131 Format::from_extension(Path::new("image.PNG")),
132 Some(Format::Png)
133 );
134 }
135
136 #[test]
137 fn extension_webp() {
138 assert_eq!(
139 Format::from_extension(Path::new("image.webp")),
140 Some(Format::WebP)
141 );
142 }
143
144 #[test]
145 fn extension_avif() {
146 assert_eq!(
147 Format::from_extension(Path::new("image.avif")),
148 Some(Format::Avif)
149 );
150 }
151
152 #[test]
153 fn extension_jxl() {
154 assert_eq!(
155 Format::from_extension(Path::new("image.jxl")),
156 Some(Format::Jxl)
157 );
158 }
159
160 #[test]
161 fn extension_qoi() {
162 assert_eq!(
163 Format::from_extension(Path::new("image.qoi")),
164 Some(Format::Qoi)
165 );
166 }
167
168 #[test]
169 fn extension_unknown() {
170 assert_eq!(Format::from_extension(Path::new("file.bmp")), None);
171 }
172
173 #[test]
174 fn extension_none() {
175 assert_eq!(Format::from_extension(Path::new("noext")), None);
176 }
177
178 #[test]
179 fn extension_empty_path() {
180 assert_eq!(Format::from_extension(&PathBuf::new()), None);
181 }
182
183 #[test]
186 fn magic_jpeg() {
187 let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00];
188 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jpeg));
189 }
190
191 #[test]
192 fn magic_png() {
193 let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
194 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Png));
195 }
196
197 #[test]
198 fn magic_webp() {
199 let mut data = Vec::new();
200 data.extend_from_slice(b"RIFF");
201 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(b"WEBP");
203 assert_eq!(Format::from_magic_bytes(&data), Some(Format::WebP));
204 }
205
206 #[test]
207 fn magic_avif_avif_brand() {
208 let mut data = vec![0x00, 0x00, 0x00, 0x20]; data.extend_from_slice(b"ftyp");
210 data.extend_from_slice(b"avif");
211 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Avif));
212 }
213
214 #[test]
215 fn magic_avif_avis_brand() {
216 let mut data = vec![0x00, 0x00, 0x00, 0x20]; data.extend_from_slice(b"ftyp");
218 data.extend_from_slice(b"avis");
219 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Avif));
220 }
221
222 #[test]
223 fn magic_jxl_bare_codestream() {
224 let data = [0xFF, 0x0A, 0x00, 0x00];
225 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jxl));
226 }
227
228 #[test]
229 fn magic_jxl_container() {
230 let mut data = vec![0x00, 0x00, 0x00, 0x0C];
231 data.extend_from_slice(b"JXL ");
232 data.extend_from_slice(&[0x0D, 0x0A, 0x87, 0x0A]);
233 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Jxl));
234 }
235
236 #[test]
237 fn magic_qoi() {
238 let mut data = Vec::new();
239 data.extend_from_slice(b"qoif");
240 data.extend_from_slice(&[0x00; 10]);
241 assert_eq!(Format::from_magic_bytes(&data), Some(Format::Qoi));
242 }
243
244 #[test]
245 fn magic_unknown() {
246 let data = [0x00, 0x00, 0x00, 0x00];
247 assert_eq!(Format::from_magic_bytes(&data), None);
248 }
249
250 #[test]
251 fn magic_empty() {
252 assert_eq!(Format::from_magic_bytes(&[]), None);
253 }
254
255 #[test]
256 fn magic_too_short_for_jpeg() {
257 assert_eq!(Format::from_magic_bytes(&[0xFF, 0xD8]), None);
258 }
259
260 #[test]
263 fn extension_string() {
264 assert_eq!(Format::Jpeg.extension(), "jpg");
265 assert_eq!(Format::Png.extension(), "png");
266 assert_eq!(Format::WebP.extension(), "webp");
267 assert_eq!(Format::Avif.extension(), "avif");
268 assert_eq!(Format::Jxl.extension(), "jxl");
269 assert_eq!(Format::Qoi.extension(), "qoi");
270 }
271
272 #[test]
275 fn can_encode_jxl_is_false() {
276 assert!(!Format::Jxl.can_encode());
277 }
278
279 #[test]
280 fn can_encode_all_others_true() {
281 assert!(Format::Jpeg.can_encode());
282 assert!(Format::Png.can_encode());
283 assert!(Format::WebP.can_encode());
284 assert!(Format::Avif.can_encode());
285 assert!(Format::Qoi.can_encode());
286 }
287}