436 lines
15 KiB
Rust
436 lines
15 KiB
Rust
//! LibreMetaverse-specific adapters around the format-neutral imaging boundary.
|
|
|
|
use libremetaverse_imaging::codec::{BlkImgDataSrc, IImage, IImageCreator, InterleavedImage};
|
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
|
use libremetaverse_types::Error;
|
|
use libremetaverse_types::compat::Object;
|
|
use std::any::Any;
|
|
use std::sync::Arc;
|
|
|
|
const DC_OFFSET: i32 = 128;
|
|
|
|
/// CoreJ2K-compatible creator that writes directly into managed image planes.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct ManagedImageCreator;
|
|
|
|
impl ManagedImageCreator {
|
|
/// Creates a creator.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// This fixed compatibility signature cannot fail.
|
|
pub const fn new() -> Result<Self, Error> {
|
|
Ok(Self)
|
|
}
|
|
|
|
/// Builds a managed image from already byte-scaled interleaved samples.
|
|
///
|
|
/// Component order follows the reference exactly: gray; gray/alpha; RGB;
|
|
/// RGBA; or RGB/bump/alpha.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`Error::Argument`] for invalid dimensions, component counts,
|
|
/// or byte lengths and [`Error::InvalidOperation`] on bounded allocation
|
|
/// failure.
|
|
#[allow(clippy::needless_pass_by_value)] // Fixed mapped `byte[]` ownership signature.
|
|
pub fn create(
|
|
&self,
|
|
width: i32,
|
|
height: i32,
|
|
num_components: i32,
|
|
bytes: Vec<u8>,
|
|
) -> Result<Box<dyn IImage>, Error> {
|
|
let components = usize::try_from(num_components).map_err(|_| Error::Argument)?;
|
|
let channels = channels_for_components(components)?;
|
|
let image = managed_from_interleaved(width, height, channels, components, &bytes)?;
|
|
Ok(Box::new(ManagedImageJ2kImage(image)))
|
|
}
|
|
|
|
/// Adapts a boxed managed image passed through the mapped `System.Object`
|
|
/// boundary into the four-component encode source used by `CoreJ2K`.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`Error::ArgumentNull`] for `Object::Undefined`,
|
|
/// [`Error::Argument`] for another object type, or a validation/allocation
|
|
/// failure for an inconsistent image.
|
|
#[allow(clippy::needless_pass_by_value)] // Fixed mapped `System.Object` ownership signature.
|
|
pub fn to_portable_image_source(
|
|
&self,
|
|
image_object: Object,
|
|
) -> Result<Box<dyn BlkImgDataSrc>, Error> {
|
|
if image_object == Object::Undefined {
|
|
return Err(Error::ArgumentNull);
|
|
}
|
|
let image = image_object
|
|
.downcast_arc::<ManagedImage>()
|
|
.ok_or(Error::Argument)?;
|
|
image.validate()?;
|
|
Ok(Box::new(ManagedImageBlockSource { image }))
|
|
}
|
|
|
|
/// Rust-native strongly typed form of [`Self::to_portable_image_source`].
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`Error::InvalidOperation`] for an inconsistent public image
|
|
/// layout.
|
|
pub fn to_portable_image_source_from_managed(
|
|
&self,
|
|
image: ManagedImage,
|
|
) -> Result<Box<dyn BlkImgDataSrc>, Error> {
|
|
image.validate()?;
|
|
Ok(Box::new(ManagedImageBlockSource {
|
|
image: Arc::new(image),
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl IImageCreator for ManagedImageCreator {}
|
|
|
|
#[derive(Debug)]
|
|
struct ManagedImageJ2kImage(ManagedImage);
|
|
|
|
impl IImage for ManagedImageJ2kImage {
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
|
|
fn as_managed_image(&self) -> Option<&ManagedImage> {
|
|
Some(&self.0)
|
|
}
|
|
}
|
|
|
|
/// Conversion helpers for the external `CoreJ2K` interleaved-image boundary.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct ManagedImageInterleavedExtensions;
|
|
|
|
impl ManagedImageInterleavedExtensions {
|
|
/// Converts original-precision component planes to managed 8-bit planes.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a typed component/layout/allocation error. Component order is
|
|
/// gray; gray/alpha; RGB; RGBA; or RGB/bump/alpha.
|
|
#[allow(clippy::needless_pass_by_value)] // Fixed mapped external-type ownership signature.
|
|
pub fn to_managed_image(image: InterleavedImage) -> Result<ManagedImage, Error> {
|
|
let components = image.number_of_components();
|
|
let channels = channels_for_components(components)?;
|
|
let width = image.width();
|
|
let height = image.height();
|
|
let pixels = checked_pixels(width, height)?;
|
|
let length = pixels.checked_mul(components).ok_or(Error::Argument)?;
|
|
let mut bytes = Vec::new();
|
|
bytes
|
|
.try_reserve_exact(length)
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
bytes.resize(length, 0);
|
|
let mut plane = vec![0; pixels];
|
|
for component in 0..components {
|
|
image.to_component_bytes(component, &mut plane)?;
|
|
for (pixel, sample) in plane.iter().enumerate() {
|
|
bytes[pixel * components + component] = *sample;
|
|
}
|
|
}
|
|
managed_from_interleaved(width, height, channels, components, &bytes)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ManagedImageBlockSource {
|
|
image: Arc<ManagedImage>,
|
|
}
|
|
|
|
impl BlkImgDataSrc for ManagedImageBlockSource {
|
|
fn width(&self) -> i32 {
|
|
self.image.width
|
|
}
|
|
|
|
fn height(&self) -> i32 {
|
|
self.image.height
|
|
}
|
|
|
|
fn number_of_components(&self) -> usize {
|
|
4
|
|
}
|
|
|
|
fn nominal_range_bits(&self, component_index: usize) -> Result<u8, Error> {
|
|
validate_component_index(component_index)?;
|
|
Ok(8)
|
|
}
|
|
|
|
fn fixed_point(&self, component_index: usize) -> Result<u8, Error> {
|
|
validate_component_index(component_index)?;
|
|
Ok(0)
|
|
}
|
|
|
|
fn is_original_signed(&self, component_index: usize) -> Result<bool, Error> {
|
|
validate_component_index(component_index)?;
|
|
Ok(false)
|
|
}
|
|
|
|
fn component_block(
|
|
&self,
|
|
component_index: usize,
|
|
x: i32,
|
|
y: i32,
|
|
width: i32,
|
|
height: i32,
|
|
) -> Result<Vec<i32>, Error> {
|
|
validate_component_index(component_index)?;
|
|
self.image.validate()?;
|
|
let x = usize::try_from(x).map_err(|_| Error::Argument)?;
|
|
let y = usize::try_from(y).map_err(|_| Error::Argument)?;
|
|
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
|
|
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
|
|
let image_width = usize::try_from(self.image.width).map_err(|_| Error::Argument)?;
|
|
let image_height = usize::try_from(self.image.height).map_err(|_| Error::Argument)?;
|
|
let Some(x_end) = x.checked_add(width) else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let Some(y_end) = y.checked_add(height) else {
|
|
return Err(Error::Argument);
|
|
};
|
|
if width == 0 || height == 0 || x_end > image_width || y_end > image_height {
|
|
return Err(Error::Argument);
|
|
}
|
|
let sample_count = width.checked_mul(height).ok_or(Error::Argument)?;
|
|
let mut samples = Vec::new();
|
|
samples
|
|
.try_reserve_exact(sample_count)
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
for row in y..y + height {
|
|
for column in x..x + width {
|
|
let pixel = row * image_width + column;
|
|
samples.push(self.sample(component_index, pixel)? - DC_OFFSET);
|
|
}
|
|
}
|
|
Ok(samples)
|
|
}
|
|
}
|
|
|
|
impl ManagedImageBlockSource {
|
|
fn sample(&self, component: usize, pixel: usize) -> Result<i32, Error> {
|
|
let has_color = self
|
|
.image
|
|
.channels
|
|
.contains(ManagedImageImageChannels::COLOR);
|
|
let has_alpha = self
|
|
.image
|
|
.channels
|
|
.contains(ManagedImageImageChannels::ALPHA);
|
|
let value = if has_alpha {
|
|
if has_color {
|
|
match component {
|
|
0 => self.image.red[pixel],
|
|
1 => self.image.green[pixel],
|
|
2 => self.image.blue[pixel],
|
|
3 => self.image.alpha[pixel],
|
|
_ => return Err(Error::IndexOutOfRange),
|
|
}
|
|
} else if component == 3 {
|
|
u8::MAX
|
|
} else {
|
|
self.image.alpha[pixel]
|
|
}
|
|
} else if has_color {
|
|
match component {
|
|
0 => self.image.red[pixel],
|
|
1 => self.image.green[pixel],
|
|
2 => self.image.blue[pixel],
|
|
3 => u8::MAX,
|
|
_ => return Err(Error::IndexOutOfRange),
|
|
}
|
|
} else {
|
|
return Err(Error::InvalidOperation);
|
|
};
|
|
Ok(i32::from(value))
|
|
}
|
|
}
|
|
|
|
fn managed_from_interleaved(
|
|
width: i32,
|
|
height: i32,
|
|
channels: ManagedImageImageChannels,
|
|
components: usize,
|
|
bytes: &[u8],
|
|
) -> Result<ManagedImage, Error> {
|
|
let pixels = checked_pixels(width, height)?;
|
|
if bytes.len() != pixels.checked_mul(components).ok_or(Error::Argument)? {
|
|
return Err(Error::Argument);
|
|
}
|
|
let mut image = ManagedImage::new(width, height, channels)?;
|
|
for pixel in 0..pixels {
|
|
let source = pixel * components;
|
|
match components {
|
|
1 => image.red[pixel] = bytes[source],
|
|
2 => {
|
|
image.red[pixel] = bytes[source];
|
|
image.alpha[pixel] = bytes[source + 1];
|
|
}
|
|
3 => {
|
|
image.red[pixel] = bytes[source];
|
|
image.green[pixel] = bytes[source + 1];
|
|
image.blue[pixel] = bytes[source + 2];
|
|
}
|
|
4 => {
|
|
image.red[pixel] = bytes[source];
|
|
image.green[pixel] = bytes[source + 1];
|
|
image.blue[pixel] = bytes[source + 2];
|
|
image.alpha[pixel] = bytes[source + 3];
|
|
}
|
|
5 => {
|
|
image.red[pixel] = bytes[source];
|
|
image.green[pixel] = bytes[source + 1];
|
|
image.blue[pixel] = bytes[source + 2];
|
|
image.bump[pixel] = bytes[source + 3];
|
|
image.alpha[pixel] = bytes[source + 4];
|
|
}
|
|
_ => return Err(Error::Argument),
|
|
}
|
|
}
|
|
Ok(image)
|
|
}
|
|
|
|
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 <= libremetaverse_imaging::DEFAULT_MAX_PIXELS)
|
|
.ok_or(Error::Argument)
|
|
}
|
|
|
|
fn channels_for_components(components: usize) -> Result<ManagedImageImageChannels, Error> {
|
|
Ok(match components {
|
|
1 => ManagedImageImageChannels::GRAY,
|
|
2 => ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
|
|
3 => ManagedImageImageChannels::COLOR,
|
|
4 => ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
|
5 => {
|
|
ManagedImageImageChannels::COLOR
|
|
| ManagedImageImageChannels::BUMP
|
|
| ManagedImageImageChannels::ALPHA
|
|
}
|
|
_ => return Err(Error::Argument),
|
|
})
|
|
}
|
|
|
|
fn validate_component_index(component_index: usize) -> Result<(), Error> {
|
|
if component_index < 4 {
|
|
Ok(())
|
|
} else {
|
|
Err(Error::IndexOutOfRange)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use libremetaverse_imaging::codec::InterleavedComponent;
|
|
|
|
#[test]
|
|
fn creator_maps_all_reference_component_orders() {
|
|
let creator = ManagedImageCreator::new().unwrap();
|
|
let cases = [
|
|
(1, vec![1], ManagedImageImageChannels::GRAY),
|
|
(
|
|
2,
|
|
vec![1, 2],
|
|
ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
|
|
),
|
|
(3, vec![1, 2, 3], ManagedImageImageChannels::COLOR),
|
|
(
|
|
4,
|
|
vec![1, 2, 3, 4],
|
|
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
|
),
|
|
(
|
|
5,
|
|
vec![1, 2, 3, 4, 5],
|
|
ManagedImageImageChannels::COLOR
|
|
| ManagedImageImageChannels::BUMP
|
|
| ManagedImageImageChannels::ALPHA,
|
|
),
|
|
];
|
|
for (components, bytes, channels) in cases {
|
|
let wrapper = creator.create(1, 1, components, bytes).unwrap();
|
|
let image = wrapper.as_managed_image().expect("managed image");
|
|
assert_eq!(image.channels, channels);
|
|
assert_eq!(image.red, [1]);
|
|
if components >= 3 {
|
|
assert_eq!(image.green, [2]);
|
|
assert_eq!(image.blue, [3]);
|
|
}
|
|
if components == 2 || components == 4 {
|
|
assert_eq!(image.alpha[0], u8::try_from(components).unwrap());
|
|
}
|
|
if components == 5 {
|
|
assert_eq!(image.bump, [4]);
|
|
assert_eq!(image.alpha, [5]);
|
|
}
|
|
}
|
|
assert!(matches!(
|
|
creator.create(1, 1, 4, vec![0; 3]),
|
|
Err(Error::Argument)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn interleaved_extension_scales_samples_and_keeps_bump_before_alpha() {
|
|
let components = vec![
|
|
InterleavedComponent::new(16, false, false, vec![65_535]).unwrap(),
|
|
InterleavedComponent::new(8, false, false, vec![2]).unwrap(),
|
|
InterleavedComponent::new(8, false, false, vec![3]).unwrap(),
|
|
InterleavedComponent::new(8, false, false, vec![4]).unwrap(),
|
|
InterleavedComponent::new(8, false, true, vec![5]).unwrap(),
|
|
];
|
|
let image = InterleavedImage::new(1, 1, components).unwrap();
|
|
let image = ManagedImageInterleavedExtensions::to_managed_image(image).unwrap();
|
|
assert_eq!(image.red, [255]);
|
|
assert_eq!(image.green, [2]);
|
|
assert_eq!(image.blue, [3]);
|
|
assert_eq!(image.bump, [4]);
|
|
assert_eq!(image.alpha, [5]);
|
|
}
|
|
|
|
#[test]
|
|
fn portable_source_matches_dc_offset_and_channel_substitution() {
|
|
let creator = ManagedImageCreator::new().unwrap();
|
|
let mut image = ManagedImage::new(
|
|
2,
|
|
1,
|
|
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
|
)
|
|
.unwrap();
|
|
image.red.copy_from_slice(&[0, 255]);
|
|
image.green.copy_from_slice(&[1, 2]);
|
|
image.blue.copy_from_slice(&[3, 4]);
|
|
image.alpha.copy_from_slice(&[5, 6]);
|
|
let source = creator
|
|
.to_portable_image_source(Object::opaque(image))
|
|
.unwrap();
|
|
assert_eq!(source.component_block(0, 0, 0, 2, 1).unwrap(), [-128, 127]);
|
|
assert_eq!(source.component_block(3, 0, 0, 2, 1).unwrap(), [-123, -122]);
|
|
assert_eq!(source.nominal_range_bits(0), Ok(8));
|
|
assert_eq!(source.fixed_point(0), Ok(0));
|
|
assert_eq!(source.is_original_signed(0), Ok(false));
|
|
assert_eq!(
|
|
source.component_block(4, 0, 0, 1, 1),
|
|
Err(Error::IndexOutOfRange)
|
|
);
|
|
|
|
let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
|
|
alpha.alpha[0] = 42;
|
|
let source = creator
|
|
.to_portable_image_source(Object::opaque(alpha))
|
|
.unwrap();
|
|
assert_eq!(source.component_block(0, 0, 0, 1, 1).unwrap(), [-86]);
|
|
assert_eq!(source.component_block(3, 0, 0, 1, 1).unwrap(), [127]);
|
|
}
|
|
}
|