Implement optional JPEG2000 codec adapter (#40)
This commit is contained in:
298
crates/libremetaverse-imaging/src/codec.rs
Normal file
298
crates/libremetaverse-imaging/src/codec.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user