Coverage Report

Created: 2026-03-10 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/io/free_functions.rs
Line
Count
Source
1
use std::fs::File;
2
use std::io::{BufRead, BufWriter, Seek, Write};
3
use std::path::Path;
4
use std::{iter, mem::size_of};
5
6
use crate::io::encoder::ImageEncoderBoxed;
7
use crate::{codecs::*, ExtendedColorType, ImageReader};
8
9
use crate::error::{
10
    ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, UnsupportedError,
11
    UnsupportedErrorKind,
12
};
13
use crate::{DynamicImage, ImageDecoder, ImageFormat};
14
15
/// Create a new image from a Reader.
16
///
17
/// Assumes the reader is already buffered. For optimal performance,
18
/// consider wrapping the reader with a `BufReader::new()`.
19
///
20
/// Try [`ImageReader`] for more advanced uses.
21
40.8k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
40.8k
    let mut reader = ImageReader::new(r);
23
40.8k
    reader.set_format(format);
24
40.8k
    reader.decode()
25
40.8k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
1.64k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
1.64k
    let mut reader = ImageReader::new(r);
23
1.64k
    reader.set_format(format);
24
1.64k
    reader.decode()
25
1.64k
}
Unexecuted instantiation: image::io::free_functions::load::<_>
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
12.3k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
12.3k
    let mut reader = ImageReader::new(r);
23
12.3k
    reader.set_format(format);
24
12.3k
    reader.decode()
25
12.3k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
2.07k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
2.07k
    let mut reader = ImageReader::new(r);
23
2.07k
    reader.set_format(format);
24
2.07k
    reader.decode()
25
2.07k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
1.80k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
1.80k
    let mut reader = ImageReader::new(r);
23
1.80k
    reader.set_format(format);
24
1.80k
    reader.decode()
25
1.80k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
2.80k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
2.80k
    let mut reader = ImageReader::new(r);
23
2.80k
    reader.set_format(format);
24
2.80k
    reader.decode()
25
2.80k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
3.32k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
3.32k
    let mut reader = ImageReader::new(r);
23
3.32k
    reader.set_format(format);
24
3.32k
    reader.decode()
25
3.32k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
7.34k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
7.34k
    let mut reader = ImageReader::new(r);
23
7.34k
    reader.set_format(format);
24
7.34k
    reader.decode()
25
7.34k
}
Unexecuted instantiation: image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
2.13k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
2.13k
    let mut reader = ImageReader::new(r);
23
2.13k
    reader.set_format(format);
24
2.13k
    reader.decode()
25
2.13k
}
image::io::free_functions::load::<std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
21
7.42k
pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<DynamicImage> {
22
7.42k
    let mut reader = ImageReader::new(r);
23
7.42k
    reader.set_format(format);
24
7.42k
    reader.decode()
25
7.42k
}
26
27
/// Saves the supplied buffer to a file at the path specified.
28
///
29
/// The image format is derived from the file extension. The buffer is assumed to have the correct
30
/// format according to the specified color type. This will lead to corrupted files if the buffer
31
/// contains malformed data.
32
0
pub fn save_buffer(
33
0
    path: impl AsRef<Path>,
34
0
    buf: &[u8],
35
0
    width: u32,
36
0
    height: u32,
37
0
    color: impl Into<ExtendedColorType>,
38
0
) -> ImageResult<()> {
39
0
    let format = ImageFormat::from_path(path.as_ref())?;
40
0
    save_buffer_with_format(path, buf, width, height, color, format)
41
0
}
42
43
/// Saves the supplied buffer to a file given the path and desired format.
44
///
45
/// The buffer is assumed to have the correct format according to the specified color type. This
46
/// will lead to corrupted files if the buffer contains malformed data.
47
0
pub fn save_buffer_with_format(
48
0
    path: impl AsRef<Path>,
49
0
    buf: &[u8],
50
0
    width: u32,
51
0
    height: u32,
52
0
    color: impl Into<ExtendedColorType>,
53
0
    format: ImageFormat,
54
0
) -> ImageResult<()> {
55
0
    let buffered_file_write = &mut BufWriter::new(File::create(path)?); // always seekable
56
0
    let encoder = encoder_for_format(format, buffered_file_write)?;
57
0
    encoder.write_image(buf, width, height, color.into())
58
0
}
59
60
1.80k
pub(crate) fn encoder_for_format<'a, W: Write + Seek>(
61
1.80k
    format: ImageFormat,
