use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpuHeader {
pub size: u16,
pub dcsqt_offset: u16,
}
impl SpuHeader {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 4 {
return Err(Error::InvalidUdf("SPU header shorter than 4 bytes"));
}
Ok(Self {
size: u16::from_be_bytes([buf[0], buf[1]]),
dcsqt_offset: u16::from_be_bytes([buf[2], buf[3]]),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpuCommand {
ForcedStartDisplay,
StartDisplay,
StopDisplay,
SetColor {
emphasis2: u8,
emphasis1: u8,
pattern: u8,
background: u8,
},
SetContrast {
emphasis2: u8,
emphasis1: u8,
pattern: u8,
background: u8,
},
SetDisplayArea {
start_x: u16,
end_x: u16,
start_y: u16,
end_y: u16,
},
SetPixelDataAddresses {
top_field_offset: u16,
bottom_field_offset: u16,
},
ChangeColorContrast {
raw: Vec<u8>,
},
EndOfSequence,
}
impl SpuCommand {
pub fn opcode(&self) -> u8 {
match self {
Self::ForcedStartDisplay => 0x00,
Self::StartDisplay => 0x01,
Self::StopDisplay => 0x02,
Self::SetColor { .. } => 0x03,
Self::SetContrast { .. } => 0x04,
Self::SetDisplayArea { .. } => 0x05,
Self::SetPixelDataAddresses { .. } => 0x06,
Self::ChangeColorContrast { .. } => 0x07,
Self::EndOfSequence => 0xFF,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpDcSq {
pub start_time: u16,
pub next_offset: u16,
pub commands: Vec<SpuCommand>,
}
pub fn spdcsq_stm_to_ms(stm: u16) -> u32 {
(u32::from(stm) * 1024) / 90
}
#[derive(Debug, Clone)]
pub struct SubPictureUnit {
pub header: SpuHeader,
pub control_sequences: Vec<SpDcSq>,
}
impl SubPictureUnit {
pub fn parse(buf: &[u8]) -> Result<Self> {
let header = SpuHeader::parse(buf)?;
let total = usize::from(header.size);
let dcsqt_off = usize::from(header.dcsqt_offset);
if total < 4 || total > buf.len() {
return Err(Error::InvalidUdf("SPU size exceeds available buffer"));
}
if dcsqt_off < 4 || dcsqt_off >= total {
return Err(Error::InvalidUdf("SPU SP_DCSQTA out of range"));
}
let mut control_sequences = Vec::new();
let mut cursor = dcsqt_off;
let mut seen_offsets = Vec::new();
loop {
if seen_offsets.contains(&cursor) {
return Err(Error::InvalidUdf("SPU DCSQ chain loops back"));
}
seen_offsets.push(cursor);
let dcsq = parse_dcsq(buf, cursor, total)?;
let next_off = usize::from(dcsq.next_offset);
let terminal = next_off == cursor;
control_sequences.push(dcsq);
if terminal {
break;
}
if next_off <= cursor || next_off >= total {
return Err(Error::InvalidUdf("SPU DCSQ next pointer out of range"));
}
cursor = next_off;
}
Ok(Self {
header,
control_sequences,
})
}
pub fn pixel_data_offsets(&self) -> Option<(u16, u16)> {
for dcsq in &self.control_sequences {
for cmd in &dcsq.commands {
if let SpuCommand::SetPixelDataAddresses {
top_field_offset,
bottom_field_offset,
} = cmd
{
return Some((*top_field_offset, *bottom_field_offset));
}
}
}
None
}
pub fn display_dimensions(&self) -> Option<(u16, u16)> {
for dcsq in &self.control_sequences {
for cmd in &dcsq.commands {
if let SpuCommand::SetDisplayArea {
start_x,
end_x,
start_y,
end_y,
} = cmd
{
let w = end_x.saturating_sub(*start_x).saturating_add(1);
let h = end_y.saturating_sub(*start_y).saturating_add(1);
return Some((w, h));
}
}
}
None
}
}
fn parse_dcsq(buf: &[u8], off: usize, total: usize) -> Result<SpDcSq> {
if off + 4 > total {
return Err(Error::InvalidUdf("SPU DCSQ header truncated"));
}
let start_time = u16::from_be_bytes([buf[off], buf[off + 1]]);
let next_offset = u16::from_be_bytes([buf[off + 2], buf[off + 3]]);
let mut i = off + 4;
let mut commands = Vec::new();
while i < total {
let opcode = buf[i];
i += 1;
let cmd = match opcode {
0x00 => SpuCommand::ForcedStartDisplay,
0x01 => SpuCommand::StartDisplay,
0x02 => SpuCommand::StopDisplay,
0x03 => {
if i + 2 > total {
return Err(Error::InvalidUdf("SPU SET_COLOR truncated"));
}
let b0 = buf[i];
let b1 = buf[i + 1];
i += 2;
SpuCommand::SetColor {
emphasis2: b0 >> 4,
emphasis1: b0 & 0x0F,
pattern: b1 >> 4,
background: b1 & 0x0F,
}
}
0x04 => {
if i + 2 > total {
return Err(Error::InvalidUdf("SPU SET_CONTR truncated"));
}
let b0 = buf[i];
let b1 = buf[i + 1];
i += 2;
SpuCommand::SetContrast {
emphasis2: b0 >> 4,
emphasis1: b0 & 0x0F,
pattern: b1 >> 4,
background: b1 & 0x0F,
}
}
0x05 => {
if i + 6 > total {
return Err(Error::InvalidUdf("SPU SET_DAREA truncated"));
}
let xp = u32::from_be_bytes([0, buf[i], buf[i + 1], buf[i + 2]]);
let yp = u32::from_be_bytes([0, buf[i + 3], buf[i + 4], buf[i + 5]]);
i += 6;
let start_x = ((xp >> 12) & 0xFFF) as u16;
let end_x = (xp & 0xFFF) as u16;
let start_y = ((yp >> 12) & 0xFFF) as u16;
let end_y = (yp & 0xFFF) as u16;
SpuCommand::SetDisplayArea {
start_x,
end_x,
start_y,
end_y,
}
}
0x06 => {
if i + 4 > total {
return Err(Error::InvalidUdf("SPU SET_DSPXA truncated"));
}
let top = u16::from_be_bytes([buf[i], buf[i + 1]]);
let bot = u16::from_be_bytes([buf[i + 2], buf[i + 3]]);
i += 4;
SpuCommand::SetPixelDataAddresses {
top_field_offset: top,
bottom_field_offset: bot,
}
}
0x07 => {
if i + 2 > total {
return Err(Error::InvalidUdf("SPU CHG_COLCON size truncated"));
}
let size = u16::from_be_bytes([buf[i], buf[i + 1]]) as usize;
if size < 2 || i + size > total {
return Err(Error::InvalidUdf(
"SPU CHG_COLCON parameter area out of range",
));
}
let raw = buf[i..i + size].to_vec();
i += size;
SpuCommand::ChangeColorContrast { raw }
}
0xFF => {
commands.push(SpuCommand::EndOfSequence);
return Ok(SpDcSq {
start_time,
next_offset,
commands,
});
}
_ => {
return Err(Error::InvalidUdf("SPU unknown command opcode"));
}
};
commands.push(cmd);
}
Err(Error::InvalidUdf(
"SPU DCSQ ran past buffer without CMD_END",
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PixelRun {
pub count: u16,
pub code: u8,
}
struct PxdReader<'a> {
bytes: &'a [u8],
byte_idx: usize,
bit_idx: u8,
}
impl<'a> PxdReader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
byte_idx: 0,
bit_idx: 0,
}
}
fn read_bits(&mut self, n: u8) -> Result<u32> {
debug_assert!(n <= 16);
let mut out: u32 = 0;
for _ in 0..n {
if self.byte_idx >= self.bytes.len() {
return Err(Error::InvalidUdf("SPU pixel data ran out of bits"));
}
let b = self.bytes[self.byte_idx];
let bit = (b >> (7 - self.bit_idx)) & 1;
out = (out << 1) | u32::from(bit);
self.bit_idx += 1;
if self.bit_idx == 8 {
self.bit_idx = 0;
self.byte_idx += 1;
}
}
Ok(out)
}
fn align_to_byte(&mut self) {
if self.bit_idx != 0 {
self.bit_idx = 0;
self.byte_idx += 1;
}
}
}
fn decode_one_run(rdr: &mut PxdReader<'_>) -> Result<PixelRun> {
let top = rdr.read_bits(2)? as u8;
if top != 0 {
let count = u16::from(top);
let code = rdr.read_bits(2)? as u8;
return Ok(PixelRun { count, code });
}
let next2 = rdr.read_bits(2)? as u8;
if next2 != 0 {
let low2 = rdr.read_bits(2)? as u8;
let code = rdr.read_bits(2)? as u8;
let count = (u16::from(next2) << 2) | u16::from(low2);
return Ok(PixelRun { count, code });
}
let then2 = rdr.read_bits(2)? as u8;
if then2 != 0 {
let mid4 = rdr.read_bits(4)? as u8;
let code = rdr.read_bits(2)? as u8;
let count = (u16::from(then2) << 4) | u16::from(mid4);
return Ok(PixelRun { count, code });
}
let count8 = rdr.read_bits(8)? as u16;
let code = rdr.read_bits(2)? as u8;
Ok(PixelRun {
count: count8,
code,
})
}
pub fn decode_rle_field(
bytes: &[u8],
width: u16,
expected_lines: u16,
) -> Result<Vec<Vec<PixelRun>>> {
if width == 0 {
return Ok(Vec::new());
}
let mut rdr = PxdReader::new(bytes);
let mut out: Vec<Vec<PixelRun>> = Vec::with_capacity(usize::from(expected_lines));
for _ in 0..expected_lines {
let mut row = Vec::new();
let mut written: u32 = 0;
let row_width = u32::from(width);
while written < row_width {
let run = decode_one_run(&mut rdr)?;
if run.count == 0 {
let remaining = row_width - written;
let clamped = u16::try_from(remaining).unwrap_or(u16::MAX);
row.push(PixelRun {
count: clamped,
code: run.code,
});
break;
}
let take = u32::from(run.count).min(row_width - written);
row.push(PixelRun {
count: take as u16,
code: run.code,
});
written += take;
}
rdr.align_to_byte();
out.push(row);
}
Ok(out)
}
pub fn render_field(bytes: &[u8], width: u16, expected_lines: u16) -> Result<Vec<u8>> {
let runs = decode_rle_field(bytes, width, expected_lines)?;
let w = usize::from(width);
let h = runs.len();
let mut out = vec![0u8; w * h];
for (y, row) in runs.iter().enumerate() {
let mut x = 0usize;
for run in row {
let n = usize::from(run.count).min(w - x);
for slot in out[y * w + x..y * w + x + n].iter_mut() {
*slot = run.code;
}
x += n;
if x >= w {
break;
}
}
}
Ok(out)
}
pub fn ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
let yc = (i32::from(y) - 16) * 76309;
let cbc = i32::from(cb) - 128;
let crc = i32::from(cr) - 128;
let r = (yc + 104597 * crc + (1 << 15)) >> 16;
let g = (yc - 25624 * cbc - 53279 * crc + (1 << 15)) >> 16;
let b = (yc + 132201 * cbc + (1 << 15)) >> 16;
(
r.clamp(0, 255) as u8,
g.clamp(0, 255) as u8,
b.clamp(0, 255) as u8,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpuBitmap {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub rgba: Vec<u8>,
}
fn contrast_to_alpha(c: u8) -> u8 {
let c = c & 0x0F;
(c << 4) | c
}
impl SubPictureUnit {
pub fn composite(
&self,
buf: &[u8],
palette: &[crate::ifo::PaletteEntry; 16],
) -> Result<Option<SpuBitmap>> {
let (start_x, start_y, width, height) = match self.display_rect() {
Some(r) => r,
None => return Ok(None),
};
let (top_off, bot_off) = match self.pixel_data_offsets() {
Some(o) => o,
None => return Ok(None),
};
if usize::from(top_off) > buf.len() || usize::from(bot_off) > buf.len() {
return Err(Error::InvalidUdf("SPU field offset past buffer"));
}
let (colors, contrasts) = self.color_contrast_maps();
let mut rgba_lut = [[0u8; 4]; 4];
for (code, slot) in rgba_lut.iter_mut().enumerate() {
let pal_idx = usize::from(colors[code] & 0x0F);
let pe = palette[pal_idx];
let (r, g, b) = ycbcr_to_rgb(pe.y, pe.cb, pe.cr);
*slot = [r, g, b, contrast_to_alpha(contrasts[code])];
}
let w = usize::from(width);
let h = usize::from(height);
let top_lines = height.div_ceil(2);
let bot_lines = height / 2;
let top_idx = render_field(&buf[usize::from(top_off)..], width, top_lines)?;
let bot_idx = render_field(&buf[usize::from(bot_off)..], width, bot_lines)?;
let mut rgba = vec![0u8; w * h * 4];
for (field_row, src) in top_idx.chunks_exact(w).enumerate() {
let dst_row = field_row * 2;
if dst_row < h {
blit_row(&mut rgba, dst_row, w, src, &rgba_lut);
}
}
for (field_row, src) in bot_idx.chunks_exact(w).enumerate() {
let dst_row = field_row * 2 + 1;
if dst_row < h {
blit_row(&mut rgba, dst_row, w, src, &rgba_lut);
}
}
Ok(Some(SpuBitmap {
x: start_x,
y: start_y,
width,
height,
rgba,
}))
}
fn display_rect(&self) -> Option<(u16, u16, u16, u16)> {
for dcsq in &self.control_sequences {
for cmd in &dcsq.commands {
if let SpuCommand::SetDisplayArea {
start_x,
end_x,
start_y,
end_y,
} = cmd
{
let w = end_x.saturating_sub(*start_x).saturating_add(1);
let h = end_y.saturating_sub(*start_y).saturating_add(1);
return Some((*start_x, *start_y, w, h));
}
}
}
None
}
fn color_contrast_maps(&self) -> ([u8; 4], [u8; 4]) {
let mut colors = [0u8; 4];
let mut contrasts = [0x0Fu8; 4];
for dcsq in &self.control_sequences {
for cmd in &dcsq.commands {
match cmd {
SpuCommand::SetColor {
emphasis2,
emphasis1,
pattern,
background,
} => {
colors = [*background, *pattern, *emphasis1, *emphasis2];
}
SpuCommand::SetContrast {
emphasis2,
emphasis1,
pattern,
background,
} => {
contrasts = [*background, *pattern, *emphasis1, *emphasis2];
}
_ => {}
}
}
}
(colors, contrasts)
}
}
fn blit_row(rgba: &mut [u8], dst_row: usize, w: usize, src: &[u8], lut: &[[u8; 4]; 4]) {
let base = dst_row * w * 4;
for (x, &code) in src.iter().enumerate() {
let px = &lut[usize::from(code) & 0x03];
let o = base + x * 4;
rgba[o..o + 4].copy_from_slice(px);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_round_trips() {
let bytes = [0x12, 0x34, 0x56, 0x78];
let h = SpuHeader::parse(&bytes).unwrap();
assert_eq!(h.size, 0x1234);
assert_eq!(h.dcsqt_offset, 0x5678);
}
#[test]
fn header_rejects_short() {
assert!(SpuHeader::parse(&[0, 1, 2]).is_err());
}
#[test]
fn delay_conversion_matches_table() {
assert_eq!(spdcsq_stm_to_ms(87), 989);
assert_eq!(spdcsq_stm_to_ms(878), 9989);
}
#[test]
fn one_run_4bit_form() {
let bytes = [0b0110_0000];
let mut rdr = PxdReader::new(&bytes);
let run = decode_one_run(&mut rdr).unwrap();
assert_eq!(run.count, 1);
assert_eq!(run.code, 2);
}
#[test]
fn one_run_8bit_form() {
let bytes = [0x17];
let mut rdr = PxdReader::new(&bytes);
let run = decode_one_run(&mut rdr).unwrap();
assert_eq!(run.count, 5);
assert_eq!(run.code, 3);
}
#[test]
fn one_run_12bit_form() {
let bytes = [0b0000_0101, 0b0001_0000];
let mut rdr = PxdReader::new(&bytes);
let run = decode_one_run(&mut rdr).unwrap();
assert_eq!(run.count, 20);
assert_eq!(run.code, 1);
}
#[test]
fn one_run_16bit_form() {
let bytes = [0x03, 0x22];
let mut rdr = PxdReader::new(&bytes);
let run = decode_one_run(&mut rdr).unwrap();
assert_eq!(run.count, 200);
assert_eq!(run.code, 2);
}
#[test]
fn one_run_end_of_line() {
let bytes = [0x00, 0x00];
let mut rdr = PxdReader::new(&bytes);
let run = decode_one_run(&mut rdr).unwrap();
assert_eq!(run.count, 0);
assert_eq!(run.code, 0);
}
#[test]
fn decode_rle_field_pads_eol_run_to_width() {
let bytes = [0xC0, 0x00, 0x10];
let rows = decode_rle_field(&bytes, 10, 1).unwrap();
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert_eq!(row[0], PixelRun { count: 3, code: 0 });
assert_eq!(row[1], PixelRun { count: 7, code: 1 });
}
#[test]
fn render_field_materialises_pixels() {
let bytes = [0xC0, 0x00, 0x10];
let pixels = render_field(&bytes, 10, 1).unwrap();
assert_eq!(pixels.len(), 10);
assert_eq!(&pixels[0..3], &[0, 0, 0]);
assert_eq!(&pixels[3..10], &[1, 1, 1, 1, 1, 1, 1]);
}
fn build_minimal_spu() -> Vec<u8> {
let mut buf = vec![0u8; 0x28];
buf[0..2].copy_from_slice(&0x0028u16.to_be_bytes());
buf[2..4].copy_from_slice(&0x0010u16.to_be_bytes());
let dcsq = 0x10;
buf[dcsq..dcsq + 2].copy_from_slice(&0x0000u16.to_be_bytes());
buf[dcsq + 2..dcsq + 4].copy_from_slice(&(dcsq as u16).to_be_bytes());
let mut o = dcsq + 4;
buf[o] = 0x03;
buf[o + 1] = 0xFE;
buf[o + 2] = 0xDC;
o += 3;
buf[o] = 0x04;
buf[o + 1] = 0xFF;
buf[o + 2] = 0x80;
o += 3;
buf[o] = 0x05;
let xp: u32 = 3; let yp: u32 = 3;
let xb = xp.to_be_bytes();
let yb = yp.to_be_bytes();
buf[o + 1] = xb[1];
buf[o + 2] = xb[2];
buf[o + 3] = xb[3];
buf[o + 4] = yb[1];
buf[o + 5] = yb[2];
buf[o + 6] = yb[3];
o += 7;
buf[o] = 0x06;
buf[o + 1..o + 3].copy_from_slice(&0x0004u16.to_be_bytes());
buf[o + 3..o + 5].copy_from_slice(&0x0004u16.to_be_bytes());
o += 5;
buf[o] = 0x01;
o += 1;
buf[o] = 0xFF;
debug_assert_eq!(o + 1, buf.len());
buf
}
#[test]
fn parse_minimal_spu_full_unit() {
let buf = build_minimal_spu();
let spu = SubPictureUnit::parse(&buf).unwrap();
assert_eq!(spu.header.size, 0x28);
assert_eq!(spu.header.dcsqt_offset, 0x10);
assert_eq!(spu.control_sequences.len(), 1);
let dcsq = &spu.control_sequences[0];
assert_eq!(dcsq.start_time, 0);
assert_eq!(dcsq.next_offset, 0x10);
assert_eq!(dcsq.commands.len(), 6);
assert_eq!(
dcsq.commands[0],
SpuCommand::SetColor {
emphasis2: 0xF,
emphasis1: 0xE,
pattern: 0xD,
background: 0xC,
}
);
assert_eq!(
dcsq.commands[1],
SpuCommand::SetContrast {
emphasis2: 0xF,
emphasis1: 0xF,
pattern: 0x8,
background: 0x0,
}
);
assert_eq!(
dcsq.commands[2],
SpuCommand::SetDisplayArea {
start_x: 0,
end_x: 3,
start_y: 0,
end_y: 3,
}
);
assert_eq!(
dcsq.commands[3],
SpuCommand::SetPixelDataAddresses {
top_field_offset: 4,
bottom_field_offset: 4,
}
);
assert_eq!(dcsq.commands[4], SpuCommand::StartDisplay);
assert_eq!(dcsq.commands[5], SpuCommand::EndOfSequence);
assert_eq!(spu.pixel_data_offsets(), Some((4, 4)));
assert_eq!(spu.display_dimensions(), Some((4, 4)));
}
#[test]
fn parse_rejects_dcsqta_out_of_range() {
let mut buf = vec![0u8; 0x10];
buf[0..2].copy_from_slice(&0x0010u16.to_be_bytes());
buf[2..4].copy_from_slice(&0x0020u16.to_be_bytes());
assert!(SubPictureUnit::parse(&buf).is_err());
}
#[test]
fn parse_rejects_dcsq_without_end() {
let mut buf = vec![0u8; 0x10];
buf[0..2].copy_from_slice(&0x0010u16.to_be_bytes());
buf[2..4].copy_from_slice(&0x0004u16.to_be_bytes());
buf[4..6].copy_from_slice(&0x0000u16.to_be_bytes());
buf[6..8].copy_from_slice(&0x0004u16.to_be_bytes());
for b in &mut buf[8..] {
*b = 0x01;
}
assert!(SubPictureUnit::parse(&buf).is_err());
}
#[test]
fn change_color_contrast_round_trips_raw() {
let mut buf = vec![0u8; 0x18];
buf[0..2].copy_from_slice(&0x0018u16.to_be_bytes());
buf[2..4].copy_from_slice(&0x0004u16.to_be_bytes());
buf[4..6].copy_from_slice(&0x0042u16.to_be_bytes());
buf[6..8].copy_from_slice(&0x0004u16.to_be_bytes());
buf[8] = 0x07;
buf[9..11].copy_from_slice(&0x0006u16.to_be_bytes());
buf[11] = 0x0F;
buf[12] = 0xFF;
buf[13] = 0xFF;
buf[14] = 0xFF;
buf[15] = 0xFF;
let spu = SubPictureUnit::parse(&buf).unwrap();
assert_eq!(spu.control_sequences.len(), 1);
let cmds = &spu.control_sequences[0].commands;
assert_eq!(cmds.len(), 2);
match &cmds[0] {
SpuCommand::ChangeColorContrast { raw } => {
assert_eq!(raw, &[0x00, 0x06, 0x0F, 0xFF, 0xFF, 0xFF]);
}
other => panic!("expected ChangeColorContrast, got {other:?}"),
}
assert_eq!(cmds[1], SpuCommand::EndOfSequence);
}
#[test]
fn opcodes_match_table() {
assert_eq!(SpuCommand::ForcedStartDisplay.opcode(), 0x00);
assert_eq!(SpuCommand::StartDisplay.opcode(), 0x01);
assert_eq!(SpuCommand::StopDisplay.opcode(), 0x02);
assert_eq!(
SpuCommand::SetColor {
emphasis2: 0,
emphasis1: 0,
pattern: 0,
background: 0
}
.opcode(),
0x03
);
assert_eq!(
SpuCommand::SetContrast {
emphasis2: 0,
emphasis1: 0,
pattern: 0,
background: 0
}
.opcode(),
0x04
);
assert_eq!(
SpuCommand::SetDisplayArea {
start_x: 0,
end_x: 0,
start_y: 0,
end_y: 0
}
.opcode(),
0x05
);
assert_eq!(
SpuCommand::SetPixelDataAddresses {
top_field_offset: 0,
bottom_field_offset: 0
}
.opcode(),
0x06
);
assert_eq!(
SpuCommand::ChangeColorContrast { raw: Vec::new() }.opcode(),
0x07
);
assert_eq!(SpuCommand::EndOfSequence.opcode(), 0xFF);
}
#[test]
fn ycbcr_to_rgb_known_points() {
assert_eq!(ycbcr_to_rgb(16, 128, 128), (0, 0, 0));
let (r, g, b) = ycbcr_to_rgb(235, 128, 128);
assert!(r >= 254 && g >= 254 && b >= 254, "white -> {r},{g},{b}");
assert_eq!(ycbcr_to_rgb(0, 128, 128), (0, 0, 0));
let (r, g, b) = ycbcr_to_rgb(128, 128, 240);
assert!(r > g && r > b, "red-dominant -> {r},{g},{b}");
let (r, g, b) = ycbcr_to_rgb(128, 240, 128);
assert!(b > r && b > g, "blue-dominant -> {r},{g},{b}");
}
#[test]
fn contrast_nibble_expands_to_alpha() {
assert_eq!(contrast_to_alpha(0x0), 0x00);
assert_eq!(contrast_to_alpha(0xF), 0xFF);
assert_eq!(contrast_to_alpha(0x8), 0x88);
}
fn solid_palette() -> [crate::ifo::PaletteEntry; 16] {
let mut p = [crate::ifo::PaletteEntry::default(); 16];
p[5] = crate::ifo::PaletteEntry {
y: 235,
cr: 128,
cb: 128,
};
p[9] = crate::ifo::PaletteEntry {
y: 16,
cr: 128,
cb: 128,
};
p
}
fn build_solid_spu() -> Vec<u8> {
let dcsqt = 0x10u16;
let mut buf = vec![0u8; 0x30];
buf[0..2].copy_from_slice(&0x0030u16.to_be_bytes());
buf[2..4].copy_from_slice(&dcsqt.to_be_bytes());
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = 0x00;
buf[7] = 0x00;
let d = 0x10usize;
buf[d..d + 2].copy_from_slice(&0x0000u16.to_be_bytes());
buf[d + 2..d + 4].copy_from_slice(&(d as u16).to_be_bytes());
let mut o = d + 4;
buf[o] = 0x03;
buf[o + 1] = 0x00;
buf[o + 2] = 0x05;
o += 3;
buf[o] = 0x04;
buf[o + 1] = 0x00;
buf[o + 2] = 0x0F;
o += 3;
buf[o] = 0x05;
let xp: u32 = 1;
let yp: u32 = 1;
buf[o + 1] = xp.to_be_bytes()[1];
buf[o + 2] = xp.to_be_bytes()[2];
buf[o + 3] = xp.to_be_bytes()[3];
buf[o + 4] = yp.to_be_bytes()[1];
buf[o + 5] = yp.to_be_bytes()[2];
buf[o + 6] = yp.to_be_bytes()[3];
o += 7;
buf[o] = 0x06;
buf[o + 1..o + 3].copy_from_slice(&0x0004u16.to_be_bytes());
buf[o + 3..o + 5].copy_from_slice(&0x0006u16.to_be_bytes());
o += 5;
buf[o] = 0x01;
buf[o + 1] = 0xFF;
buf
}
#[test]
fn composite_produces_opaque_white_rect() {
let buf = build_solid_spu();
let spu = SubPictureUnit::parse(&buf).unwrap();
let pal = solid_palette();
let bmp = spu.composite(&buf, &pal).unwrap().unwrap();
assert_eq!((bmp.x, bmp.y, bmp.width, bmp.height), (0, 0, 2, 2));
assert_eq!(bmp.rgba.len(), 2 * 2 * 4);
for px in bmp.rgba.chunks_exact(4) {
assert!(px[0] >= 254 && px[1] >= 254 && px[2] >= 254);
assert_eq!(px[3], 0xFF);
}
}
#[test]
fn composite_missing_darea_returns_none() {
let mut buf = vec![0u8; 0x14];
buf[0..2].copy_from_slice(&0x0014u16.to_be_bytes());
buf[2..4].copy_from_slice(&0x0008u16.to_be_bytes());
let d = 0x08usize;
buf[d..d + 2].copy_from_slice(&0x0000u16.to_be_bytes());
buf[d + 2..d + 4].copy_from_slice(&(d as u16).to_be_bytes());
buf[d + 4] = 0x06; buf[d + 5..d + 7].copy_from_slice(&0x0004u16.to_be_bytes());
buf[d + 7..d + 9].copy_from_slice(&0x0004u16.to_be_bytes());
buf[d + 9] = 0xFF; let spu = SubPictureUnit::parse(&buf).unwrap();
let pal = solid_palette();
assert_eq!(spu.composite(&buf, &pal).unwrap(), None);
}
}