j2k-native 0.9.0

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

use alloc::{vec, vec::Vec};
use core::mem::size_of;

use super::cdef::ChannelType;
use super::cmap::{ComponentMappingEntry, ComponentMappingType};
use super::colr::{ColorSpace as NativeColorSpace, ColorSpecificationBox};
use super::container::{implicit_mapping_budget, parse_jp2_header_box};
use super::metadata::{public_metadata_from_boxes, Jp2ColorSpec};
use super::{inspect_jp2_container, ComponentDescriptor, ImageBoxes, Jp2FileKind};
use crate::{encode_htj2k, DecodeSettings, EncodeOptions, Image};

#[test]
fn colr_is_optional_for_jph_but_remains_required_for_jp2() {
    let jph = wrapper_without_colr(*b"jph ", None, &[0]);
    let container = inspect_jp2_container(&jph).expect("JPH may omit COLR");
    assert_eq!(container.file_kind, Jp2FileKind::Jph);
    assert!(container.metadata.color_specs.is_empty());

    let jp2 = wrapper_without_colr(*b"jp2 ", None, &[0]);
    assert!(matches!(
        inspect_jp2_container(&jp2),
        Err(crate::DecodeError::Format(
            crate::FormatError::MissingRequiredBox("colr")
        ))
    ));
}

#[test]
fn no_colr_jph_preserves_application_defined_cdef_component_order() {
    let codestream = encode_htj2k(&[7], 1, 1, 1, 8, false, &EncodeOptions::default())
        .expect("encode one-component HTJ2K");
    let mut cdef = Vec::new();
    cdef.extend_from_slice(&1_u16.to_be_bytes());
    cdef.extend_from_slice(&0_u16.to_be_bytes());
    cdef.extend_from_slice(&3_u16.to_be_bytes());
    cdef.extend_from_slice(&0x476c_u16.to_be_bytes());
    let jph = wrapper_without_colr(*b"jph ", Some(&cdef), &codestream);
    let inspected = inspect_jp2_container(&jph).expect("inspect application-defined CDEF");
    assert_eq!(
        inspected.metadata.channel_definitions[0].channel_type_raw(),
        3
    );
    assert_eq!(
        inspected.metadata.channel_definitions[0].association_raw(),
        0x476c
    );

    let image = Image::new(&jph, &DecodeSettings::default()).expect("parse no-COLR JPH");
    let decoded = image
        .decode_native_components()
        .expect("decode application-defined native component");

    assert_eq!(decoded.planes().len(), 1);
    assert_eq!(decoded.planes()[0].data(), &[7]);
}

#[test]
fn repeated_metadata_boxes_replace_old_owners_without_losing_accounting() {
    let mut header = required_header_prefix(&[1, 0, 0, 0, 0, 0, 17]);
    push_child(&mut header, *b"bpcc", &[7]);
    push_child(&mut header, *b"bpcc", &[0x87]);
    push_child(&mut header, *b"pclr", &palette_payload(7));
    push_child(&mut header, *b"pclr", &palette_payload(9));
    push_child(&mut header, *b"cdef", &cdef_payload(0));
    push_child(&mut header, *b"cdef", &cdef_payload(1));
    push_child(&mut header, *b"cmap", &[0, 0, 0, 0]);
    push_child(&mut header, *b"cmap", &[0, 0, 1, 0]);

    let boxes = parse_jp2_header_box(&header, true, 0).expect("repeated metadata parses");
    assert_eq!(
        boxes.bits_per_component,
        [ComponentDescriptor {
            bit_depth: 8,
            signed: true,
        }]
    );
    assert_eq!(
        boxes.palette.as_ref().and_then(|palette| palette.map(0, 0)),
        Some(9)
    );
    assert!(matches!(
        boxes
            .channel_definition
            .as_ref()
            .and_then(|box_| box_.channel_definitions.first())
            .map(|definition| definition.channel_type),
        Some(ChannelType::Opacity)
    ));
    assert!(matches!(
        boxes
            .component_mapping
            .as_ref()
            .and_then(|box_| box_.entries.first())
            .map(|entry| entry.mapping_type),
        Some(ComponentMappingType::Palette { column: 0 })
    ));

    let expected = shallow_allocated_bytes(&boxes);
    assert_eq!(
        boxes.allocated_bytes().expect("metadata byte count"),
        expected
    );
}

