Skip to main content

ai_image/io/
format.rs

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