diff --git a/src/lib_ccx/ccx_decoders_isdb.c b/src/lib_ccx/ccx_decoders_isdb.c index 5a54340df..9eff1d00a 100644 --- a/src/lib_ccx/ccx_decoders_isdb.c +++ b/src/lib_ccx/ccx_decoders_isdb.c @@ -281,6 +281,10 @@ typedef struct */ void delete_isdb_decoder(void **isdb_ctx) { +#ifndef DISABLE_RUST + ccxr_delete_isdb_decoder(isdb_ctx); + return; +#endif ISDBSubContext *ctx = *isdb_ctx; struct ISDBText *text = NULL; struct ISDBText *text1 = NULL; @@ -312,6 +316,9 @@ static void init_layout(ISDBSubLayout *ls) void *init_isdb_decoder(void) { +#ifndef DISABLE_RUST + return ccxr_init_isdb_decoder(); +#endif ISDBSubContext *ctx; ctx = malloc(sizeof(ISDBSubContext)); @@ -1349,7 +1356,7 @@ int isdb_parse_data_group(void *codec_ctx, const uint8_t *buf, struct cc_subtitl { isdb_log("ISDB group A\n"); } - else if ((id >> 4) == 0) + else if ((id >> 4) == 0) // TODO : both branches have same condition? { isdb_log("ISDB group B\n"); } @@ -1403,6 +1410,9 @@ int isdb_parse_data_group(void *codec_ctx, const uint8_t *buf, struct cc_subtitl int isdbsub_decode(struct lib_cc_decode *dec_ctx, const uint8_t *buf, size_t buf_size, struct cc_subtitle *sub) { +#ifndef DISABLE_RUST + return ccxr_isdbsub_decode(dec_ctx, buf, buf_size, sub); +#endif const uint8_t *header_end = NULL; int ret = 0; ISDBSubContext *ctx = dec_ctx->private_data; @@ -1430,6 +1440,9 @@ int isdbsub_decode(struct lib_cc_decode *dec_ctx, const uint8_t *buf, size_t buf } int isdb_set_global_time(struct lib_cc_decode *dec_ctx, uint64_t timestamp) { +#ifndef DISABLE_RUST + return ccxr_isdb_set_global_time(dec_ctx->private_data, timestamp); +#endif ISDBSubContext *ctx = dec_ctx->private_data; ctx->timestamp = timestamp; return CCX_OK; diff --git a/src/lib_ccx/ccx_decoders_isdb.h b/src/lib_ccx/ccx_decoders_isdb.h index 0c9c46b54..43ff5d6f3 100644 --- a/src/lib_ccx/ccx_decoders_isdb.h +++ b/src/lib_ccx/ccx_decoders_isdb.h @@ -9,4 +9,11 @@ int isdbsub_decode(struct lib_cc_decode *dec_ctx, const uint8_t *buf, size_t buf void delete_isdb_decoder(void **isdb_ctx); void *init_isdb_decoder(void); +#ifndef DISABLE_RUST +extern void *ccxr_init_isdb_decoder(void); +extern void ccxr_delete_isdb_decoder(void **ctx); +extern int ccxr_isdb_set_global_time(void *ctx, uint64_t timestamp); +extern int ccxr_isdbsub_decode(struct lib_cc_decode *dec_ctx, const uint8_t *buf, size_t buf_size, struct cc_subtitle *sub); +#endif + #endif diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index 4a07a4f0d..3ada881fd 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -21,6 +21,8 @@ #include "avc_functions.h" #include "teletext.h" +#include "ccx_decoders_isdb.h" + #ifdef WITH_LIBCURL #include #endif diff --git a/src/rust/src/isdb/high.rs b/src/rust/src/isdb/high.rs new file mode 100644 index 000000000..d13098d1c --- /dev/null +++ b/src/rust/src/isdb/high.rs @@ -0,0 +1,495 @@ +//! High-level parsers for ISDB subtitle data groups and caption statements. +//! +//! Entry points for parsing PES-level ISDB subtitle data, routing through +//! data groups, caption statements, data units, and statement bodies. +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_isdb.c) | Rust | +//! |--------------------------------------|------------------------------------------| +//! | `parse_statement` | [`parse_statement`] | +//! | `parse_data_unit` | [`parse_data_unit`] | +//! | `parse_caption_statement_data` | [`parse_caption_statement_data`] | +//! | `isdb_parse_data_group` | [`isdb_parse_data_group`] | + +use crate::isdb::leaf::{parse_caption_management_data, rb16, rb24}; +use crate::isdb::mid::{append_char, get_text, parse_command}; +use crate::isdb::types::{IsdbSubContext, IsdbSubtitleData}; + +/// Parse a statement body; dispatches bytes as commands or characters +/// Return: 0 on success, -1 on error +pub fn parse_statement(ctx: &mut IsdbSubContext, buf: &[u8], size: usize) -> i32 { + let mut pos: usize = 0; + let end = size.min(buf.len()); + + while pos < end { + let code = (buf[pos] & 0xf0) >> 4; + let code_lo = buf[pos] & 0x0f; + + let ret: usize; + + if code <= 0x1 { + // Control codes C0/C1 + ret = parse_command(ctx, &buf[pos..]); + } else if code == 0x2 && code_lo == 0x0 { + // Special case: SP (space) + append_char(ctx, buf[pos]); + ret = 1; + } else if code == 0x7 && code_lo == 0xF { + // Special case: DEL + // TODO: DEL should have block in fg color + append_char(ctx, buf[pos]); + ret = 1; + } else if code <= 0x7 { + // GL area — printable character + append_char(ctx, buf[pos]); + ret = 1; + } else if code <= 0x9 { + // Control codes C0/C1 extended + ret = parse_command(ctx, &buf[pos..]); + } else if code == 0xA && code_lo == 0x0 { + // Special case: 10/0 + // TODO handle + ret = 1; + } else if code == 0xF && code_lo == 0xF { + // Special case: 15/15 + // TODO handle + ret = 1; + } else { + // GR area — printable character + append_char(ctx, buf[pos]); + ret = 1; + } + + if ret == 0 { + break; + } + pos += ret; + } + 0 +} + +/// Parse a data unit +/// Returns: 0 on success, -1 on error +pub fn parse_data_unit(ctx: &mut IsdbSubContext, buf: &[u8]) -> i32 { + if buf.len() < 5 { + return -1; + } + + // Skip unit separator (1 byte) + let unit_parameter = buf[1]; + let len = rb24(&buf[2..]) as usize; + + if unit_parameter == 0x20 { + if buf.len() >= 5 + len { + parse_statement(ctx, &buf[5..], len); + } else { + parse_statement(ctx, &buf[5..], buf.len() - 5); + } + } + 0 +} + +/// Parse caption statement data +/// Returns: IsdbSubtitleData if text was produced, else None +pub fn parse_caption_statement_data( + ctx: &mut IsdbSubContext, + buf: &[u8], +) -> Option { + if buf.is_empty() { + return None; + } + + let mut pos: usize = 0; + + let tmd = buf[pos] >> 6; + pos += 1; + + // Skip timing data + if tmd == 1 || tmd == 2 { + pos += 5; + } + + if pos + 3 > buf.len() { + return None; + } + + pos += 3; + + if pos >= buf.len() { + return None; + } + + let ret = parse_data_unit(ctx, &buf[pos..]); + if ret < 0 { + return None; + } + + // Copy data if it's there in buffer + let text = get_text(ctx); + if text.is_empty() { + return None; + } + + let start_time = ctx.prev_timestamp.unwrap_or(ctx.timestamp); + let mut end_time = ctx.timestamp; + if start_time == end_time { + end_time += 2; + } + ctx.prev_timestamp = Some(ctx.timestamp); + + Some(IsdbSubtitleData { + text, + start_time, + end_time, + }) +} + +/// Parse an ISDB data group +/// Returns: (bytes_consumed, optional on IsdbSubtitleData) +// Acc to http://www.bocra.org.bw/sites/default/files/documents/Appendix%201%20-%20Operational%20Guideline%20for%20ISDB-Tbw.pdf +// In table AP8-1 there are modification to ARIB TR-B14 in volume 3 Section 2 4.4.1 character encoding is UTF-8 +// instead of 8 bit character, just now we don't have any means to detect which country this video is +// therefore we have hardcoded UTF-8 as encoding +pub fn isdb_parse_data_group( + ctx: &mut IsdbSubContext, + buf: &[u8], +) -> (i32, Option) { + if buf.len() < 6 { + return (-1, None); + } + + let mut pos: usize = 0; + + let id = buf[pos] >> 2; + log::debug!("ISDB (Data group) id: {}", id); + + if (id >> 4) == 0 { + log::debug!("ISDB group A"); + } + // else if (id >> 4) == 0 { // TODO : FIX this as C code has similar behaviour + // log::debug!("ISDB group B"); + // } + + pos += 1; + + // Skip link_number and last_link_number + log::debug!( + "ISDB (Data group) link_number {} last_link_number {}", + buf[pos], + buf[pos + 1] + ); + pos += 2; + + if pos + 2 > buf.len() { + return (-1, None); + } + + let group_size = rb16(&buf[pos..]) as usize; + pos += 2; + log::debug!("ISDB (Data group) group_size {}", group_size); + + match ctx.prev_timestamp { + None => ctx.prev_timestamp = Some(ctx.timestamp), + Some(prev) if prev > ctx.timestamp => ctx.prev_timestamp = Some(ctx.timestamp), + _ => {} + } + + let mut subtitle = None; + let data_end = (pos + group_size).min(buf.len()); + + if (id & 0x0F) == 0 { + // Caption management + log::debug!("ISDB caption management data"); + parse_caption_management_data(ctx, &buf[pos..data_end]); + } else if (id & 0x0F) < 8 { + // Caption statement data + log::debug!("ISDB {} language", id); + subtitle = parse_caption_statement_data(ctx, &buf[pos..data_end]); + } + + pos += group_size; + + // Skip CRC (2 bytes) (TODO : to be checked) + pos += 2; + + (pos as i32, subtitle) +} + +#[cfg(test)] +mod tests { + use crate::isdb::high::*; + use crate::isdb::leaf::*; + use crate::isdb::mid::*; + use crate::isdb::types::*; + + // ---- parse_statement ---- + + #[test] + fn test_parse_statement_printable_chars() { + let mut ctx = IsdbSubContext::new(); + // Bytes 0x21-0x7E are printable GL characters + let buf = [0x41, 0x42, 0x43]; // 'A', 'B', 'C' + let ret = parse_statement(&mut ctx, &buf, buf.len()); + assert_eq!(ret, 0); + assert_eq!(ctx.text_list.len(), 1); + assert_eq!(ctx.text_list[0].buf, vec![0x41, 0x42, 0x43]); + } + + #[test] + fn test_parse_statement_space() { + let mut ctx = IsdbSubContext::new(); + // 0x20 = SP (space), special case + let buf = [0x20]; + parse_statement(&mut ctx, &buf, buf.len()); + assert_eq!(ctx.text_list.len(), 1); + assert_eq!(ctx.text_list[0].buf, vec![0x20]); + } + + #[test] + fn test_parse_statement_command() { + let mut ctx = IsdbSubContext::new(); + // 0x0D = APR (code_hi=0x00, code_lo=0xD) — a control command + // followed by printable char 0x41 + let buf = [0x0D, 0x41]; + parse_statement(&mut ctx, &buf, buf.len()); + // APR consumes 1 byte, then 0x41 appends + assert_eq!(ctx.text_list.len(), 1); + assert_eq!(ctx.text_list[0].buf, vec![0x41]); + } + + #[test] + fn test_parse_statement_mixed() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x41, 0x00, 0x42]; + parse_statement(&mut ctx, &buf, buf.len()); + assert_eq!(ctx.text_list[0].buf, vec![0x41, 0x42]); + } + + #[test] + fn test_parse_statement_size_limit() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x41, 0x42, 0x43, 0x44]; + parse_statement(&mut ctx, &buf, 2); + assert_eq!(ctx.text_list[0].buf, vec![0x41, 0x42]); + } + + #[test] + fn test_parse_statement_gr_chars() { + let mut ctx = IsdbSubContext::new(); + let buf = [0xA1, 0xB2, 0xC3]; + parse_statement(&mut ctx, &buf, buf.len()); + assert_eq!(ctx.text_list[0].buf, vec![0xA1, 0xB2, 0xC3]); + } + + // ---- parse_data_unit ---- + + #[test] + fn test_parse_data_unit_statement_body() { + let mut ctx = IsdbSubContext::new(); + let buf = [ + 0x1F, // unit separator + 0x20, // unit_parameter = statement body + 0x00, 0x00, 0x03, // len = 3 + 0x41, 0x42, 0x43, // "ABC" + ]; + let ret = parse_data_unit(&mut ctx, &buf); + assert_eq!(ret, 0); + assert_eq!(ctx.text_list[0].buf, vec![0x41, 0x42, 0x43]); + } + + #[test] + fn test_parse_data_unit_unknown_type() { + let mut ctx = IsdbSubContext::new(); + let buf = [ + 0x1F, // unit separator + 0x30, // unknown unit_parameter + 0x00, 0x00, 0x01, // len = 1 + 0x41, + ]; + let ret = parse_data_unit(&mut ctx, &buf); + assert_eq!(ret, 0); + assert!(ctx.text_list.is_empty()); // nothing parsed + } + + #[test] + fn test_parse_data_unit_too_short() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x1F, 0x20]; // too short for header + let ret = parse_data_unit(&mut ctx, &buf); + assert_eq!(ret, -1); + } + + // ---- parse_caption_statement_data ---- + + #[test] + fn test_caption_statement_data_basic() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.timestamp = 1000; + ctx.prev_timestamp = Some(500); + + let buf = [ + 0x00, // tmd=0 + 0x00, 0x00, 0x08, // len=8 + 0x1F, // unit separator + 0x20, // statement body + 0x00, 0x00, 0x03, // unit len=3 + 0x48, 0x69, 0x21, // "Hi!" + ]; + + let result = parse_caption_statement_data(&mut ctx, &buf); + assert!(result.is_some()); + let sub = result.unwrap(); + assert_eq!(sub.text, b"Hi!\n\r"); + assert_eq!(sub.start_time, 500); + assert_eq!(sub.end_time, 1000); + } + + #[test] + fn test_caption_statement_data_with_timing() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.timestamp = 2000; + ctx.prev_timestamp = Some(1000); + + // skip 5 bytes timing + let buf = [ + 0x40, // tmd=1 + 0x00, 0x00, 0x00, 0x00, 0x00, // 5 bytes timing data + 0x00, 0x00, 0x08, // len=8 + 0x1F, // unit separator + 0x20, // statement body + 0x00, 0x00, 0x03, // unit len=3 + 0x41, 0x42, 0x43, // "ABC" + ]; + + let result = parse_caption_statement_data(&mut ctx, &buf); + assert!(result.is_some()); + let sub = result.unwrap(); + assert_eq!(sub.text, b"ABC\n\r"); + } + + #[test] + fn test_caption_statement_data_no_text() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + + // produces no text + let buf = [ + 0x00, // tmd=0 + 0x00, 0x00, 0x06, // len=6 + 0x1F, // unit separator + 0x30, // unknown type + 0x00, 0x00, 0x01, // len=1 + 0x41, + ]; + + let result = parse_caption_statement_data(&mut ctx, &buf); + assert!(result.is_none()); + } + + #[test] + fn test_caption_statement_data_equal_timestamps() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.timestamp = 500; + ctx.prev_timestamp = Some(500); + + let buf = [ + 0x00, 0x00, 0x00, 0x08, 0x1F, 0x20, 0x00, 0x00, 0x03, 0x41, 0x42, 0x43, + ]; + + let result = parse_caption_statement_data(&mut ctx, &buf); + assert!(result.is_some()); + let sub = result.unwrap(); + // When start == end, end_time gets +2 + assert_eq!(sub.end_time, 502); + } + + // ---- isdb_parse_data_group ---- + + #[test] + fn test_data_group_management() { + let mut ctx = IsdbSubContext::new(); + let buf = [ + 0x00, // id=0 (management), version=0 + 0x00, // link_number + 0x00, // last_link_number + 0x00, 0x02, // group_size=2 + 0x00, // TMD=0 (Free) + 0x00, // nb_lang=0 + 0x00, 0x00, // CRC (2 bytes) + ]; + + let (consumed, subtitle) = isdb_parse_data_group(&mut ctx, &buf); + assert!(subtitle.is_none()); + assert_eq!(ctx.tmd, IsdbTmd::Free); + assert_eq!(consumed, 9); // 5 header + 2 data + 2 CRC + } + + #[test] + fn test_data_group_caption_statement() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.timestamp = 1000; + ctx.prev_timestamp = Some(500); + + // id=1 (caption statement, lang 1) + let caption_data = [ + 0x00, // tmd=0 + 0x00, 0x00, 0x08, // len=8 + 0x1F, 0x20, // unit: separator + statement body + 0x00, 0x00, 0x03, // unit len=3 + 0x58, 0x59, 0x5A, // "XYZ" + ]; + let group_size = caption_data.len() as u16; + + let mut buf = vec![ + 0x04, // id=1 (0x04 >> 2 = 1) + 0x00, // link_number + 0x00, // last_link_number + ]; + buf.extend_from_slice(&group_size.to_be_bytes()); + buf.extend_from_slice(&caption_data); + buf.extend_from_slice(&[0x00, 0x00]); // CRC + + let (consumed, subtitle) = isdb_parse_data_group(&mut ctx, &buf); + assert!(consumed > 0); + assert!(subtitle.is_some()); + let sub = subtitle.unwrap(); + assert_eq!(sub.text, b"XYZ\n\r"); + } + + #[test] + fn test_data_group_too_short() { + let ctx = &mut IsdbSubContext::new(); + let buf = [0x00, 0x00]; + let (consumed, subtitle) = isdb_parse_data_group(ctx, &buf); + assert_eq!(consumed, -1); + assert!(subtitle.is_none()); + } + + #[test] + fn test_data_group_fixes_timestamp() { + let mut ctx = IsdbSubContext::new(); + ctx.timestamp = 100; + ctx.prev_timestamp = Some(500); + + let buf = [ + 0x00, // management + 0x00, 0x00, 0x00, 0x02, // group_size=2 + 0x00, 0x00, // mgmt data + 0x00, 0x00, // CRC + ]; + + isdb_parse_data_group(&mut ctx, &buf); + // prev_timestamp should be fixed to current + assert_eq!(ctx.prev_timestamp, Some(100)); + } +} diff --git a/src/rust/src/isdb/leaf.rs b/src/rust/src/isdb/leaf.rs new file mode 100644 index 000000000..9ee301a85 --- /dev/null +++ b/src/rust/src/isdb/leaf.rs @@ -0,0 +1,463 @@ +//! Low-level helper functions for ISDB subtitle decoding. +//! These are leaf-level functions with no dependencies on mid/high-level logic. +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_isdb.c) | Rust | +//! |------------------------------------|--------------------------------------| +//! | `RB16` macro | [`rb16`] | +//! | `RB24` macro | [`rb24`] | +//! | `move_penpos` | [`move_penpos`] | +//! | `get_csi_params` | [`get_csi_params`] | +//! | `ccx_strstr_ignorespace` | [`strstr_ignorespace`] | +//! | `set_writing_format` | [`set_writing_format`] | +//! | `parse_caption_management_data` | [`parse_caption_management_data`] | + +use crate::isdb::types::{IsdbSubContext, IsdbTmd, WritingFormat}; +use log::debug; + +#[inline] +/// Read big-endian u16 from buffer (2 bytes) +pub fn rb16(buf: &[u8]) -> u16 { + u16::from_be_bytes([buf[0], buf[1]]) +} + +#[inline] +/// Read big-endian u24 (as u32) from buffer (3 bytes) +pub fn rb24(buf: &[u8]) -> u32 { + u32::from_be_bytes([0, buf[0], buf[1], buf[2]]) +} + +pub fn move_penpos(ctx: &mut IsdbSubContext, col: i32, row: i32) { + let ls = &mut ctx.current_state.layout_state; + ls.cursor_pos.x = row; + ls.cursor_pos.y = col; +} + +pub fn get_csi_params(buf: &[u8]) -> Option<(usize, u32, Option)> { + let mut i = 0; + + // Parse p1 + let mut p1: u32 = 0; + while i < buf.len() && buf[i] >= 0x30 && buf[i] <= 0x39 { + p1 = p1 * 10 + (buf[i] - 0x30) as u32; + i += 1; + } + + if i >= buf.len() { + return None; + } + + // Must be followed by 0x20 (space) or 0x3B (semicolon) + if buf[i] != 0x20 && buf[i] != 0x3B { + return None; + } + + // If terminator is 0x20 (space), only one param + if buf[i] == 0x20 { + i += 1; + return Some((i, p1, None)); + } + + // Skip semicolon separator + i += 1; + + // Parse p2 + let mut p2: u32 = 0; + while i < buf.len() && buf[i] >= 0x30 && buf[i] <= 0x39 { + p2 = p2 * 10 + (buf[i] - 0x30) as u32; + i += 1; + } + + // Skip terminator + i += 1; + + Some((i, p1, Some(p2))) +} + +pub fn strstr_ignorespace(str1: &[u8], str2: &[u8]) -> bool { + for (i, &ch) in str2.iter().enumerate() { + if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' { + continue; + } + if i >= str1.len() || str1[i] != ch { + return false; + } + } + true +} + +pub fn set_writing_format(ctx: &mut IsdbSubContext, arg: &[u8]) { + let ls = &mut ctx.current_state.layout_state; + + if arg.len() < 2 { + return; + } + + // One param: init + if arg[1] == 0x20 { + ls.format = WritingFormat::from_i32((arg[0] & 0x0F) as i32); + return; + } + + let mut pos: usize = 0; + + // P1 I1 p2 I2 P31~P3i I3 P41~P4j I4 F + if arg.get(1) == Some(&0x3B) { + ls.format = WritingFormat::HorizontalCustom; + pos += 2; + } + + if pos < arg.len() && arg.get(pos + 1) == Some(&0x3B) { + // font size param (also commented out in og code) + // match arg[pos] & 0x0f { 0 => Smol, 1 => Middle, 2 => Standard, _ => {} } + pos += 2; + } + + // P3 + debug!("character numbers in one line in decimal"); + while pos < arg.len() && arg[pos] != 0x3B && arg[pos] != 0x20 { + ctx.nb_char = arg[pos] as i32; + pos += 1; + } + + if pos >= arg.len() || arg[pos] == 0x20 { + return; + } + pos += 1; // skip 0x3B + + debug!("line numbers in decimal"); + while pos < arg.len() && arg[pos] != 0x20 { + ctx.nb_line = arg[pos] as i32; + pos += 1; + } +} + +pub fn parse_caption_management_data(ctx: &mut IsdbSubContext, buf: &[u8]) -> usize { + let mut pos: usize = 0; + + if buf.is_empty() { + return 0; + } + + ctx.tmd = IsdbTmd::from_i32((buf[pos] >> 6) as i32); + debug!("CC MGMT DATA: TMD: {}", (buf[0] >> 6)); + pos += 1; + + if ctx.tmd == IsdbTmd::Free { + debug!("Playback time is not restricted to synchronize to the clock."); + } else if ctx.tmd == IsdbTmd::OffsetTime { + /* + * This 36-bit field indicates offset time to add to the playback time when the + * clock control mode is in offset time mode. Offset time is coded in the + * order of hour, minute, second and millisecond, using nine 4-bit binary + * coded decimals (BCD). + * + * +-----------+-----------+---------+--------------+ + * | hour | minute | sec | millisecond | + * +-----------+-----------+---------+--------------+ + * | 2 (4bit) | 2 (4bit) | 2 (4bit)| 3 (4bit) | + * +-----------+-----------+---------+--------------+ + */ + if pos + 5 > buf.len() { + return pos; + } + ctx.offset_time.hour = ((buf[pos] >> 4) * 10 + (buf[pos] & 0x0f)) as i32; + pos += 1; + ctx.offset_time.min = ((buf[pos] >> 4) * 10 + (buf[pos] & 0x0f)) as i32; + pos += 1; + ctx.offset_time.sec = ((buf[pos] >> 4) * 10 + (buf[pos] & 0x0f)) as i32; + pos += 1; + ctx.offset_time.milli = ((buf[pos] >> 4) as i32 * 100) + + ((buf[pos] & 0x0f) as i32 * 10) + + (buf[pos + 1] & 0x0f) as i32; + debug!( + "CC MGMT DATA: OTD( h:{} m:{} s:{} millis: {}", + ctx.offset_time.hour, ctx.offset_time.min, ctx.offset_time.sec, ctx.offset_time.milli + ); + pos += 2; + } else { + debug!( + "Playback time is in accordance with the time of the clock, \ + which is calibrated by clock signal (TDT). Playback time is \ + given by PTS." + ); + } + + if pos >= buf.len() { + return pos; + } + + // no. of langs + ctx.nb_lang = buf[pos] as i32; + debug!("CC MGMT DATA: nb languages: {}", ctx.nb_lang); + pos += 1; + + for _ in 0..ctx.nb_lang { + if pos >= buf.len() { + break; + } + debug!("CC MGMT DATA: {}", (buf[pos] & 0x1F) >> 5); + ctx.dmf = buf[pos] & 0x0F; + debug!("CC MGMT DATA: DMF 0x{:X}", ctx.dmf); + pos += 1; + + if (ctx.dmf == 0x0C || ctx.dmf == 0x0D || ctx.dmf == 0x0E) && pos < buf.len() { + ctx.dc = buf[pos]; + debug!("Attenuation Due to Rain"); + } + + debug!("CC MGMT DATA: languages: {:?}", &buf[pos..pos+3]); + if pos + 3 > buf.len() { + break; + } + pos += 3; + + if pos >= buf.len() { + break; + } + debug!("CC MGMT DATA: Format: 0x{:X}", buf[pos] >> 4); + debug!("CC MGMT DATA: TCS: 0x{:X}", (buf[pos] >> 2) & 0x3); + + ctx.current_state.rollup_mode = (buf[pos] & 0x03) != 0; + debug!( + "CC MGMT DATA: Rollup mode: {}", + ctx.current_state.rollup_mode + ); + } + + pos +} + +#[cfg(test)] +mod tests { + use crate::isdb::leaf::*; + use crate::isdb::types::*; + + // ---- move_penpos ---- + + #[test] + fn test_move_penpos() { + let mut ctx = IsdbSubContext::new(); + move_penpos(&mut ctx, 10, 20); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 20); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 10); + } + + #[test] + fn test_move_penpos_zero() { + let mut ctx = IsdbSubContext::new(); + move_penpos(&mut ctx, 50, 100); + move_penpos(&mut ctx, 0, 0); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 0); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 0); + } + + // ---- get_csi_params ---- + + #[test] + fn test_get_csi_params_single() { + let buf = [0x31, 0x32, 0x33, 0x20]; // "123" + space + let result = get_csi_params(&buf); + assert!(result.is_some()); + let (consumed, p1, p2) = result.unwrap(); + assert_eq!(p1, 123); + assert!(p2.is_none()); + assert_eq!(consumed, 4); + } + + #[test] + fn test_get_csi_params_two() { + let buf = [0x34, 0x35, 0x3B, 0x36, 0x37, 0x20]; // "45;67" + space + let result = get_csi_params(&buf); + assert!(result.is_some()); + let (consumed, p1, p2) = result.unwrap(); + assert_eq!(p1, 45); + assert_eq!(p2, Some(67)); + assert_eq!(consumed, 6); + } + + #[test] + fn test_get_csi_params_zero() { + let buf = [0x30, 0x20]; + let result = get_csi_params(&buf); + assert!(result.is_some()); + let (_, p1, p2) = result.unwrap(); + assert_eq!(p1, 0); + assert!(p2.is_none()); + } + + #[test] + fn test_get_csi_params_invalid() { + let buf = [0x31, 0x32, 0x58]; + let result = get_csi_params(&buf); + assert!(result.is_none()); + } + + #[test] + fn test_get_csi_params_empty_digits() { + let buf = [0x20]; + let result = get_csi_params(&buf); + assert!(result.is_some()); + let (_, p1, _) = result.unwrap(); + assert_eq!(p1, 0); + } + + // ---- strstr_ignorespace ---- + + #[test] + fn test_strstr_ignorespace_match() { + assert!(strstr_ignorespace(b"hello", b"hello")); + } + + #[test] + fn test_strstr_ignorespace_with_spaces() { + assert!(strstr_ignorespace(b"h l l o", b"h l l o")); // identical + assert!(!strstr_ignorespace(b"hllo", b"h l l o")); // different lengths + } + + #[test] + fn test_strstr_ignorespace_no_match() { + assert!(!strstr_ignorespace(b"hello", b"hxllo")); + } + + #[test] + fn test_strstr_ignorespace_empty_str2() { + assert!(strstr_ignorespace(b"anything", b"")); + } + + #[test] + fn test_strstr_ignorespace_str1_shorter() { + assert!(!strstr_ignorespace(b"hi", b"hello")); + } + + // ---- set_writing_format ---- + + #[test] + fn test_swf_init_format() { + let mut ctx = IsdbSubContext::new(); + set_writing_format(&mut ctx, &[0x45, 0x20]); + assert_eq!( + ctx.current_state.layout_state.format, + WritingFormat::Horizontal1920x1080 + ); + } + + #[test] + fn test_swf_init_format_zero() { + let mut ctx = IsdbSubContext::new(); + set_writing_format(&mut ctx, &[0x40, 0x20]); + assert_eq!( + ctx.current_state.layout_state.format, + WritingFormat::HorizontalStdDensity + ); + } + + #[test] + fn test_swf_custom_format() { + let mut ctx = IsdbSubContext::new(); + let arg = [0x30, 0x3B, 0x32, 0x3B, 0x41, 0x20]; + set_writing_format(&mut ctx, &arg); + assert_eq!( + ctx.current_state.layout_state.format, + WritingFormat::HorizontalCustom + ); + assert_eq!(ctx.nb_char, 0x41); + } + + #[test] + fn test_swf_custom_with_lines() { + let mut ctx = IsdbSubContext::new(); + let arg = [0x30, 0x3B, 0x32, 0x3B, 0x41, 0x3B, 0x42, 0x20]; + set_writing_format(&mut ctx, &arg); + assert_eq!(ctx.nb_char, 0x41); + assert_eq!(ctx.nb_line, 0x42); + } + + // ---- parse_caption_management_data ---- + + #[test] + fn test_mgmt_tmd_free() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x00, 0x00]; + let consumed = parse_caption_management_data(&mut ctx, &buf); + assert_eq!(ctx.tmd, IsdbTmd::Free); + assert_eq!(ctx.nb_lang, 0); + assert_eq!(consumed, 2); + } + + #[test] + fn test_mgmt_tmd_realtime() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x40, 0x00]; + let consumed = parse_caption_management_data(&mut ctx, &buf); + assert_eq!(ctx.tmd, IsdbTmd::RealTime); + assert_eq!(ctx.nb_lang, 0); + assert_eq!(consumed, 2); + } + + #[test] + fn test_mgmt_tmd_offset() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x80, 0x12, 0x34, 0x56, 0x78, 0x09, 0x00]; + let consumed = parse_caption_management_data(&mut ctx, &buf); + assert_eq!(ctx.tmd, IsdbTmd::OffsetTime); + assert_eq!(ctx.offset_time.hour, 12); + assert_eq!(ctx.offset_time.min, 34); + assert_eq!(ctx.offset_time.sec, 56); + assert_eq!(ctx.offset_time.milli, 789); + assert_eq!(consumed, 7); + } + + #[test] + fn test_mgmt_with_language() { + let mut ctx = IsdbSubContext::new(); + let buf = [ + 0x00, // TMD=0 + 0x01, // nb_lang=1 + 0x0C, // dmf=0x0C (triggers dc read) + b'p', b'o', b'r', // language "por" + 0x03, // rollup_mode = (0x03 & 0x03) != 0 = true + ]; + let consumed = parse_caption_management_data(&mut ctx, &buf); + assert_eq!(ctx.nb_lang, 1); + assert_eq!(ctx.dmf, 0x0C); + assert_eq!(ctx.dc, b'p'); + assert!(ctx.current_state.rollup_mode); + assert_eq!(consumed, 6); + } + + #[test] + fn test_mgmt_rollup_off() { + let mut ctx = IsdbSubContext::new(); + let buf = [ + 0x00, // TMD=0 + 0x01, // nb_lang=1 + 0x03, // dmf=0x03 + b'e', b'n', b'g', // lang + 0x00, // rollup_mode = 0 + ]; + parse_caption_management_data(&mut ctx, &buf); + assert!(!ctx.current_state.rollup_mode); + } + + // ---- WritingFormat::is_horizontal ---- + + #[test] + fn test_is_horizontal() { + assert!(WritingFormat::HorizontalStdDensity.is_horizontal()); + + assert!(WritingFormat::HorizontalHighDensity.is_horizontal()); + + assert!(WritingFormat::HorizontalWesternLang.is_horizontal()); + assert!(WritingFormat::Horizontal1920x1080.is_horizontal()); + assert!(WritingFormat::Horizontal960x540.is_horizontal()); + assert!(WritingFormat::Horizontal720x480.is_horizontal()); + assert!(WritingFormat::Horizontal1280x720.is_horizontal()); + assert!(WritingFormat::HorizontalCustom.is_horizontal()); + + assert!(!WritingFormat::VerticalStdDensity.is_horizontal()); + assert!(!WritingFormat::Vertical1920x1080.is_horizontal()); + assert!(!WritingFormat::None.is_horizontal()); + } +} diff --git a/src/rust/src/isdb/mid.rs b/src/rust/src/isdb/mid.rs new file mode 100644 index 000000000..f83304c35 --- /dev/null +++ b/src/rust/src/isdb/mid.rs @@ -0,0 +1,834 @@ +//! Mid-level control and command processing for ISDB subtitle decoding. +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_isdb.c) | Rust | +//! |--------------------------------|----------------------------------| +//! | `set_position` | [`set_position`] | +//! | `append_char` | [`append_char`] | +//! | `get_text` | [`get_text`] | +//! | `parse_csi` | [`parse_csi`] | +//! | `parse_command` | [`parse_command`] | + +use crate::isdb::leaf::{get_csi_params, move_penpos, set_writing_format, strstr_ignorespace}; +use crate::isdb::types::{IsdbCcComposition, IsdbSubContext, IsdbText, DEFAULT_CLUT}; + +pub fn set_position(ctx: &mut IsdbSubContext, p1: u32, p2: u32) { + let ls = &ctx.current_state.layout_state; + let is_horizontal = ls.format.is_horizontal(); + + let (cw, ch) = if is_horizontal { + ( + (ls.font_size + ls.cell_spacing.col) * ls.font_scale.fscx / 100, + (ls.font_size + ls.cell_spacing.row) * ls.font_scale.fscy / 100, + ) + } else { + ( + (ls.font_size + ls.cell_spacing.col) * ls.font_scale.fscy / 100, + (ls.font_size + ls.cell_spacing.row) * ls.font_scale.fscx / 100, + ) + }; + + let col = p2 as i32 * cw; + let row = if is_horizontal { + p1 as i32 * ch + ch + // pen position is at bottom left + } else { + p1 as i32 * ch + ch / 2 + // pen position is at upper center, + // but in -90deg rotated coordinates, it is at middle left. + }; + + move_penpos(ctx, col, row); +} + +/// Append a character byte to the appropriate text node. +/// Finds or creates a text node at the current cursor line position. +pub fn append_char(ctx: &mut IsdbSubContext, ch: u8) -> i32 { + let is_horiz = ctx.current_state.layout_state.format.is_horizontal(); + let cursor = ctx.current_state.layout_state.cursor_pos; + + // Space taken by character + let csp = if is_horiz { + ctx.current_state.layout_state.font_size * ctx.current_state.layout_state.font_scale.fscx + / 100 + } else { + ctx.current_state.layout_state.font_size * ctx.current_state.layout_state.font_scale.fscy + / 100 + }; + + // Current Line Position + let cur_lpos = if is_horiz { cursor.x } else { cursor.y }; + + // Find existing node or insertion point (list is sorted by line position) + let mut found_idx: Option = None; + let mut insert_at: Option = None; + + for (i, text) in ctx.text_list.iter().enumerate() { + // Text Line Position + let text_lpos = if is_horiz { text.pos.x } else { text.pos.y }; + if text_lpos == cur_lpos { + found_idx = Some(i); + break; + } else if text_lpos > cur_lpos { + // Allocate Text here so that list is always sorted + insert_at = Some(i); + break; + } + } + + let idx = if let Some(i) = found_idx { + i + } else if let Some(i) = insert_at { + ctx.text_list.insert(i, IsdbText::new(cursor)); + i + } else { + ctx.text_list.push(IsdbText::new(cursor)); + ctx.text_list.len() - 1 + }; + + // Check backward movement — if cursor is behind text pos, reset text + if is_horiz { + if ctx.current_state.layout_state.cursor_pos.y < ctx.text_list[idx].pos.y { + ctx.text_list[idx].pos.y = ctx.current_state.layout_state.cursor_pos.y; + ctx.text_list[idx].buf.clear(); + } + ctx.current_state.layout_state.cursor_pos.y += csp; + ctx.text_list[idx].pos.y += csp; + } else { + if ctx.current_state.layout_state.cursor_pos.y < ctx.text_list[idx].pos.y { + ctx.text_list[idx].pos.y = ctx.current_state.layout_state.cursor_pos.y; + ctx.text_list[idx].buf.clear(); + } + ctx.current_state.layout_state.cursor_pos.x += csp; + ctx.text_list[idx].pos.x += csp; + } + + ctx.text_list[idx].buf.push(ch); + 1 +} + +pub fn get_text(ctx: &mut IsdbSubContext) -> Vec { + let mut output: Vec = Vec::new(); + + // deduplication path: if no_rollup enabled OR rollup_mode is off + // og C code says : Abhinav95: Forcing -noru to perform deduplication even if stream doesn't honor it + if ctx.cfg_no_rollup || (ctx.cfg_no_rollup == ctx.current_state.rollup_mode) { + // firt call: copy text_list → buffered_text, return empty + if ctx.buffered_text.is_empty() { + for text in &ctx.text_list { + if !text.buf.is_empty() { + let mut clone = IsdbText::new(text.pos); + clone.buf = text.buf.clone(); + ctx.buffered_text.push(clone); + } + } + return output; + } + + // update buffered_text with new/changed entries from text_list + for text in &ctx.text_list { + let mut found = false; + for sb in &mut ctx.buffered_text { + if strstr_ignorespace(&text.buf, &sb.buf) { + found = true; + // See if complete string is there if not update that string + if !strstr_ignorespace(&sb.buf, &text.buf) { + sb.buf = text.buf.clone(); + } + break; + } + } + if !found { + let mut new_sb = IsdbText::new(text.pos); + new_sb.buf = text.buf.clone(); + ctx.buffered_text.push(new_sb); + } + } + + // flush buffered entries not found in text_list + let text_list_ref: Vec> = ctx.text_list.iter().map(|t| t.buf.clone()).collect(); + ctx.buffered_text.retain(|sb| { + if sb.buf.is_empty() { + return true; + } + let found = text_list_ref.iter().any(|t| strstr_ignorespace(t, &sb.buf)); + if !found { + output.extend_from_slice(&sb.buf); + output.push(b'\n'); + output.push(b'\r'); + return false; // remove from buffered_text + } + true + }); + } else { + // simple path: output all text entries + for text in &mut ctx.text_list { + if !text.buf.is_empty() { + output.extend_from_slice(&text.buf); + output.push(b'\n'); + output.push(b'\r'); + text.buf.clear(); + } + } + } + + output +} + +/// parse a CSI (Control Sequence Introducer) command +/// Returns the number of bytes consumed. +pub fn parse_csi(ctx: &mut IsdbSubContext, buf: &[u8]) -> usize { + let mut pos: usize = 0; + let mut arg = [0u8; 10]; + let mut i: usize = 0; + + // copy bytes into arg until 0x20 terminator + while pos < buf.len() && buf[pos] != 0x20 { + if i >= 9 { + log::debug!("Unexpected CSI: too long"); + break; + } + arg[i] = buf[pos]; + pos += 1; + i += 1; + } + + // Include terminating 0x20 in arg + if i < 10 && pos < buf.len() { + arg[i] = buf[pos]; + pos += 1; + } + + if pos >= buf.len() { + return pos; + } + + let cmd = buf[pos]; + + match cmd { + // SWF - Set Writing Format + 0x53 => { + log::debug!("Command:CSI: SWF"); + set_writing_format(ctx, &arg); + } + // CCC - Composite Character Composition + 0x54 => { + log::debug!("Command:CSI: CCC"); + if let Some((_, p1, _)) = get_csi_params(&arg) { + ctx.current_state.layout_state.ccc = IsdbCcComposition::from_i32(p1 as i32); + } + } + // SDF - Set Display Format + 0x56 => { + if let Some((_, p1, Some(p2))) = get_csi_params(&arg) { + ctx.current_state.layout_state.display_area.w = p1 as i32; + ctx.current_state.layout_state.display_area.h = p2 as i32; + } + log::debug!("Command:CSI: SDF"); + } + // SSM - Character composition dot designation + 0x57 => { + if let Some((_, p1, _)) = get_csi_params(&arg) { + ctx.current_state.layout_state.font_size = p1 as i32; + } + log::debug!("Command:CSI: SSM"); + } + // SHS - Set Horizontal Spacing + 0x58 => { + if let Some((_, p1, _)) = get_csi_params(&arg) { + ctx.current_state.layout_state.cell_spacing.col = p1 as i32; + } + log::debug!("Command:CSI: SHS"); + } + // SVS - Set Vertical Spacing + 0x59 => { + if let Some((_, p1, _)) = get_csi_params(&arg) { + ctx.current_state.layout_state.cell_spacing.row = p1 as i32; + } + log::debug!("Command:CSI: SVS"); + } + // SDP - Set Display Position + 0x5F => { + if let Some((_, p1, Some(p2))) = get_csi_params(&arg) { + ctx.current_state.layout_state.display_area.x = p1 as i32; + ctx.current_state.layout_state.display_area.y = p2 as i32; + } + log::debug!("Command:CSI: SDP"); + } + // ACPS - Active Coordinate Position Set + 0x61 => { + log::debug!("Command:CSI: ACPS"); + if let Some((_, p1, Some(_))) = get_csi_params(&arg) { + ctx.current_state.layout_state.acps[0] = p1 as i32; + ctx.current_state.layout_state.acps[1] = p1 as i32; + // C code sets both to p1 + } + } + // RCS - Raster Color Command + 0x6E => { + if let Some((_, p1, _)) = get_csi_params(&arg) { + let idx = (ctx.current_state.clut_high_idx as usize) << 4 | (p1 as usize); + if idx < DEFAULT_CLUT.len() { + ctx.current_state.raster_color = DEFAULT_CLUT[idx]; + } + } + log::debug!("Command:CSI: RCS"); + } + _ => { + log::debug!("Command:CSI: Unknown command 0x{:x}", cmd); + } + } + + pos += 1; // skip command byte + pos +} + +/// parse control command byte seq +/// Return number of bytes consumed +pub fn parse_command(ctx: &mut IsdbSubContext, buf: &[u8]) -> usize { + if buf.is_empty() { + return 0; + } + + let code_lo = buf[0] & 0x0f; + let code_hi = (buf[0] & 0xf0) >> 4; + let mut pos: usize = 1; // skip command byte + + match code_hi { + 0x00 => match code_lo { + /* NUL Control code, which can be added or deleted without effecting to + information content. */ + 0x0 => log::debug!("Command: NUL"), + /* BEL Control code used when calling attention (alarm or signal) */ + // TODO add bell character here + 0x7 => log::debug!("Command: BEL"), + /* + * APB: Active position goes backward along character path in the length of + * character path of character field. When the reference point of the character + * field exceeds the edge of display area by this movement, move in the + * opposite side of the display area along the character path of the active + * position, for active position up. + */ + 0x8 => log::debug!("Command: APB"), + /* + * APF: Active position goes forward along character path in the length of + * character path of character field. When the reference point of the character + * field exceeds the edge of display area by this movement, move in the + * opposite side of the display area along the character path of the active + * position, for active position down. + */ + 0x9 => log::debug!("Command: APF"), + /* + * APD: Moves to next line along line direction in the length of line direction of + * the character field. When the reference point of the character field exceeds + * the edge of display area by this movement, move to the first line of the + * display area along the line direction. + */ + 0xA => log::debug!("Command: APD"), + /* + * APU: Moves to the previous line along line direction in the length of line + * direction of the character field. When the reference point of the character + * field exceeds the edge of display area by this movement, move to the last + * line of the display area along the line direction. + */ + 0xB => log::debug!("Command: APU"), + /* + * CS: Display area of the display screen is erased. + * Specs does not say clearly about whether we have to clear cursor + * Need Samples to see whether CS is called after pen move or before it + */ + 0xC => log::debug!("Command: CS clear Screen"), + /* APR: Active position down is made, moving to the first position of the same + * line. + */ + 0xD => log::debug!("Command: APR"), + /* LS1: Code to invoke character code set. */ + 0xE => log::debug!("Command: LS1"), + /* LS0: Code to invoke character code set. */ + 0xF => log::debug!("Command: LS0"), + /* Verify the new version of specs or packet is corrupted */ + _ => log::debug!("Command: Unknown"), + }, + 0x01 => { + match code_lo { + /* + * PAPF: Active position forward is made in specified times by parameter P1 (1byte). + * Parameter P1 shall be within the range of 04/0 to 07/15 and time shall be + * specified within the range of 0 to 63 in binary value of 6-bit from b6 to b1. + * (b8 and b7 are not used.) + */ + 0x6 => log::debug!("Command: PAPF"), + /* + * CAN: From the current active position to the end of the line is covered with + * background colour in the width of line direction in the current character + * field. Active position is not moved. + */ + 0x8 => log::debug!("Command: CAN"), + /* SS2: Code to invoke character code set. */ + 0x9 => log::debug!("Command: SS2"), + /* ESC:Code for code extension. */ + 0xB => log::debug!("Command: ESC"), + /* APS(Active Position Set): Specified times of active position down is made by P1 (1 byte) of the first + * parameter in line direction length of character field from the first position + * of the first line of the display area. Then specified times of active position + * forward is made by the second parameter P2 (1 byte) in the character path + * length of character field. Each parameter shall be within the range of 04/0 + * to 07/15 and specify time within the range of 0 to 63 in binary value of 6- + * bit from b6 to b1. (b8 and b7 are not used.) + */ + 0xC => { + log::debug!("Command: APS"); + if pos + 1 < buf.len() { + set_position(ctx, (buf[pos] & 0x3F) as u32, (buf[pos + 1] & 0x3F) as u32); + pos += 2; + } + } + /* SS3: Code to invoke character code set. */ + 0xD => log::debug!("Command: SS3"), + /* + * RS: It is information division code and declares identification and + * introduction of data header. + */ + 0xE => log::debug!("Command: RS"), + /* + * US: It is information division code and declares identification and + * introduction of data unit. + */ + 0xF => log::debug!("Command: US"), + /* Verify the new version of specs or packet is corrupted */ + _ => log::debug!("Command: Unknown"), + } + } + + 0x08 => { + match code_lo { + // BKF .. WHF - foreground color + /* BKF */ + /* RDF */ + /* GRF */ + /* YLF */ + /* BLF */ + /* MGF */ + /* CNF */ + /* WHF */ + 0x0..=0x7 => { + log::debug!( + "Command: Foreground color (0x{:X})", + DEFAULT_CLUT[code_lo as usize] + ); + ctx.current_state.fg_color = DEFAULT_CLUT[code_lo as usize]; + } + // SSZ - Small size + 0x8 => { + log::debug!("Command: SSZ"); + ctx.current_state.layout_state.font_scale.fscx = 50; + ctx.current_state.layout_state.font_scale.fscy = 50; + } + // MSZ - Medium size + 0x9 => { + log::debug!("Command: MSZ"); + ctx.current_state.layout_state.font_scale.fscx = 200; + ctx.current_state.layout_state.font_scale.fscy = 200; + } + // NSZ - Normal size + 0xA => { + log::debug!("Command: NSZ"); + ctx.current_state.layout_state.font_scale.fscx = 100; + ctx.current_state.layout_state.font_scale.fscy = 100; + } + // SZX + 0xB => { + log::debug!("Command: SZX"); + pos += 1; + } + // Verify the new version of specs or packet is corrupted + _ => log::debug!("Command: Unknown"), + } + } + 0x09 => { + match code_lo { + // COL + 0x0 => { + // Palette Col + if pos < buf.len() { + if buf[pos] == 0x20 { + log::debug!("Command: COL: Set Clut"); + pos += 1; + if pos < buf.len() { + ctx.current_state.clut_high_idx = buf[pos] & 0x0F; + } + } else if (buf[pos] & 0xF0) == 0x40 { + let ci = (buf[pos] & 0x0F) as usize; + log::debug!("Command: COL: Set Foreground 0x{:08X}", DEFAULT_CLUT[ci]); + ctx.current_state.fg_color = DEFAULT_CLUT[ci]; + } else if (buf[pos] & 0xF0) == 0x50 { + let ci = (buf[pos] & 0x0F) as usize; + log::debug!("Command: COL: Set Background 0x{:08X}", DEFAULT_CLUT[ci]); + ctx.current_state.bg_color = DEFAULT_CLUT[ci]; + } else if (buf[pos] & 0xF0) == 0x60 { + let ci = (buf[pos] & 0x0F) as usize; + log::debug!( + "Command: COL: Set Half Foreground 0x{:08X}", + DEFAULT_CLUT[ci] + ); + ctx.current_state.hfg_color = DEFAULT_CLUT[ci]; + } else if (buf[pos] & 0xF0) == 0x70 { + let ci = (buf[pos] & 0x0F) as usize; + log::debug!( + "Command: COL: Set Half Background 0x{:08X}", + DEFAULT_CLUT[ci] + ); + ctx.current_state.hbg_color = DEFAULT_CLUT[ci]; + } + pos += 1; + } + } + // FLC + 0x1 => { + log::debug!("Command: FLC"); + pos += 1; + } + // CDC + 0x2 => { + log::debug!("Command: CDC"); + pos += 3; + } + // POL + 0x3 => { + log::debug!("Command: POL"); + pos += 1; + } + // WMM + 0x4 => { + log::debug!("Command: WMM"); + pos += 3; + } + // MACRO + 0x5 => { + log::debug!("Command: MACRO"); + pos += 1; + } + // HLC + 0x7 => { + log::debug!("Command: HLC"); + pos += 1; + } + // RPC + 0x8 => { + log::debug!("Command: RPC"); + pos += 1; + } + // SPL + 0x9 => log::debug!("Command: SPL"), + // STL + 0xA => log::debug!("Command: STL"), + // CSI Code for code system extension indicated + 0xB => { + let ret = parse_csi(ctx, &buf[pos..]); + pos += ret; + } + // TIME + 0xD => { + log::debug!("Command: TIME"); + pos += 2; + } + // Verify the new version of specs or packet is corrupted + _ => log::debug!("Command: Unknown"), + } + } + _ => {} + } + + pos +} + +#[cfg(test)] +mod tests { + use crate::isdb::mid::*; + use crate::isdb::types::*; + + // ---- set_position ---- + + #[test] + fn test_set_position_horizontal() { + let mut ctx = IsdbSubContext::new(); + set_position(&mut ctx, 1, 2); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 72); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 72); + } + + #[test] + fn test_set_position_vertical() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.layout_state.format = WritingFormat::VerticalStdDensity; + set_position(&mut ctx, 1, 2); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 54); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 72); + } + + #[test] + fn test_set_position_with_spacing() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.layout_state.cell_spacing.col = 4; + ctx.current_state.layout_state.cell_spacing.row = 4; + set_position(&mut ctx, 1, 1); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 80); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 40); + } + + // ---- append_char ---- + + #[test] + fn test_append_char_creates_node() { + let mut ctx = IsdbSubContext::new(); + assert!(ctx.text_list.is_empty()); + append_char(&mut ctx, b'A'); + assert_eq!(ctx.text_list.len(), 1); + assert_eq!(ctx.text_list[0].buf, vec![b'A']); + } + + #[test] + fn test_append_char_multiple() { + let mut ctx = IsdbSubContext::new(); + append_char(&mut ctx, b'H'); + append_char(&mut ctx, b'i'); + assert_eq!(ctx.text_list.len(), 1); + assert_eq!(ctx.text_list[0].buf, vec![b'H', b'i']); + } + + #[test] + fn test_append_char_advances_cursor() { + let mut ctx = IsdbSubContext::new(); + let initial_y = ctx.current_state.layout_state.cursor_pos.y; + append_char(&mut ctx, b'X'); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, initial_y + 36); + } + + #[test] + fn test_append_char_different_lines() { + let mut ctx = IsdbSubContext::new(); + append_char(&mut ctx, b'A'); + // Move cursor to different line position + ctx.current_state.layout_state.cursor_pos.x = 100; + ctx.current_state.layout_state.cursor_pos.y = 0; + append_char(&mut ctx, b'B'); + assert_eq!(ctx.text_list.len(), 2); + } + + // ---- get_text ---- + + #[test] + fn test_get_text_simple() { + let mut ctx = IsdbSubContext::new(); + ctx.text_list.push(IsdbText { + buf: b"Hello".to_vec(), + pos: IsdbPos { x: 0, y: 0 }, + }); + let result = get_text(&mut ctx); + assert!(result.is_empty()); + assert_eq!(ctx.buffered_text.len(), 1); + } + + #[test] + fn test_get_text_rollup_mode() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.text_list.push(IsdbText { + buf: b"Line1".to_vec(), + pos: IsdbPos { x: 0, y: 0 }, + }); + let result = get_text(&mut ctx); + assert_eq!(result, b"Line1\n\r"); + assert!(ctx.text_list[0].buf.is_empty()); // cleared after output + } + + #[test] + fn test_get_text_multiple_lines_rollup() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + ctx.text_list.push(IsdbText { + buf: b"Line1".to_vec(), + pos: IsdbPos { x: 0, y: 0 }, + }); + ctx.text_list.push(IsdbText { + buf: b"Line2".to_vec(), + pos: IsdbPos { x: 36, y: 0 }, + }); + let result = get_text(&mut ctx); + assert_eq!(result, b"Line1\n\rLine2\n\r"); + } + + #[test] + fn test_get_text_empty() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.rollup_mode = true; + ctx.cfg_no_rollup = false; + let result = get_text(&mut ctx); + assert!(result.is_empty()); + } + + // ---- parse_csi ---- + + #[test] + fn test_parse_csi_swf() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x35, 0x20, 0x53]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 3); + assert_eq!( + ctx.current_state.layout_state.format, + WritingFormat::Horizontal1920x1080 + ); + } + + #[test] + fn test_parse_csi_sdf() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x39, 0x36, 0x30, 0x3B, 0x35, 0x34, 0x30, 0x20, 0x56]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 9); + assert_eq!(ctx.current_state.layout_state.display_area.w, 960); + assert_eq!(ctx.current_state.layout_state.display_area.h, 540); + } + + #[test] + fn test_parse_csi_ssm() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x32, 0x34, 0x3B, 0x32, 0x34, 0x20, 0x57]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 7); + assert_eq!(ctx.current_state.layout_state.font_size, 24); + } + + #[test] + fn test_parse_csi_shs() { + let mut ctx = IsdbSubContext::new(); + // "4 " + SHS command (0x58) + let buf = [0x34, 0x20, 0x58]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 3); + assert_eq!(ctx.current_state.layout_state.cell_spacing.col, 4); + } + + #[test] + fn test_parse_csi_svs() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x32, 0x34, 0x20, 0x59]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 4); + assert_eq!(ctx.current_state.layout_state.cell_spacing.row, 24); + } + + #[test] + fn test_parse_csi_unknown() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x30, 0x20, 0xFF]; + let consumed = parse_csi(&mut ctx, &buf); + assert_eq!(consumed, 3); // still consumes the bytes + } + + // ---- parse_command ---- + + #[test] + fn test_parse_command_nul() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x00]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 1); + } + + #[test] + fn test_parse_command_aps() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x1C, 0x41, 0x42]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 3); + assert_eq!(ctx.current_state.layout_state.cursor_pos.x, 72); + assert_eq!(ctx.current_state.layout_state.cursor_pos.y, 72); + } + + #[test] + fn test_parse_command_fg_color() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x82]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 1); + assert_eq!(ctx.current_state.fg_color, DEFAULT_CLUT[2]); // green + } + + #[test] + fn test_parse_command_ssz() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x88]; + parse_command(&mut ctx, &buf); + assert_eq!(ctx.current_state.layout_state.font_scale.fscx, 50); + assert_eq!(ctx.current_state.layout_state.font_scale.fscy, 50); + } + + #[test] + fn test_parse_command_msz() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x89]; // MSZ + parse_command(&mut ctx, &buf); + assert_eq!(ctx.current_state.layout_state.font_scale.fscx, 200); + assert_eq!(ctx.current_state.layout_state.font_scale.fscy, 200); + } + + #[test] + fn test_parse_command_nsz() { + let mut ctx = IsdbSubContext::new(); + ctx.current_state.layout_state.font_scale.fscx = 50; + ctx.current_state.layout_state.font_scale.fscy = 50; + let buf = [0x8A]; // NSZ + parse_command(&mut ctx, &buf); + assert_eq!(ctx.current_state.layout_state.font_scale.fscx, 100); + assert_eq!(ctx.current_state.layout_state.font_scale.fscy, 100); + } + + #[test] + fn test_parse_command_col_foreground() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x90, 0x43]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 2); + assert_eq!(ctx.current_state.fg_color, DEFAULT_CLUT[3]); + } + + #[test] + fn test_parse_command_col_background() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x90, 0x52]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 2); + assert_eq!(ctx.current_state.bg_color, DEFAULT_CLUT[2]); + } + + #[test] + fn test_parse_command_col_set_clut() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x90, 0x20, 0x42]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 3); + assert_eq!(ctx.current_state.clut_high_idx, 2); + } + + #[test] + fn test_parse_command_time() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x9D, 0x00, 0x00]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 3); + } + + #[test] + fn test_parse_command_csi() { + let mut ctx = IsdbSubContext::new(); + let buf = [0x9B, 0x34, 0x20, 0x58]; + let consumed = parse_command(&mut ctx, &buf); + assert_eq!(consumed, 4); + assert_eq!(ctx.current_state.layout_state.cell_spacing.col, 4); + } +} diff --git a/src/rust/src/isdb/mod.rs b/src/rust/src/isdb/mod.rs new file mode 100644 index 000000000..08afe2a0e --- /dev/null +++ b/src/rust/src/isdb/mod.rs @@ -0,0 +1,182 @@ +//! ISDB (Integrated Services Digital Broadcasting) subtitle decoder module. +//! +//! This module provides Rust implementations for decoding ISDB caption subtitles, +//! following the ARIB STD-B24 standard for Brazilian digital TV. +//! +//! # Submodules +//! For the C-to-Rust function mapping, see individual submodule([`leaf`], [`mid`], [`high`], [`types`]) docs : +//! - [`types`] - ISDB-specific types, enums, and constants +//! - [`leaf`] - Low-level helper functions +//! - [`mid`] - Mid-level control processing +//! - [`high`] - High-level parsers +//! +//! # Conversion Guide for FFI entry points for the ISDB subtitle decoder +//! Thin wrappers that bridge C callers to the Rust implementation. +//! +//! | C (ccx_decoders_isdb.c) | Rust | +//! |--------------------------------|----------------------------------| +//! | `init_isdb_decoder` | [`ccxr_init_isdb_decoder`] | +//! | `delete_isdb_decoder` | [`ccxr_delete_isdb_decoder`] | +//! | `isdb_set_global_time` | [`ccxr_isdb_set_global_time`] | +//! | `isdbsub_decode` | [`ccxr_isdbsub_decode`] | + +use crate::bindings::{cc_subtitle, ccx_encoding_type_CCX_ENC_UTF_8, lib_cc_decode}; +use crate::isdb::high::isdb_parse_data_group; +use crate::isdb::types::IsdbSubContext; +use std::ffi::c_void; +use std::os::raw::c_char; +use std::ptr::null_mut; + +mod high; +mod leaf; +mod mid; +mod types; + +/// Initialize a new ISDB decoder context. +/// Returns an opaque pointer to the context, or null on failure. +/// +/// # Safety +/// The returned pointer must be freed with `ccxr_delete_isdb_decoder`. +#[no_mangle] +pub extern "C" fn ccxr_init_isdb_decoder() -> *mut c_void { + let ctx = Box::new(IsdbSubContext::new()); + Box::into_raw(ctx) as *mut c_void +} + +/// Delete an ISDB decoder context. +/// +/// # Safety +/// - `ctx` must be a valid pointer returned by `ccxr_init_isdb_decoder`, +/// or null (in which case this is a no-op). +/// - After this call, the pointer is invalid and must not be used. +#[no_mangle] +pub unsafe extern "C" fn ccxr_delete_isdb_decoder(ctx: *mut *mut c_void) { + // if ctx points to null + if ctx.is_null() { + return; + } + + // if what ctx points to is null + let ptr = *ctx; + if ptr.is_null() { + return; + } + + // reconstruct box and drop it automatically as box goes out of scope - the rust way + let _ = Box::from_raw(ptr as *mut IsdbSubContext); + // set ctx to null + *ctx = null_mut(); +} + +/// Set the global timestamp for the ISDB decoder. +/// +/// # Safety +/// - `ctx` must be a valid pointer returned by `ccxr_init_isdb_decoder`. +#[no_mangle] +pub unsafe extern "C" fn ccxr_isdb_set_global_time(ctx: *mut c_void, timestamp: u64) -> i32 { + if ctx.is_null() { + return -1; + } + let ctx = &mut *(ctx as *mut IsdbSubContext); + ctx.timestamp = timestamp; + + 0 // CCX_OK +} + +// helper to ccxr_isdbsub_decode (function) +extern "C" { + fn add_cc_sub_text( + sub: *mut cc_subtitle, + str: *mut c_char, + start_time: i64, + end_time: i64, + info: *mut c_char, + mode: *mut c_char, + encoding: u32, + ) -> i32; +} + +/// Decode ISDB subtitles from a PES packet. +/// +/// # Safety +/// - `dec_ctx` must be a valid pointer to a `lib_cc_decode` whose `private_data` +/// was returned by `ccxr_init_isdb_decoder`. +/// - `buf` must point to `buf_size` valid bytes. +/// - `sub` must be a valid pointer to a `cc_subtitle`. +#[allow(clippy::unnecessary_cast)] +#[no_mangle] +pub unsafe extern "C" fn ccxr_isdbsub_decode( + dec_ctx: *mut lib_cc_decode, + buf: *const u8, + buf_size: usize, + sub: *mut cc_subtitle, +) -> i32 { + if dec_ctx.is_null() || buf.is_null() || sub.is_null() || buf_size == 0 { + return -1; + } + + let dec = &*dec_ctx; + let data = std::slice::from_raw_parts(buf, buf_size); + + let mut pos: usize = 0; + + // check synced PES marker (0x80) + if data[pos] != 0x80 { + log::debug!("Not a Synchronised PES"); + return -1; + } + pos += 1; + + // skip pvt data stream byte (0xFF) + pos += 1; + + if pos >= data.len() { + return -1; + } + + // parse header length; skip header + let header_end = pos + 1 + (data[pos] as usize & 0x0f); + pos += 1; + + while pos < header_end && pos < data.len() { + pos += 1; + } + + if pos >= data.len() { + return -1; + } + + // get Rust ISDB context from private_data + let ctx = &mut *(dec.private_data as *mut IsdbSubContext); + ctx.cfg_no_rollup = dec.no_rollup != 0; + + let (ret, subtitle_data) = isdb_parse_data_group(ctx, &data[pos..]); + + if ret < 0 { + return -1; + } + + if let Some(sub_data) = subtitle_data { + let mut text_buf = sub_data.text; + text_buf.push(0); + + let mut info = b"NA\0".to_vec(); + let mut mode = b"ISDB\0".to_vec(); + + add_cc_sub_text( + sub, + text_buf.as_mut_ptr() as *mut c_char, + sub_data.start_time as i64, + sub_data.end_time as i64, + info.as_mut_ptr() as *mut c_char, + mode.as_mut_ptr() as *mut c_char, + ccx_encoding_type_CCX_ENC_UTF_8 as u32, + ); + + if (*sub).start_time == (*sub).end_time { + (*sub).end_time += 2; // if start_time == end_time, extend by 2ms (matching c code) + } + } + + 1 // success! +} diff --git a/src/rust/src/isdb/types.rs b/src/rust/src/isdb/types.rs new file mode 100644 index 000000000..ae5e0df70 --- /dev/null +++ b/src/rust/src/isdb/types.rs @@ -0,0 +1,576 @@ +//! ISDB subtitle decoder types, structures, and constants. +//! +//! # Key Types +//! - [`IsdbSubContext`] - Main decoder state (Rust-owned, passed as `void*` through C) +//! - [`IsdbSubState`] - Current decoding state (layout, CLUT, rollup mode) +//! - [`IsdbSubLayout`] - Display layout (format, position, font, spacing) +//! - [`IsdbText`] - Single text node with buffer and position +//! - [`IsdbSubtitleData`] - Decoded subtitle output (text + timestamps) +//! +//! # Enums +//! - [`WritingFormat`] - Display format variants (horizontal/vertical, resolution) +//! - [`IsdbCcComposition`] - Character composition modes +//! - [`IsdbTmd`] - Time control mode +//! +//! # Constants +//! +//! - [`DEFAULT_CLUT`] - 128-entry default Color Look-Up Table +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_isdb.c) | Rust | +//! |------------------------------------|----------------------------------| +//! | `ISDBSubContext` struct | [`IsdbSubContext`] | +//! | `struct ISDBText` | [`IsdbText`] | +//! | `enum writing_format` | [`WritingFormat`] | +//! | `enum isdb_CC_composition` | [`IsdbCcComposition`] | +//! | `enum isdb_tmd` | [`IsdbTmd`] | +//! | cursor pos fields (x, y) | [`IsdbPos`] | +//! | display area fields (w, h, x, y) | [`DispArea`] | +//! | font scale fields (fscx, fscy) | [`FontScale`] | +//! | cell spacing fields (col, row) | [`CellSpacing`] | +//! | offset time fields | [`OffsetTime`] | +//! | layout fields in `ISDBSubContext` | [`IsdbSubLayout`] | +//! | state fields in `ISDBSubContext` | [`IsdbSubState`] | +//! | `add_cc_sub_text` output params | [`IsdbSubtitleData`] | +//! | `RGBA(r,g,b,a)` macro | [`rgba`] | +//! | `default_clut` array | [`DEFAULT_CLUT`] | +//! | `prev_timestamp = UINT_MAX` | `prev_timestamp: Option` | +//! | `list_head text_list_head` | `text_list: Vec` | +//! | `list_head buffered_text` | `buffered_text: Vec` | +//! | `allocate_text_node` + `malloc` | [`IsdbText::new`] | +//! | `init_layout` | [`IsdbSubLayout::default`] | +//! +// ---- unused (but defined anyway for future purposes) below ---- +//! +// | `enum fontSize` | [`FontSize`] | +// | `enum csi_command` | [`CsiCommand`] | +// | foreground/background color fields | [`Color`] | +// | `reserve_buf` | [`IsdbText::reserve`] | + +/// format: 0xAABBGGRR (alpha inverted: 255-a) +pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> u32 { + ((255 - a) as u32) << 24 // transparency + | (b as u32) << 16 // blue + | (g as u32) << 8 // green + | (r as u32) // red +} + +/// default color lookup table +pub static DEFAULT_CLUT: [u32; 128] = [ + // 0-7 + rgba(0, 0, 0, 255), + rgba(255, 0, 0, 255), + rgba(0, 255, 0, 255), + rgba(255, 255, 0, 255), + rgba(0, 0, 255, 255), + rgba(255, 0, 255, 255), + rgba(0, 255, 255, 255), + rgba(255, 255, 255, 255), + // 8-15 + rgba(0, 0, 0, 0), + rgba(170, 0, 0, 255), + rgba(0, 170, 0, 255), + rgba(170, 170, 0, 255), + rgba(0, 0, 170, 255), + rgba(170, 0, 170, 255), + rgba(0, 170, 170, 255), + rgba(170, 170, 170, 255), + // 16-23 + rgba(0, 0, 85, 255), + rgba(0, 85, 0, 255), + rgba(0, 85, 85, 255), + rgba(0, 85, 170, 255), + rgba(0, 85, 255, 255), + rgba(0, 170, 85, 255), + rgba(0, 170, 255, 255), + rgba(0, 255, 85, 255), + // 24-31 + rgba(0, 255, 170, 255), + rgba(85, 0, 0, 255), + rgba(85, 0, 85, 255), + rgba(85, 0, 170, 255), + rgba(85, 0, 255, 255), + rgba(85, 85, 0, 255), + rgba(85, 85, 85, 255), + rgba(85, 85, 170, 255), + // 32-39 + rgba(85, 85, 255, 255), + rgba(85, 170, 0, 255), + rgba(85, 170, 85, 255), + rgba(85, 170, 170, 255), + rgba(85, 170, 255, 255), + rgba(85, 255, 0, 255), + rgba(85, 255, 85, 255), + rgba(85, 255, 170, 255), + // 40-47 + rgba(85, 255, 255, 255), + rgba(170, 0, 85, 255), + rgba(170, 0, 255, 255), + rgba(170, 85, 0, 255), + rgba(170, 85, 85, 255), + rgba(170, 85, 170, 255), + rgba(170, 85, 255, 255), + rgba(170, 170, 85, 255), + // 48-55 + rgba(170, 170, 255, 255), + rgba(170, 255, 0, 255), + rgba(170, 255, 85, 255), + rgba(170, 255, 170, 255), + rgba(170, 255, 255, 255), + rgba(255, 0, 85, 255), + rgba(255, 0, 170, 255), + rgba(255, 85, 0, 255), + // 56-63 + rgba(255, 85, 85, 255), + rgba(255, 85, 170, 255), + rgba(255, 85, 255, 255), + rgba(255, 170, 0, 255), + rgba(255, 170, 85, 255), + rgba(255, 170, 170, 255), + rgba(255, 170, 255, 255), + rgba(255, 255, 85, 255), + // 64 + rgba(255, 255, 170, 255), + // 65-127: zeroed (unused in og C code) + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 65-80 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 81-96 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 97-112 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 113-127 +]; + +// ---- enums ---- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum WritingFormat { + #[default] + HorizontalStdDensity = 0, + VerticalStdDensity = 1, + HorizontalHighDensity = 2, + VerticalHighDensity = 3, + HorizontalWesternLang = 4, + Horizontal1920x1080 = 5, + Vertical1920x1080 = 6, + Horizontal960x540 = 7, + Vertical960x540 = 8, + Horizontal720x480 = 9, + Vertical720x480 = 10, + Horizontal1280x720 = 11, + Vertical1280x720 = 12, + HorizontalCustom = 100, + None = 101, +} + +impl WritingFormat { + pub fn is_horizontal(&self) -> bool { + matches!( + self, + WritingFormat::HorizontalStdDensity + | WritingFormat::HorizontalHighDensity + | WritingFormat::HorizontalWesternLang + | WritingFormat::Horizontal1920x1080 + | WritingFormat::Horizontal960x540 + | WritingFormat::Horizontal720x480 + | WritingFormat::Horizontal1280x720 + | WritingFormat::HorizontalCustom + ) + } + + pub fn from_i32(value: i32) -> Self { + match value { + 0 => WritingFormat::HorizontalStdDensity, + 1 => WritingFormat::VerticalStdDensity, + 2 => WritingFormat::HorizontalHighDensity, + 3 => WritingFormat::VerticalHighDensity, + 4 => WritingFormat::HorizontalWesternLang, + 5 => WritingFormat::Horizontal1920x1080, + 6 => WritingFormat::Vertical1920x1080, + 7 => WritingFormat::Horizontal960x540, + 8 => WritingFormat::Vertical960x540, + 9 => WritingFormat::Horizontal720x480, + 10 => WritingFormat::Vertical720x480, + 11 => WritingFormat::Horizontal1280x720, + 12 => WritingFormat::Vertical1280x720, + 100 => WritingFormat::HorizontalCustom, + _ => WritingFormat::None, + } + } +} +// commented to sort clippy warnings - also unused in c code - defined anyway for future purposes +// #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +// #[repr(i32)] +// pub enum FontSize { +// Small = 0, +// Middle = 1, +// #[default] +// Standard = 2, +// } + +// commented to sort clippy warnings - also unused in c code - defined anyway for future purposes +// CSI (Control Sequence Introducer) commands. +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// #[repr(u8)] +// pub enum CsiCommand { +// /// GSM - Character Deformation +// Gsm = 0x42, +// /// SWF - Set Writing Format +// Swf = 0x53, +// /// CCC - Composite Character Composition +// Ccc = 0x54, +// /// SDF - Set Display Format +// Sdf = 0x56, +// /// SSM - Character composition dot designation +// Ssm = 0x57, +// /// SHS - Set Horizontal Spacing +// Shs = 0x58, +// /// SVS - Set Vertical Spacing +// Svs = 0x59, +// /// PLD - Partially Line Down +// Pld = 0x5B, +// /// PLU - Partially Line Up +// Plu = 0x5C, +// /// GAA - Colouring block +// Gaa = 0x5D, +// /// SRC - Raster Colour Designation +// Src = 0x5E, +// /// SDP - Set Display Position +// Sdp = 0x5F, +// /// ACPS - Active Coordinate Position Set +// Acps = 0x61, +// /// TCC - Switch control +// Tcc = 0x62, +// /// ORN - Ornament Control +// Orn = 0x63, +// /// MDF - Font +// Mdf = 0x64, +// /// CFS - Character Font Set +// Cfs = 0x65, +// /// XCS - External Character Set +// Xcs = 0x66, +// /// PRA - Built-in sound replay +// Pra = 0x68, +// /// ACS - Alternative Character Set +// Acs = 0x69, +// /// RCS - Raster Color Command +// Rcs = 0x6E, +// /// SCS - Skip Character Set +// Scs = 0x6F, +// } + +/// ISDB Closed Caption composition mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum IsdbCcComposition { + #[default] + None = 0, + And = 2, + Or = 3, + Xor = 4, +} + +impl IsdbCcComposition { + pub fn from_i32(value: i32) -> Self { + match value { + 2 => IsdbCcComposition::And, + 3 => IsdbCcComposition::Or, + 4 => IsdbCcComposition::Xor, + _ => IsdbCcComposition::None, + } + } +} +// commented to sort clippy warnings - also unused in c code - defined anyway for future purposes +// Color indices for ISDB. +// #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +// #[repr(i32)] +// pub enum Color { +// #[default] +// Black = 0, +// FiRed = 1, +// FiGreen = 2, +// FiYellow = 3, +// FiBlue = 4, +// FiMagenta = 5, +// FiCyan = 6, +// FiWhite = 7, +// Transparent = 8, +// HiRed = 9, +// HiGreen = 10, +// HiYellow = 11, +// HiBlue = 12, +// HiMagenta = 13, +// HiCyan = 14, +// HiWhite = 15, +// } + +/// ISDB Time Mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum IsdbTmd { + #[default] + Free = 0, + RealTime = 1, + OffsetTime = 2, +} + +impl IsdbTmd { + pub fn from_i32(value: i32) -> Self { + match value { + 1 => IsdbTmd::RealTime, + 2 => IsdbTmd::OffsetTime, + _ => IsdbTmd::Free, + } + } +} + +// ---- structs ---- + +/// Position (x, y) coordinates. +#[derive(Debug, Clone, Copy, Default)] +pub struct IsdbPos { + pub x: i32, + pub y: i32, +} + +/// Display area dimensions. +#[derive(Debug, Default, Clone, Copy)] +pub struct DispArea { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, +} + +/// Font scale in percent. +#[derive(Debug, Clone, Copy)] +pub struct FontScale { + pub fscx: i32, + pub fscy: i32, +} + +impl Default for FontScale { + fn default() -> Self { + Self { + fscx: 100, + fscy: 100, + } + } +} + +/// Cell spacing (column and row). +#[derive(Debug, Clone, Copy, Default)] +pub struct CellSpacing { + pub col: i32, + pub row: i32, +} + +/// Offset time for ISDB captions. +#[derive(Debug, Clone, Copy, Default)] +pub struct OffsetTime { + pub hour: i32, + pub min: i32, + pub sec: i32, + pub milli: i32, +} + +/// ISDB subtitle layout state. +#[derive(Debug, Clone)] +pub struct IsdbSubLayout { + pub format: WritingFormat, + pub display_area: DispArea, + pub font_size: i32, + pub font_scale: FontScale, + pub cell_spacing: CellSpacing, + pub cursor_pos: IsdbPos, + pub ccc: IsdbCcComposition, + pub acps: [i32; 2], +} + +impl Default for IsdbSubLayout { + fn default() -> Self { + Self { + format: WritingFormat::default(), + display_area: DispArea::default(), + font_size: 36, + font_scale: FontScale::default(), + cell_spacing: CellSpacing::default(), + cursor_pos: IsdbPos::default(), + ccc: IsdbCcComposition::default(), + acps: [0, 0], + } + } +} + +/// ISDB subtitle state (colors, layout, flags). +// fields `auto_display`, `need_init`, and `mat_color` are never read +// hence commented to sort clippy warnings - also unused in c code - defined anyway for future purposes +#[derive(Debug, Clone, Default)] +pub struct IsdbSubState { + // pub auto_display: bool, + pub rollup_mode: bool, + // pub need_init: bool, + pub clut_high_idx: u8, + pub fg_color: u32, + pub bg_color: u32, + pub hfg_color: u32, + pub hbg_color: u32, + // pub mat_color: u32, + pub raster_color: u32, + pub layout_state: IsdbSubLayout, +} + +/// A single text entry in the ISDB subtitle display. +// fields `txt_tail` and `timestamp` are never read - +// hence commented to sort clippy warnings - also unused in c code - defined anyway for future purposes +#[derive(Debug, Clone)] +pub struct IsdbText { + pub buf: Vec, + pub pos: IsdbPos, + // pub txt_tail: usize, + // pub timestamp: u64, +} + +impl IsdbText { + /// create new text node w default buffer size + pub fn new(pos: IsdbPos) -> Self { + Self { + buf: Vec::with_capacity(128), + pos, + // txt_tail: 0, + // timestamp: 0, + } + } + + // rust counterpart of reserve_buf C function - defined anyway for tracking purpose + // pub fn reserve(&mut self, additional: usize) { + // self.buf.reserve(additional); + // } +} + +/// Main ISDB subtitle decoder context. +#[derive(Debug, Clone)] +pub struct IsdbSubContext { + pub nb_char: i32, + pub nb_line: i32, + pub timestamp: u64, + pub prev_timestamp: Option, + pub text_list: Vec, + pub buffered_text: Vec, + pub current_state: IsdbSubState, + pub tmd: IsdbTmd, + pub nb_lang: i32, + pub offset_time: OffsetTime, + pub dmf: u8, + pub dc: u8, + pub cfg_no_rollup: bool, +} + +impl IsdbSubContext { + pub fn new() -> Self { + Self { + nb_char: 0, + nb_line: 0, + timestamp: 0, + prev_timestamp: None, + text_list: Vec::new(), + buffered_text: Vec::new(), + current_state: IsdbSubState::default(), + tmd: IsdbTmd::default(), + nb_lang: 0, + offset_time: OffsetTime::default(), + dmf: 0, + dc: 0, + cfg_no_rollup: false, + } + } +} + +impl Default for IsdbSubContext { + fn default() -> Self { + Self::new() + } +} + +/// Subtitle data produced by the ISDB decoder. +/// Used to pass data from Rust to the FFI layer. +pub struct IsdbSubtitleData { + pub text: Vec, + pub start_time: u64, + pub end_time: u64, +} + +#[cfg(test)] +mod tests { + use crate::isdb::types::*; + + // ---- init_layout (covered by IsdbSubLayout::default()) ---- + + #[test] + fn test_layout_defaults() { + let ls = IsdbSubLayout::default(); + assert_eq!(ls.font_size, 36); + assert_eq!(ls.display_area.x, 0); + assert_eq!(ls.display_area.y, 0); + assert_eq!(ls.font_scale.fscx, 100); + assert_eq!(ls.font_scale.fscy, 100); + } +} diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index f4b838cda..5690e9ea9 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -25,6 +25,7 @@ pub mod file_functions; #[cfg(feature = "hardsubx_ocr")] pub mod hardsubx; pub mod hlist; +pub mod isdb; pub mod libccxr_exports; pub mod parser; pub mod track_lister; diff --git a/src/rust/wrapper.h b/src/rust/wrapper.h index 25b90c297..01f0cca4a 100644 --- a/src/rust/wrapper.h +++ b/src/rust/wrapper.h @@ -14,3 +14,4 @@ #include "../lib_ccx/ccx_gxf.h" #include "../lib_ccx/ccx_demuxer_mxf.h" #include "../lib_ccx/cc_bitstream.h" +#include "../lib_ccx/ccx_decoders_isdb.h"