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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
with:
toolchain: "1.96"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ Runnable repository examples:
- `cargo run -p j2k-tilecodec --example decompress`
([crates/j2k-tilecodec/examples/decompress.rs](crates/j2k-tilecodec/examples/decompress.rs))

JPEG 2000 callers that need reduced regions below the shared
`Downscale::Eighth` ceiling can use
`J2kDecoder::decode_region_scaled_pow2_into`. The level is an exact count of
power-of-two halvings; requests beyond any component's codestream resolution
ladder return an unsupported error instead of silently decoding at another
scale.

Runtime backend selection defaults to `Auto`: CPU remains the portable baseline,
and Metal or CUDA paths are selected only for supported shapes with validation
and benchmark evidence. Single-frame HTJ2K host-output encode stays CPU by
Expand Down
8 changes: 4 additions & 4 deletions crates/j2k-metal/src/compute/direct_roi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,10 @@ pub(super) fn idwt_input_windows_from_slices(
hh: &DirectBandSlice,
) -> PreparedIdwtInputWindows {
PreparedIdwtInputWindows {
ll: BandRequiredRegion::full(ll.window.width(), ll.window.height()),
hl: BandRequiredRegion::full(hl.window.width(), hl.window.height()),
lh: BandRequiredRegion::full(lh.window.width(), lh.window.height()),
hh: BandRequiredRegion::full(hh.window.width(), hh.window.height()),
ll: ll.window,
hl: hl.window,
lh: lh.window,
hh: hh.window,
}
}

