From e474692ff9136d95066dfe1632a21af8cdb55e3a Mon Sep 17 00:00:00 2001 From: dhanush varma Date: Thu, 11 Dec 2025 00:57:28 +0530 Subject: [PATCH 01/18] chore: ignore local build artifacts and backups --- .gitignore | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.gitignore b/.gitignore index 986573c9e..98a856c50 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,19 @@ windows/*/debug/* windows/*/CACHEDIR.TAG windows/.rustc_info.json linux/configure~ + +# local junk +build-system/ +*.backup +*.save +*.patch +*.a +README.srt +no_subs.ts + +fix_hardsubx.cmake +fix_libraries.cmake +mac/configure~ +*.current + + From 0499f4f43c23549ff9c4b73587b4d8fe450142d4 Mon Sep 17 00:00:00 2001 From: dhanush varma Date: Thu, 11 Dec 2025 01:37:39 +0530 Subject: [PATCH 02/18] rust: add SMPTE-TT encoder skeleton --- src/rust/src/encoder/mod.rs | 2 + src/rust/src/encoder/smptett.rs | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/rust/src/encoder/smptett.rs diff --git a/src/rust/src/encoder/mod.rs b/src/rust/src/encoder/mod.rs index 853abae5a..5d2d75853 100644 --- a/src/rust/src/encoder/mod.rs +++ b/src/rust/src/encoder/mod.rs @@ -7,6 +7,8 @@ use std::os::raw::{c_int, c_uchar}; pub mod common; pub mod g608; pub mod simplexml; +pub mod smptett; + /// # Safety /// This function is unsafe because it deferences to raw pointers and performs operations on pointer slices. #[no_mangle] diff --git a/src/rust/src/encoder/smptett.rs b/src/rust/src/encoder/smptett.rs new file mode 100644 index 000000000..2177d6cbb --- /dev/null +++ b/src/rust/src/encoder/smptett.rs @@ -0,0 +1,75 @@ +use crate::bindings::{ + cc_subtitle, + eia608_screen, + encoder_ctx, +}; + +use crate::encoder::common::encode_line; +use crate::libccxr_exports::time::ccxr_millis_to_time; + +use std::os::raw::c_int; + +/// Core helper: write a SMPTE-TT paragraph from text + times +fn write_stringz_as_smptett( + text: &str, + context: &mut encoder_ctx, + ms_start: i64, + ms_end: i64, +) { + let mut h1 = 0; + let mut m1 = 0; + let mut s1 = 0; + let mut ms1 = 0; + + let mut h2 = 0; + let mut m2 = 0; + let mut s2 = 0; + let mut ms2 = 0; + + unsafe { + ccxr_millis_to_time(ms_start, &mut h1, &mut m1, &mut s1, &mut ms1); + ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); + } + + let header = format!( + "

", + h1, m1, s1, ms1, + h2, m2, s2, ms2 + ); + + encode_line(context, &mut context.buffer, header.as_bytes()); + + encode_line(context, &mut context.buffer, line.as_bytes()); + encode_line(context, &mut context.buffer, &context.encoded_crlf); +} +for line in text.split('\n') { + encode_line(context, &mut context.buffer, line.as_bytes()); + encode_line(context, &mut context.buffer, &context.encoded_crlf[..]); +} + + encode_line(context, &mut context.buffer, b"

