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