62
1.80k
    buffered_write: &'a mut W,
63
1.80k
) -> ImageResult<Box<dyn ImageEncoderBoxed + 'a>> {
64
1.80k
    Ok(match format {
65
        #[cfg(feature = "png")]
66
0
        ImageFormat::Png => Box::new(png::PngEncoder::new(buffered_write)),
67
        #[cfg(feature = "jpeg")]
68
0
        ImageFormat::Jpeg => Box::new(jpeg::JpegEncoder::new(buffered_write)),
69
        #[cfg(feature = "pnm")]
70
0
        ImageFormat::Pnm => Box::new(pnm::PnmEncoder::new(buffered_write)),
71
        #[cfg(feature = "gif")]
72
0
        ImageFormat::Gif => Box::new(gif::GifEncoder::new(buffered_write)),
73
        #[cfg(feature = "ico")]
74
0
        ImageFormat::Ico => Box::new(ico::IcoEncoder::new(buffered_write)),
75
        #[cfg(feature = "bmp")]
76
0
        ImageFormat::Bmp => Box::new(bmp::BmpEncoder::new(buffered_write)),
77
        #[cfg(feature = "ff")]
78
0
        ImageFormat::Farbfeld => Box::new(farbfeld::FarbfeldEncoder::new(buffered_write)),
79
        #[cfg(feature = "tga")]
80
0
        ImageFormat::Tga => Box::new(tga::TgaEncoder::new(buffered_write)),
81
        #[cfg(feature = "exr")]
82
0
        ImageFormat::OpenExr => Box::new(openexr::OpenExrEncoder::new(buffered_write)),
83
        #[cfg(feature = "tiff")]
84
0
        ImageFormat::Tiff => Box::new(tiff::TiffEncoder::new(buffered_write)),
85
        #[cfg(feature = "avif")]
86
0
        ImageFormat::Avif => Box::new(avif::AvifEncoder::new(buffered_write)),
87
        #[cfg(feature = "qoi")]
88
0
        ImageFormat::Qoi => Box::new(qoi::QoiEncoder::new(buffered_write)),
89
        #[cfg(feature = "webp")]
90
1.80k
        ImageFormat::WebP => Box::new(webp::WebPEncoder::new_lossless(buffered_write)),
91
        #[cfg(feature = "hdr")]
92
0
        ImageFormat::Hdr => Box::new(hdr::HdrEncoder::new(buffered_write)),
93
        #[allow(unreachable_patterns)] // unreachable if all formats are enabled
94
        _ => {
95
0
            return Err(ImageError::Unsupported(
96
0
                UnsupportedError::from_format_and_kind(
97
0
                    ImageFormatHint::Unknown,
98
0
                    UnsupportedErrorKind::Format(ImageFormatHint::Name(format!("{format:?}"))),
99
0
                ),
100
0
            ));
101
        }
102
    })
103
1.80k
}
Unexecuted instantiation: image::io::free_functions::encoder_for_format::<_>
image::io::free_functions::encoder_for_format::<std::io::cursor::Cursor<alloc::vec::Vec<u8>>>
Line
Count
Source
60
1.80k
pub(crate) fn encoder_for_format<'a, W: Write + Seek>(
61
1.80k
    format: ImageFormat,
62
1.80k
    buffered_write: &'a mut W,