Expand Down
38 changes: 34 additions & 4 deletions crates/j2k-native/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,39 @@ impl<'a> Image<'a> {
/// Returns an error when the input signature, container, or codestream is invalid.
pub fn new(data: &'a [u8], settings: &DecodeSettings) -> Result<Self> {
if data.starts_with(JP2_MAGIC) {
jp2::parse(data, *settings)
jp2::parse(data, *settings, None)
} else if data.starts_with(CODESTREAM_MAGIC) {
j2c::parse(data, settings)
j2c::parse(data, settings, None)
} else {
err!(FormatError::InvalidSignature)
}
}

/// Parse an image at an exact JPEG 2000 resolution reduction level.
///
/// This low-level adapter exists for the `j2k` facade. A reduction of zero
/// preserves full resolution; each additional level halves both axes.
///
/// # Errors
///
/// Returns an error when a target-resolution hint is also configured, the
/// reduction is not representable, the codestream has too few resolution
/// levels, or the input is invalid.
#[doc(hidden)]
pub fn new_with_reduction(
data: &'a [u8],
settings: &DecodeSettings,
reduction_levels: u8,
) -> Result<Self> {
if settings.target_resolution.is_some() {
return err!(DecodingError::UnsupportedFeature(
"exact reduction cannot be combined with a target-resolution hint",
));
}
if data.starts_with(JP2_MAGIC) {
jp2::parse(data, *settings, Some(reduction_levels))
} else if data.starts_with(CODESTREAM_MAGIC) {
j2c::parse(data, settings, Some(reduction_levels))
} else {
err!(FormatError::InvalidSignature)
}
Expand All @@ -256,9 +286,9 @@ impl<'a> Image<'a> {
return Self::new(data, settings);
}
if data.starts_with(JP2_MAGIC) {
jp2::parse_with_retained_baseline(data, *settings, retained_baseline_bytes)
jp2::parse_with_retained_baseline(data, *settings, retained_baseline_bytes, None)
} else if data.starts_with(CODESTREAM_MAGIC) {
j2c::parse_with_retained_baseline(data, settings, retained_baseline_bytes)
j2c::parse_with_retained_baseline(data, settings, retained_baseline_bytes, None)
} else {
err!(FormatError::InvalidSignature)
}
Expand Down
80 changes: 80 additions & 0 deletions crates/j2k-native/src/image/contract_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ fn gray_fixture() -> (Vec<u8>, Vec<u8>) {
(samples, encoded)
}

fn insert_main_header_coc(
mut codestream: Vec<u8>,
component: u8,
decomposition_levels: u8,
) -> Vec<u8> {
let cod_offset = codestream
.windows(2)
.position(|marker| marker == [0xff, 0x52])
.expect("COD marker");
let cod_length = u16::from_be_bytes(
codestream[cod_offset + 2..cod_offset + 4]
.try_into()
.expect("COD length"),
) as usize;
let cod_end = cod_offset + 2 + cod_length;
let coding_style = codestream[cod_offset + 4];
let mut component_parameters = codestream[cod_offset + 9..cod_end].to_vec();
component_parameters[0] = decomposition_levels;

let coc_segment_length = u16::try_from(4 + component_parameters.len()).expect("COC length");
let mut coc = vec![0xff, 0x53];
coc.extend_from_slice(&coc_segment_length.to_be_bytes());
coc.push(component);
coc.push(coding_style);
coc.extend_from_slice(&component_parameters);
codestream.splice(cod_end..cod_end, coc);
codestream
}

fn expected_crop(samples: &[u8], roi: (u32, u32, u32, u32)) -> Vec<u8> {
let (x, y, width, height) = roi;
let mut cropped = Vec::new();
Expand Down Expand Up @@ -172,3 +201,54 @@ fn retained_baseline_zero_and_nonzero_paths_preserve_parse_metadata() {
retained.color_space().num_channels()
);
}

#[test]
fn exact_reduction_rejects_a_shorter_component_coc_ladder() {
let pixels = vec![17_u8; 64 * 64 * 3];
let encoded = encode(
&pixels,
64,
64,
3,
8,
false,
&EncodeOptions {
num_decomposition_levels: 4,
reversible: true,
..EncodeOptions::default()
},
)
.expect("RGB fixture encodes");
let encoded = insert_main_header_coc(encoded, 1, 2);

let Err(error) = Image::new_with_reduction(&encoded, &DecodeSettings::strict(), 3) else {
panic!("component COC must cap exact reduction");
};

assert!(matches!(
error,
crate::DecodeError::Decoding(DecodingError::UnsupportedFeature(
"requested reduction exceeds the codestream resolution ladder"
))
));
}

#[test]
fn exact_reduction_rejects_a_target_resolution_hint() {
let (_, encoded) = gray_fixture();
let settings = DecodeSettings {
target_resolution: Some((1, 1)),
..DecodeSettings::strict()
};

let Err(error) = Image::new_with_reduction(&encoded, &settings, 0) else {
panic!("two resolution policies must not be combined");
};

assert!(matches!(
error,
crate::DecodeError::Decoding(DecodingError::UnsupportedFeature(
"exact reduction cannot be combined with a target-resolution hint"
))
));
}
46 changes: 29 additions & 17 deletions crates/j2k-native/src/j2c/codestream/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) fn read_header<'a>(
reader: &mut BitReader<'a>,
settings: &DecodeSettings,
retained_baseline_bytes: usize,
exact_reduction_levels: Option<u8>,
) -> Result<Header<'a>> {
if reader.read_marker()? != markers::SIZ {
bail!(MarkerError::Expected("SIZ"));
Expand Down Expand Up @@ -214,27 +215,38 @@ pub(crate) fn read_header<'a>(
.map(super::model::ComponentInfo::num_resolution_levels)
.min()
.ok_or(ValidationError::InvalidComponentMetadata)?;
let skipped_resolution_levels =
if let Some((target_width, target_height)) = settings.target_resolution {
if target_width == 0 || target_height == 0 {
bail!(ValidationError::InvalidDimensions);
}
let width_log =
skipped_levels_to_reach_target(size_data.checked_image_width()?, target_width);
let height_log =
skipped_levels_to_reach_target(size_data.checked_image_height()?, target_height);

width_log.min(height_log)
} else {
0
let max_skipped_resolution_levels = min_num_resolution_levels
.checked_sub(1)
.ok_or(ValidationError::InvalidComponentMetadata)?;
let skipped_resolution_levels = if let Some(requested) = exact_reduction_levels {
if requested > max_skipped_resolution_levels {
bail!(DecodingError::UnsupportedFeature(
"requested reduction exceeds the codestream resolution ladder",
));
}
requested
} else if let Some((target_width, target_height)) = settings.target_resolution {
if target_width == 0 || target_height == 0 {
bail!(ValidationError::InvalidDimensions);
}
.min(min_num_resolution_levels - 1);
let width_log =
skipped_levels_to_reach_target(size_data.checked_image_width()?, target_width);
let height_log =
skipped_levels_to_reach_target(size_data.checked_image_height()?, target_height);

width_log.min(height_log).min(max_skipped_resolution_levels)
} else {
0
};

// If the user defined a maximum resolution level that is lower than the
// maximum available one, the final image needs to be shrunk further.
let resolution_shrink_factor = 1u32
.checked_shl(u32::from(skipped_resolution_levels))
.ok_or(ValidationError::InvalidDimensions)?;
let Some(resolution_shrink_factor) = 1u32.checked_shl(u32::from(skipped_resolution_levels))
else {
bail!(DecodingError::UnsupportedFeature(
"requested reduction exceeds supported image geometry",
));
};
size_data.x_resolution_shrink_factor = size_data
.x_resolution_shrink_factor
.checked_mul(resolution_shrink_factor)
Expand Down
8 changes: 6 additions & 2 deletions crates/j2k-native/src/j2c/codestream/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,12 @@ fn incomplete_packet_length_varint_is_rejected() {

#[test]
fn public_marker_function_signatures_stay_stable() {
let _: for<'a> fn(&mut BitReader<'a>, &DecodeSettings, usize) -> Result<Header<'a>> =
read_header;
let _: for<'a> fn(
&mut BitReader<'a>,
&DecodeSettings,
usize,
Option<u8>,
) -> Result<Header<'a>> = read_header;
let _: fn(&mut BitReader<'_>) -> Result<CodingStyleDefault> = cod_marker;
let _: fn(&mut BitReader<'_>, u16) -> Result<(u16, CodingStyleComponent)> = coc_marker;
let _: fn(&mut BitReader<'_>) -> Result<QuantizationInfo> = qcd_marker;
Expand Down
2 changes: 1 addition & 1 deletion crates/j2k-native/src/j2c/encode/multitile/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn direct_packet_owners_match_single_tile_marker_serialization() {

let mut reader = BitReader::new(&codestream);
assert_eq!(reader.read_marker().expect("SOC marker"), markers::SOC);
let header = read_header(&mut reader, &DecodeSettings::default(), 0)
let header = read_header(&mut reader, &DecodeSettings::default(), 0, None)
.expect("serialized single-tile header");
assert_eq!(header.plm_packet_lengths, packetized.packet_lengths);
assert_eq!(header.ppm_packets.len(), packetized.packet_headers.len());
Expand Down
27 changes: 21 additions & 6 deletions crates/j2k-native/src/j2c/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,28 @@ pub(crate) struct ComponentData {

crate::move_only::assert_move_only!(ComponentData);

pub(crate) fn parse<'a>(stream: &'a [u8], settings: &DecodeSettings) -> Result<Image<'a>> {
parse_with_retained_baseline(stream, settings, 0)
pub(crate) fn parse<'a>(
stream: &'a [u8],
settings: &DecodeSettings,
exact_reduction_levels: Option<u8>,
) -> Result<Image<'a>> {
parse_with_retained_baseline(stream, settings, 0, exact_reduction_levels)
}

pub(crate) fn parse_with_retained_baseline<'a>(
stream: &'a [u8],
settings: &DecodeSettings,
retained_baseline_bytes: usize,
exact_reduction_levels: Option<u8>,
) -> Result<Image<'a>> {
let mut strict_settings = *settings;
strict_settings.strict = true;
let parsed_codestream =
parse_raw_with_retained_baseline(stream, &strict_settings, retained_baseline_bytes)?;
let parsed_codestream = parse_raw_with_retained_baseline(
stream,
&strict_settings,
retained_baseline_bytes,
exact_reduction_levels,
)?;
let header = &parsed_codestream.header;
// Raw codestreams do not carry JP2 channel definitions. Keep the
// conventional grayscale/RGB assumptions for 1- and 3-component images,
Expand Down Expand Up @@ -133,13 +142,14 @@ pub(crate) fn parse_raw<'a>(
stream: &'a [u8],
settings: &DecodeSettings,
) -> Result<ParsedCodestream<'a>> {
parse_raw_with_retained_baseline(stream, settings, 0)
parse_raw_with_retained_baseline(stream, settings, 0, None)
}

pub(crate) fn parse_raw_with_retained_baseline<'a>(
stream: &'a [u8],
settings: &DecodeSettings,
retained_baseline_bytes: usize,
exact_reduction_levels: Option<u8>,
) -> Result<ParsedCodestream<'a>> {
let mut reader = BitReader::new(stream);

