Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and stale roadmap entries have been removed from the public documentation set.

Staged workspace version: `0.13.2`.

- `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
samples as RGB without a colour transform. A profile qualifies when it is
a restricted RGB matrix/TRC profile whose linear matrix to sRGB is within
2.5e-4 (off-diagonal row sums) and 1e-3 (diagonal) of identity, and whose
transform to sRGB moves no 8-bit channel or neutral ramp sample by more
than one code value. Other ICC profiles remain unsupported for these
outputs (#321).
- SOP marker segments a decode parses require `Lsop` 4 and an `Nsop` equal to
the tile-wide packet ordinal modulo 65536. The ordinal continues across
tile-parts and counts packets skipped without parsing; an invalid SOP in a
Expand Down
6 changes: 3 additions & 3 deletions crates/j2k/src/decode/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub(super) use u16::{
write_components_u16_output, write_plane_rows_u16, write_u16_output,
};
pub(super) use u8::{
can_decode_u8_directly, expand_rgb8_to_rgba8, write_components_u8_output, write_plane_rows_u8,
write_u8_output,
can_decode_u8_directly, expand_rgb8_to_rgba8, resolve_u8_color_space,
write_components_u8_output, write_plane_rows_u8, write_u8_output,
};

/// Shared destination and error plumbing for the 8- and 16-bit component
Expand All @@ -48,7 +48,7 @@ impl<'o> ComponentRowOutput<'o> {
fmt: PixelFormat,
) -> Self {
Self {
color_space,
color_space: resolve_u8_color_space(color_space, fmt),
has_alpha,
out,
stride,
Expand Down
26 changes: 24 additions & 2 deletions crates/j2k/src/decode/output/u8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,32 @@
//! Eight-bit direct-layout and component-plane conversion.

use crate::backend::{ColorSpace, DecodedComponents as NativeDecodedComponents};
use crate::decode::srgb8::is_srgb_equivalent_icc_profile;
use crate::J2kError;
use core::ops::Range;
use j2k_core::{PixelFormat, Unsupported};

static SRGB_ICC_AS_RGB: ColorSpace = ColorSpace::RGB;

/// The colour space whose samples `fmt` receives. A three-channel ICC
/// profile equivalent to IEC 61966-2-1 sRGB is written to RGB output as
/// RGB; every other colour space is returned unchanged.
pub(in crate::decode) fn resolve_u8_color_space(
color_space: &ColorSpace,
fmt: PixelFormat,
) -> &ColorSpace {
match (color_space, fmt) {
(
ColorSpace::Icc {
profile,
num_channels: 3,
},
PixelFormat::Rgb8 | PixelFormat::Rgba8,
) if is_srgb_equivalent_icc_profile(profile) => &SRGB_ICC_AS_RGB,
_ => color_space,
}
}

pub(in crate::decode) fn can_decode_u8_directly(
color_space: &ColorSpace,
has_alpha: bool,
Expand All @@ -30,7 +52,7 @@ pub(in crate::decode) fn write_components_u8_output(
fmt: PixelFormat,
) -> Result<(), J2kError> {
write_planes_u8(
components.color_space(),
resolve_u8_color_space(components.color_space(), fmt),
components.has_alpha(),
components.planes(),
components.dimensions(),
Expand Down Expand Up @@ -277,7 +299,7 @@ pub(in crate::decode) fn write_u8_output(
) -> Result<(), J2kError> {
let width = dims.0 as usize;
let height = dims.1 as usize;
match (color_space, has_alpha, fmt) {
match (resolve_u8_color_space(color_space, fmt), has_alpha, fmt) {
(ColorSpace::RGB, false, PixelFormat::Rgb8) => {
copy_rows_exact(decoded, out, stride, width * 3, height);
Ok(())
Expand Down
86 changes: 86 additions & 0 deletions crates/j2k/src/decode/srgb8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,92 @@ fn convert_icc_data(
}
}

/// Whether `profile` describes IEC 61966-2-1 sRGB closely enough for its
/// samples to be written as sRGB without a colour transform.
///
/// The profile must be a restricted matrix/TRC RGB profile whose linear
/// RGB-to-sRGB matrix is within [`SRGB_MATRIX_OFF_DIAGONAL_TOLERANCE`] and
/// [`SRGB_MATRIX_DIAGONAL_TOLERANCE`] of identity, and whose 8-bit transform
/// to sRGB, as [`decode_image_srgb8`] would apply it, moves no sample of the
/// per-channel and neutral ramps by more than one code value. The ramps
/// alone cannot bound mixed colours: a negative off-diagonal term clips to
/// zero on a single-channel ramp and cancels on neutrals, yet shifts a
/// mixed colour such as (30, 255, 0) by tens of code values, so the matrix
/// is checked directly. Any parse or transform failure reports the profile
/// as not sRGB.
pub(in crate::decode) fn is_srgb_equivalent_icc_profile(profile: &[u8]) -> bool {
let Some(max_profile_size) = profile.len().checked_add(1) else {
return false;
};
let Ok(source) = ColorProfile::new_from_slice_with_options(
profile,
ParsingOptions {
max_profile_size,
max_allowed_clut_size: 0,
max_allowed_trc_size: 65_536,
},
) else {
return false;
};
if source.color_space != DataColorSpace::Rgb || reject_non_restricted_profile(&source).is_err()
{
return false;
}
let srgb = ColorProfile::new_srgb();
if !is_near_identity(&source.transform_matrix(&srgb).v) {
return false;
}
let Ok(transform) =
source.create_transform_8bit(Layout::Rgb, &srgb, Layout::Rgb, TransformOptions::default())
else {
return false;
};

// Red, green, blue, and neutral ramps over every 8-bit code value.
let mut probe = Vec::new();
if probe.try_reserve_exact(256 * 4 * 3).is_err() {
return false;
}
for value in 0..=u8::MAX {
probe.extend_from_slice(&[value, 0, 0, 0, value, 0, 0, 0, value, value, value, value]);
}
let mut converted = Vec::new();
if converted.try_reserve_exact(probe.len()).is_err() {
return false;
}
converted.resize(probe.len(), 0_u8);
transform.transform(&probe, &mut converted).is_ok()
&& probe
.iter()
.zip(&converted)
.all(|(&expected, &actual)| expected.abs_diff(actual) <= 1)
}

/// Largest sum of one row's off-diagonal entries in the linear
/// source-to-sRGB matrix. Leakage into a channel that should be zero is
/// steepest on the sRGB toe, where one 8-bit code value is
/// `1 / (12.92 * 255)` (about 3.0e-4) of linear light; this keeps the
/// leakage under one code value.
const SRGB_MATRIX_OFF_DIAGONAL_TOLERANCE: f64 = 2.5e-4;

/// Largest distance of a diagonal entry from one. Its error in code values
/// peaks at full intensity, where the sRGB curve changes about 112 code
/// values per unit of linear light, so this stays near 0.1 code value.
const SRGB_MATRIX_DIAGONAL_TOLERANCE: f64 = 1e-3;

fn is_near_identity(matrix: &[[f64; 3]; 3]) -> bool {
matrix.iter().enumerate().all(|(row, entries)| {
let off_diagonal: f64 = entries
.iter()
.enumerate()
.filter(|&(column, _)| column != row)
.map(|(_, entry)| entry.abs())
.sum();
(entries[row] - 1.0).abs() <= SRGB_MATRIX_DIAGONAL_TOLERANCE
&& off_diagonal <= SRGB_MATRIX_OFF_DIAGONAL_TOLERANCE
})
}

fn reject_non_restricted_profile(profile: &ColorProfile) -> Result<(), J2kError> {
if profile.lut_a_to_b_perceptual.is_some()
|| profile.lut_a_to_b_colorimetric.is_some()
Expand Down
4 changes: 2 additions & 2 deletions crates/j2k/src/decode/stripes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::backend::{ColorSpace, Image};
use crate::J2kError;
use j2k_core::{DecodeRowsError, PixelFormat};

use super::output::{write_plane_rows_u16, write_plane_rows_u8};
use super::output::{resolve_u8_color_space, write_plane_rows_u16, write_plane_rows_u8};

#[cfg(test)]
std::thread_local! {
Expand Down Expand Up @@ -124,7 +124,7 @@ pub(crate) fn decode_image_rows_in_stripes<'a, E: core::error::Error + 'static>(
#[cfg(test)]
let skip_decode = injected == Some(InjectedFailure::BeforeRows);
let mut sink = StripeSink {
color_space: image.color_space(),
color_space: resolve_u8_color_space(image.color_space(), fmt),
has_alpha: image.has_alpha(),
fmt,
pack,
Expand Down
Loading
Loading