"); +} + +/// SMPTE-TT encoder entry for CC buffer (CEA-608) +pub fn write_cc_buffer_as_smptett( + _data: &mut eia608_screen, + _context: &mut encoder_ctx, +) -> c_int { + 0 +} + +/// SMPTE-TT encoder entry for subtitle list (text-based) +pub fn write_cc_subtitle_as_smptett( + _sub: &mut cc_subtitle, + _context: &mut encoder_ctx, +) -> c_int { + 0 +} + +/// SMPTE-TT encoder entry for bitmap subtitles (OCR) +pub fn write_cc_bitmap_as_smptett( + _sub: &mut cc_subtitle, + _context: &mut encoder_ctx, +) -> c_int { + 0 +} From 0c510c1d129b084a96710a15632ce197cd2a89d2 Mon Sep 17 00:00:00 2001 From: dhanush varma Date: Fri, 12 Dec 2025 20:21:07 +0530 Subject: [PATCH 03/18] rust: add initial SMPTE-TT encoder skeleton --- src/rust/src/encoder/smptett.rs | 73 ++++++++++++--------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/src/rust/src/encoder/smptett.rs b/src/rust/src/encoder/smptett.rs index 2177d6cbb..a6cdea5e7 100644 --- a/src/rust/src/encoder/smptett.rs +++ b/src/rust/src/encoder/smptett.rs @@ -1,34 +1,24 @@ -use crate::bindings::{ - cc_subtitle, - eia608_screen, - encoder_ctx, -}; - +use crate::bindings::{cc_subtitle, eia608_screen}; +use crate::encoder_ctx; use crate::encoder::common::encode_line; use crate::libccxr_exports::time::ccxr_millis_to_time; use std::os::raw::c_int; -/// Core helper: write a SMPTE-TT paragraph from text + times + + fn write_stringz_as_smptett( text: &str, context: &mut encoder_ctx, ms_start: i64, ms_end: i64, ) { - let mut h1 = 0; - let mut m1 = 0; - let mut s1 = 0; - let mut ms1 = 0; - - let mut h2 = 0; - let mut m2 = 0; - let mut s2 = 0; - let mut ms2 = 0; + let (mut h1, mut m1, mut s1, mut ms1) = (0, 0, 0, 0); + let (mut h2, mut m2, mut s2, mut ms2) = (0, 0, 0, 0); unsafe { ccxr_millis_to_time(ms_start, &mut h1, &mut m1, &mut s1, &mut ms1); - ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); + ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); } let header = format!( @@ -37,39 +27,30 @@ fn write_stringz_as_smptett( h2, m2, s2, ms2 ); - encode_line(context, &mut context.buffer, header.as_bytes()); + // Turn raw buffer ptr into slice + let buffer = unsafe { + std::slice::from_raw_parts_mut(context.buffer , context.capacity as usize) + }; - encode_line(context, &mut context.buffer, line.as_bytes()); - encode_line(context, &mut context.buffer, &context.encoded_crlf); -} -for line in text.split('\n') { - encode_line(context, &mut context.buffer, line.as_bytes()); - encode_line(context, &mut context.buffer, &context.encoded_crlf[..]); -} + encode_line(context, buffer, header.as_bytes()); - encode_line(context, &mut context.buffer, b"

"); -} + for line in text.split('\n') { + let buffer = unsafe { + std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) + }; + encode_line(context, buffer, line.as_bytes()); -/// SMPTE-TT encoder entry for CC buffer (CEA-608) -pub fn write_cc_buffer_as_smptett( - _data: &mut eia608_screen, - _context: &mut encoder_ctx, -) -> c_int { - 0 -} + let crlf = context.encoded_crlf; -/// SMPTE-TT encoder entry for subtitle list (text-based) -pub fn write_cc_subtitle_as_smptett( - _sub: &mut cc_subtitle, - _context: &mut encoder_ctx, -) -> c_int { - 0 + let buffer = unsafe { + std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) + }; + encode_line(context, buffer, &crlf); } -/// SMPTE-TT encoder entry for bitmap subtitles (OCR) -pub fn write_cc_bitmap_as_smptett( - _sub: &mut cc_subtitle, - _context: &mut encoder_ctx, -) -> c_int { - 0 + let buffer = unsafe { + std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) + }; + encode_line(context, buffer, b"

