//! 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) -> Result; } /// 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, /// Blue samples, or an empty vector when color is absent. pub blue: Vec, /// Bump samples, or an empty vector when bump is absent. pub bump: Vec, /// Channel flags describing the planar buffers. pub channels: ManagedImageImageChannels, /// Green samples, or an empty vector when color is absent. pub green: Vec, /// Image height in pixels. pub height: i32, /// Red or gray samples, or an empty vector when neither is present. pub red: Vec, /// 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 { 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. pub fn clone(&self) -> Result { 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, 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 { 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, 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 { 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, 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, 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, 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, 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 { 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 { 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) -> Result { 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]); } }