Coverage Report

Created: 2026-01-10 07:01

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::Density {
89
0
        match self.unit {
90
0
            PixelDensityUnit::PixelAspectRatio => jpeg_encoder::Density::None, // TODO: https://github.com/vstroebel/jpeg-encoder/issues/21
91
0
            PixelDensityUnit::Inches => jpeg_encoder::Density::Inch {
92
0
                x: self.density.0,
93
0
                y: self.density.1,
94
0
            },
95
0
            PixelDensityUnit::Centimeters => jpeg_encoder::Density::Centimeter {
96
0
                x: self.density.0,
97
0
                y: self.density.1,
98
0
            },
99
        }
100
0
    }
101
}
102
103
impl Default for PixelDensity {
104
    /// Returns a pixel density with a pixel aspect ratio of 1
105
0
    fn default() -> Self {
106
0
        PixelDensity {
107
0
            density: (1, 1),
108
0
            unit: PixelDensityUnit::PixelAspectRatio,
109
0
        }
110
0
    }
111
}
112
113
/// Errors that can occur when encoding a JPEG image
114
#[derive(Debug, Copy, Clone)]
115
enum EncoderError {
116
    /// JPEG does not support this size
117
    InvalidSize(u32, u32),
118
}
119
120
impl fmt::Display for EncoderError {
121
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122
0
        match self {
123
0
            EncoderError::InvalidSize(w, h) => f.write_fmt(format_args!(
124
0
                "Invalid image size ({w} x {h}) to encode as JPEG: \
125
0
                 width and height must be >= 1 and <= 65535"
126
            )),
127
        }
128
0
    }
