j2k-native 0.6.2

Pure-Rust JPEG 2000 and HTJ2K codec engine for j2k
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Regression scaffold for JP2 palette/component-map validation.

use j2k_native::{
    encode, encode_htj2k, ColorError, DecodeError, DecodeSettings, EncodeOptions, FormatError,
    Image,
};

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

#[test]
fn empty_cmap_with_palette_returns_error() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_empty_cmap(&codestream);

    let result = Image::new(&jp2, &DecodeSettings::default()).and_then(|image| image.decode());

    let err = result.expect_err("empty cmap must be rejected explicitly");
    assert!(
        matches!(
            err,
            DecodeError::Format(FormatError::InvalidBox)
                | DecodeError::Color(ColorError::PaletteResolutionFailed)
        ),
        "unexpected empty cmap error: {err:?}"
    );
}

#[test]
fn missing_ihdr_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let colr = jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 17]);
    let jp2 = jp2_with_header_payload(&codestream, &colr);

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("missing ihdr must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn invalid_ihdr_compression_type_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let ihdr = {
        let mut payload = Vec::new();
        payload.extend_from_slice(&4_u32.to_be_bytes());
        payload.extend_from_slice(&4_u32.to_be_bytes());
        payload.extend_from_slice(&1_u16.to_be_bytes());
        payload.extend_from_slice(&[7, 0, 0, 0]);
        jp2_box(b"ihdr", &payload)
    };
    let colr = jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 17]);
    let mut jp2h_payload = Vec::new();
    jp2h_payload.extend_from_slice(&ihdr);
    jp2h_payload.extend_from_slice(&colr);
    let jp2 = jp2_with_header_payload(&codestream, &jp2h_payload);

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("invalid ihdr compression type must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn missing_colr_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let mut ihdr = Vec::new();
    ihdr.extend_from_slice(&4_u32.to_be_bytes());
    ihdr.extend_from_slice(&4_u32.to_be_bytes());
    ihdr.extend_from_slice(&1_u16.to_be_bytes());
    ihdr.extend_from_slice(&[7, 7, 0, 0]);
    let jp2h = jp2_box(b"ihdr", &ihdr);
    let jp2 = jp2_with_header_payload(&codestream, &jp2h);

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("missing COLR must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn ihdr_dimension_mismatch_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(&codestream, &basic_jp2h_payload(5, 4, 1, 8));

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("IHDR dimensions must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn ihdr_bpc_mismatch_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(&codestream, &basic_jp2h_payload(4, 4, 1, 16));

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("IHDR BPC mismatch must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn bpcc_precision_mismatch_returns_invalid_box() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(&codestream, &bpcc_jp2h_payload(4, 4, 1, &[15]));

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("BPCC precision mismatch must reject"),
        Err(err) => err,
    };

    assert!(matches!(err, DecodeError::Format(FormatError::InvalidBox)));
}

#[test]
fn jph_file_type_rejects_classic_codestream() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload_and_file_type(
        &codestream,
        &basic_jp2h_payload(4, 4, 1, 8),
        b"jph \0\0\0\0jph ",
    );

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("JPH file type must reject classic codestreams"),
        Err(err) => err,
    };

    assert!(matches!(
        err,
        DecodeError::Format(FormatError::InvalidFileType)
    ));
}

#[test]
fn jp2_file_type_rejects_htj2k_codestream() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(&codestream, &basic_jp2h_payload(4, 4, 1, 8));

    let err = match Image::new(&jp2, &DecodeSettings::default()) {
        Ok(_) => panic!("JP2 file type must reject HTJ2K codestreams"),
        Err(err) => err,
    };

    assert!(matches!(
        err,
        DecodeError::Format(FormatError::InvalidFileType)
    ));
}

#[test]
fn jph_file_type_accepts_htj2k_codestream() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode_htj2k(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload_and_file_type(
        &codestream,
        &basic_jp2h_payload(4, 4, 1, 8),
        b"jph \0\0\0\0jph ",
    );

    let image = Image::new(&jp2, &DecodeSettings::default()).expect("JPH parses");
    let bitmap = image.decode_native().expect("JPH decodes");

    assert_eq!(bitmap.data, pixels);
}

#[test]
fn premultiplied_opacity_cdef_sets_alpha() {
    let pixels: Vec<u8> = (0..4 * 4 * 4).map(|idx| idx as u8).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 4, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(
        &codestream,
        &cdef_jp2h_payload(
            4,
            4,
            4,
            8,
            16,
            &[(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 2, 0)],
        ),
    );

    let image = Image::new(&jp2, &DecodeSettings::default()).expect("JP2 parses");

    assert!(image.has_alpha());
}

