[FEATURE] Add guarded ASS/SSA \pos positioning for CEA-608 captions (#1885)

* feat(ssa): add guarded ASS \pos positioning for CEA-608 captions

* fix(ssa): correct ASS positioning anchor, validate row adjacency, and clean up variable placement

* fix(ssa): adjust top margin to prevent clipping of top-positioned CEA-608 captions

* ssa: map CEA-608 row+col to ASS coords using FFmpeg safe-area formula and fix \an2→\an7 anchor
This commit is contained in:
Chandragupt Singh
2026-03-14 22:43:40 +05:30
committed by GitHub
parent 538e39db67
commit ed7f544e10
3 changed files with 111 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
0.96.7 (unreleased)
-------------------
- New: Added ASS/SSA \pos-based positioning for CEA-608 captions when layout is simple (12 rows) (#1726)
- Fix: Remove strdup() memory leaks in WebVTT styling encoder, fix invalid CSS rgba(0,256,0) green value, fix missing free(unescaped) on write-error path (#2154)
- Fix: Prevent crash in Rust timing module when logging out-of-range PTS/FTS timestamps from malformed streams.
- Fix: Resolve Windows MSVC debug build crash caused by cross-CRT invalid free on Rust-allocated output_filename (#2126)

View File

@@ -48,6 +48,8 @@ static const char *ssa_header =
"[Script Info]\n\
Title: Default file\n\
ScriptType: v4.00+\n\
PlayResX: 384\n\
PlayResY: 288\n\
\n\
[V4+ Styles]\n\
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n\

View File

@@ -5,6 +5,25 @@
#include "ccx_encoders_helpers.h"
#include "ocr.h"
static void ass_position_from_row_col(
int row,
int col,
int play_res_x,
int play_res_y,
int *out_x,
int *out_y)
{
/* vertical: matches FFmpeg's 0.1 + 0.0533*i formula (0.80/15 = 0.05333) */
double top = play_res_y * 0.10;
double row_step = (play_res_y * 0.80) / 15.0;
*out_y = (int)(top + row * row_step + 0.5);
/* horizontal: matches FFmpeg's 0.1 + 0.025*j (0.025 = 0.80/32, 32 cells) */
double left = play_res_x * 0.10;
double col_step = play_res_x * 0.025;
*out_x = (int)(left + col * col_step + 0.5);
}
int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end)
{
int used;
@@ -171,16 +190,31 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
int prev_line_start = -1, prev_line_end = -1; // Column in which the previous line started and ended, for autodash
int prev_line_center1 = -1, prev_line_center2 = -1; // Center column of previous line text
int empty_buf = 1;
int first_row = -1;
int last_row = -1;
int x, y;
char pos_tag[64];
for (int i = 0; i < 15; i++)
{
if (data->row_used[i])
{
empty_buf = 0;
first_row = i;
break;
}
}
if (empty_buf)
for (int i = 14; i >= 0; i--)
{
if (data->row_used[i])
{
last_row = i;
break;
}
}
if (first_row < 0)
return 0;
millis_to_time(data->start_time, &h1, &m1, &s1, &ms1);
@@ -194,6 +228,77 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
dbg_print(CCX_DMT_DECODER_608, "%s", timeline);
write_wrapped(context->out->fh, context->buffer, used);
/*
* ASS precise positioning note:
* We emit {\an7\pos(x,y)} using ASS script resolution coordinates.
* PlayResX/PlayResY are explicitly declared in the SSA header (384x288),
* which is the SSA/libass default resolution and ensures consistent
* positioning across players.
*
* Positioning is intentionally guarded to avoid regressions when
* caption layout information is ambiguous.
*/
/* ---- ASS precise positioning ---- */
/*
* Only apply ASS positioning when:
* - At least one row is present
* - And captions occupy a single logical region (12 adjacent rows)
* Otherwise, fall back to legacy SSA positioning.
*/
if ((last_row - first_row) <= 1)
{
int first_col = 31;
for (int r = first_row; r <= last_row; r++)
{
if (!data->row_used[r])
continue;
int f = -1, l = -1;
find_limit_characters(
data->characters[r],
&f,
&l,
CCX_DECODER_608_SCREEN_WIDTH);
/* Fallback: row_used but only control/padding bytes present */
if (f < 0)
{
unsigned char *line = data->characters[r];
for (int c = 0; c < CCX_DECODER_608_SCREEN_WIDTH; c++)
{
if (line[c] != ' ' && line[c] != 0 && line[c] != 0x89) /* match find_limit_characters */
{
f = c;
break;
}
}
}
/* Safety clamp: fully empty row defaults to left edge */
if (f < 0)
f = 0;
if (f < first_col)
first_col = f;
}
ass_position_from_row_col(first_row, first_col, 384, 288, &x, &y);
snprintf(
pos_tag,
sizeof(pos_tag),
"{\\an7\\pos(%d,%d)}",
x, y);
write_wrapped(context->out->fh, pos_tag, strlen(pos_tag));
}
int line_count = 0;
for (int i = 0; i < 15; i++)
{