Skip to main content

jpegxl_rs/
encode.rs

1/*
2This file is part of jpegxl-rs.
3
4jpegxl-rs is free software: you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9jpegxl-rs is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License
15along with jpegxl-rs.  If not, see <https://www.gnu.org/licenses/>.
16*/
17
18//! Encoder of JPEG XL format
19
20use std::{marker::PhantomData, mem::MaybeUninit, ops::Deref, ptr::null};
21
22use bon::bon;
23#[allow(clippy::wildcard_imports)]
24use jpegxl_sys::encoder::encode::*;
25
26use crate::{
27    common::PixelType, errors::EncodeError, memory::MemoryManager, parallel::ParallelRunner,
28};
29
30mod options;
31pub use options::*;
32
33mod metadata;
34pub use metadata::*;
35
36mod frame;
37pub use frame::*;
38
39// MARK: Utility types
40
41/// Encoder result
42pub struct EncoderResult<U: PixelType> {
43    /// Output binary data
44    pub data: Vec<u8>,
45    _pixel_type: PhantomData<U>,
46}
47
48impl<U: PixelType> Deref for EncoderResult<U> {
49    type Target = [u8];
50
51    fn deref(&self) -> &Self::Target {
52        self.data.as_ref()
53    }
54}
55
56// MARK: Encoder
57
58/// JPEG XL Encoder
59#[allow(clippy::struct_excessive_bools)]
60pub struct JxlEncoder<'prl, 'mm> {
61    /// Opaque pointer to the underlying encoder
62    enc: *mut jpegxl_sys::encoder::encode::JxlEncoder,
63    /// Opaque pointer to the encoder options
64    options_ptr: *mut JxlEncoderFrameSettings,
65
66    /// Set alpha channel
67    ///
68    /// Default: false
69    pub has_alpha: bool,
70    /// Set lossless
71    ///
72    /// Default: false
73    pub lossless: Option<bool>,
74    /// Set speed
75    ///
76    /// Default: `Squirrel` (7).
77    pub speed: EncoderSpeed,
78    /// Set quality for lossy compression: target max butteraugli distance, lower = higher quality
79    ///
80    ///  Range: 0 .. 15.<br />
81    ///    0.0 = mathematically lossless (however, use `lossless` to use true lossless). <br />
82    ///    1.0 = visually lossless. <br />
83    ///    Recommended range: 0.5 .. 3.0. <br />
84    ///    Default value: 1.0. <br />
85    ///    If `lossless` is set to `true`, this value is unused and implied to be 0.
86    pub quality: f32,
87    /// Configure the encoder to use the JPEG XL container format
88    ///
89    /// Using the JPEG XL container format allows one to store metadata such as JPEG reconstruction;
90    /// but it adds a few bytes to the encoded file for container headers
91    /// even if there is no extra metadata.
92    pub use_container: bool,
93    /// Configure the encoder to use the original color profile
94    ///
95    /// If the input image has a color profile, it will be used for the encoded image.
96    /// Otherwise, an internal fixed color profile is chosen (which should be smaller).
97    ///
98    /// When lossless re-compressing JPEG image, you must set this to true.
99    ///
100    /// Default: `false`
101    pub uses_original_profile: bool,
102    /// Set the decoding speed tier
103    ///
104    /// Minimum is 0 (highest quality), and maximum is 4 (lowest quality). Default is 0.
105    pub decoding_speed: i64,
106    /// Set initial output buffer size in bytes.
107    /// Anything less than 32 bytes will be rounded up to 32 bytes.
108    ///
109    /// Default: 512 KiB
110    pub init_buffer_size: usize,
111
112    /// Set color encoding
113    ///
114    /// Default: sRGB for int, Linear sRGB for float
115    pub color_encoding: Option<ColorEncoding>,
116
117    /// Set HDR target intensity.
118    /// Specify the target intensity in nits for 1.0 value
119    pub target_intensity: Option<f32>,
120
121    /// Set parallel runner
122    ///
123    /// Default: `None`, indicating single thread execution
124    pub parallel_runner: Option<&'prl dyn ParallelRunner>,
125
126    /// Whether box is used in encoder
127    use_box: bool,
128
129    /// Set memory manager
130    #[allow(dead_code)]
131    memory_manager: Option<&'mm dyn MemoryManager>,
132}
133
134#[bon]
135impl<'prl, 'mm> JxlEncoder<'prl, 'mm> {
136    /// Build a [`JxlEncoder`]
137    ///
138    /// # Errors
139    /// Return [`EncodeError::CannotCreateEncoder`] if it fails to create the encoder
140    #[builder(derive(Clone))]
141    pub fn new(
142        memory_manager: Option<&'mm dyn MemoryManager>,
143        #[builder(default)] has_alpha: bool,
144        lossless: Option<bool>,
145        #[builder(default)] speed: EncoderSpeed,
146        #[builder(default = 1.0)] quality: f32,
147        #[builder(default)] use_container: bool,
148        #[builder(default)] uses_original_profile: bool,
149        #[builder(default)] decoding_speed: i64,
150        init_buffer_size: Option<usize>,
151        color_encoding: Option<ColorEncoding>,
152        target_intensity: Option<f32>,
153        parallel_runner: Option<&'prl dyn ParallelRunner>,
154        #[builder(default)] use_box: bool,
155    ) -> Result<Self, EncodeError> {
156        let enc = unsafe {
157            memory_manager.map_or_else(
158                || JxlEncoderCreate(null()),
159                |mm| JxlEncoderCreate(&mm.manager()),
160            )
161        };
162
163        if enc.is_null() {
164            return Err(EncodeError::CannotCreateEncoder);
165        }
166
167        let options_ptr = unsafe { JxlEncoderFrameSettingsCreate(enc, null()) };
168
169        Ok(Self {
170            enc,
171            options_ptr,
172            has_alpha,
173            lossless,
174            speed,
175            quality,
176            use_container,
177            uses_original_profile,
178            decoding_speed,
179            init_buffer_size: init_buffer_size.map_or(512 * 1024, |v| if v < 32 { 32 } else { v }),
180            color_encoding,
181            target_intensity,
182            parallel_runner,
183            use_box,
184            memory_manager,
185        })
186    }
187}
188
189use jxl_encoder_builder::{IsUnset, SetQuality, State};
190
191impl<'prl, 'mm, S: State> JxlEncoderBuilder<'prl, 'mm, S> {
192    /// Set the `quality` parameter from a JPEG-style quality factor (0-100, higher is better
193    /// quality).
194    #[allow(dead_code)]
195    pub fn jpeg_quality(self, quality: f32) -> JxlEncoderBuilder<'prl, 'mm, SetQuality<S>>
196    where
197        S::Quality: IsUnset,
198    {
199        // SAFETY: the C API has no safety requirements.
200        self.quality(unsafe { JxlEncoderDistanceFromQuality(quality) })
201    }
202}
203
204// MARK: Private helper functions
205impl JxlEncoder<'_, '_> {
206    /// Error mapping from underlying C const to [`EncodeError`] enum
207    #[track_caller]
208    #[cfg_attr(coverage_nightly, coverage(off))]
209    fn check_enc_status(&self, status: JxlEncoderStatus) -> Result<(), EncodeError> {
210        match status {
211            JxlEncoderStatus::Success => Ok(()),
212            JxlEncoderStatus::Error => match unsafe { JxlEncoderGetError(self.enc) } {
213                JxlEncoderError::OK => unreachable!(),
214                JxlEncoderError::Generic => Err(EncodeError::GenericError),
215                JxlEncoderError::OutOfMemory => Err(EncodeError::OutOfMemory),
216                JxlEncoderError::Jbrd => Err(EncodeError::Jbrd),
217                JxlEncoderError::BadInput => Err(EncodeError::BadInput),
218                JxlEncoderError::NotSupported => Err(EncodeError::NotSupported),
219                JxlEncoderError::ApiUsage => Err(EncodeError::ApiUsage),
220            },
221            JxlEncoderStatus::NeedMoreOutput => Err(EncodeError::NeedMoreOutput),
222        }
223    }
224
225    // Set options
226    fn set_options(&self) -> Result<(), EncodeError> {
227        self.check_enc_status(unsafe {
228            JxlEncoderUseContainer(self.enc, self.use_container.into())
229        })?;
230        if let Some(lossless) = self.lossless {
231            self.check_enc_status(unsafe {
232                JxlEncoderSetFrameLossless(self.options_ptr, lossless.into())
233            })?;
234        }
235        self.check_enc_status(unsafe {
236            JxlEncoderFrameSettingsSetOption(
237                self.options_ptr,
238                JxlEncoderFrameSettingId::Effort,
239                self.speed as _,
240            )
241        })?;
242        self.check_enc_status(unsafe {
243            JxlEncoderSetFrameDistance(self.options_ptr, self.quality)
244        })?;
245        self.check_enc_status(unsafe {
246            JxlEncoderFrameSettingsSetOption(
247                self.options_ptr,
248                JxlEncoderFrameSettingId::DecodingSpeed,
249                self.decoding_speed,
250            )
251        })?;
252
253        Ok(())
254    }
255
256    // Setup the encoder
257    fn setup_encoder(
258        &self,
259        width: u32,
260        height: u32,
261        (bits, exp): (u32, u32),
262        has_alpha: bool,
263    ) -> Result<(), EncodeError> {
264        if let Some(runner) = self.parallel_runner {
265            unsafe {
266                self.check_enc_status(JxlEncoderSetParallelRunner(
267                    self.enc,
268                    runner.runner(),
269                    runner.as_opaque_ptr(),
270                ))?;
271            }
272        }
273
274        self.set_options()?;
275
276        let mut basic_info = unsafe {
277            let mut info = MaybeUninit::uninit();
278            JxlEncoderInitBasicInfo(info.as_mut_ptr());
279            info.assume_init()
280        };
281
282        basic_info.xsize = width;
283        basic_info.ysize = height;
284        basic_info.have_container = self.use_container.into();
285        basic_info.uses_original_profile = self.uses_original_profile.into();
286
287        basic_info.bits_per_sample = bits;
288        basic_info.exponent_bits_per_sample = exp;
289
290        if has_alpha {
291            basic_info.num_extra_channels = 1;
292            basic_info.alpha_bits = bits;
293            basic_info.alpha_exponent_bits = exp;
294        } else {
295            basic_info.num_extra_channels = 0;
296            basic_info.alpha_bits = 0;
297            basic_info.alpha_exponent_bits = 0;
298        }
299
300        if let Some(ColorEncoding::SrgbLuma | ColorEncoding::LinearSrgbLuma) = self.color_encoding {
301            basic_info.num_color_channels = 1;
302        }
303
304        if let Some(target_intensity) = self.target_intensity {
305            basic_info.intensity_target = target_intensity;
306        }
307
308        if let Some(pr) = self.parallel_runner {
309            pr.callback_basic_info(&basic_info);
310        }
311
312        self.check_enc_status(unsafe { JxlEncoderSetBasicInfo(self.enc, &raw const basic_info) })?;
313
314        if let Some(color_encoding) = &self.color_encoding {
315            self.check_enc_status(unsafe {
316                JxlEncoderSetColorEncoding(self.enc, &color_encoding.into())
317            })?;
318        }
319        Ok(())
320    }
321
322    // Add a frame
323    fn add_frame<T: PixelType>(&self, frame: &EncoderFrame<T>) -> Result<(), EncodeError> {
324        self.check_enc_status(unsafe {
325            JxlEncoderAddImageFrame(
326                self.options_ptr,
327                &frame.pixel_format(),
328                frame.data.as_ptr().cast(),
329                std::mem::size_of_val(frame.data),
330            )
331        })
332    }
333
334    // Add a frame from JPEG raw data
335    fn add_jpeg_frame(&self, data: &[u8]) -> Result<(), EncodeError> {
336        self.check_enc_status(unsafe {
337            JxlEncoderAddJPEGFrame(
338                self.options_ptr,
339                data.as_ptr().cast(),
340                std::mem::size_of_val(data),
341            )
342        })
343    }
344
345    fn internal(&mut self) -> Result<Vec<u8>, EncodeError> {
346        unsafe { JxlEncoderCloseInput(self.enc) };
347
348        let mut buffer = vec![0; self.init_buffer_size];
349        let mut next_out = buffer.as_mut_ptr().cast();
350        let mut avail_out = buffer.len();
351
352        let mut status;
353        loop {
354            status =
355                unsafe { JxlEncoderProcessOutput(self.enc, &raw mut next_out, &raw mut avail_out) };
356
357            if status != JxlEncoderStatus::NeedMoreOutput {
358                break;
359            }
360
361            unsafe {
362                let offset = next_out.offset_from(buffer.as_ptr());
363                debug_assert!(offset >= 0);
364
365                buffer.resize(buffer.len() * 2, 0);
366                next_out = buffer.as_mut_ptr().offset(offset);
367                avail_out = buffer.len().wrapping_add_signed(-offset);
368            }
369        }
370        buffer.truncate(next_out as usize - buffer.as_ptr() as usize);
371        self.check_enc_status(status)?;
372
373        unsafe { JxlEncoderReset(self.enc) };
374        self.options_ptr = unsafe { JxlEncoderFrameSettingsCreate(self.enc, null()) };
375
376        buffer.shrink_to_fit();
377        Ok(buffer)
378    }
379
380    // Start encoding
381    fn start_encoding<U: PixelType>(&mut self) -> Result<EncoderResult<U>, EncodeError> {
382        Ok(EncoderResult {
383            data: self.internal()?,
384            _pixel_type: PhantomData,
385        })
386    }
387}
388
389// MARK: Public interface
390impl<'prl, 'mm> JxlEncoder<'prl, 'mm> {
391    /// Set a specific encoder frame setting
392    ///
393    /// # Errors
394    /// Return [`EncodeError`] if it fails to set frame option
395    pub fn set_frame_option(
396        &mut self,
397        option: JxlEncoderFrameSettingId,
398        value: i64,
399    ) -> Result<(), EncodeError> {
400        self.check_enc_status(unsafe {
401            JxlEncoderFrameSettingsSetOption(self.options_ptr, option, value)
402        })
403    }
404
405    /// Return a wrapper type for adding multiple frames to the encoder
406    ///
407    /// # Errors
408    /// Return [`EncodeError`] if it fails to set up the encoder
409    pub fn multiple<'enc, U: PixelType>(
410        &'enc mut self,
411        width: u32,
412        height: u32,
413    ) -> Result<MultiFrames<'enc, 'prl, 'mm, U>, EncodeError> {
414        self.setup_encoder(width, height, U::bits_per_sample(), self.has_alpha)?;
415        Ok(MultiFrames::<'enc, 'prl, 'mm, U>(self, PhantomData))
416    }
417
418    /// Add a metadata box to the encoder
419    ///
420    /// # Errors
421    /// Return [`EncodeError`] if it fails to add metadata
422    pub fn add_metadata(&mut self, metadata: &Metadata, compress: bool) -> Result<(), EncodeError> {
423        let (&t, &data) = match metadata {
424            Metadata::Exif(data) => (b"Exif", data),
425            Metadata::Xmp(data) => (b"xml ", data),
426            Metadata::Jumb(data) => (b"jumb", data),
427            Metadata::Custom(t, data) => (t, data),
428        };
429        if !self.use_box {
430            self.check_enc_status(unsafe { JxlEncoderUseBoxes(self.enc) })?;
431            self.use_box = true;
432        }
433        self.check_enc_status(unsafe {
434            JxlEncoderAddBox(
435                self.enc,
436                &Metadata::box_type(t),
437                data.as_ptr().cast(),
438                data.len(),
439                compress.into(),
440            )
441        })
442    }
443
444    /// Encode a JPEG XL image from existing raw JPEG data
445    ///
446    /// Note: Only support output pixel type of `u8`. Ignore alpha channel settings
447    ///
448    /// # Errors
449    /// Return [`EncodeError`] if the internal encoder fails to encode
450    pub fn encode_jpeg(&mut self, data: &[u8]) -> Result<EncoderResult<u8>, EncodeError> {
451        if let Some(runner) = self.parallel_runner {
452            unsafe {
453                self.check_enc_status(JxlEncoderSetParallelRunner(
454                    self.enc,
455                    runner.runner(),
456                    runner.as_opaque_ptr(),
457                ))?;
458            }
459        }
460
461        self.set_options()?;
462
463        // If using container format, store JPEG reconstruction metadata
464        self.check_enc_status(unsafe { JxlEncoderStoreJPEGMetadata(self.enc, true.into()) })?;
465
466        self.add_jpeg_frame(data)?;
467        self.start_encoding()
468    }
469
470    /// Encode a JPEG XL image from pixels
471    ///
472    /// Note: Use RGB(3) channels, native endianness and no alignment.
473    /// Ignore alpha channel settings
474    ///
475    /// # Errors
476    /// Return [`EncodeError`] if the internal encoder fails to encode
477    pub fn encode<T: PixelType, U: PixelType>(
478        &mut self,
479        data: &[T],
480        width: u32,
481        height: u32,
482    ) -> Result<EncoderResult<U>, EncodeError> {
483        self.setup_encoder(width, height, U::bits_per_sample(), self.has_alpha)?;
484        self.add_frame(&EncoderFrame::new(data))?;
485        self.start_encoding::<U>()
486    }
487
488    /// Encode a JPEG XL image from a frame.
489    /// See [`EncoderFrame`] for custom options of the original pixels.
490    ///
491    /// # Errors
492    /// Return [`EncodeError`] if the internal encoder fails to encode
493    pub fn encode_frame<T: PixelType, U: PixelType>(
494        &mut self,
495        frame: &EncoderFrame<T>,
496        width: u32,
497        height: u32,
498    ) -> Result<EncoderResult<U>, EncodeError> {
499        self.setup_encoder(width, height, U::bits_per_sample(), self.has_alpha)?;
500        self.add_frame(frame)?;
501        self.start_encoding::<U>()
502    }
503}
504
505impl Drop for JxlEncoder<'_, '_> {
506    fn drop(&mut self) {
507        unsafe { JxlEncoderDestroy(self.enc) };
508    }
509}
510
511// SAFETY: JxlEncoder can be safely sent between threads. The underlying libjxl
512// encoder does not store references to thread-local state. While libjxl uses a
513// thread-local LCMS context for color management (see lib/jxl/cms/jxl_cms.cc),
514// this context is looked up dynamically via GetContext() on each use, not stored
515// in the encoder. Moving an encoder to another thread will use that thread's
516// LCMS context for subsequent operations.
517//
518// Note: JxlEncoder is NOT Sync because the underlying C API is not safe for
519// concurrent access from multiple threads.
520unsafe impl Send for JxlEncoder<'_, '_> {}
521
522/// Return a [`JxlEncoderBuilder`] with default settings
523pub fn encoder_builder<'prl, 'mm>() -> JxlEncoderBuilder<'prl, 'mm> {
524    JxlEncoder::builder()
525}
526
527// MARK: Tests
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use testresult::TestResult;
532
533    #[test]
534    #[allow(clippy::float_cmp)]
535    fn test_jpeg_quality() -> TestResult {
536        let encoder = encoder_builder().jpeg_quality(100.0).build()?;
537        assert_eq!(encoder.quality, 0.0);
538        let encoder = encoder_builder().jpeg_quality(90.0).build()?;
539        assert_eq!(encoder.quality, 1.0);
540        Ok(())
541    }
542
543    #[test]
544    fn test_usebox() -> TestResult {
545        let mut encoder = encoder_builder().build()?;
546        let metadata = Metadata::Exif(&[0, 1, 2, 3]);
547        encoder.add_metadata(&metadata, true)?;
548        assert!(encoder.use_box);
549        Ok(())
550    }
551}