j2k-native 0.6.1

Pure-Rust JPEG 2000 and HTJ2K codec engine for j2k
Documentation
use j2k_native::{
    encode, encode_precomputed_htj2k_53, encode_precomputed_htj2k_97, DecodeSettings,
    EncodeOptions, Image, J2kForwardDwt53Level, J2kForwardDwt53Output, J2kForwardDwt97Level,
    J2kForwardDwt97Output, PrecomputedHtj2k53Component, PrecomputedHtj2k53Image,
    PrecomputedHtj2k97Component, PrecomputedHtj2k97Image,
};

#[test]
fn native_encode_rejects_dimension_overflow_before_length_check() {
    let err = encode(
        &[0; 16],
        u32::MAX,
        u32::MAX,
        4,
        16,
        false,
        &EncodeOptions::default(),
    )
    .expect_err("dimension overflow should be rejected");

    assert_eq!(err, "image dimensions overflow");
}

#[test]
fn native_decode_rejects_zero_target_resolution() {
    let bytes = encode(
        &[10, 20, 30, 40],
        2,
        2,
        1,
        8,
        false,
        &EncodeOptions::default(),
    )
    .expect("encode fixture");
    let settings = DecodeSettings {
        target_resolution: Some((0, 1)),
        ..DecodeSettings::default()
    };

    assert!(Image::new(&bytes, &settings).is_err());
}

#[test]
fn jp2_decode_rejects_short_channel_definition_box() {
    let codestream = encode(
        &[10, 20, 30, 40, 50, 60],
        2,
        1,
        3,
        8,
        false,
        &EncodeOptions::default(),
    )
    .expect("encode RGB fixture");
    let jp2 = wrap_codestream_jp2_with_short_cdef(&codestream, 2, 1, 3, 8);

    let image = Image::new(&jp2, &DecodeSettings::default()).expect("parse JP2 fixture");
    let err = image
        .decode()
        .expect_err("short cdef must not silently drop a component");

    assert_eq!(err.to_string(), "invalid channel definition");
}

#[test]
fn precomputed_zero_grayscale_53_coefficients_decode_with_native_decoder() {
    let image = PrecomputedHtj2k53Image {
        width: 8,
        height: 8,
        bit_depth: 8,
        signed: false,
        components: vec![PrecomputedHtj2k53Component {
            x_rsiz: 1,
            y_rsiz: 1,
            dwt: zero_dwt53(8, 8),
        }],
    };
    let bytes = encode_precomputed_htj2k_53(&image, &precomputed_options())
        .expect("encode precomputed HTJ2K coefficients");
    let decoded = Image::new(&bytes, &DecodeSettings::default())
        .expect("native parser accepts codestream")
        .decode_native()
        .expect("native decoder accepts codestream");

    assert_eq!((decoded.width, decoded.height), (8, 8));
    assert_eq!(decoded.num_components, 1);
    assert!(decoded.data.iter().all(|&sample| sample == 128));
}

#[test]
fn precomputed_encode_writes_component_sampling_in_siz() {
    let image = PrecomputedHtj2k53Image {
        width: 16,
        height: 16,
        bit_depth: 8,
        signed: false,
        components: vec![
            PrecomputedHtj2k53Component {
                x_rsiz: 1,
                y_rsiz: 1,
                dwt: zero_dwt53(16, 16),
            },
            PrecomputedHtj2k53Component {
                x_rsiz: 2,
                y_rsiz: 2,
                dwt: zero_dwt53(8, 8),
            },
            PrecomputedHtj2k53Component {
                x_rsiz: 2,
                y_rsiz: 2,
                dwt: zero_dwt53(8, 8),
            },
        ],
    };
    let bytes = encode_precomputed_htj2k_53(&image, &precomputed_options())
        .expect("encode precomputed subsampled HTJ2K coefficients");
    let siz = find_marker(&bytes, 0x51).expect("SIZ marker");
    let component_info = siz + 40;

    assert_eq!(bytes[component_info + 1], 1);
    assert_eq!(bytes[component_info + 2], 1);
    assert_eq!(bytes[component_info + 4], 2);
    assert_eq!(bytes[component_info + 5], 2);
    assert_eq!(bytes[component_info + 7], 2);
    assert_eq!(bytes[component_info + 8], 2);
}

#[test]
fn precomputed_encode_rejects_component_sampling_geometry_mismatch() {
    let image = PrecomputedHtj2k53Image {
        width: 16,
        height: 16,
        bit_depth: 8,
        signed: false,
        components: vec![PrecomputedHtj2k53Component {
            x_rsiz: 2,
            y_rsiz: 2,
            dwt: zero_dwt53(16, 16),
        }],
    };

    let err = encode_precomputed_htj2k_53(&image, &precomputed_options())
        .expect_err("component DWT geometry must match SIZ sampling");

    assert_eq!(err, "precomputed DWT component dimensions mismatch");
}

#[test]
fn precomputed_encode_rejects_recursive_level_geometry_mismatch() {
    let mut dwt = zero_dwt53(8, 8);
    dwt.levels[0].low_width = 3;
    let image = PrecomputedHtj2k53Image {
        width: 8,
        height: 8,
        bit_depth: 8,
        signed: false,
        components: vec![PrecomputedHtj2k53Component {
            x_rsiz: 1,
            y_rsiz: 1,
            dwt,
        }],
    };

    let err = encode_precomputed_htj2k_53(&image, &precomputed_options())
        .expect_err("level geometry must match recursive 5/3 expectations");

    assert_eq!(err, "precomputed DWT recursive geometry mismatch");
}

