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