Implement ManagedImage and codec abstraction (#38)

This commit is contained in:
2026-08-09 02:11:02 +00:00
parent d980692933
commit 2c806d80ff
9 changed files with 850 additions and 92 deletions

View File

@@ -6,95 +6,30 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.Imaging.ITextureCodec`.
pub trait ITextureCodec {
/// C# member: `M:LibreMetaverse.Imaging.ITextureCodec.Decode(System.IO.Stream)`.
fn decode(
&self,
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ITextureCodec.Decode(System.IO.Stream)",
)
}
}
/// C# member: `M:LibreMetaverse.Imaging.ITextureCodec.Decode(System.IO.Stream)`.
pub use crate::managed_image::ITextureCodec;
/// C# type: `T:LibreMetaverse.Imaging.ManagedImage`.
pub struct ManagedImage {
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Alpha`.
pub alpha: Vec<u8>,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Blue`.
pub blue: Vec<u8>,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Bump`.
pub bump: Vec<u8>,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Channels`.
pub channels: libremetaverse_imaging::ManagedImageImageChannels,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Green`.
pub green: Vec<u8>,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Height`.
pub height: i32,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Red`.
pub red: Vec<u8>,
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Width`.
pub width: i32,
data: Vec<u8>,
}
impl ManagedImage {
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.#ctor(System.Int32,System.Int32,LibreMetaverse.Imaging.ManagedImage.ImageChannels)`.
pub fn new(
width: i32,
height: i32,
channels: libremetaverse_imaging::ManagedImageImageChannels,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ManagedImage.#ctor(System.Int32,System.Int32,LibreMetaverse.Imaging.ManagedImage.ImageChannels)",
)
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.Clear`.
pub fn clear(&mut self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Imaging.ManagedImage.Clear")
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.Clone`.
pub fn clone(&self) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Imaging.ManagedImage.Clone")
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ConvertChannels(LibreMetaverse.Imaging.ManagedImage.ImageChannels)`.
pub fn convert_channels(
&mut self,
channels: libremetaverse_imaging::ManagedImageImageChannels,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ManagedImage.ConvertChannels(LibreMetaverse.Imaging.ManagedImage.ImageChannels)",
)
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ExportRaw`.
pub fn export_raw(&self) -> Result<Vec<u8>, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Imaging.ManagedImage.ExportRaw")
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ResizeBilinear(System.Int32,System.Int32)`.
pub fn resize_bilinear(&mut self, width: i32, height: i32) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ManagedImage.ResizeBilinear(System.Int32,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ResizeNearestNeighbor(System.Int32,System.Int32)`.
pub fn resize_nearest_neighbor(&mut self, width: i32, height: i32) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ManagedImage.ResizeNearestNeighbor(System.Int32,System.Int32)",
)
}
}
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Alpha`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Blue`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Bump`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Channels`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Green`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Height`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Red`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.Width`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.#ctor(System.Int32,System.Int32,LibreMetaverse.Imaging.ManagedImage.ImageChannels)`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.Clear`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.Clone`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ConvertChannels(LibreMetaverse.Imaging.ManagedImage.ImageChannels)`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ExportRaw`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ResizeBilinear(System.Int32,System.Int32)`.
/// C# member: `M:LibreMetaverse.Imaging.ManagedImage.ResizeNearestNeighbor(System.Int32,System.Int32)`.
pub use crate::managed_image::ManagedImage;
/// C# type: `T:LibreMetaverse.Imaging.ManagedImage.ImageChannels`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct ManagedImageImageChannels(pub i32);
impl ManagedImageImageChannels {
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Alpha`.
pub const ALPHA: Self = Self(4);
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Bump`.
pub const BUMP: Self = Self(8);
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Color`.
pub const COLOR: Self = Self(2);
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Gray`.
pub const GRAY: Self = Self(1);
}
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Alpha`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Bump`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Color`.
/// C# member: `F:LibreMetaverse.Imaging.ManagedImage.ImageChannels.Gray`.
pub use crate::managed_image::ManagedImageImageChannels;

View File

@@ -11,6 +11,8 @@ pub mod codec {
}
mod generated;
mod managed_image;
pub use generated::*;
pub use libremetaverse_types::Error;
pub use managed_image::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS};

View File

@@ -0,0 +1,793 @@
//! Owned planar image storage and the codec abstraction boundary.
use crate::Error;
use libremetaverse_types::compat::ReadWrite;
use std::ops::{BitAnd, BitOr, BitXor, Not};
/// Maximum number of decoded pixels accepted by [`ManagedImage`].
///
/// This permits a 4096 by 4096 texture while rejecting hostile dimensions
/// before any channel allocation occurs.
pub const DEFAULT_MAX_PIXELS: usize = 4096 * 4096;
/// Maximum encoded input size codec adapters may buffer by default.
pub const DEFAULT_MAX_ENCODED_BYTES: usize = 64 * 1024 * 1024;
const MAX_CHANNEL_BYTES: usize = DEFAULT_MAX_PIXELS * 5;
/// Object-safe boundary for compressed-image decoders.
///
/// Implementations must bound buffered input to [`DEFAULT_MAX_ENCODED_BYTES`],
/// decode through the checked [`ManagedImage`] constructors, and return a typed
/// error for malformed data. Native codec-specific types do not cross this
/// boundary.
pub trait ITextureCodec {
/// Decodes a stream positioned at the beginning of an encoded image.
///
/// # Errors
///
/// Returns a typed parse, argument, allocation, or I/O-class error when
/// the encoded stream cannot be decoded within the documented limits.
fn decode(&self, stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error>;
}
/// Flags selecting the planar channels stored by [`ManagedImage`].
///
/// Unknown bits are retained for C# flag-enum compatibility. Gray and color
/// are both legal bits; when both are present, the reference constructor gives
/// gray storage precedence.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct ManagedImageImageChannels(pub i32);
impl ManagedImageImageChannels {
/// One gray plane stored in [`ManagedImage::red`].
pub const GRAY: Self = Self(1);
/// Three color planes stored in red, green, blue order.
pub const COLOR: Self = Self(2);
/// One alpha plane.
pub const ALPHA: Self = Self(4);
/// One bump plane.
pub const BUMP: Self = Self(8);
/// Returns whether all bits in `other` are present.
#[must_use]
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
}
impl BitAnd for ManagedImageImageChannels {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl BitOr for ManagedImageImageChannels {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitXor for ManagedImageImageChannels {
type Output = Self;
fn bitxor(self, rhs: Self) -> Self::Output {
Self(self.0 ^ rhs.0)
}
}
impl Not for ManagedImageImageChannels {
type Output = Self;
fn not(self) -> Self::Output {
Self(!self.0)
}
}
/// An owned image with one byte per sample and one allocation per channel.
///
/// Rows use a top-left origin. Every present plane has exactly `width * height`
/// bytes and a row stride of `width`; gray samples occupy [`Self::red`]. The
/// fields remain public to match the C# surface, so methods validate the layout
/// before indexing it and return [`Error::InvalidOperation`] if a caller has
/// supplied inconsistent buffers.
#[derive(Debug, Eq, PartialEq)]
pub struct ManagedImage {
/// Alpha samples, or an empty vector when alpha is absent.
pub alpha: Vec<u8>,
/// Blue samples, or an empty vector when color is absent.
pub blue: Vec<u8>,
/// Bump samples, or an empty vector when bump is absent.
pub bump: Vec<u8>,
/// Channel flags describing the planar buffers.
pub channels: ManagedImageImageChannels,
/// Green samples, or an empty vector when color is absent.
pub green: Vec<u8>,
/// Image height in pixels.
pub height: i32,
/// Red or gray samples, or an empty vector when neither is present.
pub red: Vec<u8>,
/// Image width in pixels.
pub width: i32,
}
impl ManagedImage {
/// Creates a blank image using the reference planar channel layout.
///
/// # Errors
///
/// Returns [`Error::Argument`] for non-positive, overflowing, or over-limit
/// dimensions, and [`Error::InvalidOperation`] if allocation fails.
pub fn new(
width: i32,
height: i32,
channels: ManagedImageImageChannels,
) -> Result<Self, Error> {
let pixels = checked_pixels(width, height)?;
let gray = channels.contains(ManagedImageImageChannels::GRAY);
let color = channels.contains(ManagedImageImageChannels::COLOR);
let alpha = channels.contains(ManagedImageImageChannels::ALPHA);
let bump = channels.contains(ManagedImageImageChannels::BUMP);
checked_storage(
pixels,
usize::from(gray || color)
+ 2 * usize::from(color && !gray)
+ usize::from(alpha)
+ usize::from(bump),
)?;
let red = allocate_plane(if gray || color { pixels } else { 0 }, 0)?;
let (green, blue) = if color && !gray {
(allocate_plane(pixels, 0)?, allocate_plane(pixels, 0)?)
} else {
(Vec::new(), Vec::new())
};
Ok(Self {
alpha: allocate_plane(if alpha { pixels } else { 0 }, 0)?,
blue,
bump: allocate_plane(if bump { pixels } else { 0 }, 0)?,
channels,
green,
height,
red,
width,
})
}
/// Validates dimensions, limits, and all public channel-buffer lengths.
///
/// # Errors
///
/// Returns [`Error::Argument`] for invalid dimensions and
/// [`Error::InvalidOperation`] for inconsistent public channel buffers.
pub fn validate(&self) -> Result<(), Error> {
let pixels = checked_pixels(self.width, self.height)?;
let gray = self.channels.contains(ManagedImageImageChannels::GRAY);
let color = self.channels.contains(ManagedImageImageChannels::COLOR);
let expected_red = if gray || color { pixels } else { 0 };
let expected_color = if color && !gray { pixels } else { 0 };
let expected_alpha = if self.channels.contains(ManagedImageImageChannels::ALPHA) {
pixels
} else {
0
};
let expected_bump = if self.channels.contains(ManagedImageImageChannels::BUMP) {
pixels
} else {
0
};
checked_storage(
pixels,
usize::from(expected_red != 0)
+ 2 * usize::from(expected_color != 0)
+ usize::from(expected_alpha != 0)
+ usize::from(expected_bump != 0),
)?;
if self.red.len() != expected_red
|| self.green.len() != expected_color
|| self.blue.len() != expected_color
|| self.alpha.len() != expected_alpha
|| self.bump.len() != expected_bump
{
return Err(Error::InvalidOperation);
}
Ok(())
}
/// Sets every allocated channel sample to zero.
///
/// # Errors
///
/// This fixed compatibility signature currently cannot fail.
pub fn clear(&mut self) -> Result<(), Error> {
self.red.fill(0);
self.green.fill(0);
self.blue.fill(0);
self.alpha.fill(0);
self.bump.fill(0);
Ok(())
}
/// Creates an independent deep copy of the image and its channel buffers.
///
/// # Errors
///
/// Returns an error for an invalid source layout or failed bounded
/// allocation.
#[allow(clippy::should_implement_trait)] // The mapped C# Clone returns Result<Self, Error>.
pub fn clone(&self) -> Result<Self, Error> {
self.validate()?;
Ok(Self {
alpha: copy_plane(&self.alpha)?,
blue: copy_plane(&self.blue)?,
bump: copy_plane(&self.bump)?,
channels: self.channels,
green: copy_plane(&self.green)?,
height: self.height,
red: copy_plane(&self.red)?,
width: self.width,
})
}
/// Converts channel storage using the same add/remove rules as the C# type.
///
/// # Errors
///
/// Returns an error for invalid dimensions or failed bounded allocation.
pub fn convert_channels(&mut self, channels: ManagedImageImageChannels) -> Result<(), Error> {
if self.channels == channels {
return Ok(());
}
let pixels = checked_pixels(self.width, self.height)?;
let add = (self.channels ^ channels) & channels;
let delete = (self.channels ^ channels) & self.channels;
let added_color = if add.contains(ManagedImageImageChannels::COLOR) {
Some((
allocate_plane(pixels, 0)?,
allocate_plane(pixels, 0)?,
allocate_plane(pixels, 0)?,
))
} else {
None
};
let added_alpha = if add.contains(ManagedImageImageChannels::ALPHA) {
Some(allocate_plane(pixels, u8::MAX)?)
} else {
None
};
let added_bump = if add.contains(ManagedImageImageChannels::BUMP) {
Some(allocate_plane(pixels, 0)?)
} else {
None
};
if let Some((red, green, blue)) = added_color {
self.red = red;
self.green = green;
self.blue = blue;
} else if delete.contains(ManagedImageImageChannels::COLOR) {
self.red.clear();
self.green.clear();
self.blue.clear();
}
if let Some(alpha) = added_alpha {
self.alpha = alpha;
} else if delete.contains(ManagedImageImageChannels::ALPHA) {
self.alpha.clear();
}
if let Some(bump) = added_bump {
self.bump = bump;
} else if delete.contains(ManagedImageImageChannels::BUMP) {
self.bump.clear();
}
self.channels = channels;
Ok(())
}
/// Exports bottom-left-origin, interleaved 32-bit RGBA data as in C#.
///
/// # Errors
///
/// Returns an error when dimensions or public planes are inconsistent,
/// the reference RGBA conversion is undefined, or allocation fails.
pub fn export_raw(&self) -> Result<Vec<u8>, Error> {
self.validate()?;
let pixels = checked_pixels(self.width, self.height)?;
let length = pixels.checked_mul(4).ok_or(Error::Argument)?;
if length > MAX_CHANNEL_BYTES {
return Err(Error::Argument);
}
let alpha = self.channels.contains(ManagedImageImageChannels::ALPHA);
let color = self.channels.contains(ManagedImageImageChannels::COLOR);
if !alpha && !color {
return Err(Error::InvalidOperation);
}
if color && (self.green.len() != pixels || self.blue.len() != pixels) {
return Err(Error::InvalidOperation);
}
let mut raw = allocate_plane(length, 0)?;
let width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
let height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
for y in 0..height {
for x in 0..width {
let source = y * width + x;
let target = ((height - 1 - y) * width + x) * 4;
if alpha && !color {
raw[target..target + 3].fill(self.alpha[source]);
raw[target + 3] = u8::MAX;
} else {
raw[target] = self.red[source];
raw[target + 1] = self.green[source];
raw[target + 2] = self.blue[source];
raw[target + 3] = if alpha { self.alpha[source] } else { u8::MAX };
}
}
}
Ok(raw)
}
/// Resizes every present plane using nearest-neighbor sampling.
///
/// # Errors
///
/// Returns an error for invalid target dimensions, inconsistent source
/// planes, checked arithmetic failure, or failed bounded allocation.
pub fn resize_nearest_neighbor(&mut self, width: i32, height: i32) -> Result<(), Error> {
checked_pixels(width, height)?;
self.validate()?;
if width == self.width && height == self.height {
return Ok(());
}
let old_width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
let old_height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
let new_width = usize::try_from(width).map_err(|_| Error::Argument)?;
let new_height = usize::try_from(height).map_err(|_| Error::Argument)?;
let red = resize_nearest_plane(&self.red, old_width, old_height, new_width, new_height)?;
let green =
resize_nearest_plane(&self.green, old_width, old_height, new_width, new_height)?;
let blue = resize_nearest_plane(&self.blue, old_width, old_height, new_width, new_height)?;
let alpha =
resize_nearest_plane(&self.alpha, old_width, old_height, new_width, new_height)?;
let bump = resize_nearest_plane(&self.bump, old_width, old_height, new_width, new_height)?;
self.width = width;
self.height = height;
self.red = red;
self.green = green;
self.blue = blue;
self.alpha = alpha;
self.bump = bump;
Ok(())
}
/// Resizes every present plane using C#-compatible bilinear interpolation.
///
/// # Errors
///
/// Returns an error for invalid target dimensions, inconsistent source
/// planes, checked arithmetic failure, or failed bounded allocation.
pub fn resize_bilinear(&mut self, width: i32, height: i32) -> Result<(), Error> {
checked_pixels(width, height)?;
self.validate()?;
if width == self.width && height == self.height {
return Ok(());
}
if self.width <= 1 || self.height <= 1 {
return self.resize_nearest_neighbor(width, height);
}
let old_width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
let old_height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
let new_width = usize::try_from(width).map_err(|_| Error::Argument)?;
let new_height = usize::try_from(height).map_err(|_| Error::Argument)?;
let red = resize_bilinear_plane(&self.red, old_width, old_height, new_width, new_height)?;
let green =
resize_bilinear_plane(&self.green, old_width, old_height, new_width, new_height)?;
let blue = resize_bilinear_plane(&self.blue, old_width, old_height, new_width, new_height)?;
let alpha =
resize_bilinear_plane(&self.alpha, old_width, old_height, new_width, new_height)?;
let bump = resize_bilinear_plane(&self.bump, old_width, old_height, new_width, new_height)?;
self.width = width;
self.height = height;
self.red = red;
self.green = green;
self.blue = blue;
self.alpha = alpha;
self.bump = bump;
Ok(())
}
/// Builds planar storage from canonical top-left-origin interleaved bytes.
///
/// Component order is gray or RGB, followed by alpha and then bump when
/// those flags are present. `stride` is the byte distance between rows and
/// may include trailing padding.
///
/// # Errors
///
/// Returns an error for invalid dimensions, ambiguous channel flags,
/// insufficient input/stride, checked arithmetic failure, or allocation
/// beyond the image limits.
pub fn from_interleaved(
width: i32,
height: i32,
channels: ManagedImageImageChannels,
stride: usize,
bytes: &[u8],
) -> Result<Self, Error> {
let pixels = checked_pixels(width, height)?;
let components = canonical_components(channels)?;
let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?;
let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?;
let row_bytes = width_usize.checked_mul(components).ok_or(Error::Argument)?;
let required = required_interleaved_bytes(height_usize, stride, row_bytes)?;
if stride < row_bytes || required > bytes.len() || required > MAX_CHANNEL_BYTES {
return Err(Error::Argument);
}
let mut image = Self::new(width, height, channels)?;
for y in 0..height_usize {
for x in 0..width_usize {
let pixel = y * width_usize + x;
let mut source = y * stride + x * components;
if channels.contains(ManagedImageImageChannels::GRAY) {
image.red[pixel] = bytes[source];
source += 1;
} else if channels.contains(ManagedImageImageChannels::COLOR) {
image.red[pixel] = bytes[source];
image.green[pixel] = bytes[source + 1];
image.blue[pixel] = bytes[source + 2];
source += 3;
}
if channels.contains(ManagedImageImageChannels::ALPHA) {
image.alpha[pixel] = bytes[source];
source += 1;
}
if channels.contains(ManagedImageImageChannels::BUMP) {
image.bump[pixel] = bytes[source];
}
}
}
debug_assert_eq!(
pixels,
image.red.len().max(image.alpha.len()).max(image.bump.len())
);
Ok(image)
}
/// Exports canonical top-left-origin interleaved bytes with a caller-chosen stride.
///
/// # Errors
///
/// Returns an error for an inconsistent image, ambiguous channel flags,
/// insufficient stride, checked arithmetic failure, or failed allocation.
pub fn to_interleaved(&self, stride: usize) -> Result<Vec<u8>, Error> {
self.validate()?;
let components = canonical_components(self.channels)?;
let width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
let height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
let row_bytes = width.checked_mul(components).ok_or(Error::Argument)?;
let minimum = required_interleaved_bytes(height, stride, row_bytes)?;
let length = stride.checked_mul(height).ok_or(Error::Argument)?;
if stride < row_bytes || minimum > length || length > MAX_CHANNEL_BYTES {
return Err(Error::Argument);
}
let mut bytes = allocate_plane(length, 0)?;
for y in 0..height {
for x in 0..width {
let pixel = y * width + x;
let mut target = y * stride + x * components;
if self.channels.contains(ManagedImageImageChannels::GRAY) {
bytes[target] = self.red[pixel];
target += 1;
} else if self.channels.contains(ManagedImageImageChannels::COLOR) {
bytes[target] = self.red[pixel];
bytes[target + 1] = self.green[pixel];
bytes[target + 2] = self.blue[pixel];
target += 3;
}
if self.channels.contains(ManagedImageImageChannels::ALPHA) {
bytes[target] = self.alpha[pixel];
target += 1;
}
if self.channels.contains(ManagedImageImageChannels::BUMP) {
bytes[target] = self.bump[pixel];
}
}
}
Ok(bytes)
}
}
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);
}
let pixels = width.checked_mul(height).ok_or(Error::Argument)?;
if pixels > DEFAULT_MAX_PIXELS {
return Err(Error::Argument);
}
Ok(pixels)
}
fn checked_storage(pixels: usize, planes: usize) -> Result<(), Error> {
if pixels.checked_mul(planes).ok_or(Error::Argument)? > MAX_CHANNEL_BYTES {
Err(Error::Argument)
} else {
Ok(())
}
}
fn allocate_plane(length: usize, value: u8) -> Result<Vec<u8>, Error> {
if length > MAX_CHANNEL_BYTES {
return Err(Error::Argument);
}
let mut plane = Vec::new();
plane
.try_reserve_exact(length)
.map_err(|_| Error::InvalidOperation)?;
plane.resize(length, value);
Ok(plane)
}
fn copy_plane(source: &[u8]) -> Result<Vec<u8>, Error> {
let mut copy = allocate_plane(source.len(), 0)?;
copy.copy_from_slice(source);
Ok(copy)
}
fn resize_nearest_plane(
source: &[u8],
old_width: usize,
old_height: usize,
new_width: usize,
new_height: usize,
) -> Result<Vec<u8>, Error> {
if source.is_empty() {
return Ok(Vec::new());
}
let mut target = allocate_plane(new_width.checked_mul(new_height).ok_or(Error::Argument)?, 0)?;
for y in 0..new_height {
let source_y = y.checked_mul(old_height).ok_or(Error::Argument)? / new_height;
for x in 0..new_width {
let source_x = x.checked_mul(old_width).ok_or(Error::Argument)? / new_width;
target[y * new_width + x] = source[source_y * old_width + source_x];
}
}
Ok(target)
}
#[allow(clippy::cast_precision_loss)] // C# explicitly performs these coordinates in f32.
fn resize_bilinear_plane(
source: &[u8],
old_width: usize,
old_height: usize,
new_width: usize,
new_height: usize,
) -> Result<Vec<u8>, Error> {
if source.is_empty() {
return Ok(Vec::new());
}
let mut target = allocate_plane(new_width.checked_mul(new_height).ok_or(Error::Argument)?, 0)?;
let x_scale = (old_width - 1) as f32 / (new_width.saturating_sub(1).max(1)) as f32;
let y_scale = (old_height - 1) as f32 / (new_height.saturating_sub(1).max(1)) as f32;
for y in 0..new_height {
let source_y = if new_height > 1 {
y as f32 * y_scale
} else {
0.0
};
for x in 0..new_width {
let source_x = if new_width > 1 {
x as f32 * x_scale
} else {
0.0
};
target[y * new_width + x] =
bilinear_sample(source, old_width, old_height, source_x, source_y);
}
}
Ok(target)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)] // Coordinates are finite, non-negative, bounded image indices; output is clamped.
fn bilinear_sample(source: &[u8], width: usize, height: usize, x: f32, y: f32) -> u8 {
let x0 = x as usize;
let y0 = y as usize;
let x1 = (x0 + 1).min(width - 1);
let y1 = (y0 + 1).min(height - 1);
let fraction_x = x - x0 as f32;
let fraction_y = y - y0 as f32;
let top = f32::from(source[y0 * width + x0]) * (1.0 - fraction_x)
+ f32::from(source[y0 * width + x1]) * fraction_x;
let bottom = f32::from(source[y1 * width + x0]) * (1.0 - fraction_x)
+ f32::from(source[y1 * width + x1]) * fraction_x;
(top * (1.0 - fraction_y) + bottom * fraction_y)
.clamp(0.0, 255.0)
.round_ties_even() as u8
}
fn canonical_components(channels: ManagedImageImageChannels) -> Result<usize, Error> {
let gray = channels.contains(ManagedImageImageChannels::GRAY);
let color = channels.contains(ManagedImageImageChannels::COLOR);
if gray && color {
return Err(Error::Argument);
}
let count = usize::from(gray)
+ 3 * usize::from(color)
+ usize::from(channels.contains(ManagedImageImageChannels::ALPHA))
+ usize::from(channels.contains(ManagedImageImageChannels::BUMP));
if count == 0 {
Err(Error::Argument)
} else {
Ok(count)
}
}
fn required_interleaved_bytes(
height: usize,
stride: usize,
row_bytes: usize,
) -> Result<usize, Error> {
height
.checked_sub(1)
.and_then(|rows| rows.checked_mul(stride))
.and_then(|prefix| prefix.checked_add(row_bytes))
.ok_or(Error::Argument)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Cursor, Read as _};
fn rgba() -> ManagedImageImageChannels {
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
}
#[test]
fn construction_layout_and_limits_are_checked_before_allocation() {
let gray_alpha = ManagedImage::new(
3,
2,
ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
)
.unwrap();
assert_eq!(gray_alpha.red.len(), 6);
assert_eq!(gray_alpha.alpha.len(), 6);
assert!(gray_alpha.green.is_empty());
assert_eq!(ManagedImage::new(0, 1, rgba()), Err(Error::Argument));
assert_eq!(ManagedImage::new(-1, 1, rgba()), Err(Error::Argument));
assert_eq!(
ManagedImage::new(i32::MAX, i32::MAX, rgba()),
Err(Error::Argument)
);
let unknown = ManagedImageImageChannels(16) | ManagedImageImageChannels::ALPHA;
assert_eq!(ManagedImage::new(1, 1, unknown).unwrap().channels, unknown);
}
#[test]
fn channel_conversion_clear_and_clone_match_reference_ownership() {
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
image.red.copy_from_slice(&[1, 2, 3, 4]);
image
.convert_channels(rgba() | ManagedImageImageChannels::BUMP)
.unwrap();
assert_eq!(image.alpha, vec![255; 4]);
assert_eq!(image.bump, vec![0; 4]);
let mut copy = image.clone().unwrap();
copy.red[0] = 99;
assert_eq!(image.red[0], 1);
copy.clear().unwrap();
assert!(copy.red.iter().all(|value| *value == 0));
}
#[test]
fn canonical_interleave_round_trip_honors_padded_stride() {
let bytes = [
1, 2, 3, 4, 5, 6, 7, 8, 99, 99, 9, 10, 11, 12, 13, 14, 15, 16, 99, 99,
];
let image = ManagedImage::from_interleaved(2, 2, rgba(), 10, &bytes).unwrap();
assert_eq!(image.red, vec![1, 5, 9, 13]);
assert_eq!(image.green, vec![2, 6, 10, 14]);
assert_eq!(image.blue, vec![3, 7, 11, 15]);
assert_eq!(image.alpha, vec![4, 8, 12, 16]);
assert_eq!(
image.to_interleaved(10).unwrap(),
[
1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 9, 10, 11, 12, 13, 14, 15, 16, 0, 0,
]
);
assert_eq!(
ManagedImage::from_interleaved(2, 2, rgba(), 7, &bytes),
Err(Error::Argument)
);
}
#[test]
fn nearest_and_bilinear_resize_all_present_planes() {
let mut nearest =
ManagedImage::new(2, 2, rgba() | ManagedImageImageChannels::BUMP).unwrap();
nearest.red.copy_from_slice(&[1, 2, 3, 4]);
nearest.green.copy_from_slice(&[10, 20, 30, 40]);
nearest.blue.copy_from_slice(&[100, 110, 120, 130]);
nearest.alpha.copy_from_slice(&[200, 201, 202, 203]);
nearest.bump.copy_from_slice(&[50, 51, 52, 53]);
nearest.resize_nearest_neighbor(4, 4).unwrap();
assert_eq!(
nearest.red,
vec![1, 1, 2, 2, 1, 1, 2, 2, 3, 3, 4, 4, 3, 3, 4, 4]
);
assert_eq!(nearest.green[15], 40);
assert_eq!(nearest.blue[15], 130);
assert_eq!(nearest.alpha[15], 203);
assert_eq!(nearest.bump[15], 53);
let mut bilinear = ManagedImage::new(2, 2, ManagedImageImageChannels::GRAY).unwrap();
bilinear.red.copy_from_slice(&[0, 10, 20, 30]);
bilinear.resize_bilinear(3, 3).unwrap();
assert_eq!(bilinear.red, vec![0, 5, 10, 10, 15, 20, 20, 25, 30]);
}
#[test]
fn export_raw_preserves_reference_flip_and_alpha_only_behavior() {
let mut image = ManagedImage::new(1, 2, rgba()).unwrap();
image.red.copy_from_slice(&[1, 2]);
image.green.copy_from_slice(&[3, 4]);
image.blue.copy_from_slice(&[5, 6]);
image.alpha.copy_from_slice(&[7, 8]);
assert_eq!(image.export_raw().unwrap(), [2, 4, 6, 8, 1, 3, 5, 7]);
let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
alpha.alpha[0] = 42;
assert_eq!(alpha.export_raw().unwrap(), [42, 42, 42, 255]);
}
#[test]
fn malformed_public_layouts_return_typed_errors_without_partial_resize() {
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
image.green.pop();
assert_eq!(image.validate(), Err(Error::InvalidOperation));
assert_eq!(image.export_raw(), Err(Error::InvalidOperation));
assert_eq!(
image.resize_nearest_neighbor(4, 4),
Err(Error::InvalidOperation)
);
assert_eq!((image.width, image.height), (2, 2));
}
struct OnePixelCodec;
impl ITextureCodec for OnePixelCodec {
fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
let mut sample = [0];
stream.read_exact(&mut sample).map_err(|_| Error::Parse {
position: 0,
context: "missing gray sample",
})?;
let mut image = ManagedImage::new(1, 1, ManagedImageImageChannels::GRAY)?;
image.red[0] = sample[0];
Ok(image)
}
}
#[test]
fn texture_codec_is_object_safe_and_uses_only_core_abstractions() {
let codec: &dyn ITextureCodec = &OnePixelCodec;
let image = codec
.decode(Box::new(Cursor::new(vec![73])))
.expect("one-pixel decode");
assert_eq!(image.red, [73]);
}
}