#[test]
fn precomputed_zero_grayscale_97_coefficients_decode_with_native_decoder() {
    let image = PrecomputedHtj2k97Image {
        width: 8,
        height: 8,
        bit_depth: 8,
        signed: false,
        components: vec![PrecomputedHtj2k97Component {
            x_rsiz: 1,
            y_rsiz: 1,
            dwt: zero_dwt97(8, 8),
        }],
    };
    let bytes = encode_precomputed_htj2k_97(&image, &precomputed_lossy_options())
        .expect("encode precomputed 9/7 HTJ2K coefficients");
    let decoded = Image::new(&bytes, &DecodeSettings::default())
        .expect("native parser accepts 9/7 codestream")
        .decode_native()
        .expect("native decoder accepts 9/7 codestream");

    assert_eq!((decoded.width, decoded.height), (8, 8));
    assert_eq!(decoded.num_components, 1);
    assert!(decoded.data.iter().all(|&sample| sample == 128));
}

#[test]
fn precomputed_97_encode_rejects_component_sampling_geometry_mismatch() {
    let image = PrecomputedHtj2k97Image {
        width: 16,
        height: 16,
        bit_depth: 8,
        signed: false,
        components: vec![PrecomputedHtj2k97Component {
            x_rsiz: 2,
            y_rsiz: 2,
            dwt: zero_dwt97(16, 16),
        }],
    };

    let err = encode_precomputed_htj2k_97(&image, &precomputed_lossy_options())
        .expect_err("component DWT geometry must match SIZ sampling");

    assert_eq!(err, "precomputed DWT component dimensions mismatch");
}

fn precomputed_options() -> EncodeOptions {
    EncodeOptions {
        num_decomposition_levels: 1,
        reversible: true,
        use_ht_block_coding: true,
        use_mct: false,
        validate_high_throughput_codestream: false,
        ..EncodeOptions::default()
    }
}

fn precomputed_lossy_options() -> EncodeOptions {
    EncodeOptions {
        num_decomposition_levels: 1,
        reversible: false,
        use_ht_block_coding: true,
        use_mct: false,
        validate_high_throughput_codestream: false,
        ..EncodeOptions::default()
    }
}

fn zero_dwt53(width: u32, height: u32) -> J2kForwardDwt53Output {
    let low_width = width.div_ceil(2);
    let low_height = height.div_ceil(2);
    let high_width = width / 2;
    let high_height = height / 2;

    J2kForwardDwt53Output {
        ll: vec![0.0; (low_width * low_height) as usize],
        ll_width: low_width,
        ll_height: low_height,
        levels: vec![J2kForwardDwt53Level {
            hl: vec![0.0; (high_width * low_height) as usize],
            lh: vec![0.0; (low_width * high_height) as usize],
            hh: vec![0.0; (high_width * high_height) as usize],
            width,
            height,
            low_width,
            low_height,
            high_width,
            high_height,
        }],
    }
}

fn zero_dwt97(width: u32, height: u32) -> J2kForwardDwt97Output {
    let low_width = width.div_ceil(2);
    let low_height = height.div_ceil(2);
    let high_width = width / 2;
    let high_height = height / 2;

    J2kForwardDwt97Output {
        ll: vec![0.0; (low_width * low_height) as usize],
        ll_width: low_width,
        ll_height: low_height,
        levels: vec![J2kForwardDwt97Level {
            hl: vec![0.0; (high_width * low_height) as usize],
            lh: vec![0.0; (low_width * high_height) as usize],
            hh: vec![0.0; (high_width * high_height) as usize],
            width,
            height,
            low_width,
            low_height,
            high_width,
            high_height,
        }],
    }
}

fn find_marker(codestream: &[u8], marker: u8) -> Option<usize> {
    codestream
        .windows(2)
        .position(|window| window == [0xff, marker])
}

fn wrap_codestream_jp2_with_short_cdef(
    codestream: &[u8],
    width: u32,
    height: u32,
    components: u16,
    bit_depth: u8,
) -> Vec<u8> {
    let mut bytes = Vec::new();
    push_box(&mut bytes, b"jP  ", &[0x0D, 0x0A, 0x87, 0x0A]);
    push_box(
        &mut bytes,
        b"ftyp",
        &[b'j', b'p', b'2', b' ', 0, 0, 0, 0, b'j', b'p', b'2', b' '],
    );

    let mut jp2h = Vec::new();
    let mut ihdr = Vec::new();
    ihdr.extend_from_slice(&height.to_be_bytes());
    ihdr.extend_from_slice(&width.to_be_bytes());
    ihdr.extend_from_slice(&components.to_be_bytes());
    ihdr.extend_from_slice(&[bit_depth.saturating_sub(1), 7, 0, 0]);
    push_box(&mut jp2h, b"ihdr", &ihdr);

    let mut colr = vec![1, 0, 0];
    colr.extend_from_slice(&16_u32.to_be_bytes());
    push_box(&mut jp2h, b"colr", &colr);

    let mut cdef = Vec::new();
    cdef.extend_from_slice(&2_u16.to_be_bytes());
    for (channel_index, association) in [(0_u16, 1_u16), (1, 2)] {
        cdef.extend_from_slice(&channel_index.to_be_bytes());
        cdef.extend_from_slice(&0_u16.to_be_bytes());
        cdef.extend_from_slice(&association.to_be_bytes());
    }
    push_box(&mut jp2h, b"cdef", &cdef);

    push_box(&mut bytes, b"jp2h", &jp2h);
    push_box(&mut bytes, b"jp2c", codestream);
    bytes
}

fn push_box(bytes: &mut Vec<u8>, box_type: &[u8; 4], payload: &[u8]) {
    let len = (8 + payload.len()) as u32;
    bytes.extend_from_slice(&len.to_be_bytes());
    bytes.extend_from_slice(box_type);
    bytes.extend_from_slice(payload);
}