#[test]
fn unspecified_cdef_association_decodes() {
    let pixels: Vec<u8> = (0..16).collect();
    let options = EncodeOptions {
        reversible: true,
        num_decomposition_levels: 1,
        ..EncodeOptions::default()
    };
    let codestream = encode(&pixels, 4, 4, 1, 8, false, &options).expect("encode fixture");
    let jp2 = jp2_with_header_payload(
        &codestream,
        &cdef_jp2h_payload(4, 4, 1, 8, 17, &[(0, u16::MAX, u16::MAX)]),
    );

    let bitmap = Image::new(&jp2, &DecodeSettings::default())
        .expect("JP2 parses")
        .decode_native()
        .expect("JP2 decodes");

    assert_eq!(bitmap.data, pixels);
}

fn jp2_with_empty_cmap(codestream: &[u8]) -> Vec<u8> {
    let ihdr = {
        let mut payload = Vec::new();
        payload.extend_from_slice(&4_u32.to_be_bytes());
        payload.extend_from_slice(&4_u32.to_be_bytes());
        payload.extend_from_slice(&1_u16.to_be_bytes());
        payload.extend_from_slice(&[7, 7, 0, 0]);
        jp2_box(b"ihdr", &payload)
    };
    let colr = jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 17]);
    let pclr = {
        let mut payload = Vec::new();
        payload.extend_from_slice(&1_u16.to_be_bytes());
        payload.push(1);
        payload.push(7);
        payload.push(0);
        jp2_box(b"pclr", &payload)
    };
    let cmap = jp2_box(b"cmap", &[]);

    let mut jp2h_payload = Vec::new();
    jp2h_payload.extend_from_slice(&ihdr);
    jp2h_payload.extend_from_slice(&colr);
    jp2h_payload.extend_from_slice(&pclr);
    jp2h_payload.extend_from_slice(&cmap);

    jp2_with_header_payload(codestream, &jp2h_payload)
}

fn basic_jp2h_payload(width: u32, height: u32, components: u16, bit_depth: u8) -> Vec<u8> {
    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]);
    let colr = jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 17]);

    let mut jp2h_payload = Vec::new();
    jp2h_payload.extend_from_slice(&jp2_box(b"ihdr", &ihdr));
    jp2h_payload.extend_from_slice(&colr);
    jp2h_payload
}

fn bpcc_jp2h_payload(width: u32, height: u32, components: u16, bpcc_payload: &[u8]) -> Vec<u8> {
    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(&[0xff, 7, 0, 0]);
    let colr = jp2_box(b"colr", &[1, 0, 0, 0, 0, 0, 17]);

    let mut jp2h_payload = Vec::new();
    jp2h_payload.extend_from_slice(&jp2_box(b"ihdr", &ihdr));
    jp2h_payload.extend_from_slice(&jp2_box(b"bpcc", bpcc_payload));
    jp2h_payload.extend_from_slice(&colr);
    jp2h_payload
}

fn cdef_jp2h_payload(
    width: u32,
    height: u32,
    components: u16,
    bit_depth: u8,
    colorspace: u32,
    definitions: &[(u16, u16, u16)],
) -> Vec<u8> {
    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]);
    let mut colr = vec![1, 0, 0];
    colr.extend_from_slice(&colorspace.to_be_bytes());
    let mut cdef = Vec::new();
    cdef.extend_from_slice(&(definitions.len() as u16).to_be_bytes());
    for (channel, channel_type, association) in definitions {
        cdef.extend_from_slice(&channel.to_be_bytes());
        cdef.extend_from_slice(&channel_type.to_be_bytes());
        cdef.extend_from_slice(&association.to_be_bytes());
    }

    let mut jp2h_payload = Vec::new();
    jp2h_payload.extend_from_slice(&jp2_box(b"ihdr", &ihdr));
    jp2h_payload.extend_from_slice(&jp2_box(b"colr", &colr));
    jp2h_payload.extend_from_slice(&jp2_box(b"cdef", &cdef));
    jp2h_payload
}

fn jp2_with_header_payload(codestream: &[u8], jp2h_payload: &[u8]) -> Vec<u8> {
    jp2_with_header_payload_and_file_type(codestream, jp2h_payload, b"jp2 \0\0\0\0jp2 ")
}

fn jp2_with_header_payload_and_file_type(
    codestream: &[u8],
    jp2h_payload: &[u8],
    ftyp_payload: &[u8],
) -> Vec<u8> {
    let mut out = Vec::new();
    out.extend_from_slice(&jp2_box(b"jP  ", &[0x0d, 0x0a, 0x87, 0x0a]));
    out.extend_from_slice(&jp2_box(b"ftyp", ftyp_payload));
    out.extend_from_slice(&jp2_box(b"jp2h", jp2h_payload));
    out.extend_from_slice(&jp2_box(b"jp2c", codestream));
    out
}