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: 8 additions & 1 deletion crates/j2k-native/src/j2c/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::codestream::{ComponentInfo, Header, QuantizationStyle, WaveletTransfo
use super::ht_block_decode::{self, HtBlockDecodeContext};
use super::idwt::IDWTOutput;
use super::progression::{progression_iterator, ProgressionData};
use super::roi::RoiPlan;
use super::roi::{tile_intersects_output_region, RoiPlan};
use super::tag_tree::TagNode;
use super::tile::{ComponentTile, ResolutionTile, Tile};
use super::{bitplane, build, idwt, mct, segment, tile, ComponentData};
Expand Down Expand Up @@ -171,6 +171,13 @@ fn decode<'a>(
tile.rect.height(),
);

if let Some(output_region) = tile_ctx.output_region {
if !tile_intersects_output_region(tile.rect, &header.size_data, output_region) {
ltrace!("tile {} outside output region, skipped", tile.idx);
continue;
}
}

decode_tile(
tile,
header,
Expand Down
27 changes: 6 additions & 21 deletions crates/j2k-native/src/j2c/decode/direct_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::{
Result, RoiPlan, SubBand, SubBandDecodeParameters, Tile, ValidationError, Vec,
};
use crate::j2c::rect::IntRect;
use crate::j2c::roi::tile_intersects_output_region;
use crate::{
HtCodeBlockPayloadRanges, J2kClassicCodeBlockPayload, J2kCodestreamRange, J2kDirectRgbaPlan,
J2kReferencedClassicPlan, J2kReferencedHtj2kPlan, J2kReferencedPayloadRecordSpan,
Expand Down Expand Up @@ -269,27 +270,11 @@ fn tile_intersects_output(
.first()
.ok_or(DecodingError::CodeBlockDecodeFailure)?;
validate_unit_sampled_component(component_info)?;
let component_tile = ComponentTile::new(tile, component_info);
let resolution_tile = ResolutionTile::new(
component_tile,
component_info.num_resolution_levels() - 1 - header.skipped_resolution_levels,
);
let x_offset = header
.size_data
.image_area_x_offset
.div_ceil(header.size_data.x_shrink_factor);
let y_offset = header
.size_data
.image_area_y_offset
.div_ceil(header.size_data.y_shrink_factor);
let request_left = output_region.x.saturating_add(x_offset);
let request_top = output_region.y.saturating_add(y_offset);
let request_right = request_left.saturating_add(output_region.width);
let request_bottom = request_top.saturating_add(output_region.height);
Ok(resolution_tile.rect.x0 < request_right
&& request_left < resolution_tile.rect.x1
&& resolution_tile.rect.y0 < request_bottom
&& request_top < resolution_tile.rect.y1)
Ok(tile_intersects_output_region(
tile.rect,
&header.size_data,
output_region,
))
}

fn payload_record_span(
Expand Down
123 changes: 122 additions & 1 deletion crates/j2k-native/src/j2c/roi.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
use alloc::vec::Vec;

use super::build::Decomposition;
use super::codestream::{Header, WaveletTransform};
use super::codestream::{Header, SizeData, WaveletTransform};
use super::decode::{DecompositionStorage, OutputRegion};
use super::rect::IntRect;
use super::tile::{ComponentTile, ResolutionTile, Tile};
use crate::{
idwt_required_input_window_for_rects, try_resize_decode_elements, J2kRequiredBandRegion, Result,
};

/// Whether a reference-grid tile can contribute to a decoded output region.
///
/// The output region is expressed in final image coordinates. Tile bounds are
/// projected into the same component- and resolution-shrunk grid with outward
/// rounding, which may retain a boundary tile but never drops a contributor.
pub(crate) fn tile_intersects_output_region(
tile_rect: IntRect,
size_data: &SizeData,
output_region: OutputRegion,
) -> bool {
let x_shrink = size_data
.x_shrink_factor
.saturating_mul(size_data.x_resolution_shrink_factor)
.max(1);
let y_shrink = size_data
.y_shrink_factor
.saturating_mul(size_data.y_resolution_shrink_factor)
.max(1);
let x_offset = size_data.image_area_x_offset.div_ceil(x_shrink);
let y_offset = size_data.image_area_y_offset.div_ceil(y_shrink);
let region = IntRect::from_ltrb(
output_region.x.saturating_add(x_offset),
output_region.y.saturating_add(y_offset),
output_region
.x
.saturating_add(output_region.width)
.saturating_add(x_offset),
output_region
.y
.saturating_add(output_region.height)
.saturating_add(y_offset),
);
let tile_rect = IntRect::from_ltrb(
tile_rect.x0 / x_shrink,
tile_rect.y0 / y_shrink,
tile_rect.x1.div_ceil(x_shrink),
tile_rect.y1.div_ceil(y_shrink),
);
tile_rect.intersects(region)
}

#[derive(Debug)]
#[expect(
clippy::struct_field_names,
Expand Down Expand Up @@ -245,3 +286,83 @@ fn required_region_from_int_rect(rect: IntRect) -> J2kRequiredBandRegion {
fn int_rect_from_required_region(region: J2kRequiredBandRegion) -> IntRect {
IntRect::from_ltrb(region.x0, region.y0, region.x1, region.y1)
}

#[cfg(test)]
mod tests {
use super::{tile_intersects_output_region, IntRect, OutputRegion, SizeData};
use crate::j2c::codestream::ComponentSizeInfo;

fn size_data(
image_offset: (u32, u32),
component_shrink: (u32, u32),
resolution_shrink: (u32, u32),
) -> SizeData {
SizeData {
reference_grid_width: 515,
reference_grid_height: 389,
image_area_x_offset: image_offset.0,
image_area_y_offset: image_offset.1,
tile_width: 128,
tile_height: 128,
tile_x_offset: 1,
tile_y_offset: 1,
component_sizes: vec![ComponentSizeInfo {
precision: 8,
signed: false,
horizontal_resolution: 1,
vertical_resolution: 1,
}],
x_shrink_factor: component_shrink.0,
y_shrink_factor: component_shrink.1,
x_resolution_shrink_factor: resolution_shrink.0,
y_resolution_shrink_factor: resolution_shrink.1,
}
}

#[test]
fn tile_intersection_handles_nonzero_image_and_tile_origins() {
let size_data = size_data((3, 5), (1, 1), (4, 2));
let first_tile = IntRect::from_ltrb(3, 5, 129, 129);
let next_tile = IntRect::from_ltrb(129, 5, 257, 129);
let top_left = OutputRegion {
x: 0,
y: 0,
width: 8,
height: 8,
};

assert!(tile_intersects_output_region(
first_tile, &size_data, top_left
));
assert!(!tile_intersects_output_region(
next_tile, &size_data, top_left
));
}

#[test]
fn tile_intersection_combines_component_and_resolution_shrink() {
let size_data = size_data((3, 5), (2, 2), (2, 4));
let tile = IntRect::from_ltrb(129, 129, 257, 257);

assert!(tile_intersects_output_region(
tile,
&size_data,
OutputRegion {
x: 31,
y: 15,
width: 2,
height: 2,
}
));
assert!(!tile_intersects_output_region(
tile,
&size_data,
OutputRegion {
x: 0,
y: 0,
width: 8,
height: 8,
}
));
}
}
148 changes: 148 additions & 0 deletions crates/j2k/tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1766,3 +1766,151 @@ fn region_decode_inside_tiles_matches_whole_decode_crop() {
}
}
}

#[test]
fn scaled_region_decode_on_multi_tile_codestream_matches_scaled_whole_decode_crop() {
// Reduced-resolution region decode on a multi-tile codestream: the
// output region lives in scaled image coordinates while tile rects
// stay on the reference grid. Cover a grid with partial edge tiles,
// interior, straddling, and far-corner regions at every downscale.
let (width, height) = (320_u32, 288_u32);
let bytes = encode_tiled_rgb_codestream(width, height, (128, 128));
let fmt = PixelFormat::Rgb8;
let bytes_per_pixel = fmt.bytes_per_pixel();

for scale in [
Downscale::None,
Downscale::Half,
Downscale::Quarter,
Downscale::Eighth,
] {
let denominator = scale.denominator();
let (scaled_width, scaled_height) =
(width.div_ceil(denominator), height.div_ceil(denominator));
let mut whole_decoder = J2kDecoder::new(&bytes).expect("whole decoder");
let whole_stride = scaled_width as usize * bytes_per_pixel;
let mut whole = vec![0_u8; whole_stride * scaled_height as usize];
whole_decoder
.decode_scaled_into(
&mut j2k::J2kScratchPool::new(),
&mut whole,
whole_stride,
fmt,
scale,
)
.expect("scaled whole decode");

for roi in [
Rect {
x: 24,
y: 24,
w: 64,
h: 64,
},
Rect {
x: 96,
y: 96,
w: 80,
h: 80,
},
Rect {
x: 240,
y: 232,
w: 72,
h: 48,
},
] {
let scaled_roi = roi.scaled_covering(scale);
let mut region_decoder = J2kDecoder::new(&bytes).expect("region decoder");
let region_stride = scaled_roi.w as usize * bytes_per_pixel;
let mut region = vec![0_u8; region_stride * scaled_roi.h as usize];
let outcome = region_decoder
.decode_region_scaled_into(
&mut j2k::J2kScratchPool::new(),
&mut region,
region_stride,
fmt,
roi,
scale,
)
.expect("scaled region decode");
assert_eq!(outcome.decoded, scaled_roi);
assert_eq!(
region,
crop_bytes(&whole, scaled_width as usize, bytes_per_pixel, scaled_roi),
"region {},{} {}x{} at 1/{denominator} disagrees with scaled whole decode",
roi.x,
roi.y,
roi.w,
roi.h,
);
}
}
}

#[test]
fn region_decode_on_subsampled_codestream_matches_whole_decode_crop() {
let (width, height) = (96_u32, 80_u32);
let luma = (0..height)
.flat_map(|y| (0..width).map(move |x| masked_fixture_byte(x + y * 3)))
.collect::<Vec<_>>();
let chroma_width = width / 2;
let chroma_height = height / 2;
let chroma_blue = (0..chroma_height)
.flat_map(|y| (0..chroma_width).map(move |x| masked_fixture_byte(64 + x * 2 + y * 5)))
.collect::<Vec<_>>();
let chroma_red = (0..chroma_height)
.flat_map(|y| (0..chroma_width).map(move |x| masked_fixture_byte(192 + x * 3 + y * 2)))
.collect::<Vec<_>>();
let planes = [
J2kLosslessComponentPlane {
data: &luma,
x_rsiz: 1,
y_rsiz: 1,
},
J2kLosslessComponentPlane {
data: &chroma_blue,
x_rsiz: 2,
y_rsiz: 2,
},
J2kLosslessComponentPlane {
data: &chroma_red,
x_rsiz: 2,
y_rsiz: 2,
},
];
let samples = J2kLosslessComponentSamples::new(&planes, width, height, 8, false)
.expect("subsampled component samples");
let encoded = encode_j2k_lossless_components(
samples,
&J2kLosslessEncodeOptions::default()
.with_backend(EncodeBackendPreference::CpuOnly)
.with_block_coding_mode(J2kBlockCodingMode::Classic)
.with_reversible_transform(ReversibleTransform::None53)
.with_max_decomposition_levels(Some(1)),
)
.expect("subsampled encode");

for roi in [
Rect {
x: 8,
y: 6,
w: 16,
h: 18,
},
Rect {
x: 28,
y: 24,
w: 24,
h: 24,
},
Rect {
x: 68,
y: 52,
w: 20,
h: 20,
},
] {
assert_region_decode_matches_whole_decode_crop(&encoded.codestream, (width, height), roi);
}
}