Coverage Report

Created: 2026-03-07 07:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/jpeg/encoder.rs
Line
Count
Source
1
#![allow(clippy::too_many_arguments)]
2
use std::io::Write;
3
use std::{error, fmt};
4
5
use crate::error::{
6
    EncodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind,
7
};
8
use crate::{ColorType, DynamicImage, ExtendedColorType, ImageEncoder, ImageFormat};
9
10
use jpeg_encoder::Encoder;
11
12
/// Represents a unit in which the density of an image is measured
13
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14
pub enum PixelDensityUnit {
15
    /// Represents the absence of a unit, the values indicate only a
16
    /// [pixel aspect ratio](https://en.wikipedia.org/wiki/Pixel_aspect_ratio)
17
    PixelAspectRatio,
18
19
    /// Pixels per inch (2.54 cm)
20
    Inches,
21
22
    /// Pixels per centimeter
23
    Centimeters,
24
}
25
26
/// Controls the resolution of the color information.
27
///
28
/// Human eye is much less sensitive to the detail of color than brightness.
29
/// JPEG can exploit this to significantly reduce the file size by storing color information
30
/// (Cb and Cr channels) in a lower resolution than brightness (Y channel) without visual quality loss.
31
///
32
/// See the documentation on each variant for details.
33
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34
#[non_exhaustive]
35
pub enum ChromaSubsampling {
36
    /// **4:4:4** Color information is encoded in full resolution. Results in larger file size.
37
    ///
38
    /// Recommended when the image has small brightly colored elements, e.g. artwork or screenshots.
39
    S444,
40
    /// **4:2:2** The resolution of color information is reduced by a factor of 2 in the horizontal direction.
41
    S422,
42
    /// **4:2:0** The resolution of color information is reduced by a factor of 2 both horizontally and vertically.
43
    ///
44
    /// Results in a smaller file size. Well suited for photographs where it incurs no visial quality loss.
45
    S420,
46
}
47
48
impl ChromaSubsampling {
49
0
    fn to_encoder_repr(self) -> jpeg_encoder::SamplingFactor {
50
0
        match self {
51
0
            ChromaSubsampling::S444 => jpeg_encoder::SamplingFactor::R_4_4_4,
52
0
            ChromaSubsampling::S422 => jpeg_encoder::SamplingFactor::R_4_2_2,
53
0
            ChromaSubsampling::S420 => jpeg_encoder::SamplingFactor::R_4_2_0,
54
        }
55
0
    }
56
}
57
58
/// Represents the pixel density of an image
59
///
60
/// For example, a 300 DPI image is represented by:
61
///
62
/// ```rust
63
/// use image::codecs::jpeg::*;
64
/// let hdpi = PixelDensity::dpi(300);
65
/// assert_eq!(hdpi, PixelDensity {density: (300,300), unit: PixelDensityUnit::Inches})
66
/// ```
67
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68
pub struct PixelDensity {
69
    /// A couple of values for (Xdensity, Ydensity)
70
    pub density: (u16, u16),
71
    /// The unit in which the density is measured
72
    pub unit: PixelDensityUnit,
73
}
74
75
impl PixelDensity {
76
    /// Creates the most common pixel density type:
77
    /// the horizontal and the vertical density are equal,
78
    /// and measured in pixels per inch.
79
    #[must_use]
80
0
    pub fn dpi(density: u16) -> Self {
81
0
        PixelDensity {
82
0
            density: (density, density),
83
0
            unit: PixelDensityUnit::Inches,
84
0
        }
85
0
    }
86
87
    /// Converts pixel density to the representation used by jpeg-encoder crate
88
0
    fn to_encoder_repr(self) -> jpeg_encoder::PixelDensity {
89
0
        let unit = match self.unit {
90
0
            PixelDensityUnit::PixelAspectRatio => jpeg_encoder::PixelDensityUnit::PixelAspectRatio,
91
0
            PixelDensityUnit::Inches => jpeg_encoder::PixelDensityUnit::Inches,
92
0
            PixelDensityUnit::Centimeters => jpeg_encoder::PixelDensityUnit::Centimeters,
93
        };
94
0
        jpeg_encoder::PixelDensity {
95
0
            density: self.density,
96
0
            unit,
97
0
        }
98
0
    }
99
}
100
101
impl Default for PixelDensity {
102
    /// Returns a pixel density with a pixel aspect ratio of 1
103
0
    fn default() -> Self {
104
0
        PixelDensity {
105
0
            density: (1, 1),
106
0
            unit: PixelDensityUnit::PixelAspectRatio,
107
0
        }
108
0
    }
109
}
110
111
/// Errors that can occur when encoding a JPEG image
112
#[derive(Debug, Copy, Clone)]
113
enum EncoderError {
114
    /// JPEG does not support this size
115
    InvalidSize(u32, u32),
116
}
117
118
impl fmt::Display for EncoderError {
119
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120
0
        match self {
121
0
            EncoderError::InvalidSize(w, h) => f.write_fmt(format_args!(
122
0
                "Invalid image size ({w} x {h}) to encode as JPEG: \
123
0
                 width and height must be >= 1 and <= 65535"
124
            )),
125
        }
126
0
    }