"); } + From 8107a94190191f12e6ed7d1e5bde532fa756d2bb Mon Sep 17 00:00:00 2001 From: dhanush varma Date: Fri, 12 Dec 2025 20:23:03 +0530 Subject: [PATCH 04/18] rust: add initial SMPTE-TT encoder skeleton --- src/rust/Cargo.lock | 147 +++----------------------------------------- 1 file changed, 7 insertions(+), 140 deletions(-) diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 8f2bc03fc..8181bb82c 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -109,29 +109,6 @@ dependencies = [ "which", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.108", - "which", -] - [[package]] name = "bindgen" version = "0.71.1" @@ -141,7 +118,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "log", "prettyplease", "proc-macro2", @@ -164,31 +141,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "bon" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" -dependencies = [ - "darling", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.108", -] - [[package]] name = "camino" version = "1.2.1" @@ -210,8 +162,7 @@ dependencies = [ "num-integer", "palette", "pkg-config", - "rsmpeg 0.14.2+ffmpeg.6.1", - "rsmpeg 0.18.0+ffmpeg.8.0", + "rsmpeg", "serial_test", "strum_macros 0.25.3", "tempfile", @@ -307,41 +258,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.108", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.108", -] - [[package]] name = "deranged" version = "0.5.5" @@ -434,12 +350,6 @@ dependencies = [ "toml", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -667,12 +577,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -710,15 +614,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1140,25 +1035,11 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rsmpeg" -version = "0.14.2+ffmpeg.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927012cd6ae43519f519741f4a69602ce3a47cf84750784da124dffd03527cc0" -dependencies = [ - "libc", - "paste", - "rusty_ffmpeg 0.13.3+ffmpeg.6.1", - "thiserror 1.0.69", -] - -[[package]] -name = "rsmpeg" -version = "0.18.0+ffmpeg.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523351495c9ff0bf4b99ed1f42f1415fc709526ddb63526cff85022b387c5811" +version = "0.15.3+ffmpeg.7.1" +source = "git+https://github.com/CCExtractor/rsmpeg.git#2ddc3c05af684893ee9c6874924fc112bff0401e" dependencies = [ - "bon", "paste", - "rusty_ffmpeg 0.16.7+ffmpeg.8", + "rusty_ffmpeg", "thiserror 2.0.17", ] @@ -1215,30 +1096,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rusty_ffmpeg" -version = "0.13.3+ffmpeg.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716adffa5f909c8533611b1dab9ab5666bece35687845865b75ed6a990fc239c" -dependencies = [ - "bindgen 0.69.5", - "camino", - "libc", - "once_cell", - "pkg-config", - "vcpkg", -] - [[package]] name = "rusty_ffmpeg" version = "0.16.7+ffmpeg.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25d40a46450059278c9f9f2616018910b647877a66a2093a83f115f59763967" +source = "git+https://github.com/CCExtractor/rusty_ffmpeg.git#bdbaf09d5fdda4f3fd1278cdd11abe2a47dd9a99" dependencies = [ "bindgen 0.71.1", "camino", "once_cell", "pkg-config", + "vcpkg", ] [[package]] From 0820e95ded4f01efcc64f1dcaea2b16c66dcafa8 Mon Sep 17 00:00:00 2001 From: dhanush varma Date: Fri, 12 Dec 2025 20:49:42 +0530 Subject: [PATCH 05/18] =?UTF-8?q?Fix=20SMPTE-TT=20encoder=20helper:=20poin?= =?UTF-8?q?ter=E2=86=92slice=20conversion,=20XML=20escaping,=20borrow=20fi?= =?UTF-8?q?x,=20unimplemented=20stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rust/src/encoder/smptett.rs | 89 ++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/src/rust/src/encoder/smptett.rs b/src/rust/src/encoder/smptett.rs index a6cdea5e7..00087b767 100644 --- a/src/rust/src/encoder/smptett.rs +++ b/src/rust/src/encoder/smptett.rs @@ -1,56 +1,99 @@ -use crate::bindings::{cc_subtitle, eia608_screen}; -use crate::encoder_ctx; +use crate::bindings::{cc_subtitle, eia608_screen, encoder_ctx}; use crate::encoder::common::encode_line; use crate::libccxr_exports::time::ccxr_millis_to_time; use std::os::raw::c_int; - +fn escape_xml(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + _ => out.push(c), + } + } + out +} fn write_stringz_as_smptett( text: &str, context: &mut encoder_ctx, ms_start: i64, ms_end: i64, -) { +){ let (mut h1, mut m1, mut s1, mut ms1) = (0, 0, 0, 0); let (mut h2, mut m2, mut s2, mut ms2) = (0, 0, 0, 0); unsafe { ccxr_millis_to_time(ms_start, &mut h1, &mut m1, &mut s1, &mut ms1); - ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); + ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); } + // Convert raw pointer buffer → Rust slice + let buffer: &mut [u8] = unsafe { + std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) + }; + + // Copy CRLF slice so we don't immutably borrow from context + let crlf = context.encoded_crlf; + let header = format!( "

", h1, m1, s1, ms1, h2, m2, s2, ms2 ); - // Turn raw buffer ptr into slice - let buffer = unsafe { - std::slice::from_raw_parts_mut(context.buffer , context.capacity as usize) - }; - encode_line(context, buffer, header.as_bytes()); + encode_line(context, buffer, &crlf); for line in text.split('\n') { - let buffer = unsafe { - std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) - }; - encode_line(context, buffer, line.as_bytes()); - - let crlf = context.encoded_crlf; + let escaped = escape_xml(line); + encode_line(context, buffer, escaped.as_bytes()); + encode_line(context, buffer, &crlf); + } - let buffer = unsafe { - std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) - }; + encode_line(context, buffer, b"

"); encode_line(context, buffer, &crlf); } - let buffer = unsafe { - std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) - }; - encode_line(context, buffer, b"

"); + + +pub fn write_cc_buffer_as_smptett( + _: &mut encoder_ctx, + _: &eia608_screen, + _: i64, + _: i64, +) -> c_int { + unimplemented!() } +pub fn write_cc_subtitle_as_smptett(_: &mut encoder_ctx, _: &cc_subtitle) -> c_int { + unimplemented!() +} + +pub fn write_cc_bitmap_as_smptett( + _: &mut encoder_ctx, + _: &[u8], + _: usize, + _: usize, +) -> c_int { + unimplemented!() +} + +#[cfg(test)] +mod tests { + use super::escape_xml; + + #[test] + fn test_smptett_escape_basic() { + assert_eq!(escape_xml("a & b < c"), "a & b < c"); + assert_eq!( + escape_xml("\"quote\" 'apos' & < >"), + ""quote" 'apos' & < >" + ); + } +} From b71ad400a8e1c89d59a548981afee48f750b1354 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Sun, 14 Dec 2025 15:00:37 +0530 Subject: [PATCH 06/18] Complete SMPTE-TT encoder port to Rust - Implemented write_cc_buffer_as_smptett with full styling support * Handles italics, bold, underline, and font color styling * Calculates proper row/column positioning for SMPTE-TT * Converts HTML-style tags to SMPTE-TT style attributes - Implemented write_cc_subtitle_as_smptett * Processes linked list of subtitle structures * Properly manages memory and frees C-allocated data * Handles CC_TEXT subtitle types - Implemented write_cc_bitmap_as_smptett * Feature-gated for OCR support (hardsubx_ocr) * Proper cleanup of bitmap data - Added helper functions: * escape_xml() for XML entity escaping * write_stringz_as_smptett() for core formatting * process_line_styling() for style tag conversion * write_wrapped() for safe file I/O - Added comprehensive test coverage: * XML escaping tests * Styling conversion tests (italic, bold, no styling) * All tests passing (268 total) Matches C implementation behavior while using safe Rust idioms. Fixes #1789 --- src/rust/src/encoder/smptett.rs | 414 +++++++++++++++++++++++++++++--- 1 file changed, 381 insertions(+), 33 deletions(-) diff --git a/src/rust/src/encoder/smptett.rs b/src/rust/src/encoder/smptett.rs index 00087b767..9d67e6a71 100644 --- a/src/rust/src/encoder/smptett.rs +++ b/src/rust/src/encoder/smptett.rs @@ -1,8 +1,16 @@ -use crate::bindings::{cc_subtitle, eia608_screen, encoder_ctx}; -use crate::encoder::common::encode_line; +use crate::bindings::{cc_subtitle, eia608_screen, encoder_ctx, ccx_encoding_type_CCX_ENC_UNICODE, subtype_CC_TEXT}; +use crate::encoder::common::{encode_line, write_raw}; use crate::libccxr_exports::time::ccxr_millis_to_time; +use lib_ccxr::util::log::DebugMessageFlag; +use lib_ccxr::debug; -use std::os::raw::c_int; +use std::os::raw::{c_int, c_void}; +use std::ffi::CStr; +use std::io; + +// Constants from C code +const ROWS: i32 = 15; +const COLUMNS: i32 = 32; fn escape_xml(s: &str) -> String { let mut out = String::with_capacity(s.len()); @@ -19,18 +27,37 @@ fn escape_xml(s: &str) -> String { out } +/// Write data to file descriptor with retry logic +fn write_wrapped(fd: c_int, buf: &[u8]) -> Result<(), io::Error> { + let mut remaining = buf.len(); + let mut current_buf = buf.as_ptr(); + + while remaining > 0 { + let written = write_raw(fd, current_buf as *const c_void, remaining); + if written == -1 { + return Err(io::Error::last_os_error()); + } + unsafe { + current_buf = current_buf.add(written as usize); + } + remaining -= written as usize; + } + + Ok(()) +} + fn write_stringz_as_smptett( text: &str, context: &mut encoder_ctx, ms_start: i64, ms_end: i64, -){ +) { let (mut h1, mut m1, mut s1, mut ms1) = (0, 0, 0, 0); let (mut h2, mut m2, mut s2, mut ms2) = (0, 0, 0, 0); unsafe { ccxr_millis_to_time(ms_start, &mut h1, &mut m1, &mut s1, &mut ms1); - ccxr_millis_to_time(ms_end, &mut h2, &mut m2, &mut s2, &mut ms2); + ccxr_millis_to_time(ms_end - 1, &mut h2, &mut m2, &mut s2, &mut ms2); } // Convert raw pointer buffer → Rust slice @@ -38,55 +65,354 @@ fn write_stringz_as_smptett( std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) }; - // Copy CRLF slice so we don't immutably borrow from context - let crlf = context.encoded_crlf; - let header = format!( - "

