Files
MetaCrate/crates/libremetaverse-imaging/src/codec.rs
Chili Palmer 16501f2331
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Implement optional JPEG2000 codec adapter (#40)
2026-08-09 03:42:18 +00:00

299 lines
9.3 KiB
Rust

//! Project-owned boundaries for JPEG 2000 codec types exposed by the C# API.
use crate::{DEFAULT_MAX_PIXELS, Error, ManagedImage};
use std::any::Any;
use std::fmt::Debug;
use std::marker::PhantomData;
/// A type-erased decoded image returned by an image creator.
pub trait IImage: Any + Debug + Send + Sync {
/// Returns the concrete image wrapper for checked downcasting.
fn as_any(&self) -> &dyn Any;
/// Returns the already-created managed image when this is a compatible
/// decode target.
fn as_managed_image(&self) -> Option<&ManagedImage> {
None
}
}
/// Marker corresponding to `CoreJ2K`'s image-creator boundary.
pub trait IImageCreator: Send + Sync {}
/// Typed marker corresponding to `CoreJ2K`'s generic image creator.
#[derive(Clone, Copy, Debug, Default)]
pub struct ImageCreator<T>(pub PhantomData<T>);
/// One decoded JPEG 2000 component at its original integer precision.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterleavedComponent {
precision: u8,
signed: bool,
alpha: bool,
samples: Vec<i32>,
}
impl InterleavedComponent {
/// Creates a checked component without reducing its sample precision.
///
/// # Errors
///
/// Returns [`Error::Argument`] for precision outside 1 through 31 bits or
/// for samples outside the declared signed or unsigned range.
pub fn new(precision: u8, signed: bool, alpha: bool, samples: Vec<i32>) -> Result<Self, Error> {
if !(1..=31).contains(&precision) {
return Err(Error::Argument);
}
let (minimum, maximum) = component_range(precision, signed);
if samples
.iter()
.any(|sample| i64::from(*sample) < minimum || i64::from(*sample) > maximum)
{
return Err(Error::Argument);
}
Ok(Self {
precision,
signed,
alpha,
samples,
})
}
/// Declared component precision in bits.
#[must_use]
pub const fn precision(&self) -> u8 {
self.precision
}
/// Whether component samples use signed representation.
#[must_use]
pub const fn is_signed(&self) -> bool {
self.signed
}
/// Whether the container marks this component as alpha.
#[must_use]
pub const fn is_alpha(&self) -> bool {
self.alpha
}
/// Original integer samples, in top-left row-major order.
#[must_use]
pub fn samples(&self) -> &[i32] {
&self.samples
}
}
/// A bounded decoded image retaining component precision and order.
///
/// The historical external type calls this representation interleaved, while
/// its public conversion API is component-oriented. Keeping the components in
/// separate owned planes avoids a second full-image allocation and retains the
/// original JPEG 2000 integer samples until byte conversion is requested.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterleavedImage {
width: i32,
height: i32,
components: Vec<InterleavedComponent>,
}
impl InterleavedImage {
/// Creates a checked decoded component image.
///
/// # Errors
///
/// Returns [`Error::Argument`] for invalid/over-limit dimensions, zero or
/// more than five components, or a plane whose size differs from
/// `width * height`.
pub fn new(
width: i32,
height: i32,
components: Vec<InterleavedComponent>,
) -> Result<Self, Error> {
let pixels = checked_pixels(width, height)?;
if components.is_empty()
|| components.len() > 5
|| components
.iter()
.any(|component| component.samples.len() != pixels)
{
return Err(Error::Argument);
}
pixels
.checked_mul(components.len())
.filter(|samples| *samples <= DEFAULT_MAX_PIXELS * 5)
.ok_or(Error::Argument)?;
Ok(Self {
width,
height,
components,
})
}
/// Decoded width after any discard-level reduction.
#[must_use]
pub const fn width(&self) -> i32 {
self.width
}
/// Decoded height after any discard-level reduction.
#[must_use]
pub const fn height(&self) -> i32 {
self.height
}
/// Number of decoded components in codestream order.
#[must_use]
pub fn number_of_components(&self) -> usize {
self.components.len()
}
/// Component metadata and original samples.
#[must_use]
pub fn components(&self) -> &[InterleavedComponent] {
&self.components
}
/// Scales one component into an 8-bit plane using CoreJ2K-compatible full
/// range conversion.
///
/// # Errors
///
/// Returns [`Error::IndexOutOfRange`] for an invalid component index and
/// [`Error::Argument`] when `destination` is not exactly one image plane.
pub fn to_component_bytes(
&self,
component_index: usize,
destination: &mut [u8],
) -> Result<(), Error> {
let component = self
.components
.get(component_index)
.ok_or(Error::IndexOutOfRange)?;
if destination.len() != component.samples.len() {
return Err(Error::Argument);
}
for (destination, sample) in destination.iter_mut().zip(&component.samples) {
*destination = scale_sample(*sample, component.precision, component.signed);
}
Ok(())
}
}
/// Encode-side sample source corresponding to `CoreJ2K`'s block image source.
pub trait BlkImgDataSrc: Debug + Send + Sync {
/// Image width.
fn width(&self) -> i32;
/// Image height.
fn height(&self) -> i32;
/// Number of presented components.
fn number_of_components(&self) -> usize;
/// Nominal range bits for one component.
///
/// # Errors
///
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
fn nominal_range_bits(&self, component_index: usize) -> Result<u8, Error>;
/// Fixed-point fractional bits for one component.
///
/// # Errors
///
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
fn fixed_point(&self, component_index: usize) -> Result<u8, Error>;
/// Whether one presented component is signed at the source.
///
/// # Errors
///
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
fn is_original_signed(&self, component_index: usize) -> Result<bool, Error>;
/// Returns a checked top-left row-major rectangle with the source DC offset
/// applied, exactly as `CoreJ2K` expects.
///
/// # Errors
///
/// Returns a typed index or argument error for an invalid component or
/// rectangle.
fn component_block(
&self,
component_index: usize,
x: i32,
y: i32,
width: i32,
height: i32,
) -> Result<Vec<i32>, Error>;
}
fn checked_pixels(width: i32, height: i32) -> Result<usize, Error> {
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
if width == 0 || height == 0 {
return Err(Error::Argument);
}
width
.checked_mul(height)
.filter(|pixels| *pixels <= DEFAULT_MAX_PIXELS)
.ok_or(Error::Argument)
}
fn component_range(precision: u8, signed: bool) -> (i64, i64) {
if signed {
let magnitude = 1_i64 << (precision - 1);
(-magnitude, magnitude - 1)
} else {
(0, (1_i64 << precision) - 1)
}
}
fn scale_sample(sample: i32, precision: u8, signed: bool) -> u8 {
if signed {
let old_max = 1_i64 << (precision - 1);
let scaled = (i64::from(sample) * 128) / old_max + 128;
u8::try_from(scaled.clamp(0, 255)).unwrap_or_default()
} else {
let old_max = (1_u64 << precision) - 1;
let scaled = (u64::try_from(sample).unwrap_or_default() * 255) / old_max;
u8::try_from(scaled).unwrap_or(u8::MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn component_conversion_preserves_precision_and_signed_range() {
let unsigned = InterleavedComponent::new(16, false, false, vec![0, 32_768, 65_535])
.expect("16-bit component");
let signed = InterleavedComponent::new(8, true, false, vec![-128, 0, 127])
.expect("signed component");
let unsigned = InterleavedImage::new(3, 1, vec![unsigned]).expect("unsigned image");
let signed = InterleavedImage::new(3, 1, vec![signed]).expect("signed image");
let mut bytes = [0; 3];
unsigned
.to_component_bytes(0, &mut bytes)
.expect("unsigned bytes");
assert_eq!(bytes, [0, 127, 255]);
signed
.to_component_bytes(0, &mut bytes)
.expect("signed bytes");
assert_eq!(bytes, [0, 128, 255]);
}
#[test]
fn interleaved_image_rejects_bad_layouts_and_ranges() {
assert_eq!(
InterleavedComponent::new(0, false, false, vec![]),
Err(Error::Argument)
);
assert_eq!(
InterleavedComponent::new(8, false, false, vec![256]),
Err(Error::Argument)
);
let component = InterleavedComponent::new(8, false, false, vec![0]).unwrap();
assert_eq!(
InterleavedImage::new(2, 1, vec![component]),
Err(Error::Argument)
);
}
}