129
}
130
131
impl From<EncoderError> for ImageError {
132
0
    fn from(e: EncoderError) -> ImageError {
133
0
        ImageError::Encoding(EncodingError::new(ImageFormat::Jpeg.into(), e))
134
0
    }
135
}
136
137
impl error::Error for EncoderError {}
138
139
/// The representation of a JPEG encoder
140
pub struct JpegEncoder<W: Write> {
141
    encoder: Encoder<W>,
142
}
143
144
impl<W: Write> JpegEncoder<W> {
145
    /// Create a new encoder that writes its output to ```w```
146
0
    pub fn new(w: W) -> JpegEncoder<W> {
147
0
        JpegEncoder::new_with_quality(w, 75)
148
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
149
150
    /// Create a new encoder that writes its output to ```w```, and has
151
    /// the quality parameter ```quality``` with a value in the range 1-100
152
    /// where 1 is the worst and 100 is the best.
153
    ///
154
    /// By default quality settings 90 or above use [chroma subsampling](ChromaSubsampling)
155
    /// mode [4:4:4](ChromaSubsampling::S444), while quality below 90 subsampling mode
156
    /// [4:2:0](ChromaSubsampling::S420).
157
    /// This can be overridden using [Self::set_chroma_subsampling].
158
0
    pub fn new_with_quality(w: W, quality: u8) -> JpegEncoder<W> {
159
0
        JpegEncoder {
160
0
            encoder: Encoder::new(w, quality),
161
0
        }
162
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
163
164
    /// Sets the chroma subsampling mode. See [ChromaSubsampling] for details.
165
0
    pub fn set_chroma_subsampling(&mut self, sampling: ChromaSubsampling) {
166
0
        self.encoder.set_sampling_factor(sampling.to_encoder_repr());
167
0
    }
168
169
    /// Spend extra time optimizing Huffman tables. Slightly reduces file size at the cost of encoding speed.
170
    ///
171
    /// Defaults to **false**.
172
0
    pub fn set_optimize_huffman_tables(&mut self, optimize: bool) {
173
0
        self.encoder.set_optimized_huffman_tables(optimize);
174
0
    }
175
176
    /// Set the pixel density of the images the encoder will encode.
177
    /// If this method is not called, then a default pixel aspect ratio of 1x1 will be applied,
178
    /// and no DPI information will be stored in the image.
179
0
    pub fn set_pixel_density(&mut self, pixel_density: PixelDensity) {
180
0
        self.encoder.set_density(pixel_density.to_encoder_repr());
181
0
    }
182
183
    /// Encodes the image stored in the raw byte buffer ```image```
184
    /// that has dimensions ```width``` and ```height```
185
    /// and ```ColorType``` ```c```
186
    ///
187
    /// # Panics
188
    ///
189
    /// Panics if `width * height * color_type.bytes_per_pixel() != image.len()`.
190
    #[track_caller]
191
0
    fn encode(
192
0
        self,
193
0
        image: &[u8],
194
0
        width: u32,
195
0
        height: u32,
196
0
        color_type: ExtendedColorType,
197
0
    ) -> ImageResult<()> {
198
0
        let expected_buffer_len = color_type.buffer_size(width, height);
199
0
        assert_eq!(
200
            expected_buffer_len,
201
0
            image.len() as u64,
202
0
            "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image",
203
0
            image.len(),
204
        );
205
206
0
        let (width, height) = match (u16::try_from(width), u16::try_from(height)) {
207
0
            (Ok(w @ 1..), Ok(h @ 1..)) => (w, h),
208
0
            _ => return Err(EncoderError::InvalidSize(width, height).into()),
209
        };
210
211
0
        let encode_jpeg = |color: jpeg_encoder::ColorType| {
212
0
            self.encoder
213
0
                .encode(image, width, height, color)
214
0
                .map_err(|err| {
215
0
                    ImageError::Encoding(EncodingError::new(
216
0
                        ImageFormatHint::Exact(ImageFormat::Jpeg),
217
0
                        err,
218
0
                    ))
219
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}
220
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}
221
222
0
        match color_type {
223
            ExtendedColorType::L8 => {
224
0
                let color = jpeg_encoder::ColorType::Luma;
225
0
                encode_jpeg(color)
226
            }
227
            ExtendedColorType::Rgb8 => {
228
0
                let color = jpeg_encoder::ColorType::Rgb;
229
0
                encode_jpeg(color)
230
            }
231
0
            _ => Err(ImageError::Unsupported(
232
0
                UnsupportedError::from_format_and_kind(
233
0
                    ImageFormat::Jpeg.into(),
234
0
                    UnsupportedErrorKind::Color(color_type),
235
0
                ),
236
0
            )),
237
        }
238
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
239
}
240
241
// E x i f \0 \0
242
/// The header for an EXIF APP1 segment
243
const EXIF_HEADER: [u8; 6] = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00];
244
const APP1: u8 = 1;
245
246
impl<W: Write> ImageEncoder for JpegEncoder<W> {
247
    #[track_caller]
248
0
    fn write_image(
249
0
        self,
250
0
        buf: &[u8],
251
0
        width: u32,
252
0
        height: u32,
253
0
        color_type: ExtendedColorType,
254
0
    ) -> ImageResult<()> {
255
0
        self.encode(buf, width, height, color_type)
256
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
257
258
0
    fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
259
0
        self.encoder.add_icc_profile(&icc_profile).map_err(|_| {
260
0
            UnsupportedError::from_format_and_kind(
261
0
                ImageFormat::Jpeg.into(),
262
0
                UnsupportedErrorKind::GenericFeature("ICC chunk too large".to_string()),
263
            )
264
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}
265
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
266
267
0
    fn set_exif_metadata(&mut self, exif: Vec<u8>) -> Result<(), UnsupportedError> {
268
0
        let mut formatted = EXIF_HEADER.to_vec();
269
0
        formatted.extend_from_slice(&exif);
270
0
        self.encoder
271
0
            .add_app_segment(APP1, &formatted)
272
0
            .map_err(|_| {
273
0
                UnsupportedError::from_format_and_kind(
274
0
                    ImageFormat::Jpeg.into(),
275
0
                    UnsupportedErrorKind::GenericFeature("Exif chunk too large".to_string()),
276
                )
277
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}
278
0
        Ok(())
279
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
280
281
0
    fn make_compatible_img(
282
0
        &self,
283
0
        _: crate::io::encoder::MethodSealedToImage,
284
0
        img: &DynamicImage,
285
0
    ) -> Option<DynamicImage> {
286
        use ColorType::*;
287
0
        match img.color() {
288
0
            L8 | Rgb8 => None,
289
0
            La8 | L16 | La16 => Some(img.to_luma8().into()),
290
0
            Rgba8 | Rgb16 | Rgb32F | Rgba16 | Rgba32F => Some(img.to_rgb8().into()),
291
        }
292
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
293
}
294
295
#[cfg(test)]
296
mod tests {
297
    use std::io::Cursor;
298
299
    #[cfg(feature = "benchmarks")]
300
    extern crate test;
301
    #[cfg(feature = "benchmarks")]
302
    use test::Bencher;
303
304
    use crate::{ColorType, DynamicImage, ExtendedColorType, ImageEncoder, ImageError};
305
    use crate::{ImageDecoder as _, ImageFormat};
306
307
    use super::super::{JpegDecoder, JpegEncoder};
308
309
    fn decode(encoded: &[u8]) -> Vec<u8> {
310
        let decoder = JpegDecoder::new(Cursor::new(encoded)).expect("Could not decode image");
311
312
        let mut decoded = vec![0; decoder.total_bytes() as usize];
313
        decoder
314
            .read_image(&mut decoded)
315
            .expect("Could not decode image");
316
        decoded
317
    }
318
319
    #[test]
320
    fn roundtrip_sanity_check() {
321
        // create a 1x1 8-bit image buffer containing a single red pixel
322
        let img = [255u8, 0, 0];
323
324
        // encode it into a memory buffer
325
        let mut encoded_img = Vec::new();
326
        {
327
            let encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
328
            encoder
329
                .write_image(&img, 1, 1, ExtendedColorType::Rgb8)
330
                .expect("Could not encode image");
331
        }
332
333
        // decode it from the memory buffer
334
        {
335
            let decoded = decode(&encoded_img);
336
            // note that, even with the encode quality set to 100, we do not get the same image
337
            // back. Therefore, we're going to assert that it's at least red-ish:
338
            assert_eq!(3, decoded.len());
339
            assert!(decoded[0] > 0x80);
340
            assert!(decoded[1] < 0x80);
341
            assert!(decoded[2] < 0x80);
342
        }
343
    }
344
345
    #[test]
346
    fn grayscale_roundtrip_sanity_check() {
347
        // create a 2x2 8-bit image buffer containing a white diagonal
348
        let img = [255u8, 0, 0, 255];
349
350
        // encode it into a memory buffer
351
        let mut encoded_img = Vec::new();
352
        {
353
            let encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
354
            encoder
355
                .write_image(&img[..], 2, 2, ExtendedColorType::L8)
356
                .expect("Could not encode image");
357
        }
358
359
        // decode it from the memory buffer
360
        {
361
            let decoded = decode(&encoded_img);
362
            // note that, even with the encode quality set to 100, we do not get the same image
363
            // back. Therefore, we're going to assert that the diagonal is at least white-ish:
364
            assert_eq!(4, decoded.len());
365
            assert!(decoded[0] > 0x80);
366
            assert!(decoded[1] < 0x80);
367
            assert!(decoded[2] < 0x80);
368
            assert!(decoded[3] > 0x80);
369
        }
370
    }
371
372
    #[test]
373
    fn roundtrip_exif_icc() {
374
        // create a 2x2 8-bit image buffer containing a white diagonal
375
        let img = [255u8, 0, 0, 255];
376
377
        let exif = vec![1, 2, 3];
378
        let icc = vec![4, 5, 6];
379
380
        // encode it into a memory buffer
381
        let mut encoded_img = Vec::new();
382
        {
383
            let mut encoder = JpegEncoder::new_with_quality(&mut encoded_img, 100);
384
385
            encoder.set_exif_metadata(exif.clone()).unwrap();
386
            encoder.set_icc_profile(icc.clone()).unwrap();
387
388
            encoder
389
                .write_image(&img[..], 2, 2, ExtendedColorType::L8)
390
                .expect("Could not encode image");
391
        }
392
393
        let mut decoder =
394
            JpegDecoder::new(Cursor::new(encoded_img)).expect("Could not decode image");
395
        let decoded_exif = decoder
396
            .exif_metadata()
397
            .expect("Error decoding Exif")
398
            .expect("Exif is empty");
399
        assert_eq!(exif, decoded_exif);
400
        let decoded_icc = decoder
401
            .icc_profile()
402
            .expect("Error decoding ICC")
403
            .expect("ICC is empty");
404
        assert_eq!(icc, decoded_icc);
405
    }
406
407
    #[test]
408
    fn test_image_too_large() {
409
        // JPEG cannot encode images larger than 65,535×65,535
410
        // create a 65,536×1 8-bit black image buffer
411
        let img = [0; 65_536];
412
        // Try to encode an image that is too large
413
        let mut encoded = Vec::new();
414
        let encoder = JpegEncoder::new_with_quality(&mut encoded, 100);
415
        let result = encoder.write_image(&img, 65_536, 1, ExtendedColorType::L8);
416
        match result {
417
            Err(ImageError::Encoding(_)) => (),
418
            other => {
419
                panic!(
420
                    "Encoding an image that is too large should return an EncodingError \
421
                                it returned {other:?} instead"
422
                )
423
            }
424
        }
425
    }
426
427
    #[test]
428
    fn check_color_types() {
429
        const ALL: &[ColorType] = &[
430
            ColorType::L8,
431
            ColorType::L16,
432
            ColorType::La8,
433
            ColorType::Rgb8,
434
            ColorType::Rgba8,
435
            ColorType::La16,
436
            ColorType::Rgb16,
437
            ColorType::Rgba16,
438
            ColorType::Rgb32F,
439
            ColorType::Rgba32F,
440
        ];
441
442
        for color in ALL {
443
            let image = DynamicImage::new(1, 1, *color);
444
445
            image
446
                .write_to(&mut Cursor::new(vec![]), ImageFormat::Jpeg)
447
                .expect("supported or converted");
448
        }
449
    }
450
451
    #[cfg(feature = "benchmarks")]
452
    #[bench]
453
    fn bench_jpeg_encoder_new(b: &mut Bencher) {
454
        b.iter(|| {
455
            let mut y = vec![];
456
            let _x = JpegEncoder::new(&mut y);
457
        });
458
    }
459
}