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