Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and stale roadmap entries have been removed from the public documentation set.

Staged workspace version: `0.13.2`.

- A JP2 whose codestream has more components than its declared color space
and no `cdef` box keeps that color space under strict decoding instead of
being reinterpreted from its component count. The extra components are
unspecified (ISO/IEC 15444-1 I.5.3.6) and decoded output omits them:
`Image::ignored_components` counts them. An sRGB JP2 over four components,
as Kakadu 7 wrote from RGBA TIFFs without `-jp2_alpha`, decodes to `Rgb8`,
or to `Rgba8` with opaque alpha, instead of failing as CMYK with "backend
color space cannot be mapped to requested 8-bit pixel format". Lenient
decoding still infers alpha for one extra component, raw codestreams keep
their component-count interpretation, and recode re-encodes every
component. HTJ2K batch jobs whose direct color plan is unsupported fall
back to the generic decode instead of failing. Bounded 8-bit and 16-bit
rows use the resolved color channels and alpha state, and recode validation
compares every component even when display output omits unspecified ones.
- `decode_region_into`, `decode_region_scaled_pow2_into`, `decode_into`,
and `decode_rows_u8_bounded` accept a JP2 whose ICC profile is equivalent
to IEC 61966-2-1 sRGB when the output is `Rgb8` or `Rgba8`, writing its
Expand Down
51 changes: 46 additions & 5 deletions crates/j2k-native/src/color/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,28 @@ use crate::{

const BITS_PER_ASSOCIATION_WORD: usize = 64;

/// The color interpretation of an image's codestream components.
pub(crate) struct ResolvedColor {
pub(crate) color_space: ColorSpace,
pub(crate) has_alpha: bool,
/// Whether one of the documented lenient metadata recoveries was used.
pub(crate) used_lenient_metadata_recovery: bool,
/// Trailing codestream components beyond the color (and alpha) channels
/// that no channel definition describes. ISO/IEC 15444-1 I.5.3.6 leaves
/// such components to the application; decoded output omits them.
pub(crate) ignored_components: usize,
}

/// Resolve the color space and alpha channel of `boxes` over `header`'s
/// components. `declared_color` is whether `boxes` carry the file's own
/// color specification rather than one synthesized for a raw codestream.
pub(crate) fn resolve_alpha_and_color_space(
boxes: &ImageBoxes,
header: &Header<'_>,
settings: &DecodeSettings,
retained_baseline_bytes: usize,
) -> Result<(ColorSpace, bool, bool)> {
declared_color: bool,
) -> Result<ResolvedColor> {
let mut num_components = header.component_infos.len();
if settings.resolve_palette_indices {
if let Some(palette_box) = &boxes.palette {
Expand Down Expand Up @@ -57,15 +73,24 @@ pub(crate) fn resolve_alpha_and_color_space(

let actual_num_components = header.component_infos.len();
let mut used_lenient_metadata_recovery = false;
if boxes.palette.is_none()
&& actual_num_components != usize::from(color_space.num_channels() + u16::from(has_alpha))
{
let mut ignored_components = 0;
let declared_channels = usize::from(color_space.num_channels()) + usize::from(has_alpha);
if boxes.palette.is_none() && actual_num_components != declared_channels {
if !settings.strict
&& actual_num_components == usize::from(color_space.num_channels()) + 1
&& !has_alpha
{
has_alpha = true;
used_lenient_metadata_recovery = true;
} else if declared_color
&& boxes.channel_definition.is_none()
&& declares_color_space(boxes)
&& actual_num_components > declared_channels
{
// Without a channel definition, the components after those of
// the declared color space are unspecified (ISO/IEC 15444-1
// I.5.3.6), not a reason to reinterpret the color space.
ignored_components = actual_num_components - declared_channels;
} else if actual_num_components == 1 || (actual_num_components == 2 && has_alpha) {
color_space = ColorSpace::Gray;
} else if actual_num_components == 3 {
Expand All @@ -84,7 +109,23 @@ pub(crate) fn resolve_alpha_and_color_space(
}
}

Ok((color_space, has_alpha, used_lenient_metadata_recovery))
Ok(ResolvedColor {
color_space,
has_alpha,
used_lenient_metadata_recovery,
ignored_components,
})
}

/// Whether the primary color specification names a color space, as an
/// enumerated value or an ICC profile, rather than leaving it unknown.
fn declares_color_space(boxes: &ImageBoxes) -> bool {
matches!(
boxes
.primary_color_specification()
.map(|specification| &specification.color_space),
Some(jp2::colr::ColorSpace::Enumerated(_) | jp2::colr::ColorSpace::Icc(_))
)
}

fn get_color_space(
Expand Down
4 changes: 3 additions & 1 deletion crates/j2k-native/src/color/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ mod types;

#[cfg(test)]
pub(crate) use cielab::cielab_to_rgb;
pub(crate) use metadata::{resolve_alpha_and_color_space, validate_and_reorder_channels};
pub(crate) use metadata::{
resolve_alpha_and_color_space, validate_and_reorder_channels, ResolvedColor,
};
pub(crate) use output_planes::native_component_plane_dimensions;
pub use output_planes::{
ComponentPlane, ComponentPlaneParts, ComponentRowSink, DecodedComponents,
Expand Down
56 changes: 42 additions & 14 deletions crates/j2k-native/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
interleave_and_convert_region, resolve_palette_indices, try_resize_decode_elements,
validate_and_reorder_channels, validate_interleaved_output_buffer, validate_roi, Bitmap,
ColorSpace, DecodedComponents, DecodedNativeComponents, DecoderContext, DecodingError,
FormatError, HtCodeBlockDecoder, Result, CODESTREAM_MAGIC, JP2_MAGIC,
FormatError, HtCodeBlockDecoder, ResolvedColor, Result, CODESTREAM_MAGIC, JP2_MAGIC,
};

mod allocation;
Expand Down Expand Up @@ -136,6 +136,9 @@ pub struct Image<'a> {
pub(crate) has_alpha: bool,
/// The color space of the image.
pub(crate) color_space: ColorSpace,
/// Trailing codestream components that decoded output omits: extra
/// channels no channel definition describes (see [`ResolvedColor`]).
pub(crate) ignored_components: usize,
/// Codec-owned bytes already live when this image was parsed, set by
/// `new_with_retained_baseline`. Only the planning entry points charge it:
/// `prepare_region_decoder_with_context` and the `build_*_plan*_with_context`
Expand Down Expand Up @@ -179,25 +182,19 @@ impl<'a> ImageSource<'a> {
pub(crate) struct ImageProperties {
boxes: ImageBoxes,
settings: DecodeSettings,
color_space: ColorSpace,
has_alpha: bool,
used_lenient_metadata_recovery: bool,
color: ResolvedColor,
}

impl ImageProperties {
pub(crate) const fn new(
boxes: ImageBoxes,
settings: DecodeSettings,
color_space: ColorSpace,
has_alpha: bool,
used_lenient_metadata_recovery: bool,
color: ResolvedColor,
) -> Self {
Self {
boxes,
settings,
color_space,
has_alpha,
used_lenient_metadata_recovery,
color,
}
}
}
Expand Down Expand Up @@ -254,9 +251,13 @@ impl<'a> Image<'a> {
let ImageProperties {
boxes,
settings,
color_space,
has_alpha,
used_lenient_metadata_recovery,
color:
ResolvedColor {
color_space,
has_alpha,
used_lenient_metadata_recovery,
ignored_components,
},
} = properties;
let metadata_bytes = retained_metadata_bytes(&header, &boxes, &color_space)?;
allocation::combine_retained_bytes(retained_baseline_bytes, metadata_bytes)?;
Expand All @@ -269,6 +270,7 @@ impl<'a> Image<'a> {
used_lenient_metadata_recovery,
has_alpha,
color_space,
ignored_components,
retained_baseline_bytes,
#[cfg(feature = "std")]
packet_cache: packet_directory::PacketDirectoryCache::default(),
Expand Down Expand Up @@ -396,6 +398,31 @@ impl<'a> Image<'a> {
&self.color_space
}

/// Trailing codestream components that decoded output omits.
///
/// Without a channel definition box, ISO/IEC 15444-1 I.5.3.6 leaves the
/// components after those of the declared color space unspecified: a
/// JP2 declaring sRGB over four components has three color channels and
/// an extra component, not CMYK. Decoded components, planes and bitmaps
/// hold only the color (and alpha) channels.
#[must_use]
pub fn ignored_components(&self) -> usize {
self.ignored_components
}

/// Keep every codestream component in decoded output, as a re-encode of
/// the whole codestream needs.
#[doc(hidden)]
pub fn keep_ignored_components(&mut self) {
self.ignored_components = 0;
}

/// Drop the [`Self::ignored_components`] from the end of `components`.
pub(crate) fn drop_ignored_components(&self, components: &mut Vec<ComponentData>) {
let kept = components.len().saturating_sub(self.ignored_components);
components.truncate(kept);
}

/// Return the primary JP2 restricted ICC profile, when present.
#[doc(hidden)]
#[must_use]
Expand Down Expand Up @@ -449,7 +476,7 @@ impl<'a> Image<'a> {
if self.settings.resolve_palette_indices && self.boxes.palette.is_some() {
return false;
}
if self.boxes.channel_definition.is_some() {
if self.boxes.channel_definition.is_some() || self.ignored_components != 0 {
return false;
}
!matches!(
Expand Down Expand Up @@ -1222,6 +1249,7 @@ impl<'a> Image<'a> {
retained_baseline_bytes,
)?;
}
self.drop_ignored_components(decoded_image.decoded_components);

let bit_depth = decoded_image
.decoded_components
Expand Down
3 changes: 2 additions & 1 deletion crates/j2k-native/src/image/native/rows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ impl<'i> NativePlanesSink<'i> {
/// yet.
fn new(image: &'i Image<'_>, dimensions: (u32, u32)) -> Self {
Self {
components: &image.header.component_infos,
components: &image.header.component_infos
[..image.header.component_infos.len() - image.ignored_components],
color_space: &image.color_space,
dimensions,
planes: Vec::new(),
Expand Down
26 changes: 25 additions & 1 deletion crates/j2k-native/src/image/output_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl<'a> Image<'a> {
let parallel = decoder_context.cpu_decode_parallelism() == CpuDecodeParallelism::Auto;
let retained_baseline_bytes =
self.run_decode(decoder_context, None, None, true, retained_baseline_bytes)?;
self.drop_ignored_components(&mut decoder_context.tile_decode_context.channel_data);

let components = &decoder_context.tile_decode_context.channel_data;
let component_owner_capacity = components.capacity();
Expand Down Expand Up @@ -251,6 +252,7 @@ impl<'a> Image<'a> {
let parallel = decoder_context.cpu_decode_parallelism() == CpuDecodeParallelism::Auto;
let retained_image_bytes =
self.run_decode(decoder_context, Some(roi), None, true, retained_image_bytes)?;
self.drop_ignored_components(&mut decoder_context.tile_decode_context.channel_data);

let components = &decoder_context.tile_decode_context.channel_data;
let component_owner_capacity = components.capacity();
Expand Down Expand Up @@ -495,13 +497,17 @@ impl<'a> Image<'a> {
) -> Result<()> {
validate_roi((self.width(), self.height()), roi)?;
self.validate_component_plane_precision()?;
let mut kept_sink = KeptComponentsSink {
sink: &mut *sink,
ignored_components: self.ignored_components,
};
let charged_image_bytes = self.run_decode_rows(
decoder_context,
Some(roi),
None,
true,
self.retained_header_metadata_bytes()?,
Some(&mut *sink),
Some(&mut kept_sink),
)?;
if decoder_context.tile_decode_context.stream_rows {
return Ok(());
Expand Down Expand Up @@ -677,3 +683,21 @@ impl<'a> Image<'a> {
)
}
}

/// Hands a sink the streamed rows of every component but the image's
/// [`Image::ignored_components`].
struct KeptComponentsSink<'s> {
sink: &'s mut dyn ComponentRowSink,
ignored_components: usize,
}

impl ComponentRowSink for KeptComponentsSink<'_> {
fn write_rows(&mut self, x: u32, y: u32, planes: &[ComponentPlane<'_>]) {
let kept = planes.len().saturating_sub(self.ignored_components);
self.sink.write_rows(x, y, &planes[..kept]);
}

fn rgb8_region(&mut self, width: u32, height: u32) -> Option<&mut [u8]> {
self.sink.rgb8_region(width, height)
}
}
6 changes: 4 additions & 2 deletions crates/j2k-native/src/j2c/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,15 @@ pub(crate) fn parse_with_retained_baseline<'a>(
retained_baseline_bytes,
)?;

let (color_space, has_alpha, _) = resolve_alpha_and_color_space(
let mut color = resolve_alpha_and_color_space(
&boxes,
&parsed_codestream.header,
&strict_settings,
retained_baseline_bytes,
false,
)?;
let properties = ImageProperties::new(boxes, *settings, color_space, has_alpha, false);
color.used_lenient_metadata_recovery = false;
let properties = ImageProperties::new(boxes, *settings, color);
if retained_baseline_bytes == 0 {
Image::from_parsed_parts(
ImageSource::new(stream, parsed_codestream.data),
Expand Down
14 changes: 4 additions & 10 deletions crates/j2k-native/src/jp2/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,21 +181,15 @@ pub(crate) fn parse_with_retained_baseline(
image_boxes.component_mapping = Some(ComponentMappingBox { entries: mappings });
}

let (color_space, has_alpha, recovered_alpha_metadata) = resolve_alpha_and_color_space(
let mut color = resolve_alpha_and_color_space(
&image_boxes,
&parsed_codestream.header,
&settings,
retained_baseline_bytes,
true,
)?;
let used_lenient_metadata_recovery =
container.used_lenient_metadata_recovery || recovered_alpha_metadata;
let properties = ImageProperties::new(
image_boxes,
settings,
color_space,
has_alpha,
used_lenient_metadata_recovery,
);
color.used_lenient_metadata_recovery |= container.used_lenient_metadata_recovery;
let properties = ImageProperties::new(image_boxes, settings, color);
if retained_baseline_bytes == 0 {
Image::from_parsed_parts(
ImageSource::new(data, parsed_codestream.data),
Expand Down
2 changes: 1 addition & 1 deletion crates/j2k-native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub use color::pack_rgb8;
pub(crate) use color::{
convert_color_space, interleave_and_convert_after_mct, interleave_and_convert_region,
resolve_alpha_and_color_space, resolve_palette_indices, validate_and_reorder_channels,
validate_interleaved_output_buffer,
validate_interleaved_output_buffer, ResolvedColor,
};
pub use color::{
Bitmap, ColorSpace, ComponentPlane, DecodedComponents, DecodedNativeComponents,
Expand Down
5 changes: 4 additions & 1 deletion crates/j2k/src/batch/direct/planning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ fn direct_color_plan_uses_only_htj2k(plan: &J2kDirectColorPlan) -> bool {
fn is_unsupported_direct_color_plan_error(error: NativeDecodeError) -> bool {
matches!(
error,
NativeDecodeError::Decoding(NativeDecodingError::UnsupportedFeature(_))
NativeDecodeError::Decoding(
NativeDecodingError::UnsupportedFeature(_)
| NativeDecodingError::DirectPlanUnsupported(_)
)
)
}
5 changes: 4 additions & 1 deletion crates/j2k/src/recode/pixel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ pub(super) fn recode(
});
let encoded = {
let components = {
let source = Image::new(bytes, &DecodeSettings::default())
let mut source = Image::new(bytes, &DecodeSettings::default())
.map_err(|err| map_native_decode_error(err, "source JPEG 2000 parse failed"))?;
// The re-encode keeps the source's JP2 metadata, so it carries
// every codestream component, unspecified extras included.
source.keep_ignored_components();
source.decode_native_components().map_err(|err| {
map_native_decode_error(err, "source JPEG 2000 component pixel fallback failed")
})?
Expand Down
Loading
Loading