// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. use std::io::IoSliceMut; use crate::{ api::{ Endianness, JxlBasicInfo, JxlBitDepth, JxlColorEncoding, JxlColorProfile, JxlColorType, JxlDataFormat, JxlDecoderOptions, JxlExtraChannel, JxlPixelFormat, inner::codestream_parser::SectionState, }, bit_reader::BitReader, error::{Error, Result}, frame::{DecoderState, Frame, Section}, headers::{ FileHeader, JxlHeader, color_encoding::ColorSpace, encodings::UnconditionalCoder, frame_header::FrameHeader, toc::IncrementalTocReader, }, icc::IncrementalIccReader, }; use super::{CodestreamParser, SectionBuffer}; use crate::api::ToneMapping; fn check_size_limit( pixel_limit: Option, (xs, ys): (usize, usize), num_ec: usize, ) -> Result<()> { if let Some(limit) = pixel_limit { let xs = xs.max(16); // xsize is always at least 64 bytes. let total_pixels = xs.saturating_mul(ys).saturating_mul(3 + num_ec); if total_pixels >= limit { return Err(Error::ImageSizeTooLarge(xs, ys)); } }; Ok(()) } impl CodestreamParser { #[cold] pub(super) fn process_non_section(&mut self, decode_options: &JxlDecoderOptions) -> Result<()> { if self.decoder_state.is_none() && self.file_header.is_none() { // We don't have a file header yet. Try parsing that. let mut br = BitReader::new(&self.non_section_buf); br.skip_bits(self.non_section_bit_offset as usize)?; let file_header = FileHeader::read(&mut br)?; let xsize = file_header.size.xsize() as usize; let ysize = file_header.size.ysize() as usize; check_size_limit( decode_options.pixel_limit, (xsize, ysize), file_header.image_metadata.extra_channel_info.len(), )?; if let Some(preview) = &file_header.image_metadata.preview { check_size_limit( decode_options.pixel_limit, (preview.xsize() as usize, preview.ysize() as usize), file_header.image_metadata.extra_channel_info.len(), )?; } let data = &file_header.image_metadata; self.animation = data.animation.clone(); self.basic_info = Some(JxlBasicInfo { size: if data.orientation.is_transposing() { (ysize, xsize) } else { (xsize, ysize) }, bit_depth: if data.bit_depth.floating_point_sample() { JxlBitDepth::Float { bits_per_sample: data.bit_depth.bits_per_sample(), exponent_bits_per_sample: data.bit_depth.exponent_bits_per_sample(), } } else { JxlBitDepth::Int { bits_per_sample: data.bit_depth.bits_per_sample(), } }, orientation: data.orientation, extra_channels: data .extra_channel_info .iter() .map(|info| JxlExtraChannel { ec_type: info.ec_type, alpha_associated: info.alpha_associated(), }) .collect(), animation: data .animation .as_ref() .map(|anim| crate::api::JxlAnimation { tps_numerator: anim.tps_numerator, tps_denominator: anim.tps_denominator, num_loops: anim.num_loops, have_timecodes: anim.have_timecodes, }), uses_original_profile: !data.xyb_encoded, tone_mapping: ToneMapping { intensity_target: data.tone_mapping.intensity_target, min_nits: data.tone_mapping.min_nits, relative_to_max_display: data.tone_mapping.relative_to_max_display, linear_below: data.tone_mapping.linear_below, }, preview_size: data .preview .as_ref() .map(|p| (p.xsize() as usize, p.ysize() as usize)), }); self.file_header = Some(file_header); let bits = br.total_bits_read(); self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; } if self.decoder_state.is_none() && self.embedded_color_profile.is_none() { let file_header = self.file_header.as_ref().unwrap(); // Parse (or extract from file header) the ICC profile. let mut br = BitReader::new(&self.non_section_buf); br.skip_bits(self.non_section_bit_offset as usize)?; let embedded_color_profile = if file_header.image_metadata.color_encoding.want_icc { if self.icc_parser.is_none() { self.icc_parser = Some(IncrementalIccReader::new(&mut br)?); } let icc_parser = self.icc_parser.as_mut().unwrap(); let mut bits = br.total_bits_read(); for _ in 0..icc_parser.remaining() { match icc_parser.read_one(&mut br) { Ok(()) => bits = br.total_bits_read(), Err(Error::OutOfBounds(c)) => { self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; // Estimate >= one bit per remaining character to read. return Err(Error::OutOfBounds(c + icc_parser.remaining() / 8)); } Err(e) => return Err(e), } } let icc_result = self.icc_parser.take().unwrap().finalize(&mut br); self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; JxlColorProfile::Icc(icc_result?) } else { JxlColorProfile::Simple(JxlColorEncoding::from_internal( &file_header.image_metadata.color_encoding, )?) }; // Determine default output color profile following libjxl logic: // - For XYB: use embedded if can_output_to(), else linear sRGB fallback // - For non-XYB: use embedded color profile let output_color_profile = if file_header.image_metadata.xyb_encoded { let is_gray = file_header.image_metadata.color_encoding.color_space == ColorSpace::Gray; // Use embedded if we can output to it, otherwise fall back to linear sRGB let base_encoding = if embedded_color_profile.can_output_to() { match &embedded_color_profile { JxlColorProfile::Simple(enc) => enc.clone(), JxlColorProfile::Icc(_) => { unreachable!("can_output_to returns false for ICC") } } } else { JxlColorEncoding::linear_srgb(is_gray) }; JxlColorProfile::Simple(base_encoding) } else { embedded_color_profile.clone() }; self.embedded_color_profile = Some(embedded_color_profile.clone()); // Only set default output_color_profile if not already configured by user if self.output_color_profile.is_none() { self.output_color_profile = Some(output_color_profile); } else { // Validate user's output color profile choice (libjxl compatibility) // For non-XYB without CMS: only same encoding as embedded is allowed let user_profile = self.output_color_profile.as_ref().unwrap(); if !file_header.image_metadata.xyb_encoded && decode_options.cms.is_none() && *user_profile != embedded_color_profile { return Err(Error::NonXybOutputNoCMS); } } // Only set default pixel_format if not already configured (e.g. via rewind) if self.pixel_format.is_none() { self.pixel_format = Some(JxlPixelFormat { color_type: if file_header.image_metadata.color_encoding.color_space == ColorSpace::Gray { JxlColorType::Grayscale } else { JxlColorType::Rgb }, color_data_format: Some(JxlDataFormat::F32 { endianness: Endianness::native(), }), extra_channel_format: vec![ Some(JxlDataFormat::F32 { endianness: Endianness::native() }); file_header.image_metadata.extra_channel_info.len() ], }); } let mut br = BitReader::new(&self.non_section_buf); br.skip_bits(self.non_section_bit_offset as usize)?; br.jump_to_byte_boundary()?; self.non_section_buf.consume(br.total_bits_read() / 8); // We now have image information. let mut decoder_state = DecoderState::new(self.file_header.take().unwrap()); decoder_state.render_spotcolors = decode_options.render_spot_colors; decoder_state.high_precision = decode_options.high_precision; decoder_state.premultiply_output = decode_options.premultiply_output; self.decoder_state = Some(decoder_state); // Reset bit offset to 0 since we've consumed everything up to a byte boundary self.non_section_bit_offset = 0; return Ok(()); } let decoder_state = self.decoder_state.as_mut().unwrap(); if self.frame_header.is_none() { // We don't have a frame header yet. Try parsing that. let mut br = BitReader::new(&self.non_section_buf); br.skip_bits(self.non_section_bit_offset as usize)?; // For preview frames, use the preview dimensions instead of main image dimensions let nonserialized = if !self.preview_done { decoder_state .file_header .preview_frame_header_nonserialized() .unwrap_or_else(|| decoder_state.file_header.frame_header_nonserialized()) } else { decoder_state.file_header.frame_header_nonserialized() }; let mut frame_header = FrameHeader::read_unconditional(&(), &mut br, &nonserialized)?; frame_header.postprocess(&nonserialized); check_size_limit( decode_options.pixel_limit, frame_header.size(), frame_header.num_extra_channels as usize, )?; // Initialize storage buffers for available sections. self.lf_global_section = None; self.lf_sections.clear(); self.hf_global_section = None; self.hf_sections = (0..frame_header.num_groups()) .map(|_| (0..frame_header.passes.num_passes).map(|_| None).collect()) .collect(); self.candidate_hf_sections.clear(); self.frame_header = Some(frame_header); let bits = br.total_bits_read(); self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; } let toc = { let mut br = BitReader::new(&self.non_section_buf); br.skip_bits(self.non_section_bit_offset as usize)?; if self.toc_parser.is_none() { let num_toc_entries = self.frame_header.as_ref().unwrap().num_toc_entries(); self.toc_parser = Some(IncrementalTocReader::new(num_toc_entries as u32, &mut br)?); } let toc_parser = self.toc_parser.as_mut().unwrap(); let mut bits = br.total_bits_read(); while !toc_parser.is_complete() { match toc_parser.read_step(&mut br) { Ok(()) => bits = br.total_bits_read(), Err(Error::OutOfBounds(c)) => { self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; // Estimate >= 16 bits per remaining entry to read. return Err(Error::OutOfBounds( c + toc_parser.remaining_entries() as usize * 2, )); } Err(e) => return Err(e), } } br.jump_to_byte_boundary()?; bits = br.total_bits_read(); self.non_section_buf.consume(bits / 8); self.non_section_bit_offset = (bits % 8) as u8; self.toc_parser.take().unwrap().finalize() }; // Save file_header before creating frame (for preview frame recovery) self.saved_file_header = self.decoder_state.as_ref().map(|ds| ds.file_header.clone()); let frame = Frame::from_header_and_toc( self.frame_header.take().unwrap(), toc, self.decoder_state.take().unwrap(), )?; let mut sections: Vec<_> = frame .toc() .entries .iter() .map(|x| SectionBuffer { len: *x as usize, data: vec![], section: Section::LfGlobal, // will be fixed later }) .collect(); let order = if frame.toc().permuted { frame.toc().permutation.0.clone() } else { (0..sections.len() as u32).collect() }; if sections.len() > 1 { let base_sections = [Section::LfGlobal, Section::HfGlobal]; let lf_sections = (0..frame.header().num_lf_groups()).map(|x| Section::Lf { group: x }); let hf_sections = (0..frame.header().passes.num_passes).flat_map(|p| { (0..frame.header().num_groups()).map(move |g| Section::Hf { group: g, pass: p as usize, }) }); for section in base_sections .into_iter() .chain(lf_sections) .chain(hf_sections) { sections[order[frame.get_section_idx(section)] as usize].section = section; } } self.sections = sections.into_iter().collect(); self.ready_section_data = 0; // Move data from the pre-section buffer into the sections. for buf in self.sections.iter_mut() { if self.non_section_buf.is_empty() { break; } let mut data = Vec::new(); data.try_reserve_exact(buf.len)?; data.resize(buf.len, 0); buf.data = data; self.ready_section_data += self .non_section_buf .take(&mut [IoSliceMut::new(&mut buf.data)]); } self.section_state = SectionState::new(frame.header().num_lf_groups(), frame.header().num_groups()); self.frame = Some(frame); Ok(()) } }