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}