use crate::consts::PixelFormat;
use crate::ffi;
use crate::ffi::Color;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PixelColorError {
#[error("pixel format {format:?} needs {expected} bytes per pixel, got {actual}")]
InsufficientBytes {
format: PixelFormat,
expected: usize,
actual: usize,
},
#[error("compressed pixel format {0:?} cannot be addressed pixel-by-pixel")]
CompressedFormat(PixelFormat),
}
pub fn bytes_per_pixel(format: PixelFormat) -> Option<usize> {
use PixelFormat::*;
Some(match format {
PIXELFORMAT_UNCOMPRESSED_GRAYSCALE => 1,
PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA => 2,
PIXELFORMAT_UNCOMPRESSED_R5G6B5 => 2,
PIXELFORMAT_UNCOMPRESSED_R5G5B5A1 => 2,
PIXELFORMAT_UNCOMPRESSED_R4G4B4A4 => 2,
PIXELFORMAT_UNCOMPRESSED_R8G8B8 => 3,
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 => 4,
PIXELFORMAT_UNCOMPRESSED_R32 => 4,
PIXELFORMAT_UNCOMPRESSED_R32G32B32 => 12,
PIXELFORMAT_UNCOMPRESSED_R32G32B32A32 => 16,
PIXELFORMAT_UNCOMPRESSED_R16 => 2,
PIXELFORMAT_UNCOMPRESSED_R16G16B16 => 6,
PIXELFORMAT_UNCOMPRESSED_R16G16B16A16 => 8,
PIXELFORMAT_COMPRESSED_DXT1_RGB
| PIXELFORMAT_COMPRESSED_DXT1_RGBA
| PIXELFORMAT_COMPRESSED_DXT3_RGBA
| PIXELFORMAT_COMPRESSED_DXT5_RGBA
| PIXELFORMAT_COMPRESSED_ETC1_RGB
| PIXELFORMAT_COMPRESSED_ETC2_RGB
| PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA
| PIXELFORMAT_COMPRESSED_PVRT_RGB
| PIXELFORMAT_COMPRESSED_PVRT_RGBA
| PIXELFORMAT_COMPRESSED_ASTC_4x4_RGBA
| PIXELFORMAT_COMPRESSED_ASTC_8x8_RGBA => return None,
})
}
fn validate_slice(len: usize, format: PixelFormat) -> Result<usize, PixelColorError> {
match bytes_per_pixel(format) {
None => Err(PixelColorError::CompressedFormat(format)),
Some(expected) if len < expected => Err(PixelColorError::InsufficientBytes {
format,
expected,
actual: len,
}),
Some(expected) => Ok(expected),
}
}
pub fn get_pixel_color(bytes: &[u8], format: PixelFormat) -> Result<Color, PixelColorError> {
validate_slice(bytes.len(), format)?;
Ok(unsafe { ffi::GetPixelColor(bytes.as_ptr() as *mut _, format as i32) })
}
pub fn set_pixel_color(
bytes: &mut [u8],
color: Color,
format: PixelFormat,
) -> Result<(), PixelColorError> {
validate_slice(bytes.len(), format)?;
unsafe {
ffi::SetPixelColor(bytes.as_mut_ptr() as *mut _, color, format as i32);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consts::PixelFormat;
const UNCOMPRESSED_FORMATS: &[(PixelFormat, usize)] = &[
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_GRAYSCALE, 1),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA, 2),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R5G6B5, 2),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R5G5B5A1, 2),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R4G4B4A4, 2),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8, 3),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 4),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R32, 4),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R32G32B32, 12),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R32G32B32A32, 16),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R16, 2),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R16G16B16, 6),
(PixelFormat::PIXELFORMAT_UNCOMPRESSED_R16G16B16A16, 8),
];
#[test]
fn bytes_per_pixel_agrees_with_raylib() {
for &(format, expected) in UNCOMPRESSED_FORMATS {
let bpp = bytes_per_pixel(format)
.unwrap_or_else(|| panic!("{format:?} should be uncompressed"));
assert_eq!(
bpp, expected,
"{format:?}: table says {expected}, fn says {bpp}"
);
let raylib_bpp = unsafe { crate::ffi::GetPixelDataSize(1, 1, format as i32) } as usize;
assert_eq!(
bpp, raylib_bpp,
"{format:?}: rust says {bpp}, raylib's GetPixelDataSize(1,1,...) says {raylib_bpp}"
);
}
}
#[test]
fn bytes_per_pixel_none_for_every_compressed_variant() {
use PixelFormat::*;
for format in [
PIXELFORMAT_COMPRESSED_DXT1_RGB,
PIXELFORMAT_COMPRESSED_DXT1_RGBA,
PIXELFORMAT_COMPRESSED_DXT3_RGBA,
PIXELFORMAT_COMPRESSED_DXT5_RGBA,
PIXELFORMAT_COMPRESSED_ETC1_RGB,
PIXELFORMAT_COMPRESSED_ETC2_RGB,
PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA,
PIXELFORMAT_COMPRESSED_PVRT_RGB,
PIXELFORMAT_COMPRESSED_PVRT_RGBA,
PIXELFORMAT_COMPRESSED_ASTC_4x4_RGBA,
PIXELFORMAT_COMPRESSED_ASTC_8x8_RGBA,
] {
assert_eq!(
bytes_per_pixel(format),
None,
"{format:?} should return None"
);
}
}
#[test]
fn get_returns_insufficient_bytes_on_short_slice() {
let empty = &[] as &[u8];
assert_eq!(
get_pixel_color(empty, PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8),
Err(PixelColorError::InsufficientBytes {
format: PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
expected: 4,
actual: 0,
}),
);
let two_bytes = [0u8; 2];
assert_eq!(
get_pixel_color(&two_bytes, PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8),
Err(PixelColorError::InsufficientBytes {
format: PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
expected: 4,
actual: 2,
}),
);
}
#[test]
fn set_returns_insufficient_bytes_on_short_slice() {
let mut empty: Vec<u8> = vec![];
assert_eq!(
set_pixel_color(
&mut empty,
Color::new(255, 0, 0, 255),
PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
),
Err(PixelColorError::InsufficientBytes {
format: PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
expected: 4,
actual: 0,
}),
);
let mut two_bytes = [0u8; 2];
assert_eq!(
set_pixel_color(
&mut two_bytes,
Color::new(255, 0, 0, 255),
PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
),
Err(PixelColorError::InsufficientBytes {
format: PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
expected: 4,
actual: 2,
}),
);
}
#[test]
fn get_returns_compressed_format_for_compressed_input() {
let bytes = [0u8; 8];
assert_eq!(
get_pixel_color(&bytes, PixelFormat::PIXELFORMAT_COMPRESSED_DXT1_RGB),
Err(PixelColorError::CompressedFormat(
PixelFormat::PIXELFORMAT_COMPRESSED_DXT1_RGB
)),
);
}
#[test]
fn set_returns_compressed_format_for_compressed_input() {
let mut bytes = [0u8; 8];
assert_eq!(
set_pixel_color(
&mut bytes,
Color::new(0, 0, 0, 0),
PixelFormat::PIXELFORMAT_COMPRESSED_DXT1_RGB,
),
Err(PixelColorError::CompressedFormat(
PixelFormat::PIXELFORMAT_COMPRESSED_DXT1_RGB
)),
);
}
fn set_pixel_color_is_unimplemented(format: PixelFormat) -> bool {
use PixelFormat::*;
matches!(
format,
PIXELFORMAT_UNCOMPRESSED_R32
| PIXELFORMAT_UNCOMPRESSED_R32G32B32
| PIXELFORMAT_UNCOMPRESSED_R32G32B32A32
| PIXELFORMAT_UNCOMPRESSED_R16
| PIXELFORMAT_UNCOMPRESSED_R16G16B16
| PIXELFORMAT_UNCOMPRESSED_R16G16B16A16
)
}
#[test]
fn round_trip_all_ff_for_every_uncompressed_format() {
let input = Color::new(0xFF, 0xFF, 0xFF, 0xFF);
for &(format, bpp) in UNCOMPRESSED_FORMATS {
if set_pixel_color_is_unimplemented(format) {
continue;
}
let mut bytes = vec![0u8; bpp];
set_pixel_color(&mut bytes, input, format)
.unwrap_or_else(|e| panic!("set_pixel_color({format:?}) errored: {e}"));
let got = get_pixel_color(&bytes, format)
.unwrap_or_else(|e| panic!("get_pixel_color({format:?}) errored: {e}"));
assert_eq!(
got, input,
"{format:?}: all-FF round-trip diverged (got {got:?}, expected {input:?})"
);
}
}
fn tolerance(format: PixelFormat) -> (u8, u8) {
use PixelFormat::*;
match format {
PIXELFORMAT_UNCOMPRESSED_R8G8B8 => (0, 0),
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 => (0, 0),
PIXELFORMAT_UNCOMPRESSED_R5G6B5 => (4, 0),
PIXELFORMAT_UNCOMPRESSED_R5G5B5A1 => (80, 0),
PIXELFORMAT_UNCOMPRESSED_R4G4B4A4 => (12, 0),
PIXELFORMAT_UNCOMPRESSED_GRAYSCALE => (0, 0),
PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA => (0, 0),
f => unreachable!("tolerance() called with non-uncompressed format {f:?}"),
}
}
fn within_tolerance(got: u8, expected: u8, tol: u8) -> bool {
got.abs_diff(expected) <= tol
}
#[test]
fn round_trip_channel_distinct_with_tolerance() {
let input = Color::new(0xC0, 0x80, 0x40, 0xFF);
for &(format, bpp) in UNCOMPRESSED_FORMATS {
if set_pixel_color_is_unimplemented(format) {
continue;
}
let mut bytes = vec![0u8; bpp];
set_pixel_color(&mut bytes, input, format).unwrap();
let got = get_pixel_color(&bytes, format).unwrap();
use PixelFormat::*;
match format {
PIXELFORMAT_UNCOMPRESSED_GRAYSCALE => {
assert_eq!(got.r, got.g, "{format:?}: R==G after collapse");
assert_eq!(got.g, got.b, "{format:?}: G==B after collapse");
assert_eq!(got.a, 255, "{format:?}: alpha forced to 255");
}
PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA => {
assert_eq!(got.r, got.g, "{format:?}: R==G after collapse");
assert_eq!(got.g, got.b, "{format:?}: G==B after collapse");
assert_eq!(got.a, input.a, "{format:?}: alpha exact");
}
_ => {
let (rgb_tol, a_tol) = tolerance(format);
assert!(
within_tolerance(got.r, input.r, rgb_tol),
"{format:?}: R got {} expected {} (tol {rgb_tol})",
got.r,
input.r
);
assert!(
within_tolerance(got.g, input.g, rgb_tol),
"{format:?}: G got {} expected {} (tol {rgb_tol})",
got.g,
input.g
);
assert!(
within_tolerance(got.b, input.b, rgb_tol),
"{format:?}: B got {} expected {} (tol {rgb_tol})",
got.b,
input.b
);
assert!(
within_tolerance(got.a, input.a, a_tol),
"{format:?}: A got {} expected {} (tol {a_tol})",
got.a,
input.a
);
}
}
}
}
#[test]
fn trailing_bytes_are_ignored() {
let exact = [0x11, 0x22, 0x33, 0x44];
let long = [
0x11, 0x22, 0x33, 0x44, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, ];
let got_exact =
get_pixel_color(&exact, PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8).unwrap();
let got_long =
get_pixel_color(&long, PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8).unwrap();
assert_eq!(got_exact, got_long, "trailing bytes must not affect read");
let mut exact_out = [0u8; 4];
let mut long_out = [0u8; 16];
let sentinel = 0xA5;
long_out[4..].fill(sentinel);
let color = Color::new(0x11, 0x22, 0x33, 0x44);
set_pixel_color(
&mut exact_out,
color,
PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
)
.unwrap();
set_pixel_color(
&mut long_out,
color,
PixelFormat::PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
)
.unwrap();
assert_eq!(&long_out[..4], &exact_out, "first 4 bytes must match");
for (i, &b) in long_out[4..].iter().enumerate() {
assert_eq!(
b,
sentinel,
"byte {} past the pixel must be untouched",
i + 4
);
}
}
}