/* dectx3gsub.c
Copyright (c) 2003-2021 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 TX3G subtitles to UTF-8 subtitles with limited HTML-style markup (, , ).
*
* TX3G == MPEG 4, Part 17 (ISO/IEC 14496-17) == 3GPP Timed Text (26.245)
* A full reference to the format can be found here:
* http://www.3gpp.org/ftp/Specs/html-info/26245.htm
*
* @author David Foster (davidfstr)
*/
#include
#include
#include "handbrake/handbrake.h"
#include "handbrake/colormap.h"
struct hb_work_private_s
{
int line; // SSA line number
};
typedef enum {
BOLD = 0x1,
ITALIC = 0x2,
UNDERLINE = 0x4
} FaceStyleFlag;
#define MAX_MARKUP_LEN 40
#define SSA_PREAMBLE_LEN 24
typedef struct {
uint16_t startChar; // NOTE: indices in terms of *character* (not: byte) positions
uint16_t endChar;
uint16_t fontID;
uint8_t faceStyleFlags; // FaceStyleFlag
uint8_t fontSize;
uint32_t textColorRGBA;
} StyleRecord;
// NOTE: None of these macros check for buffer overflow
#define READ_U8() *pos; pos += 1;
#define READ_U16() (pos[0] << 8) | pos[1]; pos += 2;
#define READ_U32() (pos[0] << 24) | (pos[1] << 16) | (pos[2] << 8) | pos[3]; pos += 4;
#define READ_ARRAY(n) pos; pos += n;
#define SKIP_ARRAY(n) pos += n;
#define WRITE_CHAR(c) {dst[0]=c; dst += 1;}
#define FOURCC(str) ((((uint32_t) str[0]) << 24) | \
(((uint32_t) str[1]) << 16) | \
(((uint32_t) str[2]) << 8) | \
(((uint32_t) str[3]) << 0))
#define IS_10xxxxxx(c) ((c & 0xC0) == 0x80)
static int write_ssa_markup(char *dst, StyleRecord *style)
{
if (style == NULL)
{
sprintf(dst, "{\\r}");
return strlen(dst);
}
sprintf(dst, "{\\i%d\\b%d\\u%d\\1c&H%X&\\1a&H%02X&}",
!!(style->faceStyleFlags & ITALIC),
!!(style->faceStyleFlags & BOLD),
!!(style->faceStyleFlags & UNDERLINE),
HB_RGB_TO_BGR(style->textColorRGBA >> 8),
255 - (style->textColorRGBA & 0xFF)); // SSA alpha is inverted 0==opaque
return strlen(dst);
}
static hb_buffer_t *tx3g_decode_to_ssa(hb_work_private_t *pv, hb_buffer_t *in)
{
uint8_t *pos = in->data;
uint8_t *end = in->data + in->size;
uint16_t numStyleRecords = 0;
StyleRecord *styleRecords = NULL;
/*
* Parse the packet as a TX3G TextSample.
*
* Look for a single StyleBox ('styl') and read all contained StyleRecords.
* Ignore all other box types.
*
* NOTE: Buffer overflows on read are not checked.
*/
uint16_t textLength = READ_U16();
uint8_t *text = READ_ARRAY(textLength);
while ( pos < end )
{
/*
* Read TextSampleModifierBox
*/
uint32_t size = READ_U32();
if ( size == 0 )
{
size = pos - end; // extends to end of packet
}
if ( size == 1 )
{
hb_log( "dectx3gsub: TextSampleModifierBox has unsupported large size" );
break;
}
uint32_t type = READ_U32();
if (type == FOURCC("uuid"))
{
hb_log( "dectx3gsub: TextSampleModifierBox has unsupported extended type" );
break;
}
if (type == FOURCC("styl"))
{
// Found a StyleBox. Parse the contained StyleRecords
if ( numStyleRecords != 0 )
{
hb_log( "dectx3gsub: found additional StyleBoxes on subtitle; skipping" );
SKIP_ARRAY(size);
continue;
}
numStyleRecords = READ_U16();
if (numStyleRecords > 0)
styleRecords = calloc(numStyleRecords, sizeof(StyleRecord));
int i;
for (i = 0; i < numStyleRecords; i++)
{
styleRecords[i].startChar = READ_U16();
styleRecords[i].endChar = READ_U16();
styleRecords[i].fontID = READ_U16();
styleRecords[i].faceStyleFlags = READ_U8();
styleRecords[i].fontSize = READ_U8();
styleRecords[i].textColorRGBA = READ_U32();
}
}
else
{
// Found some other kind of TextSampleModifierBox. Skip it.
SKIP_ARRAY(size);
}
}
/*
* Copy text to output buffer, and add HTML markup for the style records
*/
int maxOutputSize = textLength + SSA_PREAMBLE_LEN + (numStyleRecords * MAX_MARKUP_LEN);
hb_buffer_t *out = hb_buffer_init( maxOutputSize );
if ( out == NULL )
goto fail;
uint8_t *dst = out->data;
uint8_t *start;
int charIndex = 0;
int styleIndex = 0;
sprintf((char*)dst, "%d,,Default,,0,0,0,,", pv->line);
dst += strlen((char*)dst);
start = dst;
for (pos = text, end = text + textLength; pos < end; pos++)
{
if (IS_10xxxxxx(*pos))
{
// Is a non-first byte of a multi-byte UTF-8 character
WRITE_CHAR(*pos);
continue; // ...without incrementing 'charIndex'
}
if (styleIndex < numStyleRecords)
{
if (styleRecords[styleIndex].endChar == charIndex)
{
if (styleIndex + 1 >= numStyleRecords ||
styleRecords[styleIndex+1].startChar > charIndex)
{
dst += write_ssa_markup((char*)dst, NULL);
}
styleIndex++;
}
if (styleRecords[styleIndex].startChar == charIndex)
{
dst += write_ssa_markup((char*)dst, &styleRecords[styleIndex]);
}
}
if (*pos == '\n')
{
WRITE_CHAR('\\');
WRITE_CHAR('N');
}
else
{
WRITE_CHAR(*pos);
}
charIndex++;
}
if (start == dst)
{
// No text in the subtitle. This sub is just filler, drop it.
free(styleRecords);
hb_buffer_close(&out);
return NULL;
}
*dst = '\0';
dst++;
// Trim output buffer to the actual amount of data written
out->size = dst - out->data;
// Copy metadata from the input packet to the output packet
out->s.frametype = HB_FRAME_SUBTITLE;
out->s.start = in->s.start;
out->s.stop = in->s.stop;
out->s.scr_sequence = in->s.scr_sequence;
fail:
free(styleRecords);
return out;
}
#undef READ_U8
#undef READ_U16
#undef READ_U32
#undef READ_ARRAY
#undef SKIP_ARRAY
#undef WRITE_CHAR
#undef WRITE_START_TAG
#undef WRITE_END_TAG
static int dectx3gInit( hb_work_object_t * w, hb_job_t * job )
{
hb_work_private_t * pv;
pv = calloc( 1, sizeof( hb_work_private_t ) );
if (pv == NULL)
return 1;
w->private_data = pv;
// TODO:
// parse w->subtitle->extradata txg3 sample description into
// SSA format and replace extradata.
// For now we just create a generic SSA Script Info.
int height = job->title->geometry.height - job->crop[0] - job->crop[1];
int width = job->title->geometry.width - job->crop[2] - job->crop[3];
hb_subtitle_add_ssa_header(w->subtitle, HB_FONT_SANS,
.066 * job->title->geometry.height,
width, height);
return 0;
}
static int dectx3gWork( 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 = *buf_in;
if ( in->s.stop == 0 ) {
hb_log( "dectx3gsub: subtitle packet lacks duration" );
}
if (in->s.flags & HB_BUF_FLAG_EOF)
{
*buf_out = in;
*buf_in = NULL;
return HB_WORK_DONE;
}
*buf_out = tx3g_decode_to_ssa(pv, in);
return HB_WORK_OK;
}
static void dectx3gClose( hb_work_object_t * w )
{
free(w->private_data);
}
hb_work_object_t hb_dectx3gsub =
{
WORK_DECTX3GSUB,
"TX3G Subtitle Decoder",
dectx3gInit,
dectx3gWork,
dectx3gClose
};