jxl_oxide/
lib.rs

1//! jxl-oxide is a JPEG XL decoder written in pure Rust. It's internally organized into a few
2//! small crates. This crate acts as a blanket and provides a simple interface made from those
3//! crates to decode the actual image.
4//!
5//! # Decoding an image
6//!
7//! Decoding a JPEG XL image starts with constructing [`JxlImage`]. First create a builder using
8//! [`JxlImage::builder`], and use [`open`][JxlImageBuilder::open] to read a file:
9//!
10//! ```no_run
11//! # use jxl_oxide::JxlImage;
12//! let image = JxlImage::builder().open("input.jxl").expect("Failed to read image header");
13//! println!("{:?}", image.image_header()); // Prints the image header
14//! ```
15//!
16//! Or, if you're reading from a reader that implements [`Read`][std::io::Read], you can use
17//! [`read`][JxlImageBuilder::read]:
18//!
19//! ```no_run
20//! # use jxl_oxide::JxlImage;
21//! # let reader = std::io::empty();
22//! let image = JxlImage::builder().read(reader).expect("Failed to read image header");
23//! println!("{:?}", image.image_header()); // Prints the image header
24//! ```
25//!
26//! In async context, you'll probably want to feed byte buffers directly. In this case, create an
27//! image struct with *uninitialized state* using [`build_uninit`][JxlImageBuilder::build_uninit],
28//! and call [`feed_bytes`][UninitializedJxlImage::feed_bytes] and
29//! [`try_init`][UninitializedJxlImage::try_init]:
30//!
31//! ```no_run
32//! # struct StubReader(&'static [u8]);
33//! # impl StubReader {
34//! #     fn read(&self) -> StubReaderFuture { StubReaderFuture(self.0) }
35//! # }
36//! # struct StubReaderFuture(&'static [u8]);
37//! # impl std::future::Future for StubReaderFuture {
38//! #     type Output = jxl_oxide::Result<&'static [u8]>;
39//! #     fn poll(
40//! #         self: std::pin::Pin<&mut Self>,
41//! #         cx: &mut std::task::Context<'_>,
42//! #     ) -> std::task::Poll<Self::Output> {
43//! #         std::task::Poll::Ready(Ok(self.0))
44//! #     }
45//! # }
46//! #
47//! # use jxl_oxide::{JxlImage, InitializeResult};
48//! # async fn run() -> jxl_oxide::Result<()> {
49//! # let reader = StubReader(&[
50//! #   0xff, 0x0a, 0x30, 0x54, 0x10, 0x09, 0x08, 0x06, 0x01, 0x00, 0x78, 0x00,
51//! #   0x4b, 0x38, 0x41, 0x3c, 0xb6, 0x3a, 0x51, 0xfe, 0x00, 0x47, 0x1e, 0xa0,
52//! #   0x85, 0xb8, 0x27, 0x1a, 0x48, 0x45, 0x84, 0x1b, 0x71, 0x4f, 0xa8, 0x3e,
53//! #   0x8e, 0x30, 0x03, 0x92, 0x84, 0x01,
54//! # ]);
55//! let mut uninit_image = JxlImage::builder().build_uninit();
56//! let image = loop {
57//!     uninit_image.feed_bytes(reader.read().await?);
58//!     match uninit_image.try_init()? {
59//!         InitializeResult::NeedMoreData(uninit) => {
60//!             uninit_image = uninit;
61//!         }
62//!         InitializeResult::Initialized(image) => {
63//!             break image;
64//!         }
65//!     }
66//! };
67//! println!("{:?}", image.image_header()); // Prints the image header
68//! # Ok(())
69//! # }
70//! ```
71//!
72//! `JxlImage` parses the image header and embedded ICC profile (if there's any). Use
73//! [`JxlImage::render_frame`] to render the image.
74//!
75//! ```no_run
76//! # use jxl_oxide::Render;
77//! use jxl_oxide::{JxlImage, RenderResult};
78//!
79//! # fn present_image(_: Render) {}
80//! # fn main() -> jxl_oxide::Result<()> {
81//! # let image = JxlImage::builder().open("input.jxl").unwrap();
82//! for keyframe_idx in 0..image.num_loaded_keyframes() {
83//!     let render = image.render_frame(keyframe_idx)?;
84//!     present_image(render);
85//! }
86//! # Ok(())
87//! # }
88//! ```
89//!
90//! # Color management
91//! jxl-oxide has basic color management support, which enables color transformation between
92//! well-known color encodings and parsing simple, matrix-based ICC profiles. However, jxl-oxide
93//! alone does not support conversion to and from arbitrary ICC profiles, notably CMYK profiles.
94//! This includes converting from embedded ICC profiles.
95//!
96//! Use [`JxlImage::request_color_encoding`] or [`JxlImage::request_icc`] to set color encoding of
97//! rendered images. Conversion to and/or from ICC profiles may occur if you do this; in that case,
98//! external CMS need to be set using [`JxlImage::set_cms`].
99//!
100//! ```no_run
101//! # use jxl_oxide::{EnumColourEncoding, JxlImage, RenderingIntent};
102//! # use jxl_oxide::NullCms as MyCustomCms;
103//! # let reader = std::io::empty();
104//! let mut image = JxlImage::builder().read(reader).expect("Failed to read image header");
105//! image.set_cms(MyCustomCms);
106//!
107//! let color_encoding = EnumColourEncoding::display_p3(RenderingIntent::Perceptual);
108//! image.request_color_encoding(color_encoding);
109//! ```
110//!
111//! External CMS is set to Little CMS 2 by default if `lcms2` feature is enabled. You can
112//! explicitly disable this by setting CMS to [`NullCms`].
113//!
114//! ```no_run
115//! # use jxl_oxide::{JxlImage, NullCms};
116//! # let reader = std::io::empty();
117//! let mut image = JxlImage::builder().read(reader).expect("Failed to read image header");
118//! image.set_cms(NullCms);
119//! ```
120//!
121//! ## Not using `set_cms` for color management
122//! If implementing `ColorManagementSystem` is difficult for your use case, color management can be
123//! done separately using ICC profile of rendered images. [`JxlImage::rendered_icc`] returns ICC
124//! profile for further processing.
125//!
126//! ```no_run
127//! # use jxl_oxide::Render;
128//! use jxl_oxide::{JxlImage, RenderResult};
129//!
130//! # fn present_image_with_cms(_: Render, _: &[u8]) {}
131//! # fn main() -> jxl_oxide::Result<()> {
132//! # let image = JxlImage::builder().open("input.jxl").unwrap();
133//! let icc_profile = image.rendered_icc();
134//! for keyframe_idx in 0..image.num_loaded_keyframes() {
135//!     let render = image.render_frame(keyframe_idx)?;
136//!     present_image_with_cms(render, &icc_profile);
137//! }
138//! # Ok(())
139//! # }
140//! ```
141//!
142//! # Feature flags
143//! - `rayon`: Enable multithreading with Rayon. (*default*)
144//! - `image`: Enable integration with `image` crate.
145//! - `lcms2`: Enable integration with Little CMS 2.
146//! - `moxcms`: Enable integration with `moxcms` crate.
147
148#![cfg_attr(docsrs, feature(doc_auto_cfg))]
149
150use std::sync::Arc;
151
152use jxl_bitstream::{Bitstream, ContainerParser, ParseEvent};
153use jxl_frame::FrameContext;
154use jxl_image::BitDepth;
155use jxl_oxide_common::{Bundle, Name};
156use jxl_render::ImageBuffer;
157use jxl_render::ImageWithRegion;
158use jxl_render::Region;
159use jxl_render::{IndexedFrame, RenderContext};
160
161pub use jxl_color::{ColorEncodingWithProfile, ColorManagementSystem, NullCms, PreparedTransform};
162pub use jxl_frame::header as frame;
163pub use jxl_frame::{Frame, FrameHeader};
164pub use jxl_grid::{AlignedGrid, AllocTracker};
165pub use jxl_image::color::{self, EnumColourEncoding, RenderingIntent};
166pub use jxl_image::{self as image, ExtraChannelType, ImageHeader};
167pub use jxl_jbr as jpeg_bitstream;
168pub use jxl_threadpool::JxlThreadPool;
169
170mod aux_box;
171mod fb;
172pub mod integration;
173#[cfg(feature = "lcms2")]
174mod lcms2;
175#[cfg(feature = "moxcms")]
176mod moxcms;
177
178#[cfg(feature = "lcms2")]
179pub use self::lcms2::Lcms2;
180#[cfg(feature = "moxcms")]
181pub use self::moxcms::Moxcms;
182pub use aux_box::{AuxBoxData, AuxBoxList, RawExif};
183pub use fb::{FrameBuffer, FrameBufferSample, ImageStream};
184
185pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
186
187#[cfg(feature = "rayon")]
188fn default_pool() -> JxlThreadPool {
189    JxlThreadPool::rayon_global()
190}
191
192#[cfg(not(feature = "rayon"))]
193fn default_pool() -> JxlThreadPool {
194    JxlThreadPool::none()
195}
196
197/// JPEG XL image decoder builder.
198#[derive(Debug, Default)]
199pub struct JxlImageBuilder {
200    pool: Option<JxlThreadPool>,
201    tracker: Option<AllocTracker>,
202}
203
204impl JxlImageBuilder {
205    /// Sets a custom thread pool.
206    pub fn pool(mut self, pool: JxlThreadPool) -> Self {
207        self.pool = Some(pool);
208        self
209    }
210
211    /// Sets an allocation tracker.
212    pub fn alloc_tracker(mut self, tracker: AllocTracker) -> Self {
213        self.tracker = Some(tracker);
214        self
215    }
216
217    /// Consumes the builder, and creates an empty, uninitialized JPEG XL image decoder.
218    pub fn build_uninit(self) -> UninitializedJxlImage {
219        UninitializedJxlImage {
220            pool: self.pool.unwrap_or_else(default_pool),
221            tracker: self.tracker,
222            reader: ContainerParser::new(),
223            buffer: Vec::new(),
224            aux_boxes: AuxBoxList::new(),
225        }
226    }
227
228    /// Consumes the builder, and creates a JPEG XL image decoder by reading image from the reader.
229    pub fn read(self, mut reader: impl std::io::Read) -> Result<JxlImage> {
230        let mut uninit = self.build_uninit();
231        let mut buf = vec![0u8; 4096];
232        let mut buf_valid = 0usize;
233        let mut image = loop {
234            let count = reader.read(&mut buf[buf_valid..])?;
235            if count == 0 {
236                return Err(std::io::Error::new(
237                    std::io::ErrorKind::UnexpectedEof,
238                    "reader ended before parsing image header",
239                )
240                .into());
241            }
242            buf_valid += count;
243            let consumed = uninit.feed_bytes(&buf[..buf_valid])?;
244            buf.copy_within(consumed..buf_valid, 0);
245            buf_valid -= consumed;
246
247            match uninit.try_init()? {
248                InitializeResult::NeedMoreData(x) => {
249                    uninit = x;
250                }
251                InitializeResult::Initialized(x) => {
252                    break x;
253                }
254            }
255        };
256
257        while !image.inner.end_of_image {
258            let count = reader.read(&mut buf[buf_valid..])?;
259            if count == 0 {
260                break;
261            }
262            buf_valid += count;
263            let consumed = image.feed_bytes(&buf[..buf_valid])?;
264            buf.copy_within(consumed..buf_valid, 0);
265            buf_valid -= consumed;
266        }
267
268        buf.truncate(buf_valid);
269        image.finalize()?;
270        Ok(image)
271    }
272
273    /// Consumes the builder, and creates a JPEG XL image decoder by reading image from the file.
274    pub fn open(self, path: impl AsRef<std::path::Path>) -> Result<JxlImage> {
275        let file = std::fs::File::open(path)?;
276        self.read(file)
277    }
278}
279
280/// Empty, uninitialized JPEG XL image.
281///
282/// # Examples
283/// ```no_run
284/// # fn read_bytes() -> jxl_oxide::Result<&'static [u8]> { Ok(&[]) }
285/// # use jxl_oxide::{JxlImage, InitializeResult};
286/// # fn main() -> jxl_oxide::Result<()> {
287/// let mut uninit_image = JxlImage::builder().build_uninit();
288/// let image = loop {
289///     let buf = read_bytes()?;
290///     uninit_image.feed_bytes(buf)?;
291///     match uninit_image.try_init()? {
292///         InitializeResult::NeedMoreData(uninit) => {
293///             uninit_image = uninit;
294///         }
295///         InitializeResult::Initialized(image) => {
296///             break image;
297///         }
298///     }
299/// };
300/// println!("{:?}", image.image_header());
301/// # Ok(())
302/// # }
303/// ```
304pub struct UninitializedJxlImage {
305    pool: JxlThreadPool,
306    tracker: Option<AllocTracker>,
307    reader: ContainerParser,
308    buffer: Vec<u8>,
309    aux_boxes: AuxBoxList,
310}
311
312impl UninitializedJxlImage {
313    /// Feeds more data into the decoder.
314    ///
315    /// Returns total consumed bytes from the buffer.
316    pub fn feed_bytes(&mut self, buf: &[u8]) -> Result<usize> {
317        for event in self.reader.feed_bytes(buf) {
318            match event? {
319                ParseEvent::BitstreamKind(_) => {}
320                ParseEvent::Codestream(buf) => {
321                    self.buffer.extend_from_slice(buf);
322                }
323                aux_box_event => {
324                    self.aux_boxes.handle_event(aux_box_event)?;
325                }
326            }
327        }
328        Ok(self.reader.previous_consumed_bytes())
329    }
330
331    /// Returns the internal reader.
332    #[inline]
333    pub fn reader(&self) -> &ContainerParser {
334        &self.reader
335    }
336
337    /// Try to initialize an image with the data fed into so far.
338    ///
339    /// # Returns
340    /// - `Ok(InitializeResult::Initialized(_))` if the initialization was successful,
341    /// - `Ok(InitializeResult::NeedMoreData(_))` if the data was not enough, and
342    /// - `Err(_)` if there was a decode error during the initialization, meaning invalid bitstream
343    ///   was given.
344    pub fn try_init(mut self) -> Result<InitializeResult> {
345        let mut bitstream = Bitstream::new(&self.buffer);
346        let image_header = match ImageHeader::parse(&mut bitstream, ()) {
347            Ok(x) => x,
348            Err(e) if e.unexpected_eof() => {
349                return Ok(InitializeResult::NeedMoreData(self));
350            }
351            Err(e) => {
352                return Err(e.into());
353            }
354        };
355
356        let embedded_icc = if image_header.metadata.colour_encoding.want_icc() {
357            let icc = match jxl_color::icc::read_icc(&mut bitstream) {
358                Ok(x) => x,
359                Err(e) if e.unexpected_eof() => {
360                    return Ok(InitializeResult::NeedMoreData(self));
361                }
362                Err(e) => {
363                    return Err(e.into());
364                }
365            };
366            tracing::debug!("Image has an embedded ICC profile");
367            let icc = jxl_color::icc::decode_icc(&icc)?;
368            Some(icc)
369        } else {
370            None
371        };
372        bitstream.zero_pad_to_byte()?;
373
374        let image_header = Arc::new(image_header);
375        let skip_bytes = if image_header.metadata.preview.is_some() {
376            let frame = match Frame::parse(
377                &mut bitstream,
378                FrameContext {
379                    image_header: image_header.clone(),
380                    tracker: self.tracker.as_ref(),
381                    pool: self.pool.clone(),
382                },
383            ) {
384                Ok(x) => x,
385                Err(e) if e.unexpected_eof() => {
386                    return Ok(InitializeResult::NeedMoreData(self));
387                }
388                Err(e) => {
389                    return Err(e.into());
390                }
391            };
392
393            let bytes_read = bitstream.num_read_bits() / 8;
394            let x = frame.toc().total_byte_size();
395            if self.buffer.len() < bytes_read + x {
396                return Ok(InitializeResult::NeedMoreData(self));
397            }
398
399            x
400        } else {
401            0usize
402        };
403
404        let bytes_read = bitstream.num_read_bits() / 8 + skip_bytes;
405        self.buffer.drain(..bytes_read);
406
407        let render_spot_color = !image_header.metadata.grayscale();
408
409        let mut builder = RenderContext::builder().pool(self.pool.clone());
410        if let Some(icc) = embedded_icc {
411            builder = builder.embedded_icc(icc);
412        }
413        if let Some(tracker) = self.tracker {
414            builder = builder.alloc_tracker(tracker);
415        }
416        #[cfg_attr(not(any(feature = "lcms2", feature = "moxcms")), allow(unused_mut))]
417        let mut ctx = builder.build(image_header.clone())?;
418        #[cfg(feature = "lcms2")]
419        ctx.set_cms(Lcms2);
420        #[cfg(all(not(feature = "lcms2"), feature = "moxcms"))]
421        ctx.set_cms(Moxcms);
422
423        let mut image = JxlImage {
424            pool: self.pool.clone(),
425            reader: self.reader,
426            image_header,
427            ctx,
428            render_spot_color,
429            inner: JxlImageInner {
430                end_of_image: false,
431                buffer: Vec::new(),
432                buffer_offset: bytes_read,
433                frame_offsets: Vec::new(),
434                aux_boxes: self.aux_boxes,
435            },
436        };
437        image.inner.feed_bytes_inner(&mut image.ctx, &self.buffer)?;
438
439        Ok(InitializeResult::Initialized(image))
440    }
441}
442
443/// Initialization result from [`UninitializedJxlImage::try_init`].
444pub enum InitializeResult {
445    /// The data was not enough. Feed more data into the returned image.
446    NeedMoreData(UninitializedJxlImage),
447    /// The image is successfully initialized.
448    Initialized(JxlImage),
449}
450
451/// JPEG XL image.
452#[derive(Debug)]
453pub struct JxlImage {
454    pool: JxlThreadPool,
455    reader: ContainerParser,
456    image_header: Arc<ImageHeader>,
457    ctx: RenderContext,
458    render_spot_color: bool,
459    inner: JxlImageInner,
460}
461
462/// # Constructors and data-feeding methods
463impl JxlImage {
464    /// Creates a decoder builder with default options.
465    pub fn builder() -> JxlImageBuilder {
466        JxlImageBuilder::default()
467    }
468
469    /// Reads an image from the reader with default options.
470    pub fn read_with_defaults(reader: impl std::io::Read) -> Result<JxlImage> {
471        Self::builder().read(reader)
472    }
473
474    /// Opens an image in the filesystem with default options.
475    pub fn open_with_defaults(path: impl AsRef<std::path::Path>) -> Result<JxlImage> {
476        Self::builder().open(path)
477    }
478
479    /// Feeds more data into the decoder.
480    ///
481    /// Returns total consumed bytes from the buffer.
482    pub fn feed_bytes(&mut self, buf: &[u8]) -> Result<usize> {
483        for event in self.reader.feed_bytes(buf) {
484            match event? {
485                ParseEvent::BitstreamKind(_) => {}
486                ParseEvent::Codestream(buf) => {
487                    self.inner.feed_bytes_inner(&mut self.ctx, buf)?;
488                }
489                aux_box_event => {
490                    self.inner.aux_boxes.handle_event(aux_box_event)?;
491                }
492            }
493        }
494        Ok(self.reader.previous_consumed_bytes())
495    }
496
497    /// Signals the end of bitstream.
498    ///
499    /// This is automatically done if `open()` or `read()` is used to decode the image.
500    pub fn finalize(&mut self) -> Result<()> {
501        self.inner.aux_boxes.eof()?;
502        Ok(())
503    }
504}
505
506/// # Image and decoder metadata accessors
507impl JxlImage {
508    /// Returns the image header.
509    #[inline]
510    pub fn image_header(&self) -> &ImageHeader {
511        &self.image_header
512    }
513
514    /// Returns the image width with orientation applied.
515    #[inline]
516    pub fn width(&self) -> u32 {
517        self.image_header.width_with_orientation()
518    }
519
520    /// Returns the image height with orientation applied.
521    #[inline]
522    pub fn height(&self) -> u32 {
523        self.image_header.height_with_orientation()
524    }
525
526    /// Returns the *original* ICC profile embedded in the image.
527    #[inline]
528    pub fn original_icc(&self) -> Option<&[u8]> {
529        self.ctx.embedded_icc()
530    }
531
532    /// Returns the ICC profile that describes rendered images.
533    ///
534    /// The returned profile will change if different color encoding is specified using
535    /// [`request_icc`][Self::request_icc] or
536    /// [`request_color_encoding`][Self::request_color_encoding].
537    pub fn rendered_icc(&self) -> Vec<u8> {
538        let encoding = self.ctx.requested_color_encoding();
539        match encoding.encoding() {
540            color::ColourEncoding::Enum(encoding) => {
541                jxl_color::icc::colour_encoding_to_icc(encoding)
542            }
543            color::ColourEncoding::IccProfile(_) => encoding.icc_profile().to_vec(),
544        }
545    }
546
547    /// Returns the CICP tag of the color encoding of rendered images, if there's any.
548    #[inline]
549    pub fn rendered_cicp(&self) -> Option<[u8; 4]> {
550        let encoding = self.ctx.requested_color_encoding();
551        encoding.encoding().cicp()
552    }
553
554    /// Returns the pixel format of the rendered image.
555    pub fn pixel_format(&self) -> PixelFormat {
556        let encoding = self.ctx.requested_color_encoding();
557        let is_grayscale = encoding.is_grayscale();
558        let has_black = encoding.is_cmyk();
559        let mut has_alpha = false;
560        for ec_info in &self.image_header.metadata.ec_info {
561            if ec_info.is_alpha() {
562                has_alpha = true;
563            }
564        }
565
566        match (is_grayscale, has_black, has_alpha) {
567            (false, false, false) => PixelFormat::Rgb,
568            (false, false, true) => PixelFormat::Rgba,
569            (false, true, false) => PixelFormat::Cmyk,
570            (false, true, true) => PixelFormat::Cmyka,
571            (true, _, false) => PixelFormat::Gray,
572            (true, _, true) => PixelFormat::Graya,
573        }
574    }
575
576    /// Returns what HDR transfer function the image uses, if there's any.
577    ///
578    /// Returns `None` if the image is not HDR one.
579    pub fn hdr_type(&self) -> Option<HdrType> {
580        self.ctx.suggested_hdr_tf().and_then(|tf| match tf {
581            color::TransferFunction::Pq => Some(HdrType::Pq),
582            color::TransferFunction::Hlg => Some(HdrType::Hlg),
583            _ => None,
584        })
585    }
586
587    /// Returns whether the spot color channels will be rendered.
588    #[inline]
589    pub fn render_spot_color(&self) -> bool {
590        self.render_spot_color
591    }
592
593    /// Sets whether the spot colour channels will be rendered.
594    #[inline]
595    pub fn set_render_spot_color(&mut self, render_spot_color: bool) -> &mut Self {
596        if render_spot_color && self.image_header.metadata.grayscale() {
597            tracing::warn!("Spot colour channels are not rendered on grayscale images");
598            return self;
599        }
600        self.render_spot_color = render_spot_color;
601        self
602    }
603
604    /// Returns the list of auxiliary boxes in the JPEG XL container.
605    ///
606    /// The list may contain Exif and XMP metadata.
607    pub fn aux_boxes(&self) -> &AuxBoxList {
608        &self.inner.aux_boxes
609    }
610
611    /// Returns the number of currently loaded keyframes.
612    #[inline]
613    pub fn num_loaded_keyframes(&self) -> usize {
614        self.ctx.loaded_keyframes()
615    }
616
617    /// Returns the number of currently loaded frames, including frames that are not displayed
618    /// directly.
619    #[inline]
620    pub fn num_loaded_frames(&self) -> usize {
621        self.ctx.loaded_frames()
622    }
623
624    /// Returns whether the image is loaded completely, without missing animation keyframes or
625    /// partially loaded frames.
626    #[inline]
627    pub fn is_loading_done(&self) -> bool {
628        self.inner.end_of_image
629    }
630
631    /// Returns frame data by keyframe index.
632    pub fn frame_by_keyframe(&self, keyframe_index: usize) -> Option<&IndexedFrame> {
633        self.ctx.keyframe(keyframe_index)
634    }
635
636    /// Returns the frame header for the given keyframe index, or `None` if the keyframe does not
637    /// exist.
638    pub fn frame_header(&self, keyframe_index: usize) -> Option<&FrameHeader> {
639        let frame = self.ctx.keyframe(keyframe_index)?;
640        Some(frame.header())
641    }
642
643    /// Returns frame data by frame index, including frames that are not displayed directly.
644    ///
645    /// There are some situations where a frame is not displayed directly:
646    /// - It may be marked as reference only, and meant to be only used by other frames.
647    /// - It may contain LF image (which is 8x downsampled version) of another VarDCT frame.
648    /// - Zero duration frame that is not the last frame of image is blended with following frames
649    ///   and displayed together.
650    pub fn frame(&self, frame_idx: usize) -> Option<&IndexedFrame> {
651        self.ctx.frame(frame_idx)
652    }
653
654    /// Returns the offset of frame within codestream, in bytes.
655    pub fn frame_offset(&self, frame_index: usize) -> Option<usize> {
656        self.inner.frame_offsets.get(frame_index).copied()
657    }
658
659    /// Returns the thread pool used by the renderer.
660    #[inline]
661    pub fn pool(&self) -> &JxlThreadPool {
662        &self.pool
663    }
664
665    /// Returns the internal reader.
666    pub fn reader(&self) -> &ContainerParser {
667        &self.reader
668    }
669}
670
671/// # Color management methods
672impl JxlImage {
673    /// Sets color management system implementation to be used by the renderer.
674    #[inline]
675    pub fn set_cms(&mut self, cms: impl ColorManagementSystem + Send + Sync + 'static) {
676        self.ctx.set_cms(cms);
677    }
678
679    /// Requests the decoder to render in specific color encoding, described by an ICC profile.
680    ///
681    /// # Errors
682    /// This function will return an error if it cannot parse the ICC profile.
683    pub fn request_icc(&mut self, icc_profile: &[u8]) -> Result<()> {
684        self.ctx
685            .request_color_encoding(ColorEncodingWithProfile::with_icc(icc_profile)?);
686        Ok(())
687    }
688
689    /// Requests the decoder to render in specific color encoding, described by
690    /// `EnumColourEncoding`.
691    pub fn request_color_encoding(&mut self, color_encoding: EnumColourEncoding) {
692        self.ctx
693            .request_color_encoding(ColorEncodingWithProfile::new(color_encoding))
694    }
695}
696
697/// # Rendering to image buffers
698impl JxlImage {
699    /// Renders the given keyframe.
700    pub fn render_frame(&self, keyframe_index: usize) -> Result<Render> {
701        self.render_frame_cropped(keyframe_index)
702    }
703
704    /// Renders the given keyframe with optional cropping region.
705    pub fn render_frame_cropped(&self, keyframe_index: usize) -> Result<Render> {
706        let image = self.ctx.render_keyframe(keyframe_index)?;
707
708        let image_region = self
709            .ctx
710            .image_region()
711            .apply_orientation(&self.image_header);
712        let frame = self.ctx.keyframe(keyframe_index).unwrap();
713        let frame_header = frame.header();
714        let target_frame_region = image_region.translate(-frame_header.x0, -frame_header.y0);
715
716        let is_cmyk = self.ctx.requested_color_encoding().is_cmyk();
717        let result = Render {
718            keyframe_index,
719            name: frame_header.name.clone(),
720            duration: frame_header.duration,
721            orientation: self.image_header.metadata.orientation,
722            image,
723            extra_channels: self.convert_ec_info(),
724            target_frame_region,
725            color_bit_depth: self.image_header.metadata.bit_depth,
726            is_cmyk,
727            render_spot_color: self.render_spot_color,
728        };
729        Ok(result)
730    }
731
732    /// Renders the currently loading keyframe.
733    pub fn render_loading_frame(&mut self) -> Result<Render> {
734        self.render_loading_frame_cropped()
735    }
736
737    /// Renders the currently loading keyframe with optional cropping region.
738    pub fn render_loading_frame_cropped(&mut self) -> Result<Render> {
739        let (frame, image) = self.ctx.render_loading_keyframe()?;
740        let frame_header = frame.header();
741        let name = frame_header.name.clone();
742        let duration = frame_header.duration;
743
744        let image_region = self
745            .ctx
746            .image_region()
747            .apply_orientation(&self.image_header);
748        let frame = self
749            .ctx
750            .frame(self.ctx.loaded_frames())
751            .or_else(|| self.ctx.frame(self.ctx.loaded_frames() - 1))
752            .unwrap();
753        let frame_header = frame.header();
754        let target_frame_region = image_region.translate(-frame_header.x0, -frame_header.y0);
755
756        let is_cmyk = self.ctx.requested_color_encoding().is_cmyk();
757        let result = Render {
758            keyframe_index: self.ctx.loaded_keyframes(),
759            name,
760            duration,
761            orientation: self.image_header.metadata.orientation,
762            image,
763            extra_channels: self.convert_ec_info(),
764            target_frame_region,
765            color_bit_depth: self.image_header.metadata.bit_depth,
766            is_cmyk,
767            render_spot_color: self.render_spot_color,
768        };
769        Ok(result)
770    }
771
772    /// Returns current cropping region (region of interest).
773    pub fn current_image_region(&self) -> CropInfo {
774        let region = self.ctx.image_region();
775        region.into()
776    }
777
778    /// Sets the cropping region (region of interest).
779    ///
780    /// Subsequent rendering methods will crop the image buffer according to the region.
781    pub fn set_image_region(&mut self, region: CropInfo) -> &mut Self {
782        self.ctx.request_image_region(region.into());
783        self
784    }
785}
786
787/// # JPEG bitstream reconstruction
788impl JxlImage {
789    /// Returns availability and validity of JPEG bitstream reconstruction data.
790    pub fn jpeg_reconstruction_status(&self) -> JpegReconstructionStatus {
791        match self.inner.aux_boxes.jbrd() {
792            AuxBoxData::Data(jbrd) => {
793                let header = jbrd.header();
794                let Ok(exif) = self.inner.aux_boxes.first_exif() else {
795                    return JpegReconstructionStatus::Invalid;
796                };
797                let xml = self.inner.aux_boxes.first_xml();
798
799                if header.expected_icc_len() > 0 {
800                    if !self.image_header.metadata.colour_encoding.want_icc() {
801                        return JpegReconstructionStatus::Invalid;
802                    } else if self.original_icc().is_none() {
803                        return JpegReconstructionStatus::NeedMoreData;
804                    }
805                }
806                if header.expected_exif_len() > 0 {
807                    if exif.is_decoding() {
808                        return JpegReconstructionStatus::NeedMoreData;
809                    } else if exif.is_not_found() {
810                        return JpegReconstructionStatus::Invalid;
811                    }
812                }
813                if header.expected_xmp_len() > 0 {
814                    if xml.is_decoding() {
815                        return JpegReconstructionStatus::NeedMoreData;
816                    } else if xml.is_not_found() {
817                        return JpegReconstructionStatus::Invalid;
818                    }
819                }
820
821                JpegReconstructionStatus::Available
822            }
823            AuxBoxData::Decoding => {
824                if self.num_loaded_frames() >= 2 {
825                    return JpegReconstructionStatus::Invalid;
826                }
827                let Some(frame) = self.frame(0) else {
828                    return JpegReconstructionStatus::NeedMoreData;
829                };
830                let frame_header = frame.header();
831                if frame_header.encoding != jxl_frame::header::Encoding::VarDct {
832                    return JpegReconstructionStatus::Invalid;
833                }
834                if !frame_header.frame_type.is_normal_frame() {
835                    return JpegReconstructionStatus::Invalid;
836                }
837                JpegReconstructionStatus::NeedMoreData
838            }
839            AuxBoxData::NotFound => JpegReconstructionStatus::Unavailable,
840        }
841    }
842
843    /// Reconstructs JPEG bitstream and writes the image to writer.
844    ///
845    /// # Errors
846    /// Returns an error if the reconstruction data is not available, incomplete or invalid, or
847    /// if there was an error writing the image.
848    ///
849    /// Note that reconstruction may fail even if `jpeg_reconstruction_status` returned `Available`.
850    pub fn reconstruct_jpeg(&self, jpeg_output: impl std::io::Write) -> Result<()> {
851        let aux_boxes = &self.inner.aux_boxes;
852        let jbrd = match aux_boxes.jbrd() {
853            AuxBoxData::Data(jbrd) => jbrd,
854            AuxBoxData::Decoding => {
855                return Err(jxl_jbr::Error::ReconstructionDataIncomplete.into());
856            }
857            AuxBoxData::NotFound => {
858                return Err(jxl_jbr::Error::ReconstructionUnavailable.into());
859            }
860        };
861        if self.num_loaded_frames() == 0 {
862            return Err(jxl_jbr::Error::FrameDataIncomplete.into());
863        }
864
865        let jbrd_header = jbrd.header();
866        let expected_icc_len = jbrd_header.expected_icc_len();
867        let expected_exif_len = jbrd_header.expected_exif_len();
868        let expected_xmp_len = jbrd_header.expected_xmp_len();
869
870        let icc = if expected_icc_len > 0 {
871            self.original_icc().unwrap_or(&[])
872        } else {
873            &[]
874        };
875
876        let exif = if expected_exif_len > 0 {
877            let b = aux_boxes.first_exif()?;
878            b.map(|x| x.payload()).unwrap_or(&[])
879        } else {
880            &[]
881        };
882
883        let xmp = if expected_xmp_len > 0 {
884            aux_boxes.first_xml().unwrap_or(&[])
885        } else {
886            &[]
887        };
888
889        let frame = self.frame(0).unwrap();
890        jbrd.reconstruct(frame, icc, exif, xmp, &self.pool)?
891            .write(jpeg_output)?;
892
893        Ok(())
894    }
895}
896
897/// # Private methods
898impl JxlImage {
899    fn convert_ec_info(&self) -> Vec<ExtraChannel> {
900        self.image_header
901            .metadata
902            .ec_info
903            .iter()
904            .map(|ec_info| ExtraChannel {
905                ty: ec_info.ty,
906                name: ec_info.name.clone(),
907                bit_depth: ec_info.bit_depth,
908            })
909            .collect()
910    }
911}
912
913#[derive(Debug)]
914struct JxlImageInner {
915    end_of_image: bool,
916    buffer: Vec<u8>,
917    buffer_offset: usize,
918    frame_offsets: Vec<usize>,
919    aux_boxes: AuxBoxList,
920}
921
922impl JxlImageInner {
923    fn feed_bytes_inner(&mut self, ctx: &mut RenderContext, mut buf: &[u8]) -> Result<()> {
924        if buf.is_empty() {
925            return Ok(());
926        }
927
928        if self.end_of_image {
929            self.buffer.extend_from_slice(buf);
930            return Ok(());
931        }
932
933        if let Some(loading_frame) = ctx.current_loading_frame() {
934            debug_assert!(self.buffer.is_empty());
935            let len = buf.len();
936            buf = loading_frame.feed_bytes(buf)?;
937            let count = len - buf.len();
938            self.buffer_offset += count;
939
940            if loading_frame.is_loading_done() {
941                let is_last = loading_frame.header().is_last;
942                ctx.finalize_current_frame();
943                if is_last {
944                    self.end_of_image = true;
945                    self.buffer = buf.to_vec();
946                    return Ok(());
947                }
948            }
949            if buf.is_empty() {
950                return Ok(());
951            }
952        }
953
954        self.buffer.extend_from_slice(buf);
955        let mut buf = &*self.buffer;
956        while !buf.is_empty() {
957            let mut bitstream = Bitstream::new(buf);
958            let frame = match ctx.load_frame_header(&mut bitstream) {
959                Ok(x) => x,
960                Err(e) if e.unexpected_eof() => {
961                    self.buffer = buf.to_vec();
962                    return Ok(());
963                }
964                Err(e) => {
965                    return Err(e.into());
966                }
967            };
968            let frame_index = frame.index();
969            assert_eq!(self.frame_offsets.len(), frame_index);
970            self.frame_offsets.push(self.buffer_offset);
971
972            let read_bytes = bitstream.num_read_bits() / 8;
973            buf = &buf[read_bytes..];
974            let len = buf.len();
975            buf = frame.feed_bytes(buf)?;
976            let read_bytes = read_bytes + (len - buf.len());
977            self.buffer_offset += read_bytes;
978
979            if frame.is_loading_done() {
980                let is_last = frame.header().is_last;
981                ctx.finalize_current_frame();
982                if is_last {
983                    self.end_of_image = true;
984                    self.buffer = buf.to_vec();
985                    return Ok(());
986                }
987            }
988        }
989
990        self.buffer.clear();
991        Ok(())
992    }
993}
994
995/// Pixel format of the rendered image.
996#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
997pub enum PixelFormat {
998    /// Grayscale, single channel
999    Gray,
1000    /// Grayscale with alpha, two channels
1001    Graya,
1002    /// RGB, three channels
1003    Rgb,
1004    /// RGB with alpha, four channels
1005    Rgba,
1006    /// CMYK, four channels
1007    Cmyk,
1008    /// CMYK with alpha, five channels
1009    Cmyka,
1010}
1011
1012impl PixelFormat {
1013    /// Returns the number of channels of the image.
1014    #[inline]
1015    pub fn channels(self) -> usize {
1016        match self {
1017            PixelFormat::Gray => 1,
1018            PixelFormat::Graya => 2,
1019            PixelFormat::Rgb => 3,
1020            PixelFormat::Rgba => 4,
1021            PixelFormat::Cmyk => 4,
1022            PixelFormat::Cmyka => 5,
1023        }
1024    }
1025
1026    /// Returns whether the image is grayscale.
1027    #[inline]
1028    pub fn is_grayscale(self) -> bool {
1029        matches!(self, Self::Gray | Self::Graya)
1030    }
1031
1032    /// Returns whether the image has an alpha channel.
1033    #[inline]
1034    pub fn has_alpha(self) -> bool {
1035        matches!(
1036            self,
1037            PixelFormat::Graya | PixelFormat::Rgba | PixelFormat::Cmyka
1038        )
1039    }
1040
1041    /// Returns whether the image has a black channel.
1042    #[inline]
1043    pub fn has_black(self) -> bool {
1044        matches!(self, PixelFormat::Cmyk | PixelFormat::Cmyka)
1045    }
1046}
1047
1048/// HDR transfer function type, returned by [`JxlImage::hdr_type`].
1049#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1050pub enum HdrType {
1051    /// Perceptual quantizer.
1052    Pq,
1053    /// Hybrid log-gamma.
1054    Hlg,
1055}
1056
1057/// The result of loading the keyframe.
1058#[derive(Debug)]
1059pub enum LoadResult {
1060    /// The frame is loaded with the given keyframe index.
1061    Done(usize),
1062    /// More data is needed to fully load the frame.
1063    NeedMoreData,
1064    /// No more frames are present.
1065    NoMoreFrames,
1066}
1067
1068/// The result of loading and rendering the keyframe.
1069#[derive(Debug)]
1070pub enum RenderResult {
1071    /// The frame is rendered.
1072    Done(Render),
1073    /// More data is needed to fully render the frame.
1074    NeedMoreData,
1075    /// No more frames are present.
1076    NoMoreFrames,
1077}
1078
1079/// The result of rendering a keyframe.
1080#[derive(Debug)]
1081pub struct Render {
1082    keyframe_index: usize,
1083    name: Name,
1084    duration: u32,
1085    orientation: u32,
1086    image: Arc<ImageWithRegion>,
1087    extra_channels: Vec<ExtraChannel>,
1088    target_frame_region: Region,
1089    color_bit_depth: BitDepth,
1090    is_cmyk: bool,
1091    render_spot_color: bool,
1092}
1093
1094impl Render {
1095    /// Returns the keyframe index.
1096    #[inline]
1097    pub fn keyframe_index(&self) -> usize {
1098        self.keyframe_index
1099    }
1100
1101    /// Returns the name of the frame.
1102    #[inline]
1103    pub fn name(&self) -> &str {
1104        &self.name
1105    }
1106
1107    /// Returns how many ticks this frame is presented.
1108    #[inline]
1109    pub fn duration(&self) -> u32 {
1110        self.duration
1111    }
1112
1113    /// Returns the orientation of the image.
1114    #[inline]
1115    pub fn orientation(&self) -> u32 {
1116        self.orientation
1117    }
1118
1119    /// Creates a stream that writes to borrowed buffer.
1120    ///
1121    /// The stream will include black and alpha channels, if exist, in addition to color channels.
1122    /// Orientation is applied.
1123    pub fn stream(&self) -> ImageStream {
1124        ImageStream::from_render(self, false)
1125    }
1126
1127    /// Creates a stream that writes to borrowed buffer.
1128    ///
1129    /// The stream will include black channels if exist, but not alpha channels. Orientation is
1130    /// applied.
1131    pub fn stream_no_alpha(&self) -> ImageStream {
1132        ImageStream::from_render(self, true)
1133    }
1134
1135    /// Creates a buffer with interleaved channels, with orientation applied.
1136    ///
1137    /// All extra channels are included. Use [`stream`](Render::stream) if only color, black and
1138    /// alpha channels are needed.
1139    #[inline]
1140    pub fn image_all_channels(&self) -> FrameBuffer {
1141        let fb: Vec<_> = self.image.buffer().iter().collect();
1142        let mut bit_depth = vec![self.color_bit_depth; self.image.color_channels()];
1143        for ec in &self.extra_channels {
1144            bit_depth.push(ec.bit_depth);
1145        }
1146        let regions: Vec<_> = self
1147            .image
1148            .regions_and_shifts()
1149            .iter()
1150            .map(|(region, _)| *region)
1151            .collect();
1152
1153        FrameBuffer::from_grids(
1154            &fb,
1155            &bit_depth,
1156            &regions,
1157            self.target_frame_region,
1158            self.orientation,
1159        )
1160    }
1161
1162    /// Creates a separate buffer by channel, with orientation applied.
1163    ///
1164    /// All extra channels are included.
1165    pub fn image_planar(&self) -> Vec<FrameBuffer> {
1166        let grids = self.image.buffer();
1167        let bit_depth_it = std::iter::repeat_n(self.color_bit_depth, self.image.color_channels())
1168            .chain(self.extra_channels.iter().map(|ec| ec.bit_depth));
1169        let region_it = self
1170            .image
1171            .regions_and_shifts()
1172            .iter()
1173            .map(|(region, _)| *region);
1174
1175        bit_depth_it
1176            .zip(region_it)
1177            .zip(grids)
1178            .map(|((bit_depth, region), x)| {
1179                FrameBuffer::from_grids(
1180                    &[x],
1181                    &[bit_depth],
1182                    &[region],
1183                    self.target_frame_region,
1184                    self.orientation,
1185                )
1186            })
1187            .collect()
1188    }
1189
1190    /// Returns the color channels.
1191    ///
1192    /// Orientation is not applied.
1193    #[inline]
1194    pub fn color_channels(&self) -> &[ImageBuffer] {
1195        let color_channels = self.image.color_channels();
1196        &self.image.buffer()[..color_channels]
1197    }
1198
1199    /// Returns the extra channels, potentially including alpha and black channels.
1200    ///
1201    /// Orientation is not applied.
1202    #[inline]
1203    pub fn extra_channels(&self) -> (&[ExtraChannel], &[ImageBuffer]) {
1204        let color_channels = self.image.color_channels();
1205        (&self.extra_channels, &self.image.buffer()[color_channels..])
1206    }
1207}
1208
1209/// Extra channel of the image.
1210#[derive(Debug)]
1211pub struct ExtraChannel {
1212    ty: ExtraChannelType,
1213    name: Name,
1214    bit_depth: BitDepth,
1215}
1216
1217impl ExtraChannel {
1218    /// Returns the type of the extra channel.
1219    #[inline]
1220    pub fn ty(&self) -> ExtraChannelType {
1221        self.ty
1222    }
1223
1224    /// Returns the name of the channel.
1225    #[inline]
1226    pub fn name(&self) -> &str {
1227        &self.name
1228    }
1229
1230    /// Returns `true` if the channel is a black channel of CMYK image.
1231    #[inline]
1232    pub fn is_black(&self) -> bool {
1233        matches!(self.ty, ExtraChannelType::Black)
1234    }
1235
1236    /// Returns `true` if the channel is an alpha channel.
1237    #[inline]
1238    pub fn is_alpha(&self) -> bool {
1239        matches!(self.ty, ExtraChannelType::Alpha { .. })
1240    }
1241
1242    /// Returns `true` if the channel is a spot colour channel.
1243    #[inline]
1244    pub fn is_spot_colour(&self) -> bool {
1245        matches!(self.ty, ExtraChannelType::SpotColour { .. })
1246    }
1247}
1248
1249/// Cropping region information.
1250#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
1251pub struct CropInfo {
1252    pub width: u32,
1253    pub height: u32,
1254    pub left: u32,
1255    pub top: u32,
1256}
1257
1258impl From<CropInfo> for jxl_render::Region {
1259    fn from(value: CropInfo) -> Self {
1260        Self {
1261            left: value.left as i32,
1262            top: value.top as i32,
1263            width: value.width,
1264            height: value.height,
1265        }
1266    }
1267}
1268
1269impl From<jxl_render::Region> for CropInfo {
1270    fn from(value: jxl_render::Region) -> Self {
1271        Self {
1272            left: value.left.max(0) as u32,
1273            top: value.top.max(0) as u32,
1274            width: value.width,
1275            height: value.height,
1276        }
1277    }
1278}
1279
1280/// Availability and validity of JPEG bitstream reconstruction data.
1281#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1282pub enum JpegReconstructionStatus {
1283    /// JPEG bitstream reconstruction data is found. Actual reconstruction may or may not succeed.
1284    Available,
1285    /// Either JPEG bitstream reconstruction data or JPEG XL image data is invalid and cannot be
1286    /// used for actual reconstruction.
1287    Invalid,
1288    /// JPEG bitstream reconstruction data is not found. Result will *not* change.
1289    Unavailable,
1290    /// JPEG bitstream reconstruction data is not found. Result may change with more data.
1291    NeedMoreData,
1292}