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}