Expand All @@ -148,7 +158,12 @@ pub(crate) fn parse_raw_with_retained_baseline<'a>(
bail!(MarkerError::Expected("SOC"));
}

let header = codestream::read_header(&mut reader, settings, retained_baseline_bytes)?;
let header = codestream::read_header(
&mut reader,
settings,
retained_baseline_bytes,
exact_reduction_levels,
)?;
let code_stream_data = reader.tail().ok_or(FormatError::MissingCodestream)?;

Ok(ParsedCodestream {
Expand Down
10 changes: 10 additions & 0 deletions crates/j2k-native/src/j2c/tile/tile_part.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ pub(super) fn parse_tile_part<'a>(
}
}

if tile
.component_infos
.iter()
.any(|component| component.num_resolution_levels() <= main_header.skipped_resolution_levels)
{
bail!(DecodingError::UnsupportedFeature(
"tile coding style has fewer levels than the requested reduction",
));
}

let Some(remaining_bytes) = data_len.checked_sub(reader.offset() - start) else {
return if main_header.strict {
err!(TileError::Invalid)
Expand Down
1 change: 1 addition & 0 deletions crates/j2k-native/src/j2c/tile/tile_part/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,6 @@ pub(super) fn tile_part_bytes(
bytes
}

mod reduction;
mod sot;
mod transaction;
Loading