j2k-transcode 0.7.0

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

use core::f64::consts::PI;
use core::fmt;
use std::sync::LazyLock;

pub(crate) const fn low_len(sample_len: usize) -> usize {
    sample_len.div_ceil(2)
}

pub(crate) const fn high_len(sample_len: usize) -> usize {
    sample_len / 2
}

pub(crate) fn idct8_basis(sample_idx: usize, freq: usize) -> f64 {
    debug_assert!(sample_idx < 8);
    debug_assert!(freq < 8);

    idct8_basis_table()[sample_idx][freq]
}

pub(crate) fn idct8_basis_table() -> &'static [[f64; 8]; 8] {
    static BASIS: LazyLock<[[f64; 8]; 8]> = LazyLock::new(|| {
        let mut basis = [[0.0; 8]; 8];
        for (sample_idx, row) in basis.iter_mut().enumerate() {
            for (freq, value) in row.iter_mut().enumerate() {
                *value = idct8_basis_uncached(sample_idx, freq);
            }
        }
        basis
    });
    &BASIS
}

#[expect(
    clippy::cast_precision_loss,
    reason = "DCT basis indices are debug-asserted below eight and exactly representable in f64"
)]
fn idct8_basis_uncached(sample_idx: usize, freq: usize) -> f64 {
    let scale = if freq == 0 {
        (1.0_f64 / 8.0).sqrt()
    } else {
        (2.0_f64 / 8.0).sqrt()
    };
    scale * (((sample_idx as f64 + 0.5) * freq as f64 * PI) / 8.0).cos()
}

/// Error returned when a DCT block grid cannot cover the requested component.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DctGridError {
    block_count: usize,
    block_cols: usize,
    block_rows: usize,
    width: usize,
    height: usize,
}

impl DctGridError {
    const fn new(
        block_count: usize,
        block_cols: usize,
        block_rows: usize,
        width: usize,
        height: usize,
    ) -> Self {
        Self {
            block_count,
            block_cols,
            block_rows,
            width,
            height,
        }
    }

    /// Number of supplied 8x8 DCT blocks.
    #[must_use]
    pub const fn block_count(self) -> usize {
        self.block_count
    }

    /// Declared block columns.
    #[must_use]
    pub const fn block_cols(self) -> usize {
        self.block_cols
    }

    /// Declared block rows.
    #[must_use]
    pub const fn block_rows(self) -> usize {
        self.block_rows
    }

    /// Requested component width.
    #[must_use]
    pub const fn width(self) -> usize {
        self.width
    }

    /// Requested component height.
    #[must_use]
    pub const fn height(self) -> usize {
        self.height
    }
}

impl fmt::Display for DctGridError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "DCT grid has {} blocks for {}x{} grid covering requested {}x{} samples",
            self.block_count, self.block_cols, self.block_rows, self.width, self.height
        )
    }
}

impl std::error::Error for DctGridError {}

/// Error returned when a DCT-grid transform cannot be executed safely.
///
/// Grid metadata failures remain available as [`Self::Grid`]. Allocation
/// failures are reported separately so callers do not have to infer memory
/// pressure from fabricated grid dimensions.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DctTransformError {
    /// DCT block-grid metadata does not cover the requested sample plane.
    Grid(DctGridError),
    /// A caller-provided sample plane has a zero width or height.
    InvalidSamplePlaneDimensions {
        /// Declared sample-plane width.
        width: usize,
        /// Declared sample-plane height.
        height: usize,
    },
    /// A caller-provided sample plane does not match its declared dimensions.
    SamplePlaneLengthMismatch {
        /// Supplied sample count.
        sample_count: usize,
        /// Declared sample-plane width.
        width: usize,
        /// Declared sample-plane height.
        height: usize,
    },
    /// An internal symbolic-row request fell outside its derived band extent.
    SymbolicWeightIndexOutOfRange {
        /// Input sample extent.
        sample_len: usize,
        /// Requested output-row index.
        output_index: usize,
        /// Whether the requested row was high-pass.
        high_pass: bool,
    },
    /// Aggregate simultaneously-live transform workspace exceeds the host cap.
    MemoryCapExceeded {
        /// Requested host bytes.
        requested: usize,
        /// Maximum permitted host bytes.
        cap: usize,
    },
    /// The host allocator could not reserve the requested transform storage.
    HostAllocationFailed {
        /// Requested host bytes.
        bytes: usize,
    },
}

impl From<DctGridError> for DctTransformError {
    fn from(value: DctGridError) -> Self {
        Self::Grid(value)
    }
}

impl fmt::Display for DctTransformError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Grid(error) => error.fmt(f),
            Self::InvalidSamplePlaneDimensions { width, height } => {
                write!(
                    f,
                    "sample plane dimensions must be non-zero, got {width}x{height}"
                )
            }
            Self::SamplePlaneLengthMismatch {
                sample_count,
                width,
                height,
            } => write!(
                f,
                "sample plane has {sample_count} values for declared {width}x{height} dimensions"
            ),
            Self::SymbolicWeightIndexOutOfRange {
                sample_len,
                output_index,
                high_pass,
            } => write!(
                f,
                "symbolic 5/3 row {output_index} is outside the {}-pass extent for {sample_len} samples",
                if *high_pass { "high" } else { "low" }
            ),
            Self::MemoryCapExceeded { requested, cap } => write!(
                f,
                "DCT transform workspace requires {requested} bytes, exceeding the {cap}-byte cap"
            ),
            Self::HostAllocationFailed { bytes } => {
                write!(f, "DCT transform host allocation failed for {bytes} bytes")
            }
        }
    }
}

