/* decssasub.c
Copyright (c) 2003-2020 HandBrake Team
This file is part of the HandBrake source code
Homepage: .
It may be used under the terms of the GNU General Public License v2.
For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html
*/
/*
* Converts SSA subtitles to either:
* (1) TEXTSUB format: UTF-8 subtitles with limited HTML-style markup (, , ), or
* (2) PICTURESUB format, using libass.
*
* SSA format references:
* http://www.matroska.org/technical/specs/subtitles/ssa.html
* http://moodub.free.fr/video/ass-specs.doc
* vlc-1.0.4/modules/codec/subtitles/subsass.c:ParseSSAString
*
* libass references:
* libass-0.9.9/ass.h
* vlc-1.0.4/modules/codec/libass.c
*
* @author David Foster (davidfstr)
*/
#include
#include
#include
#include "handbrake/handbrake.h"
#include
#include "handbrake/decavsub.h"
#include "handbrake/colormap.h"
struct hb_work_private_s
{
hb_avsub_context_t * ctx;
hb_job_t * job;
hb_subtitle_t * subtitle;
// SSA Import
FILE * file;
int readOrder;
// Time of first desired subtitle adjusted by reader_pts_offset
uint64_t start_time;
uint64_t stop_time;
};
#define SSA_VERBOSE_PACKETS 0
static int extradataInit( hb_work_private_t * pv )
{
int events = 0;
char * events_tag = "[Events]";
char * format_tag = "Format:";
int events_len = strlen(events_tag);;
int format_len = strlen(format_tag);;
char * header = NULL;
while (1)
{
char * line = NULL;
ssize_t len;
size_t size = 0;
len = hb_getline(&line, &size, pv->file);
if (len < 0)
{
// Incomplete SSA header
free(header);
return 1;
}
if (len > 0 && line != NULL)
{
if (header != NULL)
{
char * tmp = header;
header = hb_strdup_printf("%s%s", header, line);
free(tmp);
}
else
{
header = strdup(line);
}
if (!events)
{
if (len >= events_len &&
!strncasecmp(line, events_tag, events_len))
{
events = 1;
}
}
else
{
if (len >= format_len &&
!strncasecmp(line, format_tag, format_len))
{
free(line);
break;
}
// Improperly formatted SSA header
free(header);
return 1;
}
}
free(line);
}
pv->subtitle->extradata = (uint8_t*)header;
pv->subtitle->extradata_size = strlen(header) + 1;
return 0;
}
static int decssaInit( hb_work_object_t * w, hb_job_t * job )
{
hb_work_private_t * pv;
int ii;
pv = calloc( 1, sizeof( hb_work_private_t ) );
if (pv == NULL)
{
goto fail;
}
w->private_data = pv;
pv->ctx = decavsubInit(w, job);
if (pv->ctx == NULL)
{
goto fail;
}
pv->job = job;
pv->subtitle = w->subtitle;
if (pv->subtitle->config.src_filename == NULL)
{
hb_error("No SSA subtitle file specified");
goto fail;
}
pv->file = hb_fopen(pv->subtitle->config.src_filename, "r");
if(pv->file == NULL)
{
hb_error("Could not open the SSA subtitle file '%s'\n",
pv->subtitle->config.src_filename);
goto fail;
}
// Read SSA header and store in subtitle extradata
if (extradataInit(pv))
{
goto fail;
}
/*
* Figure out the start and stop times from the chapters being
* encoded - drop subtitle not in this range.
*/
pv->start_time = 0;
for (ii = 1; ii < job->chapter_start; ++ii)
{
hb_chapter_t * chapter = hb_list_item(job->list_chapter, ii - 1);
if (chapter)
{
pv->start_time += chapter->duration;
} else {
hb_error("Could not locate chapter %d for SSA start time", ii);
}
}
pv->stop_time = pv->start_time;
for (ii = job->chapter_start; ii <= job->chapter_end; ++ii)
{
hb_chapter_t * chapter = hb_list_item(job->list_chapter, ii - 1);
if (chapter)
{
pv->stop_time += chapter->duration;
} else {
hb_error("Could not locate chapter %d for SSA start time", ii);
}
}
hb_deep_log(3, "SSA Start time %"PRId64", stop time %"PRId64,
pv->start_time, pv->stop_time);
if (job->pts_to_start != 0)
{
// Compute start_time after reader sets reader_pts_offset
pv->start_time = AV_NOPTS_VALUE;
}
return 0;
fail:
if (pv != NULL)
{
decavsubClose(pv->ctx);
if (pv->file != NULL)
{
fclose(pv->file);
}
free(pv);
w->private_data = NULL;
}
return 1;
}
#define SSA_2_HB_TIME(hr,min,sec,centi) \
( 90LL * ( hr * 1000LL * 60 * 60 +\
min * 1000LL * 60 +\
sec * 1000LL +\
centi * 10LL ) )
/*
* Parses the start and stop time from the specified SSA packet.
*
* Returns true if parsing failed; false otherwise.
*/
static int parse_timing( char *line, int64_t *start, int64_t *stop )
{
/*
* Parse Start and End fields for timing information
*/
int start_hr, start_min, start_sec, start_centi;
int end_hr, end_min, end_sec, end_centi;
// SSA subtitles have an empty layer field (bare ','). The scanf
// format specifier "%*128[^,]" will not match on a bare ','. There
// must be at least one non ',' character in the match. So the format
// specifier is placed directly next to the ':' so that the next
// expected ' ' after the ':' will be the character it matches on
// when there is no layer field.
int numPartsRead = sscanf(line, "Dialogue:%*128[^,],"
"%d:%d:%d.%d," // Start
"%d:%d:%d.%d,", // End
&start_hr, &start_min, &start_sec, &start_centi,
&end_hr, &end_min, &end_sec, &end_centi );
if ( numPartsRead != 8 )
return 1;
*start = SSA_2_HB_TIME(start_hr, start_min, start_sec, start_centi);
*stop = SSA_2_HB_TIME( end_hr, end_min, end_sec, end_centi);
return 0;
}
static char * find_field( char * pos, char * end, int fieldNum )
{
int curFieldID = 1;
while (pos < end)
{
if ( *pos++ == ',' )
{
curFieldID++;
if ( curFieldID == fieldNum )
return pos;
}
}
return NULL;
}
/*
* SSA line format:
* Dialogue: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text '\0'
* 1 2 3 4 5 6 7 8 9 10
*
* MKV-SSA packet format:
* ReadOrder,Marked, Style,Name,MarginL,MarginR,MarginV,Effect,Text '\0'
* 1 2 3 4 5 6 7 8 9
*/
static hb_buffer_t *
decode_line_to_mkv_ssa( hb_work_private_t * pv, char * line, int size )
{
hb_buffer_t * out;
// Trim trailing CR/LF
while (size > 0 && (line[size - 1] == '\n' || line[size - 1] == '\r'))
{
line[--size] = 0;
}
int64_t start, stop;
if (parse_timing(line, &start, &stop))
{
goto fail;
}
// Convert the SSA packet to MKV-SSA format, which is what libass expects
char * mkvSSA;
int numPartsRead;
char * styleToTextFields;
char * layerField = malloc(size);
// SSA subtitles have an empty layer field (bare ','). The scanf
// format specifier "%*128[^,]" will not match on a bare ','. There
// must be at least one non ',' character in the match. So the format
// specifier is placed directly next to the ':' so that the next
// expected ' ' after the ':' will be the character it matches on
// when there is no layer field.
numPartsRead = sscanf( (char *)line, "Dialogue:%128[^,],", layerField );
if ( numPartsRead != 1 )
{
free(layerField);
goto fail;
}
styleToTextFields = find_field( line, line + size, 4 );
if ( styleToTextFields == NULL ) {
free( layerField );
goto fail;
}
// The sscanf conversion above will result in an extra space
// before the layerField. Strip the space.
char *stripLayerField = layerField;
for(; *stripLayerField == ' '; stripLayerField++);
out = hb_buffer_init( size + 1 );
mkvSSA = (char*)out->data;
mkvSSA[0] = '\0';
sprintf(mkvSSA, "%d", pv->readOrder++);
strcat( mkvSSA, "," );
strcat( mkvSSA, stripLayerField );
strcat( mkvSSA, "," );
strcat( mkvSSA, (char *)styleToTextFields );
out->size = strlen(mkvSSA) + 1;
out->s.frametype = HB_FRAME_SUBTITLE;
out->s.start = start + pv->subtitle->config.offset * 90;
out->s.duration = stop - start;
out->s.stop = stop + pv->subtitle->config.offset * 90;
if (out->size == 0)
{
hb_buffer_close(&out);
}
else if (out->s.stop <= pv->start_time ||
out->s.start >= pv->stop_time)
{
// Drop subtitles that end before the start time
// or start after the stop time
hb_deep_log(3, "Discarding SSA at time start %"PRId64", stop %"PRId64,
out->s.start, out->s.stop);
hb_buffer_close(&out);
}
else
{
if (out->s.start < pv->start_time)
{
out->s.start = pv->start_time;
}
if (out->s.stop > pv->stop_time)
{
out->s.stop = pv->stop_time;
}
out->s.start -= pv->start_time;
out->s.stop -= pv->start_time;
}
free( layerField );
return out;
fail:
hb_log( "decssasub: malformed SSA subtitle packet: %.*s\n", size, line );
return NULL;
}
/*
* Read the SSA file and put the entries into the subtitle fifo for all to read
*/
static hb_buffer_t * ssa_read( hb_work_private_t * pv )
{
hb_buffer_t * out;
if (!pv->file)
{
return hb_buffer_eof_init();
}
if (pv->job->reader_pts_offset == AV_NOPTS_VALUE)
{
// We need to wait for reader to initialize it's pts offset so that
// we know where to start reading SSA.
return NULL;
}
if (pv->start_time == AV_NOPTS_VALUE)
{
pv->start_time = pv->job->reader_pts_offset;
if (pv->job->pts_to_stop > 0)
{
pv->stop_time = pv->job->pts_to_start + pv->job->pts_to_stop;
}
}
while (!feof(pv->file))
{
char * line = NULL;
ssize_t len;
size_t size = 0;
len = hb_getline(&line, &size, pv->file);
if (len > 0 && line != NULL)
{
out = decode_line_to_mkv_ssa(pv, line, len);
if (out != NULL)
{
free(line);
return out;
}
}
free(line);
if (len < 0)
{
// Error or EOF
out = hb_buffer_eof_init();
return out;
}
}
out = hb_buffer_eof_init();
return out;
}
static int decssaWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
hb_buffer_t ** buf_out )
{
hb_work_private_t * pv = w->private_data;
hb_buffer_t * in;
int result;
in = ssa_read(pv);
if (in == NULL)
{
return HB_WORK_OK;
}
result = decavsubWork(pv->ctx, &in, buf_out);
if (in != NULL)
{
hb_buffer_close(&in);
}
return result;
}
static void decssaClose( hb_work_object_t * w )
{
hb_work_private_t * pv = w->private_data;
if (pv != NULL)
{
decavsubClose(pv->ctx);
fclose(pv->file);
free(pv);
}
w->private_data = NULL;
}
hb_work_object_t hb_decssasub =
{
WORK_DECSSASUB,
"SSA Subtitle Decoder",
decssaInit,
decssaWork,
decssaClose
};