use super::{MAX_VOBSUB_IMAGE_PIXELS, SubtitlePacket, VobSubPalette};
pub fn decode_vobsub_rle(
packet: &SubtitlePacket,
sub_data: &[u8],
palette: &VobSubPalette,
) -> Vec<u8> {
let width = packet.width as usize;
let height = packet.height as usize;
if width == 0 || height == 0 {
return Vec::new();
}
let Some(pixel_count) = width.checked_mul(height) else {
return Vec::new();
};
if pixel_count > MAX_VOBSUB_IMAGE_PIXELS {
return Vec::new();
}
let Some(rgba_len) = pixel_count.checked_mul(4) else {
return Vec::new();
};
let mut rgba = vec![0u8; rgba_len];
let mut colors = [[0u8; 4]; 4];
for i in 0..4 {
let palette_color = palette.rgba[packet.color_indices[i] as usize];
let alpha = ((packet.alpha_values[i] as u32 * 255) / 15) as u8;
let bytes = palette_color.to_le_bytes();
colors[i] = [bytes[0], bytes[1], bytes[2], alpha];
}
decode_field(
packet.even_field_data(sub_data),
&mut rgba,
width,
height,
0,
&colors,
);
decode_field(
packet.odd_field_data(sub_data),
&mut rgba,
width,
height,
1,
&colors,
);
rgba
}
fn decode_field(
field_data: &[u8],
rgba: &mut [u8],
width: usize,
height: usize,
start_line: usize,
colors: &[[u8; 4]; 4],
) {
let mut byte_pos = 0;
let mut nibble_pos = 0;
let mut y = start_line;
while y < height {
let mut x = 0;
if nibble_pos != 0 {
byte_pos += 1;
nibble_pos = 0;
}
if byte_pos >= field_data.len() {
while x < width {
let pixel_offset = (y * width + x) * 4;
if pixel_offset + 4 <= rgba.len() {
rgba[pixel_offset..pixel_offset + 4].copy_from_slice(&colors[0]);
}
x += 1;
}
y += 2;
continue;
}
while x < width && byte_pos < field_data.len() {
let prev_byte_pos = byte_pos;
let prev_nibble_pos = nibble_pos;
let (color_idx, run_length, new_byte_pos, new_nibble_pos) =
read_rle_code(field_data, byte_pos, nibble_pos);
byte_pos = new_byte_pos;
nibble_pos = new_nibble_pos;
if byte_pos == prev_byte_pos && nibble_pos == prev_nibble_pos {
byte_pos += 1;
nibble_pos = 0;
while x < width {
let pixel_offset = (y * width + x) * 4;
if pixel_offset + 4 <= rgba.len() {
rgba[pixel_offset..pixel_offset + 4].copy_from_slice(&colors[0]);
}
x += 1;
}
break;
}
if run_length == 0 {
let color = &colors[color_idx];
while x < width {
let pixel_offset = (y * width + x) * 4;
if pixel_offset + 4 <= rgba.len() {
rgba[pixel_offset..pixel_offset + 4].copy_from_slice(color);
}
x += 1;
}
break;
}
let end_x = (x + run_length).min(width);
let color = &colors[color_idx];
while x < end_x {
let pixel_offset = (y * width + x) * 4;
if pixel_offset + 4 <= rgba.len() {
rgba[pixel_offset..pixel_offset + 4].copy_from_slice(color);
}
x += 1;
}
}
while x < width {
let pixel_offset = (y * width + x) * 4;
if pixel_offset + 4 <= rgba.len() {
rgba[pixel_offset..pixel_offset + 4].copy_from_slice(&colors[0]);
}
x += 1;
}
y += 2;
}
}
fn read_rle_code(
data: &[u8],
mut byte_pos: usize,
mut nibble_pos: usize,
) -> (usize, usize, usize, usize) {
let read_nibble = |data: &[u8], bp: &mut usize, np: &mut usize| -> u8 {
let nibble = if *bp >= data.len() {
0
} else {
let byte = data[*bp];
if *np == 0 {
(byte >> 4) & 0x0F
} else {
byte & 0x0F
}
};
if *np == 0 {
*np = 1;
} else {
*np = 0;
*bp += 1;
}
nibble
};
let n0 = read_nibble(data, &mut byte_pos, &mut nibble_pos) as usize;
if n0 >= 0x04 {
let run_length = n0 >> 2;
let color_idx = n0 & 0x03;
return (color_idx, run_length, byte_pos, nibble_pos);
}
let n1 = read_nibble(data, &mut byte_pos, &mut nibble_pos) as usize;
let val8 = (n0 << 4) | n1;
if val8 >= 0x10 {
let run_length = val8 >> 2;
let color_idx = val8 & 0x03;
return (color_idx, run_length, byte_pos, nibble_pos);
}
let n2 = read_nibble(data, &mut byte_pos, &mut nibble_pos) as usize;
let val12 = (val8 << 4) | n2;
if val12 >= 0x040 {
let run_length = val12 >> 2;
let color_idx = val12 & 0x03;
return (color_idx, run_length, byte_pos, nibble_pos);
}
let n3 = read_nibble(data, &mut byte_pos, &mut nibble_pos) as usize;
let val16 = (val12 << 4) | n3;
let run_length = val16 >> 2;
let color_idx = val16 & 0x03;
(color_idx, run_length, byte_pos, nibble_pos)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vobsub::{SubtitlePacketData, VobSubPalette};
#[test]
fn test_rle_code_4bit() {
let data = [0x45]; let (color, run, bp, np) = read_rle_code(&data, 0, 0);
assert_eq!(run, 1);
assert_eq!(color, 0);
assert_eq!(bp, 0);
assert_eq!(np, 1);
}
#[test]
fn test_decode_vobsub_rle_rejects_oversized_frame() {
let packet = SubtitlePacket {
timestamp_ms: 0,
duration_ms: 1000,
x: 0,
y: 0,
width: 5000,
height: 5000,
color_indices: [0, 1, 2, 3],
alpha_values: [0, 15, 15, 15],
packet_data: crate::vobsub::SubtitlePacketData::Owned(Vec::new()),
even_field_range: 0..0,
odd_field_range: 0..0,
};
assert!(decode_vobsub_rle(&packet, &[], &VobSubPalette::default()).is_empty());
}
#[test]
fn test_decode_vobsub_rle_end_of_line_uses_code_color() {
let packet = SubtitlePacket {
timestamp_ms: 0,
duration_ms: 1000,
x: 0,
y: 0,
width: 4,
height: 1,
color_indices: [0, 1, 2, 3],
alpha_values: [0, 15, 15, 15],
packet_data: SubtitlePacketData::Owned(vec![0x00, 0x02]),
even_field_range: 0..2,
odd_field_range: 0..0,
};
let mut palette = VobSubPalette::default();
palette.rgba[2] = crate::utils::rgb_to_rgba(0, 0, 255, 255);
let rgba = decode_vobsub_rle(&packet, &[], &palette);
for pixel in rgba.chunks_exact(4) {
assert_eq!(pixel, [0, 0, 255, 255]);
}
}
}