Implement optional JPEG2000 codec adapter (#40)
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

This commit is contained in:
2026-08-09 03:42:18 +00:00
parent 5d1573fc82
commit 16501f2331
24 changed files with 2776 additions and 53 deletions

View File

@@ -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)`.

View 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]);
}
}

View File

@@ -3,6 +3,7 @@
extern crate self as libremetaverse;
mod generated;
mod j2k;
mod targa;
#[cfg(test)]