55#include "ccx_encoders_helpers.h"
66#include "ocr.h"
77
8+ static void ass_position_from_row (
9+ int row ,
10+ int play_res_x ,
11+ int play_res_y ,
12+ int * out_x ,
13+ int * out_y )
14+ {
15+ // Center horizontally
16+ * out_x = play_res_x / 2 ;
17+
18+ // Map CEA-608 row (0–14) to ASS Y coordinate
19+ // SSA default PlayResY is 288
20+ int top = play_res_y * 60 / 100 ; // start of lower third
21+ int bottom = play_res_y * 95 / 100 ;
22+
23+ int y = top + (row * (bottom - top ) / 14 );
24+ * out_y = y ;
25+ }
26+
827int write_stringz_as_ssa (char * string , struct encoder_ctx * context , LLONG ms_start , LLONG ms_end )
928{
1029 int used ;
@@ -168,11 +187,18 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
168187 unsigned h1 , m1 , s1 , ms1 ;
169188 unsigned h2 , m2 , s2 , ms2 ;
170189 int wrote_something = 0 ;
190+ int used_row_count = 0 ;
171191
172192 int prev_line_start = -1 , prev_line_end = -1 ; // Column in which the previous line started and ended, for autodash
173193 int prev_line_center1 = -1 , prev_line_center2 = -1 ; // Center column of previous line text
174194 int empty_buf = 1 ;
175- for (int i = 0 ; i < 15 ; i ++ )
195+
196+ int first_row = -1 ;
197+ int x , y ;
198+ char pos_tag [64 ];
199+ int i ;
200+
201+ for (i = 0 ; i < 15 ; i ++ )
176202 {
177203 if (data -> row_used [i ])
178204 {
@@ -183,6 +209,12 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
183209 if (empty_buf )
184210 return 0 ;
185211
212+ for (i = 0 ; i < 15 ; i ++ )
213+ {
214+ if (data -> row_used [i ])
215+ used_row_count ++ ;
216+ }
217+
186218 millis_to_time (data -> start_time , & h1 , & m1 , & s1 , & ms1 );
187219 millis_to_time (data -> end_time - 1 , & h2 , & m2 , & s2 , & ms2 ); // -1 To prevent overlapping with next line.
188220 char timeline [128 ];
@@ -194,8 +226,59 @@ int write_cc_buffer_as_ssa(struct eia608_screen *data, struct encoder_ctx *conte
194226 dbg_print (CCX_DMT_DECODER_608 , "%s" , timeline );
195227
196228 write_wrapped (context -> out -> fh , context -> buffer , used );
229+
230+ /*
231+ * ASS precise positioning note:
232+ * We emit {\an2\pos(x,y)} using ASS script resolution coordinates.
233+ * PlayResX/PlayResY are explicitly declared in the SSA header (384x288),
234+ * which is the SSA/libass default resolution and ensures consistent
235+ * positioning across players.
236+ *
237+ * Positioning is intentionally guarded to avoid regressions when
238+ * caption layout information is ambiguous.
239+ */
240+
241+ /* ---- ASS precise positioning ---- */
242+
243+ first_row = -1 ;
244+
245+ /*
246+ * Only apply ASS positioning when:
247+ * - At least one row is present
248+ * - AND there is a single logical caption region
249+ * Otherwise, fall back to legacy SSA behavior.
250+ */
251+
252+ if (used_row_count > 0 && used_row_count <= 2 )
253+ {
254+ for (i = 0 ; i < 15 ; i ++ )
255+ {
256+ if (data -> row_used [i ])
257+ {
258+ first_row = i ;
259+ break ;
260+ }
261+ }
262+
263+ if (first_row >= 0 )
264+ {
265+ // SSA default resolution (used by libass / Aegisub)
266+ ass_position_from_row (first_row , 384 , 288 , & x , & y );
267+
268+ snprintf (
269+ pos_tag ,
270+ sizeof (pos_tag ),
271+ "{\\an2\\pos(%d,%d)}" ,
272+ x , y );
273+
274+ write_wrapped (context -> out -> fh , pos_tag , strlen (pos_tag ));
275+ }
276+ }
277+
278+ /* ---- end ASS positioning ---- */
279+
197280 int line_count = 0 ;
198- for (int i = 0 ; i < 15 ; i ++ )
281+ for (i = 0 ; i < 15 ; i ++ )
199282 {
200283 if (data -> row_used [i ])
201284 {
0 commit comments