Implement optional JPEG2000 codec adapter (#40)
This commit is contained in:
@@ -34036,49 +34036,14 @@ pub mod imaging {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.ManagedImageCreator`.
|
||||
pub struct ManagedImageCreator;
|
||||
impl ManagedImageCreator {
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])`.
|
||||
pub fn create(
|
||||
&self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
num_components: i32,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<Box<dyn libremetaverse_imaging::codec::IImage>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)`.
|
||||
pub fn to_portable_image_source(
|
||||
&self,
|
||||
image_object: libremetaverse_types::compat::Object,
|
||||
) -> Result<Box<dyn libremetaverse_imaging::codec::BlkImgDataSrc>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)",
|
||||
)
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)`.
|
||||
pub use crate::j2k::ManagedImageCreator;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions`.
|
||||
pub struct ManagedImageInterleavedExtensions;
|
||||
impl ManagedImageInterleavedExtensions {
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`.
|
||||
pub fn to_managed_image(
|
||||
image: libremetaverse_imaging::codec::InterleavedImage,
|
||||
) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)",
|
||||
)
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`.
|
||||
pub use crate::j2k::ManagedImageInterleavedExtensions;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.Targa`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`.
|
||||
|
||||
435
crates/libremetaverse/src/j2k.rs
Normal file
435
crates/libremetaverse/src/j2k.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
//! 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]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
extern crate self as libremetaverse;
|
||||
|
||||
mod generated;
|
||||
mod j2k;
|
||||
mod targa;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user