Coverage Report

Created: 2026-08-31 07:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}