", - h1, m1, s1, ms1, - h2, m2, s2, ms2 + "

\r\n", + h1, m1, s1, ms1, h2, m2, s2, ms2 ); - encode_line(context, buffer, header.as_bytes()); - encode_line(context, buffer, &crlf); + if context.encoding != ccx_encoding_type_CCX_ENC_UNICODE { + debug!(msg_type = DebugMessageFlag::DECODER_608; "\r{}\n", header.trim_end()); + } + + let used = encode_line(context, buffer, header.as_bytes()); + unsafe { + let out = &*context.out; + let _ = write_wrapped(out.fh, &buffer[..used as usize]); + } + // Process lines - split by \n in the string for line in text.split('\n') { - let escaped = escape_xml(line); - encode_line(context, buffer, escaped.as_bytes()); - encode_line(context, buffer, &crlf); + if !line.is_empty() { + let escaped = escape_xml(line); + let used = encode_line(context, buffer, escaped.as_bytes()); + + if context.encoding != ccx_encoding_type_CCX_ENC_UNICODE { + debug!(msg_type = DebugMessageFlag::DECODER_608; "\r{}\n", escaped); + } + + unsafe { + let out = &*context.out; + let _ = write_wrapped(out.fh, &buffer[..used as usize]); + + // Write CRLF + let crlf_slice = std::slice::from_raw_parts( + context.encoded_crlf.as_ptr(), + context.encoded_crlf_length as usize + ); + let _ = write_wrapped(out.fh, crlf_slice); + } + } } - encode_line(context, buffer, b"

