Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGES.TXT
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
0.96.7 (unreleased)
-------------------
- New: Added ASS/SSA \pos-based positioning for CEA-608 captions when layout is simple (1–2 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.

Expand Down
2 changes: 2 additions & 0 deletions src/lib_ccx/ccx_encoders_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
111 changes: 108 additions & 3 deletions src/lib_ccx/ccx_encoders_ssa.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

for (int i = 14; i >= 0; i--)
{
if (data->row_used[i])
{
last_row = i;
break;
}
}
if (empty_buf)

if (first_row < 0)
return 0;

millis_to_time(data->start_time, &h1, &m1, &s1, &ms1);
Expand All @@ -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 (1–2 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++)
{
Expand Down
Loading