1use 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
39pub struct EncoderResult<U: PixelType> {
43 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#[allow(clippy::struct_excessive_bools)]
60pub struct JxlEncoder<'prl, 'mm> {
61 enc: *mut jpegxl_sys::encoder::encode::JxlEncoder,
63 options_ptr: *mut JxlEncoderFrameSettings,
65
66 pub has_alpha: bool,
70 pub lossless: Option<bool>,
74 pub speed: EncoderSpeed,
78 pub quality: f32,
87 pub use_container: bool,
93 pub uses_original_profile: bool,
102 pub decoding_speed: i64,
106 pub init_buffer_size: usize,
111
112 pub color_encoding: Option<ColorEncoding>,
116
117 pub target_intensity: Option<f32>,
120
121 pub parallel_runner: Option<&'prl dyn ParallelRunner>,
125
126 use_box: bool,
128
129 #[allow(dead_code)]
131 memory_manager: Option<&'mm dyn MemoryManager>,
132}
133
134#[bon]
135impl<'prl, 'mm> JxlEncoder<'prl, 'mm> {
136 #[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 #[allow(dead_code)]
195 pub fn jpeg_quality(self, quality: f32) -> JxlEncoderBuilder<'prl, 'mm, SetQuality<S>>
196 where
197 S::Quality: IsUnset,
198 {
199 self.quality(unsafe { JxlEncoderDistanceFromQuality(quality) })
201 }
202}
203
204impl JxlEncoder<'_, '_> {
206 #[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 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 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 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 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 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
389impl<'prl, 'mm> JxlEncoder<'prl, 'mm> {
391 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 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 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 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 self.check_enc_status(unsafe { JxlEncoderStoreJPEGMetadata(self.enc, true.into()) })?;
465
466 self.add_jpeg_frame(data)?;
467 self.start_encoding()
468 }
469
470 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 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
511unsafe impl Send for JxlEncoder<'_, '_> {}
521
522pub fn encoder_builder<'prl, 'mm>() -> JxlEncoderBuilder<'prl, 'mm> {
524 JxlEncoder::builder()
525}
526
527#[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}