"); - encode_line(context, buffer, &crlf); + let footer = b"

\n"; + let used = encode_line(context, buffer, footer); + + if context.encoding != ccx_encoding_type_CCX_ENC_UNICODE { + debug!(msg_type = DebugMessageFlag::DECODER_608; "\r

\n"); + } + + unsafe { + let out = &*context.out; + let _ = write_wrapped(out.fh, &buffer[..used as usize]); + } } - - pub fn write_cc_buffer_as_smptett( - _: &mut encoder_ctx, - _: &eia608_screen, - _: i64, - _: i64, + data: &mut eia608_screen, + context: &mut encoder_ctx, ) -> c_int { - unimplemented!() + let (mut h1, mut m1, mut s1, mut ms1) = (0, 0, 0, 0); + let (mut h2, mut m2, mut s2, mut ms2) = (0, 0, 0, 0); + let mut wrote_something = 0; + + unsafe { + ccxr_millis_to_time(data.start_time, &mut h1, &mut m1, &mut s1, &mut ms1); + ccxr_millis_to_time(data.end_time - 1, &mut h2, &mut m2, &mut s2, &mut ms2); + } + + let buffer: &mut [u8] = unsafe { + std::slice::from_raw_parts_mut(context.buffer, context.capacity as usize) + }; + + for row in 0..15 { + if data.row_used[row] == 0 { + continue; + } + + // Calculate row position (10% + row position in 80% of screen) + let row1 = ((100.0 * row as f32) / (ROWS as f32 / 0.8)) + 10.0; + + // Find first non-space column + let mut firstcol = -1; + for column in 0..COLUMNS as usize { + let ch = data.characters[row][column]; + // Check for non-space character (0x20 is space) + if ch != 0x20 && ch != 0 { + firstcol = column as i32; + break; + } + } + + if firstcol < 0 { + continue; + } + + // Calculate column position + let col1 = ((100.0 * firstcol as f32) / (COLUMNS as f32 / 0.8)) + 10.0; + + wrote_something = 1; + + // Write opening tag with positioning + let opening = format!( + "