#[test]
fn non_strict_mid_palette_error_keeps_the_previous_complete_owner() {
    let mut header = required_header_prefix(&[1, 0, 0, 0, 0, 0, 17]);
    push_child(&mut header, *b"pclr", &palette_payload(7));
    // Two rows and two columns are declared, but the first row is truncated.
    push_child(&mut header, *b"pclr", &[0, 2, 2, 7, 7, 1]);
    push_child(&mut header, *b"cmap", &[0, 0, 1, 0]);

    let boxes = parse_jp2_header_box(&header, false, 0).expect("non-strict palette recovery");
    assert_eq!(
        boxes.palette.as_ref().and_then(|palette| palette.map(0, 0)),
        Some(7)
    );
    assert_eq!(
        boxes
            .component_mapping
            .as_ref()
            .map_or(0, |mapping| mapping.entries.len()),
        1
    );
    assert_eq!(
        boxes.allocated_bytes().expect("metadata byte count"),
        shallow_allocated_bytes(&boxes)
    );
}

#[test]
fn public_conversion_moves_icc_and_palette_payload_owners() {
    let mut header = required_header_prefix(&[2, 0, 0, 1, 2, 3, 4]);
    push_child(&mut header, *b"pclr", &palette_payload(11));
    let boxes = parse_jp2_header_box(&header, true, 0).expect("metadata parses");
    let source_icc_capacity = match &boxes.color_specifications[0].color_space {
        NativeColorSpace::Icc(profile) => profile.capacity(),
        _ => panic!("expected ICC profile"),
    };
    let palette = boxes.palette.as_ref().expect("palette");
    let source_outer_capacity = palette.entries.capacity();
    let source_entry_capacity = palette.entries[0].capacity();

    let metadata = public_metadata_from_boxes(boxes).expect("move conversion");
    let Jp2ColorSpec::IccProfile { profile } = &metadata.color_specs[0] else {
        panic!("expected public ICC profile");
    };
    let palette = metadata.palette.as_ref().expect("public palette");
    assert_eq!(profile.capacity(), source_icc_capacity);
    assert_eq!(palette.entries.capacity(), source_outer_capacity);
    assert_eq!(palette.entries[0].capacity(), source_entry_capacity);
}

#[test]
fn retained_jp2_parse_baseline_covers_nested_icc_and_palette_owners() {
    let mut header = required_header_prefix(&[2, 0, 0, 1, 2, 3, 4]);
    push_child(&mut header, *b"pclr", &palette_payload(11));
    let probe = parse_jp2_header_box(&header, true, 0).expect("probe JP2 metadata parses");
    let retained = probe.allocated_bytes().expect("probe metadata capacity");
    drop(probe);

    let exact_baseline = crate::DEFAULT_MAX_DECODE_BYTES - retained;
    parse_jp2_header_box(&header, true, exact_baseline)
        .expect("nested JP2 metadata fits exact aggregate cap");
    assert!(
        parse_jp2_header_box(&header, true, exact_baseline + 1).is_err(),
        "one byte beyond nested JP2 metadata cap must fail"
    );
}

#[test]
fn implicit_mapping_allocation_counts_external_baseline_at_exact_boundary() {
    let retained_container_bytes = 17;
    let mapping_bytes = size_of::<ComponentMappingEntry>();
    let exact_baseline = crate::DEFAULT_MAX_DECODE_BYTES - retained_container_bytes - mapping_bytes;
    let mut exact = implicit_mapping_budget(retained_container_bytes, exact_baseline)
        .expect("implicit mapping exact baseline");
    exact
        .try_vec::<ComponentMappingEntry>(1, "implicit JP2 component mappings")
        .expect("one implicit mapping fits exact aggregate cap");
    assert_eq!(exact.live_bytes(), crate::DEFAULT_MAX_DECODE_BYTES);

    let mut one_over = implicit_mapping_budget(retained_container_bytes, exact_baseline + 1)
        .expect("baseline alone remains under the cap");
    assert!(matches!(
        one_over.try_vec::<ComponentMappingEntry>(1, "implicit JP2 component mappings"),
        Err(crate::DecodeError::AllocationTooLarge { .. })
    ));
}

