use crate::error::{DecodeError, DecodedImage};
use crate::profile::Profile;
#[allow(unused_imports)] use crate::yuv;
use std::sync::atomic::AtomicBool;
#[allow(clippy::similar_names)]
pub fn decode(src: &[u8], profile: &Profile, canceled: &AtomicBool) -> Result<DecodedImage, DecodeError> {
let (w, h) = crate::decoder_helpers::validate_dimensions(src, profile, "CLCL dimensions must be positive", 2)?;
let pixel_count = w * h;
let chroma_len = pixel_count.div_ceil(2);
if pixel_count % 2 != 0 {
let total_needed = pixel_count + 2 * chroma_len;
if src.len() < total_needed {
return Err(DecodeError::BufferTooShort {
expected: total_needed,
actual: src.len(),
});
}
}
let y_len = pixel_count;
let mut dst = vec![0u8; pixel_count * 4];
let cb_off = y_len;
let cr_off = y_len + chroma_len;
for row in 0..h {
let idx = row * w;
crate::pixel_utils::check_canceled(canceled, "clcl decode canceled")?;
#[cfg(feature = "simd")]
{
let y_row = &src[idx..idx + w];
let cb_row = &src[cb_off + idx / 2..cb_off + idx / 2 + w.div_ceil(2)];
let cr_row = &src[cr_off + idx / 2..cr_off + idx / 2 + w.div_ceil(2)];
let dst_row = &mut dst[idx * 4..(idx + w) * 4];
crate::simd::clcl_row_to_bgra(y_row, cb_row, cr_row, w, dst_row);
}
#[cfg(not(feature = "simd"))]
{
for i in idx..idx + w {
let y = src[i];
let cbi = src[cb_off + i / 2];
let cri = src[cr_off + i / 2];
let n_cb = if i & 1 == 0 { cbi & 0x0F } else { cbi >> 4 };
let n_cr = if i & 1 == 0 { cri & 0x0F } else { cri >> 4 };
let pixel = yuv::yuv_to_bgra(y, n_cb << 4, n_cr << 4);
let dst_idx = i * 4;
dst[dst_idx..dst_idx + 4].copy_from_slice(&pixel);
}
}
}
#[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;
use std::sync::atomic::AtomicBool;
fn make_profile(w: i32, h: i32) -> Profile {
Profile {
prefix: 0,
width: w,
height: h,
encoding: Encoding::Rgb565,
frame_byte_length: (w * h * 2).max(0),
clcl_chroma: true,
..Default::default()
}
}
#[test]
fn zero_width_returns_invalid_format() {
let profile = make_profile(0, 100);
let result = decode(b"", &profile, &AtomicBool::new(false));
assert!(matches!(result, Err(DecodeError::InvalidFormat(..))));
}
#[test]
fn zero_height_returns_invalid_format() {
let profile = make_profile(100, 0);
let result = decode(b"", &profile, &AtomicBool::new(false));
assert!(matches!(result, Err(DecodeError::InvalidFormat(..))));
}
#[test]
fn negative_width_returns_invalid_format() {
let profile = make_profile(-1, 100);
let result = decode(b"", &profile, &AtomicBool::new(false));
assert!(matches!(result, Err(DecodeError::InvalidFormat(..))));
}
#[test]
fn buffer_too_short_returns_error() {
let profile = make_profile(10, 10);
let result = decode(&[0u8; 10], &profile, &AtomicBool::new(false));
assert!(matches!(
result,
Err(DecodeError::BufferTooShort {
expected: 200,
actual: 10
})
));
}
#[test]
fn buffer_too_short_odd_bytes() {
let profile = make_profile(2, 2);
let result = decode(&[0u8; 7], &profile, &AtomicBool::new(false));
assert!(matches!(
result,
Err(DecodeError::BufferTooShort { expected: 8, actual: 7 })
));
}
#[test]
fn gray_pixel_neutral_chroma() {
let profile = make_profile(2, 1);
let img = decode(&[128, 128, 0x88, 0x88], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [128, 128, 128, 255]);
assert_eq!(img.data[4..8], [128, 128, 128, 255]);
assert_eq!(img.width, 2);
assert_eq!(img.height, 1);
}
#[test]
fn black_with_neutral_chroma() {
let profile = make_profile(2, 1);
let img = decode(&[0, 0, 0x88, 0x88], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [0, 0, 0, 255]);
assert_eq!(img.data[4..8], [0, 0, 0, 255]);
}
#[test]
fn white_with_neutral_chroma() {
let profile = make_profile(2, 1);
let img = decode(&[255, 255, 0x88, 0x88], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [255, 255, 255, 255]);
assert_eq!(img.data[4..8], [255, 255, 255, 255]);
}
#[test]
fn low_nibble_is_first_pixel_cb() {
let profile = make_profile(2, 1);
let img = decode(&[128, 128, 0x0F, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [255, 182, 0, 255], "pixel 0 low nibble Cb=15 Cr=0");
assert_eq!(img.data[4..8], [0, 255, 0, 255], "pixel 1 high nibble Cb=0 Cr=0");
}
#[test]
fn high_nibble_is_second_pixel_chroma() {
let profile = make_profile(2, 1);
let img = decode(&[128, 128, 0xF0, 0xF0], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [0, 255, 0, 255], "pixel 0 low nibble Cb=0 Cr=0");
assert_eq!(img.data[4..8], [255, 10, 255, 255], "pixel 1 high nibble Cb=15 Cr=15");
}
#[test]
fn two_by_two_grid_all_gray() {
let profile = make_profile(2, 2);
let img = decode(
&[
128, 128, 128, 128, 0x88, 0x88, 0x88, 0x88, ],
&profile,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(img.width, 2);
assert_eq!(img.height, 2);
let expected = [128u8, 128, 128, 255];
for y in 0..2 {
for x in 0..2 {
let off = (y * 2 + x) * 4;
assert_eq!(img.data[off..off + 4], expected, "pixel ({x},{y}) mismatch");
}
}
}
#[test]
fn two_by_two_with_varying_y() {
let profile = make_profile(2, 2);
let img = decode(
&[
255, 128, 64, 0, 0x88, 0x88, 0x88, 0x88, ],
&profile,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(img.data[0..4], [255, 255, 255, 255]);
assert_eq!(img.data[4..8], [128, 128, 128, 255]);
assert_eq!(img.data[8..12], [64, 64, 64, 255]);
assert_eq!(img.data[12..16], [0, 0, 0, 255]);
}
#[test]
fn pixel_pair_shares_chroma() {
let profile = make_profile(4, 1);
let img = decode(
&[
255, 200, 100, 50, 0x88, 0x00, 0xFF, 0x00, ],
&profile,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(img.data[0..4], [255, 175, 255, 255], "pixel 0");
assert_eq!(img.data[4..8], [200, 120, 255, 255], "pixel 1");
assert_eq!(img.data[8..12], [0, 236, 0, 255], "pixel 2");
assert_eq!(img.data[12..16], [0, 186, 0, 255], "pixel 3");
}
#[test]
fn chroma_nibbles_at_extremes() {
let profile = make_profile(2, 1);
let img = decode(&[255, 255, 0xFF, 0xFF], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [255, 137, 255, 255]);
assert_eq!(img.data[4..8], [255, 137, 255, 255]);
}
#[test]
fn chroma_nibbles_at_minimum() {
let profile = make_profile(2, 1);
let img = decode(&[128, 128, 0x00, 0x00], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [0, 255, 0, 255]);
}
#[test]
fn cb_and_cr_planes_are_separate() {
let profile = make_profile(2, 1);
let img = decode(&[128, 128, 0x0F, 0xF0], &profile, &AtomicBool::new(false)).unwrap();
assert_eq!(img.data[0..4], [255, 182, 0, 255], "pixel 0 Cb=15 Cr=0");
assert_eq!(img.data[4..8], [0, 92, 255, 255], "pixel 1 Cb=0 Cr=15");
}
#[test]
fn two_by_two_image_decode() {
let profile = make_profile(2, 2);
let img = decode(
&[
128, 200, 64, 255, 0xF8, 0xF0, 0x08, 0xFF,
],
&profile,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(img.width, 2);
assert_eq!(img.height, 2);
assert_eq!(img.data[0..4], [128, 128, 128, 255]);
assert_eq!(img.data[4..8], [255, 254, 20, 255]);
assert_eq!(img.data[8..12], [0, 28, 221, 255]);
assert_eq!(img.data[12..16], [255, 137, 255, 255]);
}
}