use crate::error::{DecodeError, DecodedImage};
use crate::profile::Profile;
use std::sync::atomic::AtomicBool;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
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 bits = std::cmp::max(w, h).next_power_of_two().trailing_zeros();
let mut row_offsets = vec![0u32; w];
for y in 0..h {
crate::pixel_utils::check_canceled(canceled, "reordered_rgb555 decode canceled")?;
let dst_start = y * w * 4;
let mut z = morton_interleave(0, y as u32, bits);
for entry in &mut row_offsets {
*entry = z;
z = morton_inc_x(z);
}
let mut x = 0usize;
while x + 4 <= w {
let mut pixels = [[0u8; 2]; 4];
for (i, pixel) in pixels.iter_mut().enumerate() {
let src_idx = row_offsets[x + i] as usize * 2;
*pixel = if src_idx + 1 < src.len() {
[src[src_idx], src[src_idx + 1]]
} else {
[0, 0]
};
if !le {
pixel.swap(0, 1);
}
}
let bgra = crate::simd::rgb555_pack_to_bgra(pixels, swap);
let dst_idx = dst_start + x * 4;
dst[dst_idx..dst_idx + 16].copy_from_slice(&bgra);
x += 4;
}
for (rx, entry) in row_offsets.iter().enumerate().take(w).skip(x) {
let src_idx = *entry as usize * 2;
let raw = if src_idx + 1 < src.len() {
if le {
u16::from_le_bytes([src[src_idx], src[src_idx + 1]])
} else {
u16::from_be_bytes([src[src_idx], src[src_idx + 1]])
}
} else {
0x0000
};
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),
)
};
let off = dst_start + rx * 4;
dst[off] = crate::pixel_utils::msb_replicate_5(b5);
dst[off + 1] = crate::pixel_utils::msb_replicate_5(g5);
dst[off + 2] = crate::pixel_utils::msb_replicate_5(r5);
dst[off + 3] = 255;
}
}
Ok(DecodedImage {
data: dst,
width: w as u32,
height: h as u32,
})
}
#[inline]
pub(crate) fn morton_interleave(x: u32, y: u32, bits: u32) -> u32 {
let mut z = 0u32;
for i in 0..bits {
z |= ((x >> i) & 1) << (2 * i + 1);
z |= ((y >> i) & 1) << (2 * i);
}
z
}
#[inline]
fn morton_inc_x(z: u32) -> u32 {
let x_bits = z & 0xAAAA_AAAA; let y_bits = z & 0x5555_5555; let x_inc = ((x_bits | 0x5555_5555) + 1) & 0xAAAA_AAAA;
y_bits | x_inc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::{Encoding, Profile};
use std::sync::atomic::AtomicBool;
fn make_profile(w: i32, h: i32, swap: bool) -> Profile {
Profile {
prefix: 0,
width: w,
height: h,
encoding: Encoding::ReorderedRgb555,
frame_byte_length: w * h * 2,
little_endian: true,
swap_rgb_channels: swap,
..Default::default()
}
}
#[test]
fn zero_dimensions_returns_err() {
let profile = make_profile(0, 100, 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, 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, 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, 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, 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, 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, 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, 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, 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, false);
let img = decode(&[0xE0, 0x03], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0xFF, 0, 255]);
}
#[test]
fn decode_swap_rgb_channels() {
let profile = make_profile(1, 1, 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);
let img = decode(&[0x1F, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0, 0, 0xFF, 255]);
}
#[test]
fn two_pixel_decode() {
let profile = make_profile(2, 1, false);
let img = decode(&[0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x7F], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data, vec![0xFF, 0xFF, 0xFF, 255, 0xFF, 0xFF, 0xFF, 255]);
}
#[test]
fn two_pixel_different_colors() {
let profile = make_profile(2, 1, false);
let data = [
0x00, 0x7C, 0x00, 0x00, 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 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 output_has_correct_alpha() {
let profile = make_profile(2, 2, 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);
}
}
#[test]
fn decode_4x4_morton_order_quadrants() {
let profile = make_profile(4, 4, false);
let encoded = vec![
0x00, 0x7C, 0x00, 0x7C, 0x00, 0x7C, 0x00, 0x7C, 0x1F, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, ];
let img = decode(&encoded, &profile, &AtomicBool::new(false)).unwrap();
let data = &img.data;
assert_eq!(&data[0..4], &[0, 0, 255, 255], "(0,0)");
assert_eq!(&data[4..8], &[0, 0, 255, 255], "(1,0)");
assert_eq!(&data[8..12], &[0, 255, 0, 255], "(2,0)");
assert_eq!(&data[12..16], &[0, 255, 0, 255], "(3,0)");
assert_eq!(&data[16..20], &[0, 0, 255, 255], "(0,1)");
assert_eq!(&data[20..24], &[0, 0, 255, 255], "(1,1)");
assert_eq!(&data[24..28], &[0, 255, 0, 255], "(2,1)");
assert_eq!(&data[28..32], &[0, 255, 0, 255], "(3,1)");
assert_eq!(&data[32..36], &[255, 0, 0, 255], "(0,2)");
assert_eq!(&data[36..40], &[255, 0, 0, 255], "(1,2)");
assert_eq!(&data[40..44], &[255, 255, 255, 255], "(2,2)");
assert_eq!(&data[44..48], &[255, 255, 255, 255], "(3,2)");
assert_eq!(&data[48..52], &[255, 0, 0, 255], "(0,3)");
assert_eq!(&data[52..56], &[255, 0, 0, 255], "(1,3)");
assert_eq!(&data[56..60], &[255, 255, 255, 255], "(2,3)");
assert_eq!(&data[60..64], &[255, 255, 255, 255], "(3,3)");
}
}