Skip to main content

mozjpeg/
compress.rs

1use crate::{colorspace::ColorSpace, PixelDensity};
2use crate::colorspace::ColorSpaceExt;
3use crate::component::CompInfo;
4use crate::component::CompInfoExt;
5use crate::errormgr::unwinding_error_mgr;
6use crate::errormgr::ErrorMgr;
7use crate::fail;
8use crate::ffi;
9use crate::ffi::boolean;
10use crate::ffi::jpeg_compress_struct;
11use crate::ffi::DCTSIZE;
12use crate::ffi::JDIMENSION;
13use crate::ffi::JPEG_LIB_VERSION;
14use crate::ffi::J_BOOLEAN_PARAM;
15use crate::ffi::J_INT_PARAM;
16use crate::marker::Marker;
17use crate::qtable::QTable;
18use crate::writedst::DestinationMgr;
19use arrayvec::ArrayVec;
20use std::cmp::min;
21use std::io;
22use std::marker::PhantomPinned;
23use std::mem;
24use std::os::raw::{c_int, c_uchar, c_uint, c_ulong, c_void};
25use std::ptr;
26use std::ptr::addr_of_mut;
27use std::slice;
28
29/// Max sampling factor is 2
30pub const MAX_MCU_HEIGHT: usize = 16;
31/// Codec doesn't allow more channels than this
32pub const MAX_COMPONENTS: usize = 4;
33
34/// Create a new JPEG file from pixels
35///
36/// Wrapper for `jpeg_compress_struct`
37pub struct Compress {
38    cinfo: jpeg_compress_struct,
39
40    /// It's `Box<ErrorMgr>`, but `cinfo` references `own_err`,
41    /// so I need talismans to ward off nasal demons haunting self-referential structs
42    own_err: *mut ErrorMgr,
43    _it_is_self_referential: PhantomPinned,
44}
45
46#[derive(Copy, Clone)]
47pub enum ScanMode {
48    AllComponentsTogether = 0,
49    /// Can flash grayscale or green-tinted images
50    ScanPerComponent = 1,
51    Auto = 2,
52}
53
54pub struct CompressStarted<W> {
55    compress: Compress,
56    /// Safety: sensitive to drop order. Needs to be dropped after `Compress`
57    dest_mgr: DestinationMgr<W>,
58}
59
60impl Compress {
61    /// Compress image using input in this colorspace.
62    ///
63    /// ## Panics
64    ///
65    /// You need to wrap all use of this library in `std::panic::catch_unwind()`
66    ///
67    /// By default errors cause unwind (panic) and unwind through the C code,
68    /// which strictly speaking is not guaranteed to work in Rust (but seems to work fine, at least on x86-64 and ARM).
69    #[must_use]
70    pub fn new(color_space: ColorSpace) -> Self {
71        Self::new_err(unwinding_error_mgr(), color_space)
72    }
73
74    /// Use a specific error handler instead of the default unwinding one.
75    ///
76    /// Note that the error handler must either abort the process or unwind,
77    /// it can't gracefully return due to the design of libjpeg.
78    ///
79    /// `color_space` refers to input color space
80    #[must_use]
81    pub fn new_err(err: Box<ErrorMgr>, color_space: ColorSpace) -> Self {
82        unsafe {
83            let mut newself = Self {
84                cinfo: mem::zeroed(),
85                own_err: Box::into_raw(err),
86                _it_is_self_referential: PhantomPinned,
87            };
88            newself.cinfo.common.err = addr_of_mut!(*newself.own_err);
89
90            let s = mem::size_of_val(&newself.cinfo);
91            ffi::jpeg_CreateCompress(&mut newself.cinfo, JPEG_LIB_VERSION, s);
92
93            newself.cinfo.in_color_space = color_space;
94            newself.cinfo.input_components = color_space.num_components() as c_int;
95            ffi::jpeg_set_defaults(&mut newself.cinfo);
96
97            newself
98        }
99    }
100
101    #[doc(hidden)]
102    #[deprecated(note = "Give a Vec to start_compress instead")]
103    pub fn set_mem_dest(&self) {
104    }
105
106    /// Settings can't be changed after this call. Returns a `CompressStarted` struct that will handle the rest of the writing.
107    ///
108    /// ## Panics
109    ///
110    /// It may panic, like all functions of this library.
111    pub fn start_compress<W: io::Write>(self, writer: W) -> io::Result<CompressStarted<W>> {
112        if !self.components().iter().any(|c| c.h_samp_factor == 1) { return Err(io::Error::new(io::ErrorKind::InvalidInput, "at least one h_samp_factor must be 1")); }
113        if !self.components().iter().any(|c| c.v_samp_factor == 1) { return Err(io::Error::new(io::ErrorKind::InvalidInput, "at least one v_samp_factor must be 1")); }
114
115        // 1bpp, rounded to 4K page
116        let expected_file_size = (self.cinfo.image_width as usize * self.cinfo.image_height as usize / 8 + 4095) & !4095;
117        let write_buffer_capacity = expected_file_size.clamp(1 << 12, 1 << 16);
118
119        let mut started = CompressStarted {
120            compress: self,
121            dest_mgr: DestinationMgr::new(writer, write_buffer_capacity),
122        };
123        unsafe {
124            started.compress.cinfo.dest = started.dest_mgr.iface_c_ptr();
125            ffi::jpeg_start_compress(&mut started.compress.cinfo, boolean::from(true));
126        }
127        Ok(started)
128    }
129}
130
131impl<W> CompressStarted<W> {
132    /// Add a marker to compressed file
133    ///
134    /// Data is max 64KB
135    ///
136    /// ## Panics
137    ///
138    /// It may panic, like all functions of this library.
139    pub fn write_marker(&mut self, marker: Marker, data: &[u8]) {
140        unsafe {
141            ffi::jpeg_write_marker(
142                &mut self.compress.cinfo,
143                marker.into(),
144                data.as_ptr(),
145                data.len() as c_uint,
146            );
147        }
148    }
149
150    /// Add ICC profile to compressed file
151    ///
152    /// ## Panics
153    ///
154    /// It may panic, like all functions of this library.
155    pub fn write_icc_profile(&mut self, data: &[u8]) {
156        const OVERHEAD_LEN: usize = 14;
157        const MAX_BYTES_IN_MARKER: usize = 65533;
158        const MAX_DATA_BYTES_IN_MARKER: usize = MAX_BYTES_IN_MARKER - OVERHEAD_LEN;
159
160        if data.is_empty() {
161            fail(&mut self.compress.cinfo.common, ffi::JERR_BUFFER_SIZE);
162        }
163
164        let chunks = data.chunks(MAX_DATA_BYTES_IN_MARKER);
165        let num_chunks = chunks.len();
166
167        let mut buf = Vec::with_capacity(MAX_BYTES_IN_MARKER.min(data.len() + OVERHEAD_LEN));
168
169        chunks.enumerate().for_each(move |(current_marker, chunk)| {
170            buf.clear();
171            buf.extend_from_slice(b"ICC_PROFILE\0");
172            buf.extend([current_marker as u8, num_chunks as u8]);
173            buf.extend_from_slice(chunk);
174
175            self.write_marker(Marker::APP(2), &buf);
176        });
177    }
178
179    /// Read-only view of component information
180    #[must_use]
181    pub fn components(&self) -> &[CompInfo] {
182        self.compress.components()
183    }
184
185    fn can_write_more_lines(&self) -> bool {
186        self.compress.cinfo.next_scanline < self.compress.cinfo.image_height
187    }
188}
189
190impl Compress {
191    /// Expose components for modification, e.g. to set chroma subsampling
192    pub fn components_mut(&mut self) -> &mut [CompInfo] {
193        if self.cinfo.comp_info.is_null() {
194            return &mut [];
195        }
196        unsafe {
197            slice::from_raw_parts_mut(self.cinfo.comp_info, self.cinfo.num_components as usize)
198        }
199    }
200
201    /// Read-only view of component information
202    #[must_use]
203    pub fn components(&self) -> &[CompInfo] {
204        if self.cinfo.comp_info.is_null() {
205            return &[];
206        }
207        unsafe {
208            slice::from_raw_parts(self.cinfo.comp_info, self.cinfo.num_components as usize)
209        }
210    }
211}
212
213impl<W> CompressStarted<W> {
214    /// Returns Ok(()) if all lines in `image_src` (not necessarily all lines of the image) were written
215    ///
216    /// ## Panics
217    ///
218    /// It may panic, like all functions of this library.
219    pub fn write_scanlines(&mut self, image_src: &[u8]) -> io::Result<()> {
220        if self.compress.cinfo.raw_data_in != 0 ||
221            self.compress.cinfo.input_components <= 0 ||
222            self.compress.cinfo.image_width == 0 {
223            return Err(io::ErrorKind::InvalidInput.into());
224        }
225
226        let byte_width = self.compress.cinfo.image_width as usize * self.compress.cinfo.input_components as usize;
227        for rows in image_src.chunks(MAX_MCU_HEIGHT * byte_width) {
228            let mut row_pointers = ArrayVec::<_, MAX_MCU_HEIGHT>::new();
229            for row in rows.chunks_exact(byte_width) {
230                row_pointers.push(row.as_ptr());
231            }
232
233            let mut rows_left = row_pointers.len() as u32;
234            let mut row_pointers = row_pointers.as_ptr();
235            while rows_left > 0 {
236                unsafe {
237                    let rows_written = ffi::jpeg_write_scanlines(
238                        &mut self.compress.cinfo,
239                        row_pointers,
240                        rows_left,
241                    );
242                    debug_assert!(rows_left >= rows_written);
243                    if rows_written == 0 {
244                        return Err(io::ErrorKind::UnexpectedEof.into());
245                    }
246                    rows_left -= rows_written;
247                    row_pointers = row_pointers.add(rows_written as usize);
248                }
249            }
250        }
251        Ok(())
252    }
253
254    /// Advanced. Only possible after `set_raw_data_in()`.
255    /// Write YCbCr blocks pixels instead of usual color space
256    ///
257    /// See `raw_data_in` in libjpeg docs
258    ///
259    /// ## Panic
260    ///
261    /// Panics if raw write wasn't enabled
262    #[track_caller]
263    pub fn write_raw_data(&mut self, image_src: &[&[u8]]) -> bool {
264        if 0 == self.compress.cinfo.raw_data_in {
265            panic!("Raw data not set");
266        }
267
268        let mcu_height = self.compress.cinfo.max_v_samp_factor as usize * DCTSIZE;
269        if mcu_height > MAX_MCU_HEIGHT {
270            panic!("Subsampling factor too large");
271        }
272        assert!(mcu_height > 0);
273
274        let num_components = self.components().len();
275        if num_components > MAX_COMPONENTS || num_components > image_src.len() {
276            panic!("Too many components: declared {}, got {}", num_components, image_src.len());
277        }
278
279        for (ci, comp_info) in self.components().iter().enumerate() {
280            if comp_info.row_stride() * comp_info.col_stride() > image_src[ci].len() {
281                panic!("Bitmap too small. Expected {}x{}, got {}", comp_info.row_stride(), comp_info.col_stride(), image_src[ci].len());
282            }
283        }
284
285        let mut start_row = self.compress.cinfo.next_scanline as usize;
286        while self.can_write_more_lines() {
287            unsafe {
288                let mut row_ptrs = [[ptr::null::<u8>(); MAX_MCU_HEIGHT]; MAX_COMPONENTS];
289
290                for ((comp_info, &image_src), comp_row_ptrs) in self.components().iter().zip(image_src).zip(row_ptrs.iter_mut()) {
291                    let row_stride = comp_info.row_stride();
292
293                    let input_height = image_src.len() / row_stride;
294
295                    let comp_start_row = start_row * comp_info.v_samp_factor as usize
296                        / self.compress.cinfo.max_v_samp_factor as usize;
297                    let comp_height = min(
298                        input_height - comp_start_row,
299                        DCTSIZE * comp_info.v_samp_factor as usize,
300                    );
301                    assert!(comp_height >= 8);
302
303                    // row_ptrs were initialized to null
304                    for (src_row, row_ptr) in image_src.chunks_exact(row_stride).skip(comp_start_row).take(comp_height).zip(comp_row_ptrs.iter_mut()) {
305                        *row_ptr = src_row.as_ptr();
306                    }
307                }
308
309                let comp_ptrs: [*const *const u8; MAX_COMPONENTS] = std::array::from_fn(|ci| row_ptrs[ci].as_ptr());
310                let rows_written = ffi::jpeg_write_raw_data(&mut self.compress.cinfo, comp_ptrs.as_ptr(), mcu_height as u32) as usize;
311                if 0 == rows_written {
312                    return false;
313                }
314                start_row += rows_written;
315            }
316        }
317        true
318    }
319}
320
321impl Compress {
322    /// Set color space of JPEG being written, different from input color space
323    ///
324    /// See `jpeg_set_colorspace` in libjpeg docs
325    pub fn set_color_space(&mut self, color_space: ColorSpace) {
326        unsafe {
327            ffi::jpeg_set_colorspace(&mut self.cinfo, color_space);
328        }
329    }
330
331    /// Image size of the input
332    pub fn set_size(&mut self, width: usize, height: usize) {
333        self.cinfo.image_width = width as JDIMENSION;
334        self.cinfo.image_height = height as JDIMENSION;
335    }
336
337    /// libjpeg's `input_gamma` = image gamma of input image
338    #[deprecated(note = "it doesn't do anything")]
339    pub fn set_gamma(&mut self, gamma: f64) {
340        self.cinfo.input_gamma = gamma;
341    }
342
343    /// Sets pixel density of an image in the JFIF APP0 segment[^note].
344    /// If this method is not called, the resulting JPEG will have a default
345    /// pixel aspect ratio of 1x1.
346    ///
347    /// [^note]: This method is not related to EXIF-based intrinsic image sizing,
348    /// and does not affect rendering in browsers.
349    pub fn set_pixel_density(&mut self, density: PixelDensity) {
350        self.cinfo.density_unit = density.unit as u8;
351        self.cinfo.X_density = density.x;
352        self.cinfo.Y_density = density.y;
353    }
354
355    /// If true, it will use MozJPEG's scan optimization. Makes progressive image files smaller.
356    pub fn set_optimize_scans(&mut self, opt: bool) {
357        unsafe {
358            ffi::jpeg_c_set_bool_param(&mut self.cinfo, J_BOOLEAN_PARAM::JBOOLEAN_OPTIMIZE_SCANS, boolean::from(opt));
359        }
360        if !opt {
361            self.cinfo.scan_info = ptr::null();
362        }
363    }
364
365    /// If 1-100 (non-zero), it will use MozJPEG's smoothing.
366    pub fn set_smoothing_factor(&mut self, smoothing_factor: u8) {
367        self.cinfo.smoothing_factor = c_int::from(smoothing_factor);
368    }
369
370    /// Set to `false` to make files larger for no reason
371    pub fn set_optimize_coding(&mut self, opt: bool) {
372        self.cinfo.optimize_coding = boolean::from(opt);
373    }
374
375    /// Specifies whether multiple scans should be considered during trellis
376    /// quantization.
377    pub fn set_use_scans_in_trellis(&mut self, opt: bool) {
378        unsafe {
379            ffi::jpeg_c_set_bool_param(&mut self.cinfo, J_BOOLEAN_PARAM::JBOOLEAN_USE_SCANS_IN_TRELLIS, boolean::from(opt));
380        }
381    }
382
383    /// You can only turn it on
384    pub fn set_progressive_mode(&mut self) {
385        unsafe {
386            ffi::jpeg_simple_progression(&mut self.cinfo);
387        }
388    }
389
390    /// One scan for all components looks best. Other options may flash grayscale or green images.
391    pub fn set_scan_optimization_mode(&mut self, mode: ScanMode) {
392        unsafe {
393            ffi::jpeg_c_set_int_param(&mut self.cinfo, J_INT_PARAM::JINT_DC_SCAN_OPT_MODE, mode as c_int);
394            ffi::jpeg_set_defaults(&mut self.cinfo);
395        }
396    }
397
398    /// Reset to libjpeg v6 settings
399    ///
400    /// It gives files identical with libjpeg-turbo
401    pub fn set_fastest_defaults(&mut self) {
402        unsafe {
403            ffi::jpeg_c_set_int_param(&mut self.cinfo, J_INT_PARAM::JINT_COMPRESS_PROFILE, ffi::JINT_COMPRESS_PROFILE_VALUE::JCP_FASTEST as c_int);
404            ffi::jpeg_set_defaults(&mut self.cinfo);
405        }
406    }
407
408    /// Advanced. See `raw_data_in` in libjpeg docs.
409    pub fn set_raw_data_in(&mut self, opt: bool) {
410        self.cinfo.raw_data_in = boolean::from(opt);
411    }
412
413    /// Set image quality. Values 60-80 are recommended.
414    pub fn set_quality(&mut self, quality: f32) {
415        unsafe {
416            ffi::jpeg_set_quality(&mut self.cinfo, quality as c_int, boolean::from(false));
417        }
418    }
419
420    /// Instead of quality setting, use a specific quantization table.
421    pub fn set_luma_qtable(&mut self, qtable: &QTable) {
422        unsafe {
423            ffi::jpeg_add_quant_table(&mut self.cinfo, 0, qtable.as_ptr(), 100, 1);
424        }
425    }
426
427    /// Instead of quality setting, use a specific quantization table for color.
428    pub fn set_chroma_qtable(&mut self, qtable: &QTable) {
429        unsafe {
430            ffi::jpeg_add_quant_table(&mut self.cinfo, 1, qtable.as_ptr(), 100, 1);
431        }
432    }
433
434    /// Sets chroma subsampling, separately for Cb and Cr channels.
435    /// Instead of setting samples per pixel, like in `cinfo`'s `x_samp_factor`,
436    /// it sets size of chroma "pixels" per luma pixel.
437    ///
438    /// * `(1,1), (1,1)` == 4:4:4
439    /// * `(2,1), (2,1)` == 4:2:2
440    /// * `(2,2), (2,2)` == 4:2:0
441    pub fn set_chroma_sampling_pixel_sizes(&mut self, cb: (u8, u8), cr: (u8, u8)) {
442        let max_sampling_h = cb.0.max(cr.0);
443        let max_sampling_v = cb.1.max(cr.1);
444
445        let px_sizes = [(1, 1), cb, cr];
446        for (c, (h, v)) in self.components_mut().iter_mut().zip(px_sizes) {
447            c.h_samp_factor = (max_sampling_h / h).into();
448            c.v_samp_factor = (max_sampling_v / v).into();
449        }
450    }
451}
452
453impl<W: io::Write> CompressStarted<W> {
454    /// Finalize compression.
455    /// In case of progressive files, this may actually start processing.
456    ///
457    /// ## Panics
458    ///
459    /// It may panic, like all functions of this library.
460    #[inline]
461    pub fn finish(mut self) -> io::Result<W> {
462        unsafe {
463            ffi::jpeg_finish_compress(&mut self.compress.cinfo);
464        }
465        self.compress.cinfo.dest = ptr::null_mut();
466        drop(self.compress);
467        Ok(self.dest_mgr.into_inner())
468    }
469
470    #[doc(hidden)]
471    #[deprecated(note = "use finish(); it now returns a writer given to start_compress()")]
472    pub fn finish_compress(self) -> io::Result<W> {
473        self.finish()
474    }
475
476    /// Give up writing, return incomplete result
477    #[cold]
478    pub fn abort(mut self) -> W {
479        self.compress.cinfo.dest = ptr::null_mut();
480        self.dest_mgr.into_inner()
481    }
482}
483
484impl Drop for Compress {
485    #[inline]
486    fn drop(&mut self) {
487        unsafe {
488            self.cinfo.dest = ptr::null_mut();
489            ffi::jpeg_destroy_compress(&mut self.cinfo);
490            // ErrorMgr is destroyed after cinfo can no longer reference it
491            let _ = Box::from_raw(self.own_err);
492        }
493    }
494}
495
496#[test]
497fn write_mem() {
498    let mut cinfo = Compress::new(ColorSpace::JCS_YCbCr);
499
500    assert_eq!(3, cinfo.components().len());
501
502    cinfo.set_size(17, 33);
503
504    #[allow(deprecated)] {
505        cinfo.set_gamma(1.0);
506    }
507
508    cinfo.set_progressive_mode();
509    cinfo.set_scan_optimization_mode(ScanMode::AllComponentsTogether);
510
511    cinfo.set_raw_data_in(true);
512
513    cinfo.set_quality(88.);
514
515    cinfo.set_chroma_sampling_pixel_sizes((1, 1), (1, 1));
516    for c in cinfo.components() {
517        assert_eq!(c.v_samp_factor, 1);
518        assert_eq!(c.h_samp_factor, 1);
519    }
520
521    cinfo.set_chroma_sampling_pixel_sizes((2, 2), (2, 2));
522    for (c, samp) in cinfo.components().iter().zip([2, 1, 1]) {
523        assert_eq!(c.v_samp_factor, samp);
524        assert_eq!(c.h_samp_factor, samp);
525    }
526
527    let mut cinfo = cinfo.start_compress(Vec::new()).unwrap();
528
529    cinfo.write_marker(Marker::APP(2), b"Hello World");
530
531    assert_eq!(24, cinfo.components()[0].row_stride());
532    assert_eq!(40, cinfo.components()[0].col_stride());
533    assert_eq!(16, cinfo.components()[1].row_stride());
534    assert_eq!(24, cinfo.components()[1].col_stride());
535    assert_eq!(16, cinfo.components()[2].row_stride());
536    assert_eq!(24, cinfo.components()[2].col_stride());
537
538    let bitmaps = cinfo
539        .components()
540        .iter()
541        .map(|c| vec![128u8; c.row_stride() * c.col_stride()])
542        .collect::<Vec<_>>();
543
544    assert!(cinfo.write_raw_data(&bitmaps.iter().map(|c| &c[..]).collect::<Vec<_>>()));
545
546    cinfo.finish().unwrap();
547}
548
549#[test]
550fn convert_colorspace() {
551    let mut cinfo = Compress::new(ColorSpace::JCS_RGB);
552    cinfo.set_color_space(ColorSpace::JCS_GRAYSCALE);
553    assert_eq!(1, cinfo.components().len());
554
555    cinfo.set_size(33, 15);
556    cinfo.set_quality(44.);
557
558    let mut cinfo = cinfo.start_compress(Vec::new()).unwrap();
559
560    let scanlines = vec![127u8; 33*15*3];
561    cinfo.write_scanlines(&scanlines).unwrap();
562
563    let res = cinfo.finish().unwrap();
564    assert!(!res.is_empty());
565}