use crate::error::{DecodeError, DecodedImage};
use crate::profile::Profile;
use std::sync::atomic::AtomicBool;
pub fn decode(src: &[u8], profile: &Profile, canceled: &AtomicBool) -> Result<DecodedImage, DecodeError> {
let (w, h) = crate::decoder_helpers::validate_dimensions(src, profile, "width and height must be positive", 2)?;
let le = profile.little_endian;
let swap = profile.swap_rgb_channels;
let total_pixels = w * h;
let mut dst = vec![0u8; total_pixels * 4];
let row_stride = src.len() / h;
for y in 0..h {
crate::pixel_utils::check_canceled(canceled, "rgb555 decode canceled")?;
let row_start = y * row_stride;
let dst_start = y * w * 4;
let row_dst = &mut dst[dst_start..dst_start + w * 4];
#[cfg(all(feature = "simd", any(target_arch = "x86_64", target_arch = "x86")))]
if le {
crate::simd::rgb555_apply_row_to_bgra(&src[row_start..row_start + w * 2], row_dst);
if swap {
for p in row_dst.chunks_exact_mut(4) {
p.swap(0, 2);
}
}
continue;
}
let src_row = &src[row_start..row_start + w * 2];
for (src_pixel, dst_pixel) in src_row.chunks_exact(2).zip(row_dst.chunks_exact_mut(4)) {
let raw = if le {
u16::from_le_bytes([src_pixel[0], src_pixel[1]])
} else {
u16::from_be_bytes([src_pixel[0], src_pixel[1]])
};
let (r5, g5, b5) = if swap {
(
u32::from(raw & 0x1F),
u32::from((raw >> 5) & 0x1F),
u32::from((raw >> 10) & 0x1F),
)
} else {
(
u32::from((raw >> 10) & 0x1F),
u32::from((raw >> 5) & 0x1F),
u32::from(raw & 0x1F),
)
};
dst_pixel.copy_from_slice(&[
crate::pixel_utils::msb_replicate_5(b5),
crate::pixel_utils::msb_replicate_5(g5),
crate::pixel_utils::msb_replicate_5(r5),
255,
]);
}
}
#[allow(clippy::cast_possible_truncation)]
let out_w = w as u32;
#[allow(clippy::cast_possible_truncation)]
let out_h = h as u32;
Ok(DecodedImage {
data: dst,
width: out_w,
height: out_h,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::{Encoding, Profile};
use std::sync::atomic::AtomicBool;
fn make_profile(w: i32, h: i32, le: bool, swap: bool) -> Profile {
Profile {
prefix: 0,
width: w,
height: h,
encoding: Encoding::Rgb555,
frame_byte_length: w * h * 2,
little_endian: le,
swap_rgb_channels: swap,
..Default::default()
}
}
#[test]
fn zero_dimensions_returns_err() {
let profile = make_profile(0, 100, true, false);
let result = decode(b"", &profile, &AtomicBool::new(false));
assert!(result.is_err());
assert!(matches!(result, Err(DecodeError::InvalidFormat(_))));
}
#[test]
fn negative_dimension_returns_err() {
let profile = make_profile(-1, 100, true, false);
let result = decode(b"", &profile, &AtomicBool::new(false));
assert!(result.is_err());
assert!(matches!(result, Err(DecodeError::InvalidFormat(_))));
}
#[test]
fn too_short_returns_buffer_too_short() {
let profile = make_profile(100, 100, true, false);
let result = decode(&[0u8; 10], &profile, &AtomicBool::new(false));
assert!(result.is_err());
assert!(matches!(result, Err(DecodeError::BufferTooShort { .. })));
}
#[test]
fn buffer_too_short_reports_exact_counts() {
let profile = make_profile(10, 10, true, false);
let result = decode(&[0u8; 50], &profile, &AtomicBool::new(false));
match result {
Err(DecodeError::BufferTooShort {
expected: 200,
actual: 50,
}) => {} other => panic!("expected BufferTooShort(200, 50), got {other:?}"),
}
}
#[test]
fn dst_allocation_matches_geometry() {
let profile = make_profile(3, 2, true, false);
let pixels = vec![0u8; 3 * 2 * 2];
let img = decode(&pixels, &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data.len(), 3 * 2 * 4);
assert_eq!(img.width, 3);
assert_eq!(img.height, 2);
}
#[test]
fn solid_white_pixel() {
let profile = make_profile(1, 1, true, false);
let img = decode(&[0xFF, 0x7F], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0xFF, 0xFF, 0xFF, 255]);
}
#[test]
fn solid_black_pixel() {
let profile = make_profile(1, 1, true, false);
let img = decode(&[0x00, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0, 0, 255]);
}
#[test]
fn solid_red_pixel() {
let profile = make_profile(1, 1, true, false);
let img = decode(&[0x00, 0x7C], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0, 0xFF, 255]);
}
#[test]
fn solid_blue_pixel() {
let profile = make_profile(1, 1, true, false);
let img = decode(&[0x1F, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0xFF, 0, 0, 255]);
}
#[test]
fn solid_green_pixel() {
let profile = make_profile(1, 1, true, false);
let img = decode(&[0xE0, 0x03], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0xFF, 0, 255]);
}
#[test]
fn decode_big_endian() {
let profile = make_profile(1, 1, false, false);
let img = decode(&[0x7C, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0, 0xFF, 255]);
}
#[test]
fn decode_swap_rgb_channels() {
let profile = make_profile(1, 1, true, true);
let img = decode(&[0x00, 0x7C], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0xFF, 0, 0, 255]);
}
#[test]
fn swap_mode_red_stays_low() {
let profile = make_profile(1, 1, true, true);
let img = decode(&[0x1F, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0, 0xFF, 255]);
}
#[test]
fn multi_pixel_decode() {
let profile = make_profile(2, 1, true, false);
let img = decode(&[0xFF, 0x7F, 0xFF, 0x7F], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0xFF, 0xFF, 0xFF, 255, 0xFF, 0xFF, 0xFF, 255]);
}
#[test]
fn msb_replicate_clamping() {
assert_eq!(crate::pixel_utils::msb_replicate_5(31), 0xFF);
assert_eq!(crate::pixel_utils::msb_replicate_5(0), 0x00);
assert_eq!(crate::pixel_utils::msb_replicate_5(16), 0x84);
assert_eq!(crate::pixel_utils::msb_replicate_5(8), 0x42);
assert_eq!(crate::pixel_utils::msb_replicate_5(1), 0x08);
}
#[test]
fn two_pixel_different_colors() {
let profile = make_profile(2, 1, true, false);
let data = [
0x00, 0x7C, 0x1F, 0x00, ];
let img = decode(&data, &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(
img.data,
vec![
0, 0, 0xFF, 255, 0xFF, 0, 0, 255, ]
);
}
#[test]
fn matches_golden_gradient_first_pixel() {
let profile = make_profile(4, 4, true, false);
let img = decode(
&[
0x10, 0x00, 0x12, 0x28, 0x15, 0x54, 0x17, 0x7C, 0x52, 0x01, 0x55, 0x29, 0x57, 0x55, 0x5A, 0x7D, 0xB5, 0x02, 0xB7, 0x2A, 0xBA, 0x56, 0xBD, 0x7E, 0xF7, 0x03, 0xFA, 0x2B, 0xFD, 0x57, 0xFF, 0x7F, ],
&profile,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(img.data[0], 0x84); assert_eq!(img.data[1], 0x00); assert_eq!(img.data[2], 0x00); assert_eq!(img.data[3], 0xFF); let last = img.data.len() - 4;
assert_eq!(img.data[last], 0xFF);
assert_eq!(img.data[last + 1], 0xFF);
assert_eq!(img.data[last + 2], 0xFF);
assert_eq!(img.data[last + 3], 0xFF);
}
#[allow(clippy::cast_sign_loss)]
#[test]
fn row_stride_is_data_driven() {
let w = 55i32;
let h = 55i32;
let padded_data = vec![0u8; 6400]; let profile = Profile {
width: w,
height: h,
encoding: Encoding::Rgb555,
frame_byte_length: 6400,
little_endian: true,
..Default::default()
};
let img = decode(&padded_data, &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data.len(), (w * h * 4) as usize);
}
#[test]
fn output_has_correct_alpha() {
let profile = make_profile(2, 2, true, false);
let pixels = vec![0u8; 8];
let img = decode(&pixels, &profile, &AtomicBool::new(false)).unwrap();
for i in 0..4 {
assert_eq!(img.data[i * 4 + 3], 255);
}
}
}