j2k-transcode 0.7.2

JPEG-to-HTJ2K coefficient-domain transcode primitives for j2k
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

use super::{JpegDctComponent, JpegToHtj2kCoefficientPath, JpegToHtj2kError, JpegToHtj2kOptions};
use crate::allocation::{
    checked_add_allocation_bytes, checked_allocation_bytes, try_vec_with_capacity,
};

pub(super) fn validate_transcode_options(
    options: &JpegToHtj2kOptions,
) -> Result<(), JpegToHtj2kError> {
    validate_option_allocation(options)?;
    if !options.encode_options.use_ht_block_coding {
        return Err(JpegToHtj2kError::Unsupported(
            "jpeg_to_htj2k requires HT block coding",
        ));
    }
    if options.encode_options.use_mct {
        return Err(JpegToHtj2kError::Unsupported(
            "jpeg_to_htj2k requires use_mct=false because JPEG components stay in native color space",
        ));
    }

    match (options.coefficient_path, options.encode_options.reversible) {
        (
            JpegToHtj2kCoefficientPath::IntegerDirect53
            | JpegToHtj2kCoefficientPath::FloatDirectLinear53,
            true,
        )
        | (JpegToHtj2kCoefficientPath::FloatDirectLinear97, false) => Ok(()),
        (
            JpegToHtj2kCoefficientPath::IntegerDirect53
            | JpegToHtj2kCoefficientPath::FloatDirectLinear53,
            false,
        ) => Err(JpegToHtj2kError::Unsupported(
            "5/3 coefficient path requires reversible HTJ2K encode",
        )),
        (JpegToHtj2kCoefficientPath::FloatDirectLinear97, true) => {
            Err(JpegToHtj2kError::Unsupported(
                "9/7 coefficient path requires irreversible HTJ2K encode",
            ))
        }
    }
}

fn validate_option_allocation(options: &JpegToHtj2kOptions) -> Result<(), JpegToHtj2kError> {
    let quality_bytes =
        checked_allocation_bytes::<u64>(options.encode_options.quality_layer_byte_targets.len())?;
    let sampling_bytes = checked_allocation_bytes::<(u8, u8)>(
        options
            .encode_options
            .component_sampling
            .as_ref()
            .map_or(0, Vec::len),
    )?;
    let precinct_bytes =
        checked_allocation_bytes::<(u8, u8)>(options.encode_options.precinct_exponents.len())?;
    let option_bytes = checked_add_allocation_bytes(quality_bytes, sampling_bytes)?;
    checked_add_allocation_bytes(option_bytes, precinct_bytes)?;
    Ok(())
}

pub(super) fn validate_component_block_grid(
    component: &JpegDctComponent,
) -> Result<(), JpegToHtj2kError> {
    let block_cols = component.block_cols as usize;
    let block_rows = component.block_rows as usize;
    let expected_blocks =
        block_cols
            .checked_mul(block_rows)
            .ok_or(JpegToHtj2kError::Validation(
                "component block grid overflow",
            ))?;
    if component.dequantized_blocks.len() != expected_blocks {
        return Err(JpegToHtj2kError::Validation(
            "component block count does not match block grid",
        ));
    }

    Ok(())
}

pub(super) fn decomposition_levels_for_components(
    components: &[JpegDctComponent],
    requested_levels: u8,
) -> Result<u8, JpegToHtj2kError> {
    if requested_levels == 0 {
        return Err(JpegToHtj2kError::Unsupported(
            "jpeg_to_htj2k requires at least one decomposition level",
        ));
    }

    let available_levels = components
        .iter()
        .map(|component| available_decomposition_levels(component.width, component.height))
        .min()
        .ok_or(JpegToHtj2kError::Unsupported("missing JPEG components"))?;
    let decomposition_levels = requested_levels.min(available_levels);
    if decomposition_levels == 0 {
        return Err(JpegToHtj2kError::Unsupported(
            "component dimensions are too small for a DWT decomposition",
        ));
    }

    Ok(decomposition_levels)
}

pub(super) fn available_decomposition_levels(width: u32, height: u32) -> u8 {
    let min_dim = width.min(height);
    if min_dim <= 1 {
        0
    } else {
        u8::try_from(min_dim.ilog2()).unwrap_or(u8::MAX)
    }
}

pub(super) fn component_sampling_for_jpeg(
    components: &[JpegDctComponent],
    reference_width: u32,
    reference_height: u32,
) -> Result<Vec<(u8, u8)>, JpegToHtj2kError> {
    let max_h = components
        .iter()
        .map(|component| component.h_samp)
        .max()
        .ok_or(JpegToHtj2kError::Unsupported("missing JPEG components"))?;
    let max_v = components
        .iter()
        .map(|component| component.v_samp)
        .max()
        .ok_or(JpegToHtj2kError::Unsupported("missing JPEG components"))?;

    let mut sampling = try_vec_with_capacity(components.len())?;
    for component in components {
        if component.h_samp == 0 || component.v_samp == 0 {
            return Err(JpegToHtj2kError::Unsupported(
                "JPEG component sampling factors must be non-zero",
            ));
        }
        if max_h % component.h_samp != 0 || max_v % component.v_samp != 0 {
            return Err(JpegToHtj2kError::Unsupported(
                "fractional JPEG component sampling is not supported",
            ));
        }

        let x_rsiz = max_h / component.h_samp;
        let y_rsiz = max_v / component.v_samp;
        let expected_width = reference_width.div_ceil(u32::from(x_rsiz));
        let expected_height = reference_height.div_ceil(u32::from(y_rsiz));
        if component.width != expected_width || component.height != expected_height {
            return Err(JpegToHtj2kError::Unsupported(
                "JPEG component dimensions do not match derived SIZ sampling",
            ));
        }
        sampling.push((x_rsiz, y_rsiz));
    }
    Ok(sampling)
}