127
}
128
129
impl From<EncoderError> for ImageError {
130
0
    fn from(e: EncoderError) -> ImageError {
131
0
        ImageError::Encoding(EncodingError::new(ImageFormat::Jpeg.into(), e))
132
0
    }
133
}
134
135
impl error::Error for EncoderError {}
136
137
/// The representation of a JPEG encoder
138
pub struct JpegEncoder<W: Write> {
139
    encoder: Encoder<W>,
140
}
141
142
impl<W: Write> JpegEncoder<W> {
143
    /// Create a new encoder that writes its output to ```w```
144
0
    pub fn new(w: W) -> JpegEncoder<W> {
145
0
        JpegEncoder::new_with_quality(w, 75)
146
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_>>::new
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new
147
148
    /// Create a new encoder that writes its output to ```w```, and has
149
    /// the quality parameter ```quality``` with a value in the range 1-100
150
    /// where 1 is the worst and 100 is the best.
151
    ///
152
    /// By default quality settings 90 or above use [chroma subsampling](ChromaSubsampling)
153
    /// mode [4:4:4](ChromaSubsampling::S444), while quality below 90 subsampling mode
154
    /// [4:2:0](ChromaSubsampling::S420).
155
    /// This can be overridden using [Self::set_chroma_subsampling].
156
0
    pub fn new_with_quality(w: W, quality: u8) -> JpegEncoder<W> {
157
0
        JpegEncoder {
158
0
            encoder: Encoder::new(w, quality),
159
0
        }
160
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_>>::new_with_quality
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new_with_quality
161
162
    /// Sets the chroma subsampling mode. See [ChromaSubsampling] for details.
163
0
    pub fn set_chroma_subsampling(&mut self, sampling: ChromaSubsampling) {
164
0
        self.encoder.set_sampling_factor(sampling.to_encoder_repr());
165
0
    }
166
167
    /// Spend extra time optimizing Huffman tables. Slightly reduces file size at the cost of encoding speed.
168
    ///
169
    /// Defaults to **false**.
170
0
    pub fn set_optimize_huffman_tables(&mut self, optimize: bool) {
171
0
        self.encoder.set_optimized_huffman_tables(optimize);
172
0
    }
173
174
    /// Progressive files allow showing a low-resolution view of the entire image before it's fully downloaded.
175
    /// Useful for large images that will be displayed on the web.
176
    ///
177
    /// Defaults to **false**.
178
0
    pub fn set_progressive(&mut self, progressive: bool) {
179
0
        self.encoder.set_progressive(progressive);
180
0
    }
181
182
    /// Set the pixel density of the images the encoder will encode.
183
    /// If this method is not called, then a default pixel aspect ratio of 1x1 will be applied,
184
    /// and no DPI information will be stored in the image.
185
0
    pub fn set_pixel_density(&mut self, pixel_density: PixelDensity) {
186
0
        self.encoder.set_density(pixel_density.to_encoder_repr());
187
0
    }
188
189
    /// Encodes the image stored in the raw byte buffer ```image```
190
    /// that has dimensions ```width``` and ```height```
191
    /// and ```ColorType``` ```c```
192
    ///
193
    /// # Panics
194
    ///
195
    /// Panics if `width * height * color_type.bytes_per_pixel() != image.len()`.
196
    #[track_caller]
197
0
    fn encode(
198
0
        self,
199
0
        image: &[u8],
200
0
        width: u32,
201
0
        height: u32,
202
0
        color_type: ExtendedColorType,
203
0
    ) -> ImageResult<()> {
204
0
        let expected_buffer_len = color_type.buffer_size(width, height);
205
0
        assert_eq!(
206
            expected_buffer_len,
207
0
            image.len() as u64,
208
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
209
0
            image.len(),
210
        );
211
212
0
        let (width, height) = match (u16::try_from(width), u16::try_from(height)) {
213
0
            (Ok(w @ 1..), Ok(h @ 1..)) => (w, h),
214
0
            _ => return Err(EncoderError::InvalidSize(width, height).into()),
215
        };
216
217
0
        let encode_jpeg = |color: jpeg_encoder::ColorType| {
218
0
            self.encoder
219
0
                .encode(image, width, height, color)
220
0
                .map_err(|err| {
221
0
                    ImageError::Encoding(EncodingError::new(
222
0
                        ImageFormatHint::Exact(ImageFormat::Jpeg),
223
0
                        err,
224
0
                    ))
225
0
                })
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_>>::encode::{closure#0}::{closure#0}
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode::{closure#0}::{closure#0}
226
0
        };
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_>>::encode::{closure#0}
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode::{closure#0}
227
228
0
        match color_type {
229
            ExtendedColorType::L8 => {
230
0
                let color = jpeg_encoder::ColorType::Luma;
231
0
                encode_jpeg(color)
232
            }
233
            ExtendedColorType::Rgb8 => {
234
0
                let color = jpeg_encoder::ColorType::Rgb;
235
0
                encode_jpeg(color)
236
            }
237
0
            _ => Err(ImageError::Unsupported(
238
0
                UnsupportedError::from_format_and_kind(
239
0
                    ImageFormat::Jpeg.into(),
240
0
                    UnsupportedErrorKind::Color(color_type),
241
0
                ),
242
0
            )),
243
        }
244
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_>>::encode
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode
245
}
246
247
impl<W: Write> ImageEncoder for JpegEncoder<W> {
248
    #[track_caller]
249
0
    fn write_image(
250
0
        self,
251
0
        buf: &[u8],
252
0
        width: u32,
253
0
        height: u32,
254
0
        color_type: ExtendedColorType,
255
0
    ) -> ImageResult<()> {
256
0
        self.encode(buf, width, height, color_type)
257
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::write_image
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image
258
259
0
    fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
260
0
        self.encoder.add_icc_profile(&icc_profile).map_err(|_| {
261
0
            UnsupportedError::from_format_and_kind(
262
0
                ImageFormat::Jpeg.into(),
263
0
                UnsupportedErrorKind::GenericFeature("ICC chunk too large".to_string()),
264
            )
265
0
        })
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::set_icc_profile::{closure#0}
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::set_icc_profile::{closure#0}
266
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::set_icc_profile
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::set_icc_profile
267
268
0
    fn set_exif_metadata(&mut self, exif: Vec<u8>) -> Result<(), UnsupportedError> {
269
0
        self.encoder.add_exif_metadata(&exif).map_err(|_| {
270
0
            UnsupportedError::from_format_and_kind(
271
0
                ImageFormat::Jpeg.into(),
272
0
                UnsupportedErrorKind::GenericFeature("Exif chunk too large".to_string()),
273
            )
274
0
        })?;
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::set_exif_metadata::{closure#0}
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::set_exif_metadata::{closure#0}
275
0
        Ok(())
276
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::set_exif_metadata
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::set_exif_metadata
277
278
0
    fn make_compatible_img(
279
0
        &self,
280
0
        _: crate::io::encoder::MethodSealedToImage,
281
0
        img: &DynamicImage,
282
0
    ) -> Option<DynamicImage> {
283
        use ColorType::*;
284
0
        match img.color() {
285
0
            L8 | Rgb8 => None,
286
0
            La8 | L16 | La16 => Some(img.to_luma8().into()),
287
0
            Rgba8 | Rgb16 | Rgb32F | Rgba16 | Rgba32F => Some(img.to_rgb8().into()),
288
        }
289
0
    }
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<_> as image::io::encoder::ImageEncoder>::make_compatible_img
Unexecuted instantiation: <image::codecs::jpeg::encoder::JpegEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::make_compatible_img
290
}
291
292
#[cfg(test)]
293
mod tests {
294
    use std::io::Cursor;
295
296
    #[cfg(feature = "benchmarks")]
297
    extern crate test;
298
    #[cfg(feature = "benchmarks")]
299
    use test::Bencher;
300
301
    use crate::{ColorType, DynamicImage, ExtendedColorType, ImageEncoder, ImageError};
302
    use crate::{ImageDecoder as _, ImageFormat};
303
304
    use super::super::{JpegDecoder, JpegEncoder};
305
306
    fn decode(encoded: &[u8]) -> Vec<u8> {
307
        let decoder = JpegDecoder::new(Cursor::new(encoded)).expect("Could not decode image");
308
309
        let mut decoded = vec![0; decoder.total_bytes() as usize];
310
        decoder
311
            .read_image(&mut decoded)
312
            .expect("Could not decode image");
313
        decoded
314
    }
315
316
    #[test]
317
    fn roundtrip_sanity_check() {
318
        // create a 1x1 8-bit image buffer containing a single red pixel
319
        let img = [255u8, 0, 0];
320
321
        // encode it into a memory buffer
322
        let mut encoded_img = Vec::new();
323
        {
324
            let encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
325
            encoder
326
                .write_image(&img, 1, 1, ExtendedColorType::Rgb8)
327
                .expect("Could not encode image");
328
        }
329
330
        // decode it from the memory buffer
331
        {
332
            let decoded = decode(&encoded_img);
333
            // note that, even with the encode quality set to 100, we do not get the same image
334
            // back. Therefore, we're going to assert that it's at least red-ish:
335
            assert_eq!(3, decoded.len());
336
            assert!(decoded[0] > 0x80);
337
            assert!(decoded[1] < 0x80);
338
            assert!(decoded[2] < 0x80);
339
        }
340
    }
341
342
    #[test]
343
    fn grayscale_roundtrip_sanity_check() {
344
        // create a 2x2 8-bit image buffer containing a white diagonal
345
        let img = [255u8, 0, 0, 255];
346
347
        // encode it into a memory buffer
348
        let mut encoded_img = Vec::new();
349
        {
350
            let encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
351
            encoder
352
                .write_image(&img[..], 2, 2, ExtendedColorType::L8)
353
                .expect("Could not encode image");
354
        }
355
356
        // decode it from the memory buffer
357
        {
358
            let decoded = decode(&encoded_img);
359
            // note that, even with the encode quality set to 100, we do not get the same image
360
            // back. Therefore, we're going to assert that the diagonal is at least white-ish:
361
            assert_eq!(4, decoded.len());
362
            assert!(decoded[0] > 0x80);
363
            assert!(decoded[1] < 0x80);
364
            assert!(decoded[2] < 0x80);
365
            assert!(decoded[3] > 0x80);
366
        }
367
    }
368
369
    #[test]
370
    fn roundtrip_exif_icc() {
371
        // create a 2x2 8-bit image buffer containing a white diagonal
372
        let img = [255u8, 0, 0, 255];
373
374
        let exif = vec![1, 2, 3];
375
        let icc = vec![4, 5, 6];
376
377
        // encode it into a memory buffer
378
        let mut encoded_img = Vec::new();
379
        {
380
            let mut encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
381
382
            encoder.set_exif_metadata(exif.clone()).unwrap();
383
            encoder.set_icc_profile(icc.clone()).unwrap();
384
385
            encoder
386
                .write_image(&img[..], 2, 2, ExtendedColorType::L8)
387
                .expect("Could not encode image");
388
        }
389
390
        let mut decoder =
391
            JpegDecoder::new(Cursor::new(encoded_img)).expect("Could not decode image");
392
        let decoded_exif = decoder
393
            .exif_metadata()
394
            .expect("Error decoding Exif")
395
            .expect("Exif is empty");
396
        assert_eq!(exif, decoded_exif);
397
        let decoded_icc = decoder
398
            .icc_profile()
399
            .expect("Error decoding ICC")
400
            .expect("ICC is empty");
401
        assert_eq!(icc, decoded_icc);
402
    }
403
404
    #[test]
405
    fn test_image_too_large() {
406
        // JPEG cannot encode images larger than 65,535×65,535
407
        // create a 65,536×1 8-bit black image buffer
408
        let img = [0; 65_536];
409
        // Try to encode an image that is too large
410
        let mut encoded = Vec::new();
411
        let encoder = JpegEncoder::new_with_quality(&mut encoded, 100);
412
        let result = encoder.write_image(&img, 65_536, 1, ExtendedColorType::L8);
413
        match result {
414
            Err(ImageError::Encoding(_)) => (),
415
            other => {
416
                panic!(
417
                    "Encoding an image that is too large should return an EncodingError \
418
                                it returned {other:?} instead"
419
                )
420
            }
421
        }
422
    }
423
424
    #[test]
425
    fn check_color_types() {
426
        const ALL: &[ColorType] = &[
427
            ColorType::L8,
428
            ColorType::L16,
429
            ColorType::La8,
430
            ColorType::Rgb8,
431
            ColorType::Rgba8,
432
            ColorType::La16,
433
            ColorType::Rgb16,
434
            ColorType::Rgba16,
435
            ColorType::Rgb32F,
436
            ColorType::Rgba32F,
437
        ];
438
439
        for color in ALL {
440
            let image = DynamicImage::new(1, 1, *color);
441
442
            image
443
                .write_to(&mut Cursor::new(vec![]), ImageFormat::Jpeg)
444
                .expect("supported or converted");
445
        }
446
    }
447
448
    #[cfg(feature = "benchmarks")]
449
    #[bench]
450
    fn bench_jpeg_encoder_new(b: &mut Bencher) {
451
        b.iter(|| {
452
            let mut y = vec![];
453
            let _x = JpegEncoder::new(&mut y);
454
        });
455
    }
456
}