fn required_header_prefix(color_payload: &[u8]) -> Vec<u8> {
    let mut header = Vec::new();
    let mut image_header = Vec::new();
    image_header.extend_from_slice(&1_u32.to_be_bytes());
    image_header.extend_from_slice(&1_u32.to_be_bytes());
    image_header.extend_from_slice(&1_u16.to_be_bytes());
    image_header.extend_from_slice(&[0xff, 7, 0, 0]);
    push_child(&mut header, *b"ihdr", &image_header);
    push_child(&mut header, *b"colr", color_payload);
    header
}

fn wrapper_without_colr(brand: [u8; 4], cdef: Option<&[u8]>, codestream: &[u8]) -> Vec<u8> {
    let mut output = Vec::new();
    push_child(&mut output, *b"jP  ", &[0x0d, 0x0a, 0x87, 0x0a]);
    let mut file_type = Vec::new();
    file_type.extend_from_slice(&brand);
    file_type.extend_from_slice(&0_u32.to_be_bytes());
    file_type.extend_from_slice(&brand);
    push_child(&mut output, *b"ftyp", &file_type);

    let mut image_header = Vec::new();
    image_header.extend_from_slice(&1_u32.to_be_bytes());
    image_header.extend_from_slice(&1_u32.to_be_bytes());
    image_header.extend_from_slice(&1_u16.to_be_bytes());
    image_header.extend_from_slice(&[7, 7, 0, 0]);
    let mut header = Vec::new();
    push_child(&mut header, *b"ihdr", &image_header);
    if let Some(cdef) = cdef {
        push_child(&mut header, *b"cdef", cdef);
    }
    push_child(&mut output, *b"jp2h", &header);
    push_child(&mut output, *b"jp2c", codestream);
    output
}

fn palette_payload(value: u8) -> Vec<u8> {
    vec![0, 1, 1, 7, value]
}

fn cdef_payload(channel_type: u16) -> Vec<u8> {
    let mut payload = Vec::new();
    payload.extend_from_slice(&1_u16.to_be_bytes());
    payload.extend_from_slice(&0_u16.to_be_bytes());
    payload.extend_from_slice(&channel_type.to_be_bytes());
    payload.extend_from_slice(&0_u16.to_be_bytes());
    payload
}

fn push_child(output: &mut Vec<u8>, box_type: [u8; 4], payload: &[u8]) {
    let box_len = u32::try_from(payload.len() + 8).expect("small test box");
    output.extend_from_slice(&box_len.to_be_bytes());
    output.extend_from_slice(&box_type);
    output.extend_from_slice(payload);
}

fn shallow_allocated_bytes(boxes: &ImageBoxes) -> usize {
    let mut bytes = boxes.bits_per_component.capacity() * size_of::<ComponentDescriptor>();
    bytes += boxes.color_specifications.capacity() * size_of::<ColorSpecificationBox>();
    for color in &boxes.color_specifications {
        if let NativeColorSpace::Icc(profile) = &color.color_space {
            bytes += profile.capacity();
        }
    }
    if let Some(palette) = &boxes.palette {
        bytes += palette.columns.capacity() * size_of::<crate::jp2::pclr::PaletteColumn>();
        bytes += palette.entries.capacity() * size_of::<Vec<u64>>();
        bytes += palette.entries.iter().map(Vec::capacity).sum::<usize>() * size_of::<u64>();
    }
    if let Some(mapping) = &boxes.component_mapping {
        bytes += mapping.entries.capacity() * size_of::<ComponentMappingEntry>();
    }
    if let Some(definition) = &boxes.channel_definition {
        bytes += definition.channel_definitions.capacity()
            * size_of::<crate::jp2::cdef::ChannelDefinition>();
    }
    bytes
}