\n ", + h1, m1, s1, ms1, h2, m2, s2, ms2, col1, row1 + ); + + if context.encoding != ccx_encoding_type_CCX_ENC_UNICODE { + debug!(msg_type = DebugMessageFlag::DECODER_608; "\r{}\n", opening.trim_end()); + } + + let used = encode_line(context, buffer, opening.as_bytes()); + unsafe { + let out = &*context.out; + let _ = write_wrapped(out.fh, &buffer[..used as usize]); + } + + // Get the decoded line - store it in context.subline + unsafe { + // This function exists in the C code and should be available via bindings + extern "C" { + fn get_decoder_line_encoded( + ctx: *mut encoder_ctx, + buf: *mut u8, + line_num: c_int, + data: *const eia608_screen, + ) -> u32; + } + + get_decoder_line_encoded( + context as *mut encoder_ctx, + context.subline, + row as c_int, + data as *const eia608_screen, + ); + } + + // Convert subline to Rust string for processing + let line_text = unsafe { + let subline_slice = std::slice::from_raw_parts( + context.subline, + 4096 // Max line length + ); + let null_pos = subline_slice.iter().position(|&b| b == 0).unwrap_or(4096); + String::from_utf8_lossy(&subline_slice[..null_pos]).to_string() + }; + + // Process styling (italics, bold, underline, colors) + let formatted = process_line_styling(&line_text); + + let used = encode_line(context, buffer, formatted.as_bytes()); + unsafe { + let out = &*context.out; + let _ = write_wrapped(out.fh, &buffer[..used as usize]); + + // Write CRLF + let crlf_slice = std::slice::from_raw_parts( + context.encoded_crlf.as_ptr(), + context.encoded_crlf_length as usize + ); + let _ = write_wrapped(out.fh, crlf_slice); + } + + // Write closing tags + let closing = b"