// 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.
impl Frame { /// Add conversion stages for non-float output formats. /// This is needed before saving to U8/U16/F16 formats to convert from the pipeline's f32. fn add_conversion_stages<P: RenderPipeline>( mut pipeline: RenderPipelineBuilder<P>,
channels: &[usize],
data_format: JxlDataFormat,
clamp_range_for_f16: Option<(f32, f32)>,
) -> RenderPipelineBuilder<P> { usecrate::render::stages::{
ConvertF32ToF16Stage, ConvertF32ToU8Stage, ConvertF32ToU16Stage,
};
match data_format {
JxlDataFormat::U8 { bit_depth } => { for &channel in channels {
pipeline =
pipeline.add_inout_stage(ConvertF32ToU8Stage::new(channel, bit_depth));
}
}
JxlDataFormat::U16 { bit_depth, .. } => { for &channel in channels {
pipeline =
pipeline.add_inout_stage(ConvertF32ToU16Stage::new(channel, bit_depth));
}
}
JxlDataFormat::F16 { .. } => { for &channel in channels {
pipeline = pipeline.add_inout_stage(
ConvertF32ToF16Stage::new_with_clamp_range(channel, clamp_range_for_f16),
);
}
} // F32 doesn't need conversion - the pipeline already uses f32
JxlDataFormat::F32 { .. } => {}
}
pipeline
}
/// Check if CMS will consume a black channel that the user requested in the output. fn check_cms_consumed_black_channel(
black_channel: Option<usize>,
in_channels: usize,
out_channels: usize,
pixel_format: &JxlPixelFormat,
) -> Result<()> { iflet Some(k_pipeline_idx) = black_channel
&& out_channels < in_channels
{ // K channel is consumed (4->3 conversion) let k_ec_idx = k_pipeline_idx - 3; if pixel_format
.extra_channel_format
.get(k_ec_idx)
.is_some_and(|f| f.is_some())
{ return Err(Error::CmsConsumedChannelRequested {
channel_index: k_ec_idx,
channel_type: "Black".to_string(),
});
}
}
Ok(())
}
/// Returns `true` if any pixels were written to the output buffers during /// this call, `false` if the call was a no-op for the buffers (e.g. no new /// HF groups, no flush work, or the render pipeline was not yet ready). pubfn decode_and_render_hf_groups(
&mutself,
api_buffers: &mut Option<&mut [JxlOutputBuffer<'_>]>,
pixel_format: &JxlPixelFormat,
groups: Vec<(usize, Vec<(usize, BitReader)>)>,
do_flush: bool,
output_profile: &JxlColorProfile,
) -> Result<bool> { ifself.render_pipeline.is_none() || self.lf_global.is_none() {
assert_eq!(groups.iter().map(|x| x.1.len()).sum::<usize>(), 0); // We don't yet have any output ready (as the pipeline would be initialized otherwise), // so exit without doing anything. return Ok(false);
}
// Temporarily remove the reference/lf frames to be saved; we will move them back once // rendering is done. letmut reference_frame_data = std::mem::take(&mutself.reference_frame_data); letmut lf_frame_data = std::mem::take(&mutself.lf_frame_data);
iflet Some(ref_images) = &mut reference_frame_data {
buffers.extend(ref_images.iter_mut().map(|img| { let rect = Rect {
size: img.size(),
origin: (0, 0),
};
Some(JxlOutputBuffer::from_image_rect_mut(
img.get_rect_mut(rect).into_raw(),
))
}));
};
iflet Some(lf_images) = &mut lf_frame_data {
buffers.extend(lf_images.iter_mut().map(|img| { let rect = Rect {
size: img.size(),
origin: (0, 0),
};
Some(JxlOutputBuffer::from_image_rect_mut(
img.get_rect_mut(rect).into_raw(),
))
}));
};
// STEP 1: if we are requesting a flush, and did not flush before, mark modular channels // as having been decoded as 0. if !self.was_flushed_once && do_flush { self.was_flushed_once = true; self.groups_to_flush.extend(0..self.header.num_groups());
modular_global.zero_fill_empty_channels( self.header.passes.num_passes as usize, self.header.num_groups(), self.header.num_lf_groups(),
)?;
}
// STEP 2: ensure that groups that will be re-rendered are marked as such. // VarDCT data to be rendered. for (g, _) in groups.iter() { self.groups_to_flush.insert(*g);
pipeline!(self, p, p.mark_group_to_rerender(*g));
} // Modular data to be re-rendered.
{ let modular_global = &mutself.lf_global.as_mut().unwrap().modular_global; for (group, passes) in groups.iter() { for (pass, _) in passes.iter() {
modular_global.mark_group_to_be_read(2 + *pass, *group);
}
} letmut pass_to_pipeline = |_, group, _, _| { self.groups_to_flush.insert(group);
pipeline!(self, p, p.mark_group_to_rerender(group));
Ok(())
};
modular_global.process_output(&self.header, true, &='color:red'>mut pass_to_pipeline)?;
}
// STEP 3: decode the groups, eagerly rendering VarDCT channels and noise. for (group, mut passes) in groups { ifself.decode_hf_group(group, &mut passes, &mut buffer_splitter, do_flush)? { self.changed_since_last_flush
.insert((group, RenderUnit::VarDCT));
}
}
// STEP 4: process all modular transforms that can now be processed, // flushing buffers that will not be used again, if either we are forcing a render now // or we are done with the file. ifself.incomplete_groups == 0 || do_flush { let modular_global = &mutself.lf_global.as_mut().unwrap().modular_global; letmut pass_to_pipeline = |chan, group, complete, image: Option<Image<i32>>| { self.changed_since_last_flush
.insert((group, RenderUnit::Modular(chan)));
pipeline!( self,
p,
p.set_buffer_for_group(
chan,
group,
complete,
image.unwrap(),
&mut buffer_splitter
)?
);
Ok(())
};
modular_global.process_output(&self.header, false, &e='color:red'>mut pass_to_pipeline)?;
// STEP 5: re-render VarDCT/noise data in rendered groups for which it was // not rendered, or re-send to pipeline modular channels that were not // updated in those groups. for g in std::mem::take(&mutself.groups_to_flush) { ifself
.changed_since_last_flush
.take(&(g, RenderUnit::VarDCT))
.is_none()
{ self.decode_hf_group(g, &mut [], &mut buffer_splitter, true)?;
} let modular_global = &mutself.lf_global.as_mut().unwrap().modular_global; letmut pass_to_pipeline = |chan, group, complete, image| {
pipeline!( self,
p,
p.set_buffer_for_group(chan, group, complete, image, &mut buffer_splitter)?
);
Ok(())
}; for c in modular_global.channel_range() { ifself
.changed_since_last_flush
.take(&(g, RenderUnit::Modular(c)))
.is_none()
{
modular_global.flush_output(g, c, &mut pass_to_pipeline)?;
}
}
}
}
let regions = buffer_splitter.into_changed_regions(); let rendered = !regions.is_empty() && self.header.frame_type == FrameType::RegularFrame;
ifself.header.frame_type == FrameType::LFFrame && self.header.lf_level == 1 { if do_flush && let Some(buffers) = api_buffers { returnself.maybe_preview_lf_frame(
pixel_format,
buffers,
Some(®ions[..]),
output_profile,
);
} elseifself.incomplete_groups == 0 { // If we are not requesting another flush at the end of the LF frame, we // probably have a partial render. Ensure we re-render the LF frame when // decoding the actual frame. self.decoder_state.lf_frame_was_rendered = false;
}
}
// Calculate the actual number of API-provided buffers based on pixel_format. // This is the number of buffers the caller provides, NOT the theoretical max. // When extra_channel_format[i] is None, that channel doesn't get a buffer. let num_api_buffers = std::iter::once(&pixel_format.color_data_format)
.chain(pixel_format.extra_channel_format.iter())
.filter(|x| x.is_some())
.count();
assert_eq!(
pixel_format.extra_channel_format.len(),
frame_header.num_extra_channels as usize
);
if frame_header.lf_level != 0 { for i in0..3 {
pipeline = pipeline.add_save_stage(
&[i],
Orientation::Identity,
num_api_buffers + i,
JxlColorType::Grayscale,
JxlDataFormat::f32(), false,
);
}
} if frame_header.can_be_referenced && frame_header.save_before_ct { for i in0..num_channels {
pipeline = pipeline.add_save_stage(
&[i],
Orientation::Identity,
num_api_buffers + i,
JxlColorType::Grayscale,
JxlDataFormat::f32(), false,
);
}
}
let output_color_info = OutputColorInfo::from_header(&decoder_state.file_header)?;
// Determine output TF: use output profile's TF if available, else fall back to embedded profile's TF. // Note: output_color_info (luminances, opsin matrix) always comes from the embedded profile; // CMS handles any primaries conversion if the output profile differs. let output_tf = output_profile
.transfer_function()
.map(|tf| {
TransferFunction::from_api_tf(
tf,
output_color_info.intensity_target,
output_color_info.luminances,
)
})
.unwrap_or_else(|| output_color_info.tf.clone());
// Clamp transfer-domain values while converting to f16 so we don't // emit wild out-of-range values to downstream consumers. // // PQ has a bounded signal domain [0,1]. // HLG may carry modest overshoot/undershoot (e.g. from narrow-range // workflows), so preserve headroom with a looser clamp. let clamp_range_for_f16 = match &output_tf {
TransferFunction::Pq { .. } => Some((0.0, 1.0)),
TransferFunction::Hlg { .. } => Some((-0.074, 1.1)),
_ => None,
};
// Find the Black (K) extra channel if present. // In JXL, CMYK is stored as 3 color channels (CMY) + K as extra channel. // Pipeline index of K = extra_channel_index + 3 let black_channel: Option<usize> = decoder_state
.file_header
.image_metadata
.extra_channel_info
.iter()
.enumerate()
.find(|x| x.1.ec_type == ExtraChannel::Black)
.map(|(k_idx, _)| k_idx + 3);
let xyb_encoded = decoder_state.file_header.image_metadata.xyb_encoded;
// Insert CMS stage if profiles differ. // Following libjxl: use EITHER CMS OR FromLinearStage, never both. // - If output matches original encoding: only FromLinearStage is needed // - If output differs: CMS handles everything including TF conversion // // For XYB images, XybStage outputs LINEAR data in the embedded profile's primaries, // so the CMS input should be the LINEAR version of the embedded profile. // For ICC embedded profiles with XYB, XybStage outputs linear sRGB (see xyb.rs). let cms_input_profile = if xyb_encoded { // XYB outputs linear, so use linear version of input profile for CMS
input_profile.with_linear_tf().or_else(|| { // For ICC profiles with XYB, XybStage outputs linear sRGB
Some(JxlColorProfile::Simple(JxlColorEncoding::linear_srgb( false,
)))
})
} else { // Non-XYB: data is in the embedded profile's space including TF
Some(input_profile.clone())
};
// Compare ORIGINAL input profile (not linearized cms_input_profile) with output. // This matches libjxl (53042ec5) dec_xyb.cc:184: // color_encoding_is_original = orig_color_encoding.SameColorEncoding(c_desired); let color_encoding_is_original = input_profile.same_color_encoding(output_profile); letmut cms_used = false;
// Skip CMS if channel counts differ (grayscale↔RGB) - like libjxl's not_mixing_color_and_grey. // Exception: CMYK (4) → RGB (3) is allowed via CMS. let src_channels = cms_input_profile
.as_ref()
.map(|p| p.channels())
.unwrap_or(3); let dst_channels = output_profile.channels(); let channel_counts_compatible =
src_channels == dst_channels || (src_channels == 4 && dst_channels == 3);
if !color_encoding_is_original
&& channel_counts_compatible
&& let Some(cms) = cms
&& let Some(cms_input) = cms_input_profile
{ // Use frame width as max_pixels since rows can be that wide let max_pixels = frame_header.size_upsampled().0; // Use CMS input profile's channel count, matching libjxl's c_src_.Channels() // For CMYK, channels() returns 4; for RGB, 3; for grayscale, 1. let in_channels = cms_input.channels(); let (out_channels, transformers) = cms.initialize_transforms( 1, // num transforms (1 for single-threaded)
max_pixels,
cms_input,
output_profile.clone(),
output_color_info.intensity_target,
)?; // CMS cannot add channels - reject transforms that would if out_channels > in_channels { return Err(Error::CmsChannelCountIncrease {
in_channels,
out_channels,
});
} // Only pass black_channel to CmsStage if CMS is actually processing CMYK input. // For XYB images, even if original was CMYK, CMS input is linear RGB. let cms_black_channel = if in_channels == 4 {
black_channel
} else {
None
}; Self::check_cms_consumed_black_channel(
cms_black_channel,
in_channels,
out_channels,
pixel_format,
)?; if !transformers.is_empty() {
pipeline = pipeline.add_inplace_stage(CmsStage::new(
transformers,
in_channels,
out_channels,
cms_black_channel,
max_pixels,
));
cms_used = true;
}
}
// XYB output is linear, so apply transfer function: // - Only if output is non-linear AND // - CMS was not used (CMS already handles the full conversion including TF) if xyb_encoded && !output_tf.is_linear() && !cms_used {
pipeline = pipeline.add_inplace_stage(FromLinearStage::new(0, output_tf.clone()));
}
if frame_header.needs_blending() {
pipeline = pipeline.add_inplace_stage(BlendingStage::new(
frame_header,
&decoder_state.file_header,
decoder_state.reference_frames.clone(),
)?); // TODO(veluca): we might not need to add an extend stage if the image size is // compatible with the frame size.
pipeline = pipeline.add_extend_stage(ExtendToImageDimensionsStage::new(
frame_header,
&decoder_state.file_header,
decoder_state.reference_frames.clone(),
)?);
}
if frame_header.can_be_referenced && !frame_header.save_before_ct { for i in0..num_channels {
pipeline = pipeline.add_save_stage(
&[i],
Orientation::Identity,
num_api_buffers + i,
JxlColorType::Grayscale,
JxlDataFormat::f32(), false,
);
}
}
if decoder_state.render_spotcolors { for (i, info) in decoder_state
.file_header
.image_metadata
.extra_channel_info
.iter()
.enumerate()
{ if info.ec_type == ExtraChannel::SpotColor {
pipeline = pipeline
.add_inplace_stage(SpotColorStage::new(i, info.spot_color.unwrap()));
}
}
}
if frame_header.is_visible() { let color_space = decoder_state
.file_header
.image_metadata
.color_encoding
.color_space; let num_color_channels = if color_space == ColorSpace::Gray { 1
} else { 3
}; // Find the alpha channel info (index and metadata) if the color type requires alpha let alpha_channel_info = if pixel_format.color_type.has_alpha() {
decoder_state
.file_header
.image_metadata
.extra_channel_info
.iter()
.enumerate()
.find(|x| x.1.ec_type == ExtraChannel::Alpha)
} else {
None
}; let alpha_in_color = alpha_channel_info.map(|x| x.0 + 3); // Check if the source alpha is already premultiplied (alpha_associated) let source_alpha_associated =
alpha_channel_info.is_some_and(|(_, info)| info.alpha_associated()); if pixel_format.color_type.is_grayscale() && num_color_channels == 3 { return Err(Error::NotGrayscale);
} // Determine if we need to fill opaque alpha: // - color_type requests alpha (has_alpha() is true) // - but no actual alpha channel exists in the image (alpha_in_color is None) let fill_opaque_alpha = pixel_format.color_type.has_alpha() && alpha_in_color.is_none();
// Determine if we should premultiply: // - premultiply_output is requested // - there is an alpha channel in the output // - source is not already premultiplied (to avoid double-premultiplication) let should_premultiply = decoder_state.premultiply_output
&& alpha_in_color.is_some()
&& !source_alpha_associated;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.