Skip to main content

zenjxl_decoder/api/
convenience.rs

1// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6//! High-level convenience API for decoding JXL images.
7//!
8//! For most use cases, [`decode`] is all you need:
9//!
10//! ```no_run
11//! let data = std::fs::read("image.jxl").unwrap();
12//! let image = zenjxl_decoder::decode(&data).unwrap();
13//! let (w, h) = (image.width, image.height);
14//! let rgba: &[u8] = &image.data;
15//! ```
16//!
17//! Use [`read_header`] to inspect metadata without decoding pixels.
18//!
19//! For streaming input, incremental decoding, or fine-grained control over
20//! pixel format and color management, use the lower-level [`JxlDecoder`]
21//! typestate API instead.
22//!
23//! [`JxlDecoder`]: super::JxlDecoder
24
25use super::{
26    GainMapBundle, JxlBasicInfo, JxlColorProfile, JxlColorType, JxlDataFormat, JxlDecoder,
27    JxlDecoderLimits, JxlDecoderOptions, JxlOutputBuffer, JxlPixelFormat, ProcessingResult, states,
28};
29use crate::error::{Error, Result};
30use crate::headers::extra_channels::ExtraChannel;
31use crate::image::{OwnedRawImage, Rect};
32
33/// A decoded JXL image with interleaved RGBA (or GrayAlpha) u8 pixel data.
34#[non_exhaustive]
35pub struct JxlImage {
36    /// Image width in pixels.
37    pub width: usize,
38    /// Image height in pixels.
39    pub height: usize,
40    /// Interleaved pixel data: RGBA or GrayAlpha, row-major, tightly packed.
41    /// Length = `width * height * channels` where channels is 4 (RGBA) or 2 (GrayAlpha).
42    pub data: Vec<u8>,
43    /// Number of channels per pixel (4 for RGBA, 2 for GrayAlpha).
44    pub channels: usize,
45    /// True if the source image is grayscale (output is GrayAlpha).
46    pub is_grayscale: bool,
47    /// Image metadata from the file header.
48    pub info: JxlBasicInfo,
49    /// The color profile of the output pixels.
50    pub output_profile: JxlColorProfile,
51    /// The color profile embedded in the file.
52    pub embedded_profile: JxlColorProfile,
53    /// HDR gain map bundle from a `jhgm` container box, if present.
54    pub gain_map: Option<GainMapBundle>,
55    /// Raw EXIF data from the `Exif` container box (TIFF header offset stripped).
56    /// `None` for bare codestreams or files without an `Exif` box.
57    pub exif: Option<Vec<u8>>,
58    /// Raw XMP data from the `xml ` container box.
59    /// `None` for bare codestreams or files without an `xml ` box.
60    pub xmp: Option<Vec<u8>>,
61}
62
63/// Image metadata extracted from the file header, without decoding pixels.
64#[non_exhaustive]
65pub struct JxlImageInfo {
66    /// Image metadata (dimensions, bit depth, orientation, extra channels, animation).
67    pub info: JxlBasicInfo,
68    /// The color profile embedded in the file.
69    pub embedded_profile: JxlColorProfile,
70}
71
72/// Decode a JXL image from a byte slice to RGBA u8 pixels.
73///
74/// For grayscale images, returns GrayAlpha u8 (2 channels).
75/// For color images, returns RGBA u8 (4 channels).
76/// Alpha is always included; images without alpha get opaque (255) alpha.
77///
78/// Decodes only the first frame. For animation support, use the
79/// [`JxlDecoder`](super::JxlDecoder) streaming API.
80///
81/// Uses default security limits and parallel decoding (if the `threads`
82/// feature is enabled). For custom limits or cancellation, use
83/// [`decode_with`].
84///
85/// # Example
86///
87/// ```no_run
88/// let data = std::fs::read("photo.jxl").unwrap();
89/// let image = zenjxl_decoder::decode(&data).unwrap();
90/// assert_eq!(image.data.len(), image.width * image.height * image.channels);
91/// ```
92pub fn decode(data: &[u8]) -> Result<JxlImage> {
93    decode_with(data, JxlDecoderOptions::default())
94}
95
96/// Decode a JXL image with custom decoder options.
97///
98/// Same output format as [`decode`] (RGBA u8 or GrayAlpha u8), but allows
99/// configuring security limits, cancellation, parallel mode, and CMS.
100pub fn decode_with(data: &[u8], options: JxlDecoderOptions) -> Result<JxlImage> {
101    let mut input: &[u8] = data;
102
103    // Phase 1: Initialized → WithImageInfo (parse header + ICC)
104    let decoder = JxlDecoder::<states::Initialized>::new(options);
105    let mut decoder = match decoder.process(&mut input)? {
106        ProcessingResult::Complete { result } => result,
107        ProcessingResult::NeedsMoreInput { .. } => {
108            return Err(Error::OutOfBounds(0));
109        }
110    };
111
112    let info = decoder.basic_info().clone();
113    let embedded_profile = decoder.embedded_color_profile().clone();
114    let (width, height) = info.size;
115
116    // Determine color type: add alpha to whatever the image naturally is
117    let is_grayscale = decoder.current_pixel_format().color_type.is_grayscale();
118    let color_type = if is_grayscale {
119        JxlColorType::GrayscaleAlpha
120    } else {
121        JxlColorType::Rgba
122    };
123    let channels = color_type.samples_per_pixel();
124
125    // Find main alpha channel for interleaving
126    let main_alpha = info
127        .extra_channels
128        .iter()
129        .position(|ec| ec.ec_type == ExtraChannel::Alpha);
130
131    let u8_format = JxlDataFormat::U8 { bit_depth: 8 };
132
133    // Set pixel format: interleave alpha into color channels, keep other extras as u8
134    let pixel_format = JxlPixelFormat {
135        color_type,
136        color_data_format: Some(u8_format),
137        extra_channel_format: info
138            .extra_channels
139            .iter()
140            .enumerate()
141            .map(|(i, _)| {
142                if Some(i) == main_alpha {
143                    None // interleaved into RGBA/GrayAlpha
144                } else {
145                    Some(u8_format)
146                }
147            })
148            .collect(),
149    };
150    decoder.set_pixel_format(pixel_format);
151
152    let output_profile = decoder.output_color_profile().clone();
153
154    // Count non-interleaved extra channels (everything except the main alpha)
155    let extra_count = info.extra_channels.len() - usize::from(main_alpha.is_some());
156
157    // Phase 2: WithImageInfo → WithFrameInfo (parse frame header)
158    let decoder = match decoder.process(&mut input)? {
159        ProcessingResult::Complete { result } => result,
160        ProcessingResult::NeedsMoreInput { .. } => {
161            return Err(Error::OutOfBounds(0));
162        }
163    };
164
165    // Allocate output buffers
166    let row_bytes = width * channels; // 1 byte per sample for u8
167    let mut output = OwnedRawImage::new_uninit((row_bytes, height))?;
168    #[cfg(feature = "threads")]
169    output.prefault_parallel();
170
171    let mut extra_outputs: Vec<OwnedRawImage> = (0..extra_count)
172        .map(|_| OwnedRawImage::new_uninit((width, height)))
173        .collect::<Result<_>>()?;
174
175    // Phase 3: WithFrameInfo → decode pixels
176    let mut bufs: Vec<JxlOutputBuffer<'_>> = std::iter::once(&mut output)
177        .chain(extra_outputs.iter_mut())
178        .map(|img| {
179            let rect = Rect {
180                size: img.byte_size(),
181                origin: (0, 0),
182            };
183            JxlOutputBuffer::from_image_rect_mut(img.get_rect_mut(rect))
184        })
185        .collect();
186
187    let mut decoder = match decoder.process(&mut input, &mut bufs)? {
188        ProcessingResult::Complete { result } => result,
189        ProcessingResult::NeedsMoreInput { .. } => {
190            return Err(Error::OutOfBounds(0));
191        }
192    };
193
194    // Extract the gain map bundle if the box parser encountered a jhgm box.
195    // Note: if the jhgm box follows the codestream, it may not have been read
196    // yet. Use the low-level JxlDecoder API to access trailing boxes.
197    let gain_map = decoder.take_gain_map();
198
199    // Extract EXIF and XMP metadata from container boxes.
200    let exif = decoder.take_exif();
201    let xmp = decoder.take_xmp();
202
203    // Copy to tightly packed Vec<u8>
204    let total_bytes = row_bytes * height;
205    let mut pixels = Vec::with_capacity(total_bytes);
206    for y in 0..height {
207        pixels.extend_from_slice(output.row(y));
208    }
209
210    Ok(JxlImage {
211        width,
212        height,
213        data: pixels,
214        channels,
215        is_grayscale,
216        info,
217        output_profile,
218        embedded_profile,
219        gain_map,
220        exif,
221        xmp,
222    })
223}
224
225/// Read image metadata without decoding pixels.
226///
227/// Parses the file header and ICC profile. Returns dimensions, bit depth,
228/// orientation, extra channel info, animation info, and color profile.
229///
230/// This is fast (~1μs for sRGB images, ~7μs for images with ICC profiles).
231///
232/// # Example
233///
234/// ```no_run
235/// let data = std::fs::read("photo.jxl").unwrap();
236/// let header = zenjxl_decoder::read_header(&data).unwrap();
237/// let (w, h) = header.info.size;
238/// println!("{w}x{h}");
239/// ```
240pub fn read_header(data: &[u8]) -> Result<JxlImageInfo> {
241    read_header_with(data, JxlDecoderLimits::default())
242}
243
244/// Read image metadata with custom security limits.
245pub fn read_header_with(data: &[u8], limits: JxlDecoderLimits) -> Result<JxlImageInfo> {
246    let mut input: &[u8] = data;
247    let options = JxlDecoderOptions {
248        limits,
249        ..JxlDecoderOptions::default()
250    };
251    let decoder = JxlDecoder::<states::Initialized>::new(options);
252    let decoder = match decoder.process(&mut input)? {
253        ProcessingResult::Complete { result } => result,
254        ProcessingResult::NeedsMoreInput { .. } => {
255            return Err(Error::OutOfBounds(0));
256        }
257    };
258
259    Ok(JxlImageInfo {
260        info: decoder.basic_info().clone(),
261        embedded_profile: decoder.embedded_color_profile().clone(),
262    })
263}