63
1.80k
) -> ImageResult<Box<dyn ImageEncoderBoxed + 'a>> {
64
1.80k
    Ok(match format {
65
        #[cfg(feature = "png")]
66
0
        ImageFormat::Png => Box::new(png::PngEncoder::new(buffered_write)),
67
        #[cfg(feature = "jpeg")]
68
0
        ImageFormat::Jpeg => Box::new(jpeg::JpegEncoder::new(buffered_write)),
69
        #[cfg(feature = "pnm")]
70
0
        ImageFormat::Pnm => Box::new(pnm::PnmEncoder::new(buffered_write)),
71
        #[cfg(feature = "gif")]
72
0
        ImageFormat::Gif => Box::new(gif::GifEncoder::new(buffered_write)),
73
        #[cfg(feature = "ico")]
74
0
        ImageFormat::Ico => Box::new(ico::IcoEncoder::new(buffered_write)),
75
        #[cfg(feature = "bmp")]
76
0
        ImageFormat::Bmp => Box::new(bmp::BmpEncoder::new(buffered_write)),
77
        #[cfg(feature = "ff")]
78
0
        ImageFormat::Farbfeld => Box::new(farbfeld::FarbfeldEncoder::new(buffered_write)),
79
        #[cfg(feature = "tga")]
80
0
        ImageFormat::Tga => Box::new(tga::TgaEncoder::new(buffered_write)),
81
        #[cfg(feature = "exr")]
82
0
        ImageFormat::OpenExr => Box::new(openexr::OpenExrEncoder::new(buffered_write)),
83
        #[cfg(feature = "tiff")]
84
0
        ImageFormat::Tiff => Box::new(tiff::TiffEncoder::new(buffered_write)),
85
        #[cfg(feature = "avif")]
86
0
        ImageFormat::Avif => Box::new(avif::AvifEncoder::new(buffered_write)),
87
        #[cfg(feature = "qoi")]
88
0
        ImageFormat::Qoi => Box::new(qoi::QoiEncoder::new(buffered_write)),
89
        #[cfg(feature = "webp")]
90
1.80k
        ImageFormat::WebP => Box::new(webp::WebPEncoder::new_lossless(buffered_write)),
91
        #[cfg(feature = "hdr")]
92
0
        ImageFormat::Hdr => Box::new(hdr::HdrEncoder::new(buffered_write)),
93
        #[allow(unreachable_patterns)] // unreachable if all formats are enabled
94
        _ => {
95
0
            return Err(ImageError::Unsupported(
96
0
                UnsupportedError::from_format_and_kind(
97
0
                    ImageFormatHint::Unknown,
98
0
                    UnsupportedErrorKind::Format(ImageFormatHint::Name(format!("{format:?}"))),
99
0
                ),
100
0
            ));
101
        }
102
    })
