use std::collections::{HashMap, VecDeque};
use std::io::{Read, SeekFrom};
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, Error, Frame, MediaType, Packet, PixelFormat, Result,
StreamInfo, TimeBase, VideoFrame, VideoPlane,
};
use oxideav_core::{ContainerRegistry, Demuxer, ProbeData, ProbeScore, ReadSeek};
use oxideav_core::{Decoder, Encoder};
use crate::PGS_CODEC_ID;
pub const SEG_PDS: u8 = 0x14;
pub const SEG_ODS: u8 = 0x15;
pub const SEG_PCS: u8 = 0x16;
pub const SEG_WDS: u8 = 0x17;
pub const SEG_END: u8 = 0x80;
#[derive(Clone, Debug)]
pub struct RawSegment {
pub pts_90k: u32,
pub dts_90k: u32,
pub seg_type: u8,
pub body: Vec<u8>,
}
pub fn read_segment(buf: &[u8], pos: usize) -> Result<(RawSegment, usize)> {
if pos + 13 > buf.len() {
return Err(Error::NeedMore);
}
if &buf[pos..pos + 2] != b"PG" {
return Err(Error::invalid("PGS: segment missing 'PG' magic"));
}
let pts_90k = u32::from_be_bytes([buf[pos + 2], buf[pos + 3], buf[pos + 4], buf[pos + 5]]);
let dts_90k = u32::from_be_bytes([buf[pos + 6], buf[pos + 7], buf[pos + 8], buf[pos + 9]]);
let seg_type = buf[pos + 10];
let size = u16::from_be_bytes([buf[pos + 11], buf[pos + 12]]) as usize;
let end = pos + 13 + size;
if end > buf.len() {
return Err(Error::NeedMore);
}
Ok((
RawSegment {
pts_90k,
dts_90k,
seg_type,
body: buf[pos + 13..end].to_vec(),
},
end,
))
}
#[derive(Clone, Debug)]
pub struct PresentationComposition {
pub width: u16,
pub height: u16,
pub composition_number: u16,
pub objects: Vec<CompositionObject>,
}
#[derive(Clone, Debug)]
pub struct CompositionObject {
pub object_id: u16,
pub window_id: u8,
pub cropped: bool,
pub forced: bool,
pub x: u16,
pub y: u16,
}
fn parse_pcs(body: &[u8]) -> Result<PresentationComposition> {
if body.len() < 11 {
return Err(Error::invalid("PGS PCS: body too short"));
}
let width = u16::from_be_bytes([body[0], body[1]]);
let height = u16::from_be_bytes([body[2], body[3]]);
let composition_number = u16::from_be_bytes([body[5], body[6]]);
let n_objects = body[10] as usize;
let mut cur = 11;
let mut objects = Vec::with_capacity(n_objects);
for _ in 0..n_objects {
if cur + 8 > body.len() {
return Err(Error::invalid("PGS PCS: object entry truncated"));
}
let object_id = u16::from_be_bytes([body[cur], body[cur + 1]]);
let window_id = body[cur + 2];
let flags = body[cur + 3];
let cropped = (flags & 0x80) != 0;
let forced = (flags & 0x40) != 0;
let x = u16::from_be_bytes([body[cur + 4], body[cur + 5]]);
let y = u16::from_be_bytes([body[cur + 6], body[cur + 7]]);
cur += 8;
if cropped {
if cur + 8 > body.len() {
return Err(Error::invalid("PGS PCS: cropped object missing crop rect"));
}
cur += 8;
}
objects.push(CompositionObject {
object_id,
window_id,
cropped,
forced,
x,
y,
});
}
Ok(PresentationComposition {
width,
height,
composition_number,
objects,
})
}
#[derive(Clone, Debug)]
pub struct Palette {
pub entries: [[u8; 4]; 256],
}
impl Default for Palette {
fn default() -> Self {
Self {
entries: [[0u8; 4]; 256],
}
}
}
fn ycbcr_to_rgba(y: u8, cr: u8, cb: u8, a: u8) -> [u8; 4] {
let y = y as i32;
let cb = cb as i32 - 128;
let cr = cr as i32 - 128;
let r = y + ((91881 * cr) >> 16);
let g = y - ((22554 * cb + 46802 * cr) >> 16);
let b = y + ((116130 * cb) >> 16);
[
r.clamp(0, 255) as u8,
g.clamp(0, 255) as u8,
b.clamp(0, 255) as u8,
a,
]
}
fn parse_pds_into(body: &[u8], palette: &mut Palette) -> Result<()> {
if body.len() < 2 {
return Err(Error::invalid("PGS PDS: too short"));
}
let mut cur = 2;
while cur + 5 <= body.len() {
let idx = body[cur] as usize;
let y = body[cur + 1];
let cr = body[cur + 2];
let cb = body[cur + 3];
let a = body[cur + 4];
palette.entries[idx] = ycbcr_to_rgba(y, cr, cb, a);
cur += 5;
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct Object {
pub width: u16,
pub height: u16,
pub pixels: Vec<u8>,
}
fn parse_ods_into(
body: &[u8],
fragments: &mut HashMap<u16, Vec<u8>>,
objects: &mut HashMap<u16, Object>,
) -> Result<()> {
if body.len() < 4 {
return Err(Error::invalid("PGS ODS: header too short"));
}
let object_id = u16::from_be_bytes([body[0], body[1]]);
let seq_flag = body[3];
let first = (seq_flag & 0x80) != 0;
let last = (seq_flag & 0x40) != 0;
let entry = fragments.entry(object_id).or_default();
if first {
entry.clear();
}
entry.extend_from_slice(&body[4..]);
if !last {
return Ok(());
}
let full = fragments.remove(&object_id).unwrap_or_default();
if full.len() < 7 {
return Err(Error::invalid(
"PGS ODS: assembled object data shorter than header",
));
}
let width = u16::from_be_bytes([full[3], full[4]]);
let height = u16::from_be_bytes([full[5], full[6]]);
if width == 0 || height == 0 {
return Err(Error::invalid("PGS ODS: zero width/height"));
}
let rle = &full[7..];
let pixels = decode_rle(rle, width as usize, height as usize)?;
objects.insert(
object_id,
Object {
width,
height,
pixels,
},
);
Ok(())
}
pub fn decode_rle(rle: &[u8], width: usize, height: usize) -> Result<Vec<u8>> {
let mut out = vec![0u8; width * height];
let mut i = 0;
let mut row = 0usize;
let mut col = 0usize;
while i < rle.len() {
let b0 = rle[i];
i += 1;
let (run_len, colour, line_end) = if b0 != 0 {
(1usize, b0, false)
} else {
if i >= rle.len() {
return Err(Error::invalid("PGS RLE: truncated after 0x00"));
}
let b1 = rle[i];
i += 1;
if b1 == 0 {
(0usize, 0u8, true)
} else {
let hi = b1 & 0xC0;
let len_lo = (b1 & 0x3F) as usize;
let (length, colour) = match hi {
0x00 => (len_lo, 0u8),
0x40 => {
if i >= rle.len() {
return Err(Error::invalid("PGS RLE: truncated 14-bit length"));
}
let b2 = rle[i] as usize;
i += 1;
((len_lo << 8) | b2, 0u8)
}
0x80 => {
if i >= rle.len() {
return Err(Error::invalid("PGS RLE: truncated colour"));
}
let c = rle[i];
i += 1;
(len_lo, c)
}
_ => {
if i + 1 >= rle.len() {
return Err(Error::invalid("PGS RLE: truncated 14-bit+colour run"));
}
let b2 = rle[i] as usize;
let c = rle[i + 1];
i += 2;
(((len_lo << 8) | b2), c)
}
};
(length, colour, false)
}
};
if line_end {
row += 1;
col = 0;
if row > height {
return Err(Error::invalid("PGS RLE: too many lines"));
}
continue;
}
if row >= height {
return Err(Error::invalid("PGS RLE: pixel past end of bitmap"));
}
let end = col.saturating_add(run_len).min(width);
let base = row * width + col;
for px in &mut out[base..base + (end - col)] {
*px = colour;
}
col = end;
}
Ok(out)
}
#[derive(Default)]
struct DisplaySet {
pcs: Option<PresentationComposition>,
pts_90k: u32,
palette: Palette,
object_fragments: HashMap<u16, Vec<u8>>,
objects: HashMap<u16, Object>,
last_canvas: Option<(u16, u16)>,
}
impl DisplaySet {
fn push(&mut self, seg: &RawSegment) -> Result<()> {
self.pts_90k = seg.pts_90k;
match seg.seg_type {
SEG_PCS => {
let pcs = parse_pcs(&seg.body)?;
self.last_canvas = Some((pcs.width, pcs.height));
self.pcs = Some(pcs);
}
SEG_WDS if seg.body.is_empty() => {
return Err(Error::invalid("PGS WDS: empty body"));
}
SEG_PDS => {
parse_pds_into(&seg.body, &mut self.palette)?;
}
SEG_ODS => {
parse_ods_into(&seg.body, &mut self.object_fragments, &mut self.objects)?;
}
SEG_END => {}
_ => {
}
}
Ok(())
}
fn render(&self) -> Result<Option<Vec<u8>>> {
let Some(pcs) = &self.pcs else {
return Ok(None);
};
let width = pcs.width as usize;
let height = pcs.height as usize;
if width == 0 || height == 0 {
return Err(Error::invalid("PGS PCS: zero-sized canvas"));
}
let mut canvas = vec![0u8; width * height * 4];
for co in &pcs.objects {
let Some(obj) = self.objects.get(&co.object_id) else {
continue;
};
let ox = co.x as usize;
let oy = co.y as usize;
let ow = obj.width as usize;
let oh = obj.height as usize;
let rows: Vec<Vec<u8>> = (0..oh)
.map(|row| obj.pixels[row * ow..row * ow + ow].to_vec())
.collect();
let palette = &self.palette;
crate::composite::blit_indexed(&mut canvas, width, height, &rows, ox, oy, |idx| {
palette.entries[idx as usize]
});
}
Ok(Some(canvas))
}
}
pub fn register_container(reg: &mut ContainerRegistry) {
reg.register_demuxer("pgs", open_pgs);
reg.register_extension("sup", "pgs");
reg.register_probe("pgs", probe_pgs);
}
fn probe_pgs(p: &ProbeData) -> ProbeScore {
if p.buf.len() >= 13 && &p.buf[..2] == b"PG" {
100
} else if p.ext == Some("sup") {
25
} else {
0
}
}
fn open_pgs(mut input: Box<dyn ReadSeek>, _codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
let mut buf = Vec::new();
input.seek(SeekFrom::Start(0))?;
input.read_to_end(&mut buf)?;
let time_base = TimeBase::new(1, 90_000);
let mut packets: VecDeque<Packet> = VecDeque::new();
let mut cur = 0usize;
let mut ds_start_pts: Option<u32> = None;
let mut ds_buf: Vec<u8> = Vec::new();
let mut last_canvas: Option<(u16, u16)> = None;
while cur < buf.len() {
let (seg, next) = match read_segment(&buf, cur) {
Ok(x) => x,
Err(_) => break,
};
if ds_start_pts.is_none() {
ds_start_pts = Some(seg.pts_90k);
}
ds_buf.extend_from_slice(&buf[cur..next]);
if seg.seg_type == SEG_PCS {
if let Ok(pcs) = parse_pcs(&seg.body) {
last_canvas = Some((pcs.width, pcs.height));
}
}
cur = next;
if seg.seg_type == SEG_END {
let pts = ds_start_pts.take().unwrap_or(0);
let mut packet = Packet::new(0, time_base, std::mem::take(&mut ds_buf));
packet.pts = Some(pts as i64);
packet.dts = Some(pts as i64);
packet.flags.keyframe = true;
packets.push_back(packet);
}
}
for i in 0..packets.len().saturating_sub(1) {
let (Some(a), Some(b)) = (packets[i].pts, packets[i + 1].pts) else {
continue;
};
packets[i].duration = Some((b - a).max(0));
}
let (w, h) = last_canvas.unwrap_or((0, 0));
let mut params = CodecParameters::video(CodecId::new(PGS_CODEC_ID));
params.media_type = MediaType::Subtitle;
params.width = Some(w as u32);
params.height = Some(h as u32);
params.pixel_format = Some(PixelFormat::Rgba);
let total = packets.back().and_then(|p| p.pts).unwrap_or(0);
let stream = StreamInfo {
index: 0,
time_base,
duration: Some(total),
start_time: Some(0),
params,
};
Ok(Box::new(PgsDemuxer {
streams: [stream],
packets,
}))
}
struct PgsDemuxer {
streams: [StreamInfo; 1],
packets: VecDeque<Packet>,
}
impl Demuxer for PgsDemuxer {
fn format_name(&self) -> &str {
"pgs"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
self.packets.pop_front().ok_or(Error::Eof)
}
fn duration_micros(&self) -> Option<i64> {
let pts = self.streams[0].duration?;
Some(pts * 100 / 9)
}
}
pub fn make_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(PgsDecoder {
codec_id: CodecId::new(PGS_CODEC_ID),
pending: VecDeque::new(),
eof: false,
}))
}
struct PgsDecoder {
codec_id: CodecId,
pending: VecDeque<Frame>,
eof: bool,
}
impl Decoder for PgsDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
let mut ds = DisplaySet::default();
let mut cur = 0;
while cur < packet.data.len() {
let (seg, next) = read_segment(&packet.data, cur)?;
ds.push(&seg)?;
cur = next;
}
let Some(pcs) = &ds.pcs else {
return Ok(());
};
let width = pcs.width as u32;
let height = pcs.height as u32;
let rendered = ds
.render()?
.unwrap_or_else(|| vec![0u8; (width as usize) * (height as usize) * 4]);
let _ = (width, height);
let frame = VideoFrame {
pts: packet.pts,
planes: vec![VideoPlane {
stride: (pcs.width as usize) * 4,
data: rendered,
}],
};
self.pending.push_back(Frame::Video(frame));
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
if let Some(f) = self.pending.pop_front() {
return Ok(f);
}
if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
}
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.pending.clear();
self.eof = false;
Ok(())
}
}
pub fn make_encoder(_params: &CodecParameters) -> Result<Box<dyn Encoder>> {
let mut out_params = CodecParameters::video(CodecId::new(PGS_CODEC_ID));
out_params.media_type = MediaType::Subtitle;
out_params.pixel_format = Some(PixelFormat::Rgba);
Ok(Box::new(PgsEncoder {
codec_id: CodecId::new(PGS_CODEC_ID),
params: out_params,
pending: VecDeque::new(),
composition_number: 0,
}))
}
struct PgsEncoder {
codec_id: CodecId,
params: CodecParameters,
pending: VecDeque<Packet>,
composition_number: u16,
}
impl Encoder for PgsEncoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn output_params(&self) -> &CodecParameters {
&self.params
}
fn send_frame(&mut self, frame: &Frame) -> Result<()> {
let Frame::Video(v) = frame else {
return Err(Error::unsupported(
"PGS encoder only accepts Frame::Video input",
));
};
if v.planes.is_empty() {
return Err(Error::invalid("PGS encoder: frame has no plane"));
}
let plane = &v.planes[0];
if plane.stride == 0 || plane.stride % 4 != 0 {
return Err(Error::invalid(
"PGS encoder: RGBA plane stride must be a positive multiple of 4",
));
}
let width = (plane.stride / 4) as u32;
let height = if width == 0 {
0
} else {
(plane.data.len() / plane.stride) as u32
};
if width == 0 || height == 0 {
return Err(Error::invalid("PGS encoder: zero-sized frame"));
}
if self.params.width.is_none() {
self.params.width = Some(width);
self.params.height = Some(height);
}
let composition_number = self.composition_number;
self.composition_number = self.composition_number.wrapping_add(1);
let pts_90k = frame_pts_90k(v).unwrap_or(0);
let bbox = tight_bbox(v, width as usize, height as usize);
let bytes = match bbox {
None => {
encode_erase_display_set(width as u16, height as u16, composition_number, pts_90k)
}
Some((bx, by, bw, bh)) => {
let (indices, palette_rgba) =
quantise_rgba_region(v, width as usize, bx, by, bw, bh)?;
encode_display_set(
width as u16,
height as u16,
bx as u16,
by as u16,
bw as u16,
bh as u16,
composition_number,
pts_90k,
&palette_rgba,
&indices,
)
}
};
let mut packet = Packet::new(0, TimeBase::new(1, 90_000), bytes);
packet.pts = Some(pts_90k as i64);
packet.dts = Some(pts_90k as i64);
packet.flags.keyframe = true;
self.pending.push_back(packet);
Ok(())
}
fn receive_packet(&mut self) -> Result<Packet> {
self.pending.pop_front().ok_or(Error::NeedMore)
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
}
fn frame_pts_90k(v: &VideoFrame) -> Option<u32> {
let pts = v.pts?;
let scaled = TimeBase::new(1, 1_000_000).rescale(pts, TimeBase::new(1, 90_000));
if scaled < 0 {
Some(0)
} else {
Some(scaled as u32)
}
}
fn tight_bbox(v: &VideoFrame, width: usize, height: usize) -> Option<(usize, usize, usize, usize)> {
if width == 0 || height == 0 || v.planes.is_empty() {
return None;
}
let plane = &v.planes[0];
let needed = width * 4;
if plane.stride < needed {
return None;
}
let mut min_x = width;
let mut max_x: isize = -1;
let mut min_y = height;
let mut max_y: isize = -1;
for row in 0..height {
let line = &plane.data[row * plane.stride..row * plane.stride + needed];
let mut row_has = false;
let mut row_min = width;
let mut row_max: isize = -1;
for col in 0..width {
if line[col * 4 + 3] != 0 {
if !row_has {
row_min = col;
row_has = true;
}
row_max = col as isize;
}
}
if row_has {
if (row as isize) < min_y as isize {
min_y = row;
}
if (row as isize) > max_y {
max_y = row as isize;
}
if row_min < min_x {
min_x = row_min;
}
if row_max > max_x {
max_x = row_max;
}
}
}
if max_x < 0 || max_y < 0 {
return None;
}
let bw = (max_x as usize - min_x) + 1;
let bh = (max_y as usize - min_y) + 1;
Some((min_x, min_y, bw, bh))
}
fn quantise_rgba_region(
v: &VideoFrame,
frame_width: usize,
bx: usize,
by: usize,
bw: usize,
bh: usize,
) -> Result<(Vec<u8>, Vec<[u8; 4]>)> {
if v.planes.is_empty() {
return Err(Error::invalid("PGS encoder: RGBA frame has no plane"));
}
let plane = &v.planes[0];
let needed = frame_width * 4;
if plane.stride < needed {
return Err(Error::invalid("PGS encoder: RGBA stride too small"));
}
if bx + bw > frame_width || by * plane.stride + needed > plane.data.len() {
return Err(Error::invalid("PGS encoder: bbox out of frame"));
}
let mut palette: Vec<[u8; 4]> = Vec::with_capacity(256);
palette.push([0, 0, 0, 0]);
let mut map: HashMap<[u8; 4], u8> = HashMap::new();
map.insert([0, 0, 0, 0], 0);
let mut indices = vec![0u8; bw * bh];
let mut quantise_harder = false;
'scan: for row in 0..bh {
let src_row = by + row;
let line = &plane.data[src_row * plane.stride..src_row * plane.stride + needed];
for col in 0..bw {
let src_col = bx + col;
let px = &line[src_col * 4..src_col * 4 + 4];
let key = if px[3] == 0 {
[0, 0, 0, 0]
} else {
[px[0], px[1], px[2], px[3]]
};
if let Some(&idx) = map.get(&key) {
indices[row * bw + col] = idx;
continue;
}
if palette.len() >= 255 {
quantise_harder = true;
break 'scan;
}
let idx = palette.len() as u8;
palette.push(key);
map.insert(key, idx);
indices[row * bw + col] = idx;
}
}
if quantise_harder {
return quantise_rgba_332_region(v, frame_width, bx, by, bw, bh);
}
Ok((indices, palette))
}
fn quantise_rgba_332_region(
v: &VideoFrame,
frame_width: usize,
bx: usize,
by: usize,
bw: usize,
bh: usize,
) -> Result<(Vec<u8>, Vec<[u8; 4]>)> {
let plane = &v.planes[0];
let needed = frame_width * 4;
if plane.stride < needed {
return Err(Error::invalid("PGS encoder: RGBA stride too small"));
}
if bx + bw > frame_width {
return Err(Error::invalid("PGS encoder: bbox out of frame"));
}
let mut palette: Vec<[u8; 4]> = Vec::with_capacity(256);
palette.push([0, 0, 0, 0]);
let mut map: HashMap<[u8; 4], u8> = HashMap::new();
map.insert([0, 0, 0, 0], 0);
let mut indices = vec![0u8; bw * bh];
for row in 0..bh {
let src_row = by + row;
let line = &plane.data[src_row * plane.stride..src_row * plane.stride + needed];
for col in 0..bw {
let src_col = bx + col;
let px = &line[src_col * 4..src_col * 4 + 4];
if px[3] == 0 {
indices[row * bw + col] = 0;
continue;
}
let r = px[0] & 0xE0;
let g = px[1] & 0xE0;
let b = px[2] & 0xC0;
let a = match px[3] {
0..=63 => 0x3F,
64..=127 => 0x7F,
128..=191 => 0xBF,
_ => 0xFF,
};
let key = [r, g, b, a];
if let Some(&idx) = map.get(&key) {
indices[row * bw + col] = idx;
continue;
}
let idx = palette.len() as u8;
palette.push(key);
map.insert(key, idx);
indices[row * bw + col] = idx;
if palette.len() == 256 {
for row2 in row..bh {
let src_row2 = by + row2;
let line2 =
&plane.data[src_row2 * plane.stride..src_row2 * plane.stride + needed];
let start_col = if row2 == row { col + 1 } else { 0 };
for col2 in start_col..bw {
let src_col2 = bx + col2;
let px2 = &line2[src_col2 * 4..src_col2 * 4 + 4];
let key2 = if px2[3] == 0 {
[0, 0, 0, 0]
} else {
let r = px2[0] & 0xE0;
let g = px2[1] & 0xE0;
let b = px2[2] & 0xC0;
let a = match px2[3] {
0..=63 => 0x3F,
64..=127 => 0x7F,
128..=191 => 0xBF,
_ => 0xFF,
};
[r, g, b, a]
};
indices[row2 * bw + col2] = *map
.get(&key2)
.unwrap_or(&nearest_palette_entry(&palette, key2));
}
}
return Ok((indices, palette));
}
}
}
Ok((indices, palette))
}
fn nearest_palette_entry(palette: &[[u8; 4]], key: [u8; 4]) -> u8 {
let mut best = 0u8;
let mut best_d = i32::MAX;
for (i, entry) in palette.iter().enumerate() {
let dr = entry[0] as i32 - key[0] as i32;
let dg = entry[1] as i32 - key[1] as i32;
let db = entry[2] as i32 - key[2] as i32;
let da = entry[3] as i32 - key[3] as i32;
let d = dr * dr + dg * dg + db * db + da * da;
if d < best_d {
best_d = d;
best = i as u8;
}
}
best
}
#[allow(clippy::too_many_arguments)]
fn encode_display_set(
canvas_w: u16,
canvas_h: u16,
obj_x: u16,
obj_y: u16,
obj_w: u16,
obj_h: u16,
composition_number: u16,
pts_90k: u32,
palette: &[[u8; 4]],
indices: &[u8],
) -> Vec<u8> {
let mut out = Vec::new();
let mut pcs = Vec::new();
pcs.extend_from_slice(&canvas_w.to_be_bytes());
pcs.extend_from_slice(&canvas_h.to_be_bytes());
pcs.push(0x10); pcs.extend_from_slice(&composition_number.to_be_bytes());
pcs.push(0x80); pcs.push(0); pcs.push(0); pcs.push(1); pcs.extend_from_slice(&1u16.to_be_bytes()); pcs.push(0); pcs.push(0); pcs.extend_from_slice(&obj_x.to_be_bytes()); pcs.extend_from_slice(&obj_y.to_be_bytes()); push_segment(&mut out, pts_90k, SEG_PCS, &pcs);
let win_w = obj_w.min(canvas_w.saturating_sub(obj_x));
let win_h = obj_h.min(canvas_h.saturating_sub(obj_y));
let mut wds = Vec::new();
wds.push(1); wds.push(0); wds.extend_from_slice(&obj_x.to_be_bytes()); wds.extend_from_slice(&obj_y.to_be_bytes()); wds.extend_from_slice(&win_w.to_be_bytes());
wds.extend_from_slice(&win_h.to_be_bytes());
push_segment(&mut out, pts_90k, SEG_WDS, &wds);
let mut pds = Vec::new();
pds.push(0); pds.push(0); for (idx, rgba) in palette.iter().enumerate() {
if idx >= 256 {
break;
}
if rgba[3] == 0 {
pds.push(idx as u8);
pds.push(0);
pds.push(0x80);
pds.push(0x80);
pds.push(0);
continue;
}
let (y, cb, cr) = rgb_to_ycbcr_bt601(rgba[0], rgba[1], rgba[2]);
pds.push(idx as u8);
pds.push(y);
pds.push(cr);
pds.push(cb);
pds.push(rgba[3]);
}
push_segment(&mut out, pts_90k, SEG_PDS, &pds);
let rle = encode_rle(indices, obj_w as usize, obj_h as usize);
let mut ods = Vec::new();
ods.extend_from_slice(&1u16.to_be_bytes()); ods.push(0); ods.push(0xC0); let obj_data_len = (rle.len() + 4) as u32; ods.push(((obj_data_len >> 16) & 0xFF) as u8);
ods.push(((obj_data_len >> 8) & 0xFF) as u8);
ods.push((obj_data_len & 0xFF) as u8);
ods.extend_from_slice(&obj_w.to_be_bytes());
ods.extend_from_slice(&obj_h.to_be_bytes());
ods.extend_from_slice(&rle);
push_segment(&mut out, pts_90k, SEG_ODS, &ods);
push_segment(&mut out, pts_90k, SEG_END, &[]);
out
}
fn encode_erase_display_set(
canvas_w: u16,
canvas_h: u16,
composition_number: u16,
pts_90k: u32,
) -> Vec<u8> {
let mut out = Vec::new();
let mut pcs = Vec::new();
pcs.extend_from_slice(&canvas_w.to_be_bytes());
pcs.extend_from_slice(&canvas_h.to_be_bytes());
pcs.push(0x10); pcs.extend_from_slice(&composition_number.to_be_bytes());
pcs.push(0x80); pcs.push(0); pcs.push(0); pcs.push(0); push_segment(&mut out, pts_90k, SEG_PCS, &pcs);
let wds = vec![0u8]; push_segment(&mut out, pts_90k, SEG_WDS, &wds);
push_segment(&mut out, pts_90k, SEG_END, &[]);
out
}
fn push_segment(out: &mut Vec<u8>, pts_90k: u32, seg_type: u8, body: &[u8]) {
out.extend_from_slice(b"PG");
out.extend_from_slice(&pts_90k.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.push(seg_type);
out.extend_from_slice(&(body.len() as u16).to_be_bytes());
out.extend_from_slice(body);
}
fn rgb_to_ycbcr_bt601(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
let r = r as i32;
let g = g as i32;
let b = b as i32;
let y = ((77 * r + 150 * g + 29 * b + 128) >> 8) as u8;
let cb = (((-43 * r - 84 * g + 127 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
let cr = (((127 * r - 106 * g - 21 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
(y, cb, cr)
}
pub fn encode_rle(pixels: &[u8], width: usize, height: usize) -> Vec<u8> {
debug_assert_eq!(pixels.len(), width * height);
let mut out = Vec::with_capacity(pixels.len() + height * 2);
for row in 0..height {
let mut col = 0usize;
while col < width {
let colour = pixels[row * width + col];
let mut run = 1usize;
while col + run < width && pixels[row * width + col + run] == colour && run < 0x3FFF {
run += 1;
}
emit_run(&mut out, run, colour);
col += run;
}
out.push(0);
out.push(0);
}
out
}
fn emit_run(out: &mut Vec<u8>, run: usize, colour: u8) {
if colour == 0 {
if run < 64 {
out.push(0);
out.push(run as u8); } else {
out.push(0);
out.push(0x40 | ((run >> 8) & 0x3F) as u8);
out.push((run & 0xFF) as u8);
}
return;
}
if run == 1 {
out.push(colour);
return;
}
if run < 4 {
for _ in 0..run {
out.push(colour);
}
return;
}
if run < 64 {
out.push(0);
out.push(0x80 | (run as u8 & 0x3F));
out.push(colour);
} else {
out.push(0);
out.push(0xC0 | ((run >> 8) & 0x3F) as u8);
out.push((run & 0xFF) as u8);
out.push(colour);
}
}
#[doc(hidden)]
pub fn build_demo_display_set(
canvas: (u16, u16),
object: (u16, u16),
position: (u16, u16),
palette: &[(u8, [u8; 4])],
pixels: &[u8],
) -> Vec<u8> {
fn segment(out: &mut Vec<u8>, pts_90k: u32, seg_type: u8, body: &[u8]) {
out.extend_from_slice(b"PG");
out.extend_from_slice(&pts_90k.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.push(seg_type);
out.extend_from_slice(&(body.len() as u16).to_be_bytes());
out.extend_from_slice(body);
}
let mut out = Vec::new();
let mut pcs = Vec::new();
pcs.extend_from_slice(&canvas.0.to_be_bytes());
pcs.extend_from_slice(&canvas.1.to_be_bytes());
pcs.push(0); pcs.extend_from_slice(&1u16.to_be_bytes()); pcs.push(0); pcs.push(0); pcs.push(0); pcs.push(1); pcs.extend_from_slice(&1u16.to_be_bytes()); pcs.push(0); pcs.push(0); pcs.extend_from_slice(&position.0.to_be_bytes());
pcs.extend_from_slice(&position.1.to_be_bytes());
segment(&mut out, 0, SEG_PCS, &pcs);
let mut wds = Vec::new();
wds.push(1); wds.push(0); wds.extend_from_slice(&0u16.to_be_bytes()); wds.extend_from_slice(&0u16.to_be_bytes()); wds.extend_from_slice(&canvas.0.to_be_bytes());
wds.extend_from_slice(&canvas.1.to_be_bytes());
segment(&mut out, 0, SEG_WDS, &wds);
let mut pds = Vec::new();
pds.push(0); pds.push(0); for (idx, rgba) in palette {
let r = rgba[0] as i32;
let g = rgba[1] as i32;
let b = rgba[2] as i32;
let y = ((77 * r + 150 * g + 29 * b + 128) >> 8) as u8;
let cb = (((-43 * r - 84 * g + 127 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
let cr = (((127 * r - 106 * g - 21 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
pds.push(*idx);
pds.push(y);
pds.push(cr);
pds.push(cb);
pds.push(rgba[3]);
}
segment(&mut out, 0, SEG_PDS, &pds);
let rle = demo_rle(object, pixels);
let mut ods = Vec::new();
ods.extend_from_slice(&1u16.to_be_bytes()); ods.push(0); ods.push(0xC0); let obj_data_len = (rle.len() + 4) as u32; ods.push(((obj_data_len >> 16) & 0xFF) as u8);
ods.push(((obj_data_len >> 8) & 0xFF) as u8);
ods.push((obj_data_len & 0xFF) as u8);
ods.extend_from_slice(&object.0.to_be_bytes());
ods.extend_from_slice(&object.1.to_be_bytes());
ods.extend_from_slice(&rle);
segment(&mut out, 0, SEG_ODS, &ods);
segment(&mut out, 0, SEG_END, &[]);
out
}
fn demo_rle(object: (u16, u16), pixels: &[u8]) -> Vec<u8> {
let w = object.0 as usize;
let h = object.1 as usize;
let mut rle = Vec::new();
for row in 0..h {
for c in 0..w {
let p = pixels[row * w + c];
if p == 0 {
rle.push(0);
rle.push(1);
} else {
rle.push(p);
}
}
rle.push(0);
rle.push(0);
}
rle
}
#[doc(hidden)]
pub fn build_demo_display_set_fragmented(
canvas: (u16, u16),
object: (u16, u16),
position: (u16, u16),
palette: &[(u8, [u8; 4])],
pixels: &[u8],
fragments: usize,
) -> Vec<u8> {
fn segment(out: &mut Vec<u8>, pts_90k: u32, seg_type: u8, body: &[u8]) {
out.extend_from_slice(b"PG");
out.extend_from_slice(&pts_90k.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
out.push(seg_type);
out.extend_from_slice(&(body.len() as u16).to_be_bytes());
out.extend_from_slice(body);
}
let fragments = fragments.max(1);
let canonical = build_demo_display_set(canvas, object, position, palette, pixels);
let mut out = Vec::new();
let mut cur = 0;
while cur < canonical.len() {
let (seg, next) = read_segment(&canonical, cur).expect("demo set is well-formed");
if seg.seg_type == SEG_ODS || seg.seg_type == SEG_END {
break;
}
out.extend_from_slice(&canonical[cur..next]);
cur = next;
}
let rle = demo_rle(object, pixels);
let obj_data_len = (rle.len() + 4) as u32; let mut payload = Vec::new();
payload.push(((obj_data_len >> 16) & 0xFF) as u8);
payload.push(((obj_data_len >> 8) & 0xFF) as u8);
payload.push((obj_data_len & 0xFF) as u8);
payload.extend_from_slice(&object.0.to_be_bytes());
payload.extend_from_slice(&object.1.to_be_bytes());
payload.extend_from_slice(&rle);
let chunk_len = payload.len().div_ceil(fragments).max(1);
let mut offset = 0;
let mut idx = 0;
while offset < payload.len() || (idx == 0 && payload.is_empty()) {
let end = (offset + chunk_len).min(payload.len());
let chunk = &payload[offset..end];
let is_first = idx == 0;
let is_last = end >= payload.len();
let mut flag = 0u8;
if is_first {
flag |= 0x80;
}
if is_last {
flag |= 0x40;
}
let mut ods = Vec::new();
ods.extend_from_slice(&1u16.to_be_bytes()); ods.push(0); ods.push(flag);
ods.extend_from_slice(chunk);
segment(&mut out, 0, SEG_ODS, &ods);
offset = end;
idx += 1;
if is_last {
break;
}
}
segment(&mut out, 0, SEG_END, &[]);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rle_round_trip_simple() {
let rle: &[u8] = &[0x01, 0x01, 0x00, 0x02, 0x00, 0x00];
let px = decode_rle(rle, 4, 1).unwrap();
assert_eq!(px, vec![1, 1, 0, 0]);
}
fn lcg(state: &mut u64) -> u32 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(*state >> 32) as u32
}
#[test]
fn rle_roundtrip_random_widths_and_palettes() {
let mut st = 0x9e37_79b9_7f4a_7c15u64;
let mut sampled = 0usize;
for _ in 0..1_500 {
let width = ((lcg(&mut st) % 31) + 1) as usize; let height = ((lcg(&mut st) % 17) + 1) as usize; let palette_size = ((lcg(&mut st) % 12) + 1) as u8; let mut pixels = vec![0u8; width * height];
for px in &mut pixels {
*px = (lcg(&mut st) as u8) % palette_size;
}
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap_or_else(|e| {
panic!(
"decode failed on roundtrip w={width} h={height} palette={palette_size}: {e:?}"
)
});
assert_eq!(
back, pixels,
"round-trip diverged at w={width} h={height} palette={palette_size}"
);
sampled += 1;
}
assert_eq!(sampled, 1_500);
}
#[test]
fn rle_roundtrip_long_runs() {
for colour in [0u8, 1u8, 42u8] {
for width in [64usize, 100, 256, 600, 1024] {
let height = 3usize;
let pixels = vec![colour; width * height];
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap();
assert_eq!(
back, pixels,
"long-run roundtrip diverged at colour={colour} width={width}"
);
}
}
}
#[test]
fn rle_roundtrip_alternating_short_runs() {
let width = 16usize;
let height = 4usize;
let mut pixels = vec![0u8; width * height];
for (i, px) in pixels.iter_mut().enumerate() {
*px = if i % 2 == 0 { 0 } else { 7 };
}
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap();
assert_eq!(back, pixels);
}
#[test]
fn rle_roundtrip_one_pixel_rows() {
for height in [1usize, 2, 5, 11] {
for colour in [0u8, 1u8, 250u8] {
let pixels = vec![colour; height];
let rle = encode_rle(&pixels, 1, height);
let back = decode_rle(&rle, 1, height).unwrap();
assert_eq!(back, pixels, "1×{height} colour={colour}");
}
}
}
#[test]
fn rle_roundtrip_mixed_run_lengths_in_one_row() {
let mut pixels: Vec<u8> = Vec::new();
pixels.push(9); pixels.extend(std::iter::repeat(5).take(3)); pixels.extend(std::iter::repeat(6).take(5)); pixels.extend(std::iter::repeat(7).take(70)); pixels.extend(std::iter::repeat(0).take(80)); pixels.extend(std::iter::repeat(0).take(200)); let width = pixels.len();
let height = 1usize;
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap();
assert_eq!(back, pixels);
}
#[test]
fn rle_size_shrinks_for_long_uniform_runs() {
let width = 1000usize;
let pixels = vec![3u8; width];
let rle = encode_rle(&pixels, width, 1);
assert!(
rle.len() < 10,
"long-run encoding too verbose: {} bytes for 1000-wide flat row",
rle.len()
);
}
#[test]
fn rle_truncated_escape_returns_invalid_not_panic() {
let err = decode_rle(&[0x00], 4, 1);
assert!(err.is_err(), "truncated escape must error: {err:?}");
}
#[test]
fn rle_truncated_14bit_length_returns_invalid_not_panic() {
let err = decode_rle(&[0x00, 0x40], 4, 1);
assert!(err.is_err(), "truncated 14-bit length must error: {err:?}");
}
#[test]
fn rle_truncated_short_colour_run_returns_invalid_not_panic() {
let err = decode_rle(&[0x00, 0x82], 4, 1);
assert!(
err.is_err(),
"truncated short colour run must error: {err:?}"
);
}
#[test]
fn rle_truncated_long_colour_run_returns_invalid_not_panic() {
let err = decode_rle(&[0x00, 0xC0], 4, 1);
assert!(
err.is_err(),
"truncated 14-bit colour run must error: {err:?}"
);
let err = decode_rle(&[0x00, 0xC0, 0xFF], 4, 1);
assert!(
err.is_err(),
"14-bit colour run missing colour byte must error: {err:?}"
);
}
#[test]
fn rle_overlong_run_clamps_to_row_without_panic() {
let rle: &[u8] = &[0x00, 0xC0, 0x64, 0x05, 0x00, 0x00];
let px = decode_rle(rle, 4, 1).unwrap();
assert_eq!(px, vec![5, 5, 5, 5]);
}
#[test]
fn rle_too_many_lines_returns_invalid_not_panic() {
let rle: &[u8] = &[0x00, 0x00, 0x00, 0x00];
let err = decode_rle(rle, 1, 1);
assert!(err.is_err(), "extra EOL must error: {err:?}");
}
#[test]
fn rle_pixel_past_end_returns_invalid_not_panic() {
let rle: &[u8] = &[0x01, 0x00, 0x00, 0x02];
let err = decode_rle(rle, 1, 1);
assert!(err.is_err(), "pixel past EOL must error: {err:?}");
}
#[test]
fn rle_random_garbage_never_panics() {
let mut st = 0xfeed_face_dead_beefu64;
for _ in 0..400 {
let len = (lcg(&mut st) % 128) as usize;
let mut bytes = vec![0u8; len];
for b in &mut bytes {
*b = lcg(&mut st) as u8;
}
let width = ((lcg(&mut st) % 17) + 1) as usize;
let height = ((lcg(&mut st) % 9) + 1) as usize;
let _ = decode_rle(&bytes, width, height); }
}
#[test]
fn rle_roundtrip_all_one_colour_no_zero() {
let width = 200usize;
let height = 2usize;
let pixels = vec![42u8; width * height];
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap();
assert_eq!(back, pixels);
assert!(
rle.len() <= 16,
"uniform-row encoding too verbose: {} bytes",
rle.len()
);
}
#[test]
fn rle_roundtrip_all_transparent_no_colour() {
let width = 300usize;
let height = 4usize;
let pixels = vec![0u8; width * height];
let rle = encode_rle(&pixels, width, height);
let back = decode_rle(&rle, width, height).unwrap();
assert_eq!(back, pixels);
assert!(
rle.len() <= 28,
"transparent-row encoding too verbose: {} bytes",
rle.len()
);
}
#[test]
fn decodes_tiny_display_set() {
let pixels = [1u8, 1, 2, 3];
let palette = [
(0u8, [0u8, 0, 0, 0]), (1u8, [255u8, 0, 0, 255]),
(2u8, [0u8, 255, 0, 255]),
(3u8, [0u8, 0, 255, 255]),
];
let blob = build_demo_display_set((2, 2), (2, 2), (0, 0), &palette, &pixels);
let mut dec = make_decoder(&CodecParameters::video(CodecId::new(PGS_CODEC_ID))).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 90_000), blob).with_pts(0);
dec.send_packet(&pkt).unwrap();
let frame = dec.receive_frame().unwrap();
let Frame::Video(v) = frame else {
panic!("expected video frame");
};
assert_eq!(v.planes[0].stride, 2 * 4);
let data = &v.planes[0].data;
assert_eq!(data.len(), 2 * 2 * 4);
let r0c0 = &data[0..4];
let r0c1 = &data[4..8];
assert!(
r0c0[0] > 200 && r0c0[3] == 255,
"not red-dominant: {:?}",
r0c0
);
assert!(
r0c1[0] > 200 && r0c1[3] == 255,
"not red-dominant: {:?}",
r0c1
);
let g = &data[8..12];
let b = &data[12..16];
assert!(
g[1] > g[0] && g[1] > g[2],
"green pixel not dominant: {:?}",
g
);
assert!(
b[2] > b[0] && b[2] > b[1],
"blue pixel not dominant: {:?}",
b
);
}
fn decode_one(blob: Vec<u8>) -> Vec<u8> {
let mut dec = make_decoder(&CodecParameters::video(CodecId::new(PGS_CODEC_ID))).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 90_000), blob).with_pts(0);
dec.send_packet(&pkt).unwrap();
let Frame::Video(v) = dec.receive_frame().unwrap() else {
panic!("expected video frame");
};
v.planes[0].data.clone()
}
#[test]
fn fragmented_ods_matches_single_segment() {
let pixels = [1u8, 1, 2, 3, 0, 1, 2, 3, 3, 2, 1, 0]; let palette = [
(0u8, [0u8, 0, 0, 0]),
(1u8, [255u8, 0, 0, 255]),
(2u8, [0u8, 255, 0, 255]),
(3u8, [0u8, 0, 255, 255]),
];
let canvas = (4u16, 3u16);
let object = (4u16, 3u16);
let single = decode_one(build_demo_display_set(
canvas,
object,
(0, 0),
&palette,
&pixels,
));
for n in 2..=7usize {
let multi = decode_one(build_demo_display_set_fragmented(
canvas,
object,
(0, 0),
&palette,
&pixels,
n,
));
assert_eq!(
multi, single,
"fragmented ODS ({n} segments) decoded differently from single-segment"
);
}
}
#[test]
fn fragmented_ods_split_inside_header() {
let pixels = [1u8];
let palette = [(0u8, [0u8, 0, 0, 0]), (1u8, [10u8, 200, 30, 255])];
let blob = build_demo_display_set_fragmented((1, 1), (1, 1), (0, 0), &palette, &pixels, 32);
let data = decode_one(blob);
assert_eq!(data.len(), 4);
assert!(
data[1] > data[0] && data[1] > data[2] && data[3] == 255,
"expected opaque green-dominant pixel, got {:?}",
data
);
}
#[test]
fn incomplete_object_without_last_fragment_is_dropped_not_rendered() {
let pixels = [1u8, 1, 1, 1]; let palette = [(0u8, [0u8, 0, 0, 0]), (1u8, [200u8, 0, 0, 255])];
let frag2 = build_demo_display_set_fragmented((2, 2), (2, 2), (0, 0), &palette, &pixels, 2);
let mut truncated = Vec::new();
let mut cur = 0;
let mut seen_first_ods = false;
while cur < frag2.len() {
let (seg, next) = read_segment(&frag2, cur).unwrap();
if seg.seg_type == SEG_END {
break;
}
truncated.extend_from_slice(&frag2[cur..next]);
cur = next;
if seg.seg_type == SEG_ODS {
seen_first_ods = true;
break;
}
}
assert!(seen_first_ods, "expected to capture the first ODS fragment");
truncated.extend_from_slice(b"PG");
truncated.extend_from_slice(&0u32.to_be_bytes());
truncated.extend_from_slice(&0u32.to_be_bytes());
truncated.push(SEG_END);
truncated.extend_from_slice(&0u16.to_be_bytes());
let data = decode_one(truncated);
assert_eq!(data.len(), 2 * 2 * 4);
for chunk in data.chunks(4) {
assert_eq!(
chunk,
&[0, 0, 0, 0],
"incomplete object must not paint any pixel"
);
}
}
}