//! 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 { 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, ) -> Result, 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, Error> { if image_object == Object::Undefined { return Err(Error::ArgumentNull); } let image = image_object .downcast_arc::() .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, 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 { 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, } 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 { validate_component_index(component_index)?; Ok(8) } fn fixed_point(&self, component_index: usize) -> Result { validate_component_index(component_index)?; Ok(0) } fn is_original_signed(&self, component_index: usize) -> Result { validate_component_index(component_index)?; Ok(false) } fn component_block( &self, component_index: usize, x: i32, y: i32, width: i32, height: i32, ) -> Result, 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 { 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 { 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 { 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 { 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]); } }