103
1.80k
}
104
105
static MAGIC_BYTES: [(&[u8], &[u8], ImageFormat); 21] = [
106
    (b"\x89PNG\r\n\x1a\n", b"", ImageFormat::Png),
107
    (&[0xff, 0xd8, 0xff], b"", ImageFormat::Jpeg),
108
    (b"GIF89a", b"", ImageFormat::Gif),
109
    (b"GIF87a", b"", ImageFormat::Gif),
110
    (
111
        b"RIFF\0\0\0\0WEBP",
112
        b"\xFF\xFF\xFF\xFF\0\0\0\0",
113
        ImageFormat::WebP,
114
    ),
115
    (b"MM\x00*", b"", ImageFormat::Tiff),
116
    (b"II*\x00", b"", ImageFormat::Tiff),
117
    (b"BM", b"", ImageFormat::Bmp),
118
    (&[0, 0, 1, 0], b"", ImageFormat::Ico),
119
    (b"#?RADIANCE", b"", ImageFormat::Hdr),
120
    (b"\0\0\0\0ftypavif", b"\xFF\xFF\0\0", ImageFormat::Avif),
121
    (&[0x76, 0x2f, 0x31, 0x01], b"", ImageFormat::OpenExr), // = &exr::meta::magic_number::BYTES
122
    (b"qoif", b"", ImageFormat::Qoi),
123
    (b"P1", b"", ImageFormat::Pnm),
124
    (b"P2", b"", ImageFormat::Pnm),
125
    (b"P3", b"", ImageFormat::Pnm),
126
    (b"P4", b"", ImageFormat::Pnm),
127
    (b"P5", b"", ImageFormat::Pnm),
128
    (b"P6", b"", ImageFormat::Pnm),
129
    (b"P7", b"", ImageFormat::Pnm),
130
    (b"farbfeld", b"", ImageFormat::Farbfeld),
131
];
132
133
/// Guess image format from memory block
134
///
135
/// Makes an educated guess about the image format based on the Magic Bytes at the beginning.
136
/// TGA is not supported by this function.
137
/// This is not to be trusted on the validity of the whole memory block
138
0
pub fn guess_format(buffer: &[u8]) -> ImageResult<ImageFormat> {
139
0
    match guess_format_impl(buffer) {
140
0
        Some(format) => Ok(format),
141
0
        None => Err(ImageError::Unsupported(ImageFormatHint::Unknown.into())),
142
    }
143
0
}
144
145
0
pub(crate) fn guess_format_impl(buffer: &[u8]) -> Option<ImageFormat> {
146
0
    for &(signature, mask, format) in &MAGIC_BYTES {
147
0
        if mask.is_empty() {
148
0
            if buffer.starts_with(signature) {
149
0
                return Some(format);
150
0
            }
151
0
        } else if buffer.len() >= signature.len()
152
0
            && buffer
153
0
                .iter()
154
0
                .zip(signature.iter())
155
0
                .zip(mask.iter().chain(iter::repeat(&0xFF)))
156
0
                .all(|((&byte, &sig), &mask)| byte & mask == sig)
157
        {
158
0
            return Some(format);
159
0
        }
160
    }
161
162
0
    None
163
0
}
164
165
/// Reads all of the bytes of a decoder into a Vec<T>. No particular alignment
166
/// of the output buffer is guaranteed.
167
///
168
/// Panics if there isn't enough memory to decode the image.
169
25.2k
pub(crate) fn decoder_to_vec<T>(decoder: impl ImageDecoder) -> ImageResult<Vec<T>>
170
25.2k
where
171
25.2k
    T: crate::traits::Primitive + bytemuck::Pod,
172
{
173
25.2k
    let total_bytes = usize::try_from(decoder.total_bytes());
174
25.2k
    if total_bytes.is_err() || total_bytes.unwrap() > isize::MAX as usize {
175
0
        return Err(ImageError::Limits(LimitError::from_kind(
176
0
            LimitErrorKind::InsufficientMemory,
177
0
        )));
178
25.2k
    }
179
180
25.2k
    let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::<T>()];
181
25.2k
    decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?;
182
5.22k
    Ok(buf)
183
25.2k
}
image::io::free_functions::decoder_to_vec::<f32, alloc::boxed::Box<dyn image::io::decoder::ImageDecoder>>
Line
Count
Source
169
1.55k
pub(crate) fn decoder_to_vec<T>(decoder: impl ImageDecoder) -> ImageResult<Vec<T>>
170
1.55k
where
171
1.55k
    T: crate::traits::Primitive + bytemuck::Pod,
172
{
173
1.55k
    let total_bytes = usize::try_from(decoder.total_bytes());
174
1.55k
    if total_bytes.is_err() || total_bytes.unwrap() > isize::MAX as usize {
175
0
        return Err(ImageError::Limits(LimitError::from_kind(
176
0
            LimitErrorKind::InsufficientMemory,
177
0
        )));
178
1.55k
    }
179
180
1.55k
    let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::<T>()];
