/src/image/src/io/format.rs
Line | Count | Source |
1 | | use std::ffi::OsStr; |
2 | | use std::path::Path; |
3 | | |
4 | | use crate::error::{ImageError, ImageFormatHint, ImageResult}; |
5 | | |
6 | | /// An enumeration of supported image formats. |
7 | | /// Not all formats support both encoding and decoding. |
8 | | #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] |
9 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
10 | | #[non_exhaustive] |
11 | | pub enum ImageFormat { |
12 | | /// An Image in PNG Format |
13 | | Png, |
14 | | |
15 | | /// An Image in JPEG Format |
16 | | Jpeg, |
17 | | |
18 | | /// An Image in GIF Format |
19 | | Gif, |
20 | | |
21 | | /// An Image in WEBP Format |
22 | | WebP, |
23 | | |
24 | | /// An Image in general PNM Format |
25 | | Pnm, |
26 | | |
27 | | /// An Image in TIFF Format |
28 | | Tiff, |
29 | | |
30 | | /// An Image in TGA Format |
31 | | Tga, |
32 | | |
33 | | /// An Image in BMP Format |
34 | | Bmp, |
35 | | |
36 | | /// An Image in ICO Format |
37 | | Ico, |
38 | | |
39 | | /// An Image in Radiance HDR Format |
40 | | Hdr, |
41 | | |
42 | | /// An Image in OpenEXR Format |
43 | | OpenExr, |
44 | | |
45 | | /// An Image in farbfeld Format |
46 | | Farbfeld, |
47 | | |
48 | | /// An Image in AVIF Format |
49 | | Avif, |
50 | | |
51 | | /// An Image in QOI Format |
52 | | Qoi, |
53 | | } |
54 | | |
55 | | impl ImageFormat { |
56 | | /// Return the image format specified by a path's file extension. |
57 | | /// |
58 | | /// # Example |
59 | | /// |
60 | | /// ``` |
61 | | /// use image::ImageFormat; |
62 | | /// |
63 | | /// let format = ImageFormat::from_extension("jpg"); |
64 | | /// assert_eq!(format, Some(ImageFormat::Jpeg)); |
65 | | /// ``` |
66 | | #[inline] |
67 | 0 | pub fn from_extension<S>(ext: S) -> Option<Self> |
68 | 0 | where |
69 | 0 | S: AsRef<OsStr>, |
70 | | { |
71 | | // thin wrapper function to strip generics |
72 | 0 | fn inner(ext: &OsStr) -> Option<ImageFormat> { |
73 | 0 | let ext = ext.to_str()?.to_ascii_lowercase(); |
74 | | // NOTE: when updating this, also update extensions_str() |
75 | 0 | Some(match ext.as_str() { |
76 | 0 | "avif" => ImageFormat::Avif, |
77 | 0 | "jpg" | "jpeg" | "jfif" => ImageFormat::Jpeg, |
78 | 0 | "png" | "apng" => ImageFormat::Png, |
79 | 0 | "gif" => ImageFormat::Gif, |
80 | 0 | "webp" => ImageFormat::WebP, |
81 | 0 | "tif" | "tiff" => ImageFormat::Tiff, |
82 | 0 | "tga" => ImageFormat::Tga, |
83 | 0 | "bmp" => ImageFormat::Bmp, |
84 | 0 | "ico" => ImageFormat::Ico, |
85 | 0 | "hdr" => ImageFormat::Hdr, |
86 | 0 | "exr" => ImageFormat::OpenExr, |
87 | 0 | "pbm" | "pam" | "ppm" | "pgm" | "pnm" => ImageFormat::Pnm, |
88 | 0 | "ff" => ImageFormat::Farbfeld, |
89 | 0 | "qoi" => ImageFormat::Qoi, |
90 | 0 | _ => return None, |
91 | | }) |
92 | 0 | } |
93 | | |
94 | 0 | inner(ext.as_ref()) |
95 | 0 | } Unexecuted instantiation: <image::io::format::ImageFormat>::from_extension::<&std::ffi::os_str::OsStr> Unexecuted instantiation: <image::io::format::ImageFormat>::from_extension::<&std::ffi::os_str::OsString> |
96 | | |
97 | | /// Return the image format specified by the path's file extension. |
98 | | /// |
99 | | /// # Example |
100 | | /// |
101 | | /// ``` |
102 | | /// use image::ImageFormat; |
103 | | /// |
104 | | /// let format = ImageFormat::from_path("images/ferris.png")?; |
105 | | /// assert_eq!(format, ImageFormat::Png); |
106 | | /// |
107 | | /// # Ok::<(), image::error::ImageError>(()) |
108 | | /// ``` |
109 | | #[inline] |
110 | 0 | pub fn from_path<P>(path: P) -> ImageResult<Self> |
111 | 0 | where |
112 | 0 | P: AsRef<Path>, |
113 | | { |
114 | | // thin wrapper function to strip generics |
115 | 0 | fn inner(path: &Path) -> ImageResult<ImageFormat> { |
116 | 0 | let exact_ext = path.extension(); |
117 | 0 | exact_ext |
118 | 0 | .and_then(ImageFormat::from_extension) |
119 | 0 | .ok_or_else(|| { |
120 | 0 | let format_hint = match exact_ext { |
121 | 0 | None => ImageFormatHint::Unknown, |
122 | 0 | Some(os) => ImageFormatHint::PathExtension(os.into()), |
123 | | }; |
124 | 0 | ImageError::Unsupported(format_hint.into()) |
125 | 0 | }) |
126 | 0 | } |
127 | | |
128 | 0 | inner(path.as_ref()) |
129 | 0 | } |
130 | | |
131 | | /// Return the image format specified by a MIME type. |
132 | | /// |
133 | | /// The type and subtype are matched case-insensitively, as required by the |
134 | | /// MIME specification. MIME type parameters (such as `; charset=utf-8`) are |
135 | | /// not supported and must be stripped by the caller beforehand. |
136 | | /// |
137 | | /// # Example |
138 | | /// |
139 | | /// ``` |
140 | | /// use image::ImageFormat; |
141 | | /// |
142 | | /// let format = ImageFormat::from_mime_type("image/png").unwrap(); |
143 | | /// assert_eq!(format, ImageFormat::Png); |
144 | | /// ``` |
145 | 0 | pub fn from_mime_type<M>(mime_type: M) -> Option<Self> |
146 | 0 | where |
147 | 0 | M: AsRef<str>, |
148 | | { |
149 | | // NOTE: MIME type and subtype are case-insensitive, so normalize before matching. |
150 | 0 | let mime_type = mime_type.as_ref().to_ascii_lowercase(); |
151 | 0 | match mime_type.as_str() { |
152 | 0 | "image/avif" => Some(ImageFormat::Avif), |
153 | 0 | "image/jpeg" => Some(ImageFormat::Jpeg), |
154 | 0 | "image/png" => Some(ImageFormat::Png), |
155 | 0 | "image/gif" => Some(ImageFormat::Gif), |
156 | 0 | "image/webp" => Some(ImageFormat::WebP), |
157 | 0 | "image/tiff" => Some(ImageFormat::Tiff), |
158 | 0 | "image/x-targa" | "image/x-tga" => Some(ImageFormat::Tga), |
159 | 0 | "image/bmp" => Some(ImageFormat::Bmp), |
160 | 0 | "image/x-icon" | "image/vnd.microsoft.icon" => Some(ImageFormat::Ico), |
161 | 0 | "image/vnd.radiance" => Some(ImageFormat::Hdr), |
162 | 0 | "image/x-exr" => Some(ImageFormat::OpenExr), |
163 | 0 | "image/x-portable-bitmap" |
164 | 0 | | "image/x-portable-graymap" |
165 | 0 | | "image/x-portable-pixmap" |
166 | 0 | | "image/x-portable-anymap" => Some(ImageFormat::Pnm), |
167 | | // Qoi's MIME type is being worked on. |
168 | | // See: https://github.com/phoboslab/qoi/issues/167 |
169 | 0 | "image/x-qoi" => Some(ImageFormat::Qoi), |
170 | 0 | _ => None, |
171 | | } |
172 | 0 | } |
173 | | |
174 | | /// Return the MIME type for this image format or "application/octet-stream" if no MIME type |
175 | | /// exists for the format. |
176 | | /// |
177 | | /// Some notes on a few of the MIME types: |
178 | | /// |
179 | | /// - The portable anymap format has a separate MIME type for the pixmap, graymap and bitmap |
180 | | /// formats, but this method returns the general "image/x-portable-anymap" MIME type. |
181 | | /// - The Targa format has two common MIME types, "image/x-targa" and "image/x-tga"; this |
182 | | /// method returns "image/x-targa" for that format. |
183 | | /// - The QOI MIME type is still a work in progress. This method returns "image/x-qoi" for |
184 | | /// that format. |
185 | | /// |
186 | | /// # Example |
187 | | /// |
188 | | /// ``` |
189 | | /// use image::ImageFormat; |
190 | | /// |
191 | | /// let mime_type = ImageFormat::Png.to_mime_type(); |
192 | | /// assert_eq!(mime_type, "image/png"); |
193 | | /// ``` |
194 | | #[must_use] |
195 | 0 | pub fn to_mime_type(&self) -> &'static str { |
196 | 0 | match self { |
197 | 0 | ImageFormat::Avif => "image/avif", |
198 | 0 | ImageFormat::Jpeg => "image/jpeg", |
199 | 0 | ImageFormat::Png => "image/png", |
200 | 0 | ImageFormat::Gif => "image/gif", |
201 | 0 | ImageFormat::WebP => "image/webp", |
202 | 0 | ImageFormat::Tiff => "image/tiff", |
203 | | // the targa MIME type has two options, but this one seems to be used more |
204 | 0 | ImageFormat::Tga => "image/x-targa", |
205 | 0 | ImageFormat::Bmp => "image/bmp", |
206 | 0 | ImageFormat::Ico => "image/x-icon", |
207 | 0 | ImageFormat::Hdr => "image/vnd.radiance", |
208 | 0 | ImageFormat::OpenExr => "image/x-exr", |
209 | | // return the most general MIME type |
210 | 0 | ImageFormat::Pnm => "image/x-portable-anymap", |
211 | | // Qoi's MIME type is being worked on. |
212 | | // See: https://github.com/phoboslab/qoi/issues/167 |
213 | 0 | ImageFormat::Qoi => "image/x-qoi", |
214 | | // farbfeld's MIME type taken from https://www.wikidata.org/wiki/Q28206109 |
215 | 0 | ImageFormat::Farbfeld => "application/octet-stream", |
216 | | } |
217 | 0 | } |
218 | | |
219 | | /// Return a list of applicable extensions for this format. |
220 | | /// |
221 | | /// All currently recognized image formats specify at least on extension but for future |
222 | | /// compatibility you should not rely on this fact. The list may be empty if the format has no |
223 | | /// recognized file representation, for example in case it is used as a purely transient memory |
224 | | /// format. |
225 | | /// |
226 | | /// The method name `extensions` remains reserved for introducing another method in the future |
227 | | /// that yields a slice of `OsStr` which is blocked by several features of const evaluation. |
228 | | #[must_use] |
229 | 0 | pub fn extensions_str(self) -> &'static [&'static str] { |
230 | | // NOTE: when updating this, also update from_extension() |
231 | 0 | match self { |
232 | 0 | ImageFormat::Png => &["png"], |
233 | 0 | ImageFormat::Jpeg => &["jpg", "jpeg"], |
234 | 0 | ImageFormat::Gif => &["gif"], |
235 | 0 | ImageFormat::WebP => &["webp"], |
236 | 0 | ImageFormat::Pnm => &["pbm", "pam", "ppm", "pgm", "pnm"], |
237 | 0 | ImageFormat::Tiff => &["tiff", "tif"], |
238 | 0 | ImageFormat::Tga => &["tga"], |
239 | 0 | ImageFormat::Bmp => &["bmp"], |
240 | 0 | ImageFormat::Ico => &["ico"], |
241 | 0 | ImageFormat::Hdr => &["hdr"], |
242 | 0 | ImageFormat::OpenExr => &["exr"], |
243 | 0 | ImageFormat::Farbfeld => &["ff"], |
244 | | // According to: https://aomediacodec.github.io/av1-avif/#mime-registration |
245 | 0 | ImageFormat::Avif => &["avif"], |
246 | 0 | ImageFormat::Qoi => &["qoi"], |
247 | | } |
248 | 0 | } |
249 | | |
250 | | /// Returns whether the feature of the `image` crate for reading this |
251 | | /// `ImageFormat` is enabled. |
252 | | /// |
253 | | /// See [the list of format features](crate::codecs#supported-formats). |
254 | | #[inline] |
255 | | #[must_use] |
256 | 0 | pub fn reading_enabled(&self) -> bool { |
257 | 0 | match self { |
258 | 0 | ImageFormat::Png => cfg!(feature = "png"), |
259 | 0 | ImageFormat::Gif => cfg!(feature = "gif"), |
260 | 0 | ImageFormat::Jpeg => cfg!(feature = "jpeg"), |
261 | 0 | ImageFormat::WebP => cfg!(feature = "webp"), |
262 | 0 | ImageFormat::Tiff => cfg!(feature = "tiff"), |
263 | 0 | ImageFormat::Tga => cfg!(feature = "tga"), |
264 | 0 | ImageFormat::Bmp => cfg!(feature = "bmp"), |
265 | 0 | ImageFormat::Ico => cfg!(feature = "ico"), |
266 | 0 | ImageFormat::Hdr => cfg!(feature = "hdr"), |
267 | 0 | ImageFormat::OpenExr => cfg!(feature = "exr"), |
268 | 0 | ImageFormat::Pnm => cfg!(feature = "pnm"), |
269 | 0 | ImageFormat::Farbfeld => cfg!(feature = "ff"), |
270 | 0 | ImageFormat::Avif => cfg!(feature = "avif-native"), |
271 | 0 | ImageFormat::Qoi => cfg!(feature = "qoi"), |
272 | | } |
273 | 0 | } |
274 | | |
275 | | /// Returns whether the feature of the `image` crate for writing this |
276 | | /// `ImageFormat` is enabled. |
277 | | /// |
278 | | /// See [the list of format features](crate::codecs#supported-formats). |
279 | | #[inline] |
280 | | #[must_use] |
281 | 0 | pub fn writing_enabled(&self) -> bool { |
282 | 0 | match self { |
283 | 0 | ImageFormat::Gif => cfg!(feature = "gif"), |
284 | 0 | ImageFormat::Ico => cfg!(feature = "ico"), |
285 | 0 | ImageFormat::Jpeg => cfg!(feature = "jpeg"), |
286 | 0 | ImageFormat::Png => cfg!(feature = "png"), |
287 | 0 | ImageFormat::Bmp => cfg!(feature = "bmp"), |
288 | 0 | ImageFormat::Tiff => cfg!(feature = "tiff"), |
289 | 0 | ImageFormat::Tga => cfg!(feature = "tga"), |
290 | 0 | ImageFormat::Pnm => cfg!(feature = "pnm"), |
291 | 0 | ImageFormat::Farbfeld => cfg!(feature = "ff"), |
292 | 0 | ImageFormat::Avif => cfg!(feature = "avif"), |
293 | 0 | ImageFormat::WebP => cfg!(feature = "webp"), |
294 | 0 | ImageFormat::OpenExr => cfg!(feature = "exr"), |
295 | 0 | ImageFormat::Qoi => cfg!(feature = "qoi"), |
296 | 0 | ImageFormat::Hdr => cfg!(feature = "hdr"), |
297 | | } |
298 | 0 | } |
299 | | |
300 | | /// Return all `ImageFormat`s |
301 | 0 | pub fn all() -> impl Iterator<Item = ImageFormat> { |
302 | 0 | [ |
303 | 0 | ImageFormat::Gif, |
304 | 0 | ImageFormat::Ico, |
305 | 0 | ImageFormat::Jpeg, |
306 | 0 | ImageFormat::Png, |
307 | 0 | ImageFormat::Bmp, |
308 | 0 | ImageFormat::Tiff, |
309 | 0 | ImageFormat::Tga, |
310 | 0 | ImageFormat::Pnm, |
311 | 0 | ImageFormat::Farbfeld, |
312 | 0 | ImageFormat::Avif, |
313 | 0 | ImageFormat::WebP, |
314 | 0 | ImageFormat::OpenExr, |
315 | 0 | ImageFormat::Qoi, |
316 | 0 | ImageFormat::Hdr, |
317 | 0 | ] |
318 | 0 | .iter() |
319 | 0 | .copied() |
320 | 0 | } |
321 | | } |
322 | | |
323 | | #[cfg(test)] |
324 | | mod tests { |
325 | | use std::collections::HashSet; |
326 | | use std::path::Path; |
327 | | |
328 | | use super::{ImageFormat, ImageResult}; |
329 | | |
330 | | #[test] |
331 | | fn test_image_format_from_path() { |
332 | | fn from_path(s: &str) -> ImageResult<ImageFormat> { |
333 | | ImageFormat::from_path(Path::new(s)) |
334 | | } |
335 | | assert_eq!(from_path("./a.jpg").unwrap(), ImageFormat::Jpeg); |
336 | | assert_eq!(from_path("./a.jpeg").unwrap(), ImageFormat::Jpeg); |
337 | | assert_eq!(from_path("./a.JPEG").unwrap(), ImageFormat::Jpeg); |
338 | | assert_eq!(from_path("./a.pNg").unwrap(), ImageFormat::Png); |
339 | | assert_eq!(from_path("./a.gif").unwrap(), ImageFormat::Gif); |
340 | | assert_eq!(from_path("./a.webp").unwrap(), ImageFormat::WebP); |
341 | | assert_eq!(from_path("./a.tiFF").unwrap(), ImageFormat::Tiff); |
342 | | assert_eq!(from_path("./a.tif").unwrap(), ImageFormat::Tiff); |
343 | | assert_eq!(from_path("./a.tga").unwrap(), ImageFormat::Tga); |
344 | | assert_eq!(from_path("./a.bmp").unwrap(), ImageFormat::Bmp); |
345 | | assert_eq!(from_path("./a.Ico").unwrap(), ImageFormat::Ico); |
346 | | assert_eq!(from_path("./a.hdr").unwrap(), ImageFormat::Hdr); |
347 | | assert_eq!(from_path("./a.exr").unwrap(), ImageFormat::OpenExr); |
348 | | assert_eq!(from_path("./a.pbm").unwrap(), ImageFormat::Pnm); |
349 | | assert_eq!(from_path("./a.pAM").unwrap(), ImageFormat::Pnm); |
350 | | assert_eq!(from_path("./a.Ppm").unwrap(), ImageFormat::Pnm); |
351 | | assert_eq!(from_path("./a.pgm").unwrap(), ImageFormat::Pnm); |
352 | | assert_eq!(from_path("./a.AViF").unwrap(), ImageFormat::Avif); |
353 | | assert!(from_path("./a.txt").is_err()); |
354 | | assert!(from_path("./a").is_err()); |
355 | | } |
356 | | |
357 | | #[test] |
358 | | fn image_formats_are_recognized() { |
359 | | use ImageFormat::*; |
360 | | const ALL_FORMATS: &[ImageFormat] = &[ |
361 | | Avif, Png, Jpeg, Gif, WebP, Pnm, Tiff, Tga, Bmp, Ico, Hdr, Farbfeld, OpenExr, |
362 | | ]; |
363 | | for &format in ALL_FORMATS { |
364 | | let mut file = Path::new("file.nothing").to_owned(); |
365 | | for ext in format.extensions_str() { |
366 | | assert!(file.set_extension(ext)); |
367 | | match ImageFormat::from_path(&file) { |
368 | | Err(_) => panic!("Path {} not recognized as {:?}", file.display(), format), |
369 | | Ok(result) => assert_eq!(format, result), |
370 | | } |
371 | | } |
372 | | } |
373 | | } |
374 | | |
375 | | #[test] |
376 | | fn from_mime_type_is_case_insensitive() { |
377 | | // The type and subtype of a MIME type are case-insensitive per RFC 2045, |
378 | | // so `from_mime_type` must accept any casing (as seen in real HTTP headers). |
379 | | assert_eq!( |
380 | | ImageFormat::from_mime_type("image/png"), |
381 | | Some(ImageFormat::Png) |
382 | | ); |
383 | | assert_eq!( |
384 | | ImageFormat::from_mime_type("IMAGE/PNG"), |
385 | | Some(ImageFormat::Png) |
386 | | ); |
387 | | assert_eq!( |
388 | | ImageFormat::from_mime_type("Image/Png"), |
389 | | Some(ImageFormat::Png) |
390 | | ); |
391 | | assert_eq!( |
392 | | ImageFormat::from_mime_type("IMAGE/X-TARGA"), |
393 | | Some(ImageFormat::Tga) |
394 | | ); |
395 | | // Unknown MIME types still return `None`. |
396 | | assert_eq!(ImageFormat::from_mime_type("image/jheic"), None); |
397 | | // Parameters are not supported and must be stripped by the caller. |
398 | | assert_eq!( |
399 | | ImageFormat::from_mime_type("image/png; charset=utf-8"), |
400 | | None |
401 | | ); |
402 | | } |
403 | | |
404 | | #[test] |
405 | | fn all() { |
406 | | let all_formats: HashSet<ImageFormat> = ImageFormat::all().collect(); |
407 | | assert!(all_formats.contains(&ImageFormat::Avif)); |
408 | | assert!(all_formats.contains(&ImageFormat::Gif)); |
409 | | assert!(all_formats.contains(&ImageFormat::Bmp)); |
410 | | assert!(all_formats.contains(&ImageFormat::Farbfeld)); |
411 | | assert!(all_formats.contains(&ImageFormat::Jpeg)); |
412 | | } |
413 | | |
414 | | #[test] |
415 | | fn reading_enabled() { |
416 | | assert_eq!(cfg!(feature = "jpeg"), ImageFormat::Jpeg.reading_enabled()); |
417 | | assert_eq!( |
418 | | cfg!(feature = "ff"), |
419 | | ImageFormat::Farbfeld.reading_enabled() |
420 | | ); |
421 | | } |
422 | | |
423 | | #[test] |
424 | | fn writing_enabled() { |
425 | | assert_eq!(cfg!(feature = "jpeg"), ImageFormat::Jpeg.writing_enabled()); |
426 | | assert_eq!( |
427 | | cfg!(feature = "ff"), |
428 | | ImageFormat::Farbfeld.writing_enabled() |
429 | | ); |
430 | | } |
431 | | } |