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