impl std::error::Error for DctTransformError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Grid(error) => Some(error),
            Self::InvalidSamplePlaneDimensions { .. }
            | Self::SamplePlaneLengthMismatch { .. }
            | Self::SymbolicWeightIndexOutOfRange { .. }
            | Self::MemoryCapExceeded { .. }
            | Self::HostAllocationFailed { .. } => None,
        }
    }
}

pub(crate) fn validate_dct_block_grid(
    block_count: usize,
    block_cols: usize,
    block_rows: usize,
    width: usize,
    height: usize,
) -> Result<(), DctGridError> {
    let expected_blocks = block_cols
        .checked_mul(block_rows)
        .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;
    let covered_width = block_cols
        .checked_mul(8)
        .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;
    let covered_height = block_rows
        .checked_mul(8)
        .ok_or_else(|| DctGridError::new(block_count, block_cols, block_rows, width, height))?;

    if block_count != expected_blocks
        || width == 0
        || height == 0
        || width > covered_width
        || height > covered_height
    {
        return Err(DctGridError::new(
            block_count,
            block_cols,
            block_rows,
            width,
            height,
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::error::Error as _;

    use super::{
        high_len, idct8_basis, low_len, validate_dct_block_grid, DctGridError, DctTransformError,
    };

    #[test]
    fn band_lengths_split_even_and_odd_samples() {
        assert_eq!((low_len(0), high_len(0)), (0, 0));
        assert_eq!((low_len(1), high_len(1)), (1, 0));
        assert_eq!((low_len(8), high_len(8)), (4, 4));
        assert_eq!((low_len(9), high_len(9)), (5, 4));
    }

    #[test]
    fn idct8_basis_is_orthonormal_for_dc_and_first_ac() {
        assert!((idct8_basis(0, 0) - (1.0_f64 / 8.0).sqrt()).abs() < 1e-12);
        assert!((idct8_basis(0, 1) - 0.490_392_640_201_615_2).abs() < 1e-12);
    }

    #[test]
    fn validates_non_empty_grid_covered_by_blocks() {
        assert_eq!(validate_dct_block_grid(6, 3, 2, 24, 16), Ok(()));
    }

    #[test]
    fn rejects_overflowing_grid_dimensions() {
        let err = validate_dct_block_grid(1, usize::MAX, usize::MAX, 8, 8)
            .expect_err("overflowing dimensions must fail");
        assert_eq!(err.block_count(), 1);
        assert_eq!(err.block_cols(), usize::MAX);
        assert_eq!(err.block_rows(), usize::MAX);
    }

    #[test]
    fn rejects_zero_image_extent() {
        assert!(validate_dct_block_grid(1, 1, 1, 0, 8).is_err());
        assert!(validate_dct_block_grid(1, 1, 1, 8, 0).is_err());
    }

    #[test]
    fn transform_error_conversion_display_and_sources_preserve_typed_context() {
        let grid =
            validate_dct_block_grid(1, 2, 1, 16, 8).expect_err("block count mismatch must fail");
        let cases = [
            (
                DctTransformError::from(grid),
                "DCT grid has 1 blocks for 2x1 grid covering requested 16x8 samples",
            ),
            (
                DctTransformError::InvalidSamplePlaneDimensions {
                    width: 0,
                    height: 8,
                },
                "sample plane dimensions must be non-zero, got 0x8",
            ),
            (
                DctTransformError::SamplePlaneLengthMismatch {
                    sample_count: 7,
                    width: 2,
                    height: 4,
                },
                "sample plane has 7 values for declared 2x4 dimensions",
            ),
            (
                DctTransformError::SymbolicWeightIndexOutOfRange {
                    sample_len: 5,
                    output_index: 3,
                    high_pass: true,
                },
                "symbolic 5/3 row 3 is outside the high-pass extent for 5 samples",
            ),
            (
                DctTransformError::MemoryCapExceeded {
                    requested: 17,
                    cap: 16,
                },
                "DCT transform workspace requires 17 bytes, exceeding the 16-byte cap",
            ),
            (
                DctTransformError::HostAllocationFailed { bytes: 32 },
                "DCT transform host allocation failed for 32 bytes",
            ),
        ];

        for (index, (error, expected)) in cases.into_iter().enumerate() {
            assert_eq!(error.to_string(), expected);
            if index == 0 {
                assert_eq!(
                    error
                        .source()
                        .and_then(|source| source.downcast_ref::<DctGridError>()),
                    Some(&grid)
                );
            } else {
                assert!(error.source().is_none());
            }
        }
    }
}