181
1.55k
    decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?;
182
270
    Ok(buf)
183
1.55k
}
image::io::free_functions::decoder_to_vec::<u8, alloc::boxed::Box<dyn image::io::decoder::ImageDecoder>>
Line
Count
Source
169
20.9k
pub(crate) fn decoder_to_vec<T>(decoder: impl ImageDecoder) -> ImageResult<Vec<T>>
170
20.9k
where
171
20.9k
    T: crate::traits::Primitive + bytemuck::Pod,
172
{
173
20.9k
    let total_bytes = usize::try_from(decoder.total_bytes());
174
20.9k
    if total_bytes.is_err() || total_bytes.unwrap() > isize::MAX as usize {
175
0
        return Err(ImageError::Limits(LimitError::from_kind(
176
0
            LimitErrorKind::InsufficientMemory,
177
0
        )));
178
20.9k
    }
179
180
20.9k
    let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::<T>()];
181
20.9k
    decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?;
182
4.72k
    Ok(buf)
183
20.9k
}
image::io::free_functions::decoder_to_vec::<u16, alloc::boxed::Box<dyn image::io::decoder::ImageDecoder>>
Line
Count
Source
169
2.77k
pub(crate) fn decoder_to_vec<T>(decoder: impl ImageDecoder) -> ImageResult<Vec<T>>
170
2.77k
where
171
2.77k
    T: crate::traits::Primitive + bytemuck::Pod,
172
{
173
2.77k
    let total_bytes = usize::try_from(decoder.total_bytes());
174
2.77k
    if total_bytes.is_err() || total_bytes.unwrap() > isize::MAX as usize {
175
0
        return Err(ImageError::Limits(LimitError::from_kind(
176
0
            LimitErrorKind::InsufficientMemory,
177
0
        )));
178
2.77k
    }
179
180
2.77k
    let mut buf = vec![num_traits::Zero::zero(); total_bytes.unwrap() / size_of::<T>()];
181
2.77k
    decoder.read_image(bytemuck::cast_slice_mut(buf.as_mut_slice()))?;
182
230
    Ok(buf)
183
2.77k
}
184
185
#[test]
186
fn test_guess_format_agrees_with_extension() {
187
    let found = glob::glob("tests/images/**/*.*")
188
        .unwrap()
189
        .filter_map(|path| test_file(path.unwrap()))
190
        .collect::<std::collections::HashSet<_>>();
191
192
    // Check we’ve actually tested anything.
193
    for fmt in ImageFormat::all() {
194
        use ImageFormat::*;
195
        let found = found.contains(&fmt);
196
        if matches!(
197
            fmt,
198
            Bmp | Farbfeld | Gif | Ico | Hdr | Jpeg | OpenExr | Png | Pnm | Qoi | Tiff | WebP
199
        ) {
200
            assert!(found, "No {fmt:?} test files found");
201
        } else {
202
            assert!(!found, "Add `{fmt:?}` to test_guess_format");
203
        }
204
    }
205
206
    fn test_file(path: std::path::PathBuf) -> Option<ImageFormat> {
207
        let is_supported = |fmt| MAGIC_BYTES.iter().any(|item| fmt == item.2);
208
209
        let disppath = path.display();
210
        match ImageFormat::from_path(path.as_path()) {
211
            Ok(fmt) if is_supported(fmt) => {
212
                let buf = std::fs::read(path.as_path()).unwrap();
213
                let got = guess_format_impl(&buf);
214
                assert_eq!(Some(fmt), got, "{disppath}");
215
                Some(fmt)
216
            }
217
            Ok(fmt) => {
218
                eprintln!("{disppath}: unguessable format {fmt:?}, ignoring");
219
                None
220
            }
221
            Err(_) => {
222
                eprintln!("{disppath}: unrecognised extension, ignoring");
223
                None
224
            }
225
        }
226
    }
227
}