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    ///
55    /// Captured whether the box precedes or follows the codestream. For
56    /// animations, boxes that follow the codestream are not read (decoding
57    /// stops after the first frame).
58    pub gain_map: Option<GainMapBundle>,
59    /// Raw EXIF data from the `Exif` container box (TIFF header offset stripped).
60    /// `None` for bare codestreams or files without an `Exif` box.
61    ///
62    /// Captured whether the box precedes or follows the codestream. For
63    /// animations, boxes that follow the codestream are not read (decoding
64    /// stops after the first frame).
65    pub exif: Option<Vec<u8>>,
66    /// Raw XMP data from the `xml ` container box.
67    /// `None` for bare codestreams or files without an `xml ` box.
68    ///
69    /// Captured whether the box precedes or follows the codestream. For
70    /// animations, boxes that follow the codestream are not read (decoding
71    /// stops after the first frame).
72    pub xmp: Option<Vec<u8>>,
73}
74
75/// Image metadata extracted from the file header, without decoding pixels.
76#[non_exhaustive]
77pub struct JxlImageInfo {
78    /// Image metadata (dimensions, bit depth, orientation, extra channels, animation).
79    pub info: JxlBasicInfo,
80    /// The color profile embedded in the file.
81    pub embedded_profile: JxlColorProfile,
82}
83
84/// Decode a JXL image from a byte slice to RGBA u8 pixels.
85///
86/// For grayscale images, returns GrayAlpha u8 (2 channels).
87/// For color images, returns RGBA u8 (4 channels).
88/// Alpha is always included; images without alpha get opaque (255) alpha.
89///
90/// Decodes only the first frame. For animation support, use the
91/// [`JxlDecoder`](super::JxlDecoder) streaming API.
92///
93/// Uses default security limits and parallel decoding (if the `threads`
94/// feature is enabled). For custom limits or cancellation, use
95/// [`decode_with`].
96///
97/// # Example
98///
99/// ```no_run
100/// let data = std::fs::read("photo.jxl").unwrap();
101/// let image = zenjxl_decoder::decode(&data).unwrap();
102/// assert_eq!(image.data.len(), image.width * image.height * image.channels);
103/// ```
104pub fn decode(data: &[u8]) -> Result<JxlImage> {
105    decode_with(data, JxlDecoderOptions::default())
106}
107
108/// Decode a JXL image with custom decoder options.
109///
110/// Same output format as [`decode`] (RGBA u8 or GrayAlpha u8), but allows
111/// configuring security limits, cancellation, parallel mode, and CMS.
112pub fn decode_with(data: &[u8], options: JxlDecoderOptions) -> Result<JxlImage> {
113    let mut input: &[u8] = data;
114
115    // Phase 1: Initialized → WithImageInfo (parse header + ICC)
116    let decoder = JxlDecoder::<states::Initialized>::new(options);
117    let mut decoder = match decoder.process(&mut input)? {
118        ProcessingResult::Complete { result } => result,
119        ProcessingResult::NeedsMoreInput { .. } => {
120            return Err(Error::OutOfBounds(0));
121        }
122    };
123
124    let info = decoder.basic_info().clone();
125    let embedded_profile = decoder.embedded_color_profile().clone();
126    let (width, height) = info.size;
127
128    // Determine color type: add alpha to whatever the image naturally is
129    let is_grayscale = decoder.current_pixel_format().color_type.is_grayscale();
130    let color_type = if is_grayscale {
131        JxlColorType::GrayscaleAlpha
132    } else {
133        JxlColorType::Rgba
134    };
135    let channels = color_type.samples_per_pixel();
136
137    // Find main alpha channel for interleaving
138    let main_alpha = info
139        .extra_channels
140        .iter()
141        .position(|ec| ec.ec_type == ExtraChannel::Alpha);
142
143    let u8_format = JxlDataFormat::U8 { bit_depth: 8 };
144
145    // Set pixel format: interleave alpha into color channels, keep other extras as u8
146    let pixel_format = JxlPixelFormat {
147        color_type,
148        color_data_format: Some(u8_format),
149        extra_channel_format: info
150            .extra_channels
151            .iter()
152            .enumerate()
153            .map(|(i, _)| {
154                if Some(i) == main_alpha {
155                    None // interleaved into RGBA/GrayAlpha
156                } else {
157                    Some(u8_format)
158                }
159            })
160            .collect(),
161    };
162    decoder.set_pixel_format(pixel_format);
163
164    let output_profile = decoder.output_color_profile().clone();
165
166    // Count non-interleaved extra channels (everything except the main alpha)
167    let extra_count = info.extra_channels.len() - usize::from(main_alpha.is_some());
168
169    // Phase 2: WithImageInfo → WithFrameInfo (parse frame header)
170    let decoder = match decoder.process(&mut input)? {
171        ProcessingResult::Complete { result } => result,
172        ProcessingResult::NeedsMoreInput { .. } => {
173            return Err(Error::OutOfBounds(0));
174        }
175    };
176
177    // Allocate output buffers
178    let row_bytes = width * channels; // 1 byte per sample for u8
179    let mut output = OwnedRawImage::new_uninit((row_bytes, height))?;
180    #[cfg(feature = "threads")]
181    output.prefault_parallel();
182
183    let mut extra_outputs: Vec<OwnedRawImage> = (0..extra_count)
184        .map(|_| OwnedRawImage::new_uninit((width, height)))
185        .collect::<Result<_>>()?;
186
187    // Phase 3: WithFrameInfo → decode pixels
188    let mut bufs: Vec<JxlOutputBuffer<'_>> = std::iter::once(&mut output)
189        .chain(extra_outputs.iter_mut())
190        .map(|img| {
191            let rect = Rect {
192                size: img.byte_size(),
193                origin: (0, 0),
194            };
195            JxlOutputBuffer::from_image_rect_mut(img.get_rect_mut(rect))
196        })
197        .collect();
198
199    let mut decoder = match decoder.process(&mut input, &mut bufs)? {
200        ProcessingResult::Complete { result } => result,
201        ProcessingResult::NeedsMoreInput { .. } => {
202            return Err(Error::OutOfBounds(0));
203        }
204    };
205
206    // Phase 4: drain trailing container boxes, then extract box metadata.
207    // The jhgm (gain map), Exif, and xml boxes may follow the codestream —
208    // jxl-encoder's `append_gain_map_bundle` writes `signature + ftyp + jxlc
209    // + jhgm` — and the box parser only reaches them by processing past the
210    // final frame (#20). Animations stop after the first frame, so boxes
211    // trailing a multi-frame codestream stay unread; use the low-level
212    // [`JxlDecoder`] API to collect those.
213    let (gain_map, exif, xmp) = if !decoder.has_more_frames() && !input.is_empty() {
214        match decoder.process(&mut input)? {
215            ProcessingResult::Complete { mut result } => take_box_metadata(&mut result),
216            ProcessingResult::NeedsMoreInput { mut fallback, .. } => {
217                take_box_metadata(&mut fallback)
218            }
219        }
220    } else {
221        take_box_metadata(&mut decoder)
222    };
223
224    // Copy to tightly packed Vec<u8>
225    let total_bytes = row_bytes * height;
226    let mut pixels = Vec::with_capacity(total_bytes);
227    for y in 0..height {
228        pixels.extend_from_slice(output.row(y));
229    }
230
231    Ok(JxlImage {
232        width,
233        height,
234        data: pixels,
235        channels,
236        is_grayscale,
237        info,
238        output_profile,
239        embedded_profile,
240        gain_map,
241        exif,
242        xmp,
243    })
244}
245
246/// Takes the gain map, EXIF, and XMP captured by the box parser, in one call
247/// usable from any decoder state.
248fn take_box_metadata<S: states::JxlState>(
249    decoder: &mut JxlDecoder<S>,
250) -> (Option<GainMapBundle>, Option<Vec<u8>>, Option<Vec<u8>>) {
251    (
252        decoder.take_gain_map(),
253        decoder.take_exif(),
254        decoder.take_xmp(),
255    )
256}
257
258/// Reconstruct the original JPEG bytes from a JXL file that carries a JBRD
259/// (JPEG Bitstream Reconstruction Data) box — i.e. a JXL produced by lossless
260/// JPEG transcoding (`jxl_encoder::LosslessConfig::encode_jpeg_transcode`).
261///
262/// Returns `Ok(Some(bytes))` with the **byte-exact** original JPEG when the
263/// file carries reconstruction data, or `Ok(None)` when it does not (a JXL that
264/// was not produced from a JPEG). This is the pure-Rust counterpart to
265/// `djxl <in.jxl> <out.jpg> --reconstruct_jpeg`.
266///
267/// Requires the `jpeg` cargo feature.
268///
269/// # Example
270///
271/// ```no_run
272/// let jxl = std::fs::read("photo.jxl").unwrap();
273/// if let Some(jpeg) = zenjxl_decoder::reconstruct_jpeg(&jxl).unwrap() {
274///     std::fs::write("photo_reconstructed.jpg", &jpeg).unwrap();
275/// }
276/// ```
277#[cfg(feature = "jpeg")]
278pub fn reconstruct_jpeg(data: &[u8]) -> Result<Option<Vec<u8>>> {
279    reconstruct_jpeg_with(data, JxlDecoderOptions::default())
280}
281
282/// Reconstruct the original JPEG bytes with custom decoder options.
283///
284/// See [`reconstruct_jpeg`]. Drives a full frame decode (reconstruction needs
285/// the captured quantized coefficients) and then returns the JBRD-reconstructed
286/// JPEG. The decoded pixels themselves are discarded.
287#[cfg(feature = "jpeg")]
288pub fn reconstruct_jpeg_with(data: &[u8], options: JxlDecoderOptions) -> Result<Option<Vec<u8>>> {
289    let mut input: &[u8] = data;
290
291    // Phase 1: parse header.
292    let decoder = JxlDecoder::<states::Initialized>::new(options);
293    let mut decoder = match decoder.process(&mut input)? {
294        ProcessingResult::Complete { result } => result,
295        ProcessingResult::NeedsMoreInput { .. } => return Err(Error::OutOfBounds(0)),
296    };
297
298    let info = decoder.basic_info().clone();
299    let (width, height) = info.size;
300
301    // Output format mirrors `decode_with`: natural color type + interleaved
302    // alpha. The pixels are discarded — we only need a complete frame decode so
303    // the JBRD coefficient capture runs.
304    let is_grayscale = decoder.current_pixel_format().color_type.is_grayscale();
305    let color_type = if is_grayscale {
306        JxlColorType::GrayscaleAlpha
307    } else {
308        JxlColorType::Rgba
309    };
310    let channels = color_type.samples_per_pixel();
311    let main_alpha = info
312        .extra_channels
313        .iter()
314        .position(|ec| ec.ec_type == ExtraChannel::Alpha);
315    let u8_format = JxlDataFormat::U8 { bit_depth: 8 };
316    let pixel_format = JxlPixelFormat {
317        color_type,
318        color_data_format: Some(u8_format),
319        extra_channel_format: info
320            .extra_channels
321            .iter()
322            .enumerate()
323            .map(|(i, _)| {
324                if Some(i) == main_alpha {
325                    None
326                } else {
327                    Some(u8_format)
328                }
329            })
330            .collect(),
331    };
332    decoder.set_pixel_format(pixel_format);
333    let extra_count = info.extra_channels.len() - usize::from(main_alpha.is_some());
334
335    // Phase 2: parse frame header.
336    let decoder = match decoder.process(&mut input)? {
337        ProcessingResult::Complete { result } => result,
338        ProcessingResult::NeedsMoreInput { .. } => return Err(Error::OutOfBounds(0)),
339    };
340
341    // Phase 3: decode the frame into throwaway buffers.
342    let row_bytes = width * channels;
343    let mut output = OwnedRawImage::new_uninit((row_bytes, height))?;
344    let mut extra_outputs: Vec<OwnedRawImage> = (0..extra_count)
345        .map(|_| OwnedRawImage::new_uninit((width, height)))
346        .collect::<Result<_>>()?;
347    let mut bufs: Vec<JxlOutputBuffer<'_>> = core::iter::once(&mut output)
348        .chain(extra_outputs.iter_mut())
349        .map(|img| {
350            let rect = Rect {
351                size: img.byte_size(),
352                origin: (0, 0),
353            };
354            JxlOutputBuffer::from_image_rect_mut(img.get_rect_mut(rect))
355        })
356        .collect();
357
358    let mut decoder = match decoder.process(&mut input, &mut bufs)? {
359        ProcessingResult::Complete { result } => result,
360        ProcessingResult::NeedsMoreInput { .. } => return Err(Error::OutOfBounds(0)),
361    };
362
363    // Phase 4: drain the remaining input so the box parser consumes the trailing
364    // Exif / xml metadata boxes (which follow the codestream in a JPEG-transcode
365    // container). Only then can `take_jpeg_reconstruction` stitch EXIF/XMP back
366    // into the reconstructed APPn markers. A JBRD transcode has a single frame,
367    // so this drains to `NeedsMoreInput` once the boxes are parsed.
368    loop {
369        if input.is_empty() {
370            break;
371        }
372        let before = input.len();
373        match decoder.process(&mut input)? {
374            ProcessingResult::NeedsMoreInput { fallback, .. } => {
375                decoder = fallback;
376                if input.len() == before {
377                    break; // no progress — avoid spinning
378                }
379            }
380            // Unexpected second frame; the trailing boxes before it are parsed.
381            ProcessingResult::Complete { mut result } => {
382                return Ok(result.take_jpeg_reconstruction());
383            }
384        }
385    }
386
387    Ok(decoder.take_jpeg_reconstruction())
388}
389
390/// Read image metadata without decoding pixels.
391///
392/// Parses the file header and ICC profile. Returns dimensions, bit depth,
393/// orientation, extra channel info, animation info, and color profile.
394///
395/// This is fast (~1μs for sRGB images, ~7μs for images with ICC profiles).
396///
397/// # Example
398///
399/// ```no_run
400/// let data = std::fs::read("photo.jxl").unwrap();
401/// let header = zenjxl_decoder::read_header(&data).unwrap();
402/// let (w, h) = header.info.size;
403/// println!("{w}x{h}");
404/// ```
405pub fn read_header(data: &[u8]) -> Result<JxlImageInfo> {
406    read_header_with(data, JxlDecoderLimits::default())
407}
408
409/// Read image metadata with custom security limits.
410pub fn read_header_with(data: &[u8], limits: JxlDecoderLimits) -> Result<JxlImageInfo> {
411    let mut input: &[u8] = data;
412    let options = JxlDecoderOptions {
413        limits,
414        ..JxlDecoderOptions::default()
415    };
416    let decoder = JxlDecoder::<states::Initialized>::new(options);
417    let decoder = match decoder.process(&mut input)? {
418        ProcessingResult::Complete { result } => result,
419        ProcessingResult::NeedsMoreInput { .. } => {
420            return Err(Error::OutOfBounds(0));
421        }
422    };
423
424    Ok(JxlImageInfo {
425        info: decoder.basic_info().clone(),
426        embedded_profile: decoder.embedded_color_profile().clone(),
427    })
428}