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