/* ssautil.c
Copyright (c) 2003-2019 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
*/
#include
#include
#include "hb.h"
#include "ssautil.h"
struct hb_subtitle_style_s
{
char * name;
char * font_name;
int font_size;
uint32_t flags;
uint32_t fg_rgb; // foreground color
uint32_t alt_rgb; // secondary color
uint32_t ol_rgb; // outline color
uint32_t bg_rgb; // background color
uint32_t fg_alpha; // foreground alpha
uint32_t alt_alpha; // secondary alpha
uint32_t ol_alpha; // outline alpha
uint32_t bg_alpha; // background alpha
};
struct hb_subtitle_style_context_s
{
hb_subtitle_style_t current;
int event_style_default;
hb_subtitle_style_t * styles;
int styles_count;
int styles_size;
int style_default;
};
struct hb_tx3g_output_buf_s
{
int alloc;
int size;
uint8_t * buf;
};
struct hb_tx3g_style_context_s
{
hb_tx3g_output_buf_t style_atoms;
int style_atom_count;
int style_start;
int height;
hb_subtitle_style_context_t * in_style;
hb_subtitle_style_t out_style;
uint8_t flush;
};
static void ssa_style_reset(hb_subtitle_style_context_t * ctx)
{
if (ctx->styles != NULL && ctx->style_default < ctx->styles_count)
{
ctx->current = ctx->styles[ctx->event_style_default];
}
else
{
ctx->current.font_name = HB_FONT_SANS;
ctx->current.font_size = 72;
ctx->current.flags = 0;
ctx->current.fg_rgb = 0x00FFFFFF;
ctx->current.alt_rgb = 0x00FFFFFF;
ctx->current.ol_rgb = 0x000F0F0F;
ctx->current.bg_rgb = 0x000F0F0F;
ctx->current.fg_alpha = 0xFF;
ctx->current.alt_alpha = 0xFF;
ctx->current.ol_alpha = 0xFF;
ctx->current.bg_alpha = 0xFF;
}
}
static int ssa_style_set(hb_subtitle_style_context_t * ctx, const char * style)
{
int ii;
if (ctx->styles != NULL && style != NULL && style[0] != 0)
{
for (ii = 0; ii < ctx->styles_count; ii++)
{
if (!strcasecmp(ctx->styles[ii].name, style))
{
ctx->current = ctx->styles[ii];
return ii;
}
}
}
ssa_style_reset(ctx);
return ctx->style_default;
}
static int ssa_update_style(const char *ssa, hb_subtitle_style_context_t *ctx)
{
int pos, end, index;
hb_subtitle_style_t * style = &ctx->current;
if (ssa[0] != '{')
return 0;
pos = 1;
while (ssa[pos] != '}' && ssa[pos] != '\0')
{
index = -1;
// Skip any malformed markup junk
while (strchr("\\}", ssa[pos]) == NULL) pos++;
pos++;
// Check for an index that is in some markup (e.g. font color)
if (isdigit(ssa[pos]))
{
index = ssa[pos++] - 0x30;
}
// Find the end of this markup clause
end = pos;
while (strchr("\\}", ssa[end]) == NULL) end++;
// Handle simple integer valued attributes
if (strchr("ibu", ssa[pos]) != NULL && isdigit(ssa[pos+1]))
{
int val = strtol(ssa + pos + 1, NULL, 0);
switch (ssa[pos])
{
case 'i':
style->flags = (style->flags & ~HB_STYLE_FLAG_ITALIC) |
!!val * HB_STYLE_FLAG_ITALIC;
break;
case 'b':
style->flags = (style->flags & ~HB_STYLE_FLAG_BOLD) |
!!val * HB_STYLE_FLAG_BOLD;
break;
case 'u':
style->flags = (style->flags & ~HB_STYLE_FLAG_UNDERLINE) |
!!val * HB_STYLE_FLAG_UNDERLINE;
break;
}
}
if (ssa[pos] == 'r')
{
// Style reset
char * style = hb_strndup(ssa + pos + 1, end - (pos + 1));
ssa_style_set(ctx, style);
free(style);
}
if (ssa[pos] == 'c' && ssa[pos+1] == '&' && ssa[pos+2] == 'H')
{
// Font color markup
char *endptr;
uint32_t bgr;
bgr = strtol(ssa + pos + 3, &endptr, 16);
if (*endptr == '&')
{
switch (index)
{
case -1:
case 1:
style->fg_rgb = HB_BGR_TO_RGB(bgr);
break;
case 2:
style->alt_rgb = HB_BGR_TO_RGB(bgr);
break;
case 3:
style->ol_rgb = HB_BGR_TO_RGB(bgr);
break;
case 4:
style->bg_rgb = HB_BGR_TO_RGB(bgr);
break;
default:
// Unknown color index, ignore
break;
}
}
}
if ((ssa[pos] == 'a' && ssa[pos+1] == '&' && ssa[pos+2] == 'H') ||
(!strcmp(ssa+pos, "alpha") && ssa[pos+5] == '&' && ssa[pos+6] == 'H'))
{
// Font alpha markup
char *endptr;
uint8_t alpha;
int alpha_pos = 3;
if (ssa[1] == 'l')
alpha_pos = 7;
alpha = strtol(ssa + pos + alpha_pos, &endptr, 16);
if (*endptr == '&')
{
// SSA alpha is inverted 0 is opaque
alpha = 255 - alpha;
switch (index)
{
case -1:
case 1:
style->fg_alpha = alpha;
break;
case 2:
style->alt_alpha = alpha;
break;
case 3:
style->ol_alpha = alpha;
break;
case 4:
style->bg_alpha = alpha;
break;
default:
// Unknown alpha index, ignore
break;
}
}
}
pos = end;
}
if (ssa[pos] == '}')
pos++;
return pos;
}
static char * ssa_to_text(const char *in, int *consumed,
hb_subtitle_style_context_t *ctx)
{
int markup_len = 0;
int in_pos = 0;
int out_pos = 0;
char *out = malloc(strlen(in) + 1); // out will never be longer than in
for (in_pos = 0; in[in_pos] != '\0'; in_pos++)
{
if ((markup_len = ssa_update_style(in + in_pos, ctx)))
{
*consumed = in_pos + markup_len;
out[out_pos++] = '\0';
return out;
}
// Check escape codes
if (in[in_pos] == '\\')
{
in_pos++;
switch (in[in_pos])
{
case '\0':
in_pos--;
break;
case 'N':
case 'n':
out[out_pos++] = '\n';
break;
case 'h':
out[out_pos++] = ' ';
break;
default:
out[out_pos++] = in[in_pos];
break;
}
}
else
{
out[out_pos++] = in[in_pos];
}
}
*consumed = in_pos;
out[out_pos++] = '\0';
return out;
}
static char * get_field(char ** pos)
{
char * result = NULL;
if (pos == NULL || *pos == NULL || **pos == 0)
{
return NULL;
}
char * start = *pos;
while (isspace(*start)) start++;
char * end = strchr(start, ',');
if (end != NULL)
{
result = hb_strndup(start, end - start);
*pos = end + 1;
}
else
{
result = strdup(start);
*pos = NULL;
}
return result;
}
static char * sgetline(char * str)
{
char * eol;
if (str == NULL)
{
return NULL;
}
// find end of line
eol = strchr(str, '\n');
if (eol != NULL)
{
if (eol > str && *(eol - 1) == '\r')
{
eol--;
}
}
if (eol != NULL)
{
return hb_strndup(str, eol - str);
}
else
{
return strdup(str);
}
}
static char ** get_fields(char * line, int last)
{
int count = 0, ii;
char * pos;
if (line == NULL || *line == 0)
{
return NULL;
}
// count number of fields
count = 1;
pos = line;
while ((pos = strchr(pos, ',')) != NULL)
{
count++;
pos++;
}
if (last > 0 && count > last)
{
count = last;
}
char ** result = calloc(count + 1, sizeof(char*));
pos = line;
for (ii = 0; ii < count - 1; ii++)
{
result[ii] = get_field(&pos);
}
result[ii] = strdup(pos);
return result;
}
static int field_index(char ** fields, char * name)
{
int ii;
if (fields == NULL || name == NULL)
{
return -1;
}
for (ii = 0; fields[ii] != NULL; ii++)
{
if (!strcasecmp(name, fields[ii]))
{
return ii;
}
}
return -1;
}
static const char * field_value(char ** style, int index)
{
if (index >= 0 && index < hb_str_vlen(style))
{
return style[index];
}
return NULL;
}
typedef struct ssa_style_indicies_s
{
int style_name_index;
int font_name_index;
int font_size_index;
int fg_color_index;
int alt_color_index;
int ol_color_index;
int bg_color_index;
int bold_index;
int italic_index;
int underline_index;
} ssa_style_indicies_t;
static void fill_field_indicies(char **fields, ssa_style_indicies_t * indices)
{
indices->style_name_index = field_index(fields, "Name");
indices->font_name_index = field_index(fields, "Fontname");
indices->font_size_index = field_index(fields, "Fontsize");
indices->fg_color_index = field_index(fields, "PrimaryColour");
indices->alt_color_index = field_index(fields, "SecondaryColour");
indices->ol_color_index = field_index(fields, "OutlineColour");
indices->bg_color_index = field_index(fields, "BackColour");
indices->bold_index = field_index(fields, "Bold");
indices->italic_index = field_index(fields, "Italic");
indices->underline_index = field_index(fields, "Underline");
}
static int add_style(hb_subtitle_style_context_t *ctx,
char ** style, ssa_style_indicies_t *field_indices)
{
const char * name;
const char * value;
int size;
uint32_t rgb;
uint32_t alpha;
uint32_t flag;
int style_index;
if (style == NULL)
{
return 0;
}
if (ctx->styles_count + 1 > ctx->styles_size)
{
void * tmp;
ctx->styles_size = (ctx->styles_count + 1) * 2;
tmp = realloc(ctx->styles, ctx->styles_size *
sizeof(hb_subtitle_style_t));
if (tmp == NULL)
{
return 1;
}
ctx->styles = tmp;
}
style_index = ctx->styles_count;
name = field_value(style, field_indices->style_name_index);
if (name == NULL)
{
name = "Default";
}
if (!strcasecmp(name, "Default"))
{
ctx->style_default = style_index;
ctx->event_style_default = ctx->style_default;
}
ctx->styles[style_index].name = strdup(name);
value = field_value(style, field_indices->font_name_index);
if (value == NULL)
{
value = HB_FONT_SANS;
}
ctx->styles[style_index].font_name = strdup(value);
value = field_value(style, field_indices->font_size_index);
if (value == NULL)
{
size = 72;
}
else
{
size = strtol(value, NULL, 0);
}
ctx->styles[style_index].font_size = size;
value = field_value(style, field_indices->fg_color_index);
if (value == NULL || strlen(value) < 3)
{
rgb = 0x00ffffff;
alpha = 0xff;
}
else
{
int abgr = strtol(value+2, NULL, 16);
rgb = HB_BGR_TO_RGB(abgr);
alpha = abgr >> 24;
}
ctx->styles[style_index].fg_rgb = rgb;
// SSA alpha is inverted 0 is opaque
ctx->styles[style_index].fg_alpha = 255 - alpha;
value = field_value(style, field_indices->alt_color_index);
if (value == NULL || strlen(value) < 3)
{
rgb = 0x00ffffff;
alpha = 0xff;
}
else
{
int abgr = strtol(value+2, NULL, 16);
rgb = HB_BGR_TO_RGB(abgr);
alpha = abgr >> 24;
}
ctx->styles[style_index].alt_rgb = rgb;
ctx->styles[style_index].alt_alpha = alpha;
value = field_value(style, field_indices->ol_color_index);
if (value == NULL || strlen(value) < 3)
{
rgb = 0x000f0f0f;
alpha = 0xff;
}
else
{
int abgr = strtol(value+2, NULL, 16);
rgb = HB_BGR_TO_RGB(abgr);
alpha = abgr >> 24;
}
ctx->styles[style_index].ol_rgb = rgb;
ctx->styles[style_index].ol_alpha = alpha;
value = field_value(style, field_indices->bg_color_index);
if (value == NULL || strlen(value) < 3)
{
rgb = 0x000f0f0f;
alpha = 0xff;
}
else
{
int abgr = strtol(value+2, NULL, 16);
rgb = HB_BGR_TO_RGB(abgr);
alpha = abgr >> 24;
}
ctx->styles[style_index].bg_rgb = rgb;
ctx->styles[style_index].bg_alpha = alpha;
ctx->styles[style_index].flags = 0;
value = field_value(style, field_indices->bold_index);
if (value == NULL)
{
flag = HB_STYLE_FLAG_BOLD;
}
else
{
flag = strtol(value, NULL, 0) ? HB_STYLE_FLAG_BOLD : 0;
}
ctx->styles[style_index].flags |= flag;
value = field_value(style, field_indices->italic_index);
if (value == NULL)
{
flag = HB_STYLE_FLAG_ITALIC;
}
else
{
flag = strtol(value, NULL, 0) ? HB_STYLE_FLAG_ITALIC : 0;
}
ctx->styles[style_index].flags |= flag;
value = field_value(style, field_indices->underline_index);
if (value == NULL)
{
flag = HB_STYLE_FLAG_UNDERLINE;
}
else
{
flag = strtol(value, NULL, 0) ? HB_STYLE_FLAG_UNDERLINE : 0;
}
ctx->styles[style_index].flags |= flag;
ctx->styles_count = style_index + 1;
return 0;
}
hb_subtitle_style_context_t * hb_subtitle_style_init(const char * ssa_header)
{
hb_subtitle_style_context_t * ctx;
ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL)
{
return NULL;
}
if (ssa_header != NULL)
{
// Find beginning of styles
char * pos = strstr(ssa_header, "[V4");
if (pos != NULL)
{
pos = strstr(pos, "\nFormat:");
if (pos != NULL)
{
char ** fields;
int next = 7;
char * line = sgetline(pos + 8);
fields = get_fields(line, 0);
free(line);
if (fields != NULL)
{
ssa_style_indicies_t field_indices;
fill_field_indicies(fields, &field_indices);
pos = strstr(pos, "\nStyle:");
while (pos != NULL)
{
char ** style;
line = sgetline(pos + next);
style = get_fields(line, 0);
free(line);
if (add_style(ctx, style, &field_indices))
{
hb_str_vfree(style);
break;
}
pos = strchr(pos + next, '\n');
next = 1;
hb_str_vfree(style);
}
hb_str_vfree(fields);
}
}
}
}
ssa_style_reset(ctx);
return ctx;
}
void hb_subtitle_style_close(hb_subtitle_style_context_t ** pctx)
{
if (pctx == NULL || *pctx == NULL)
{
return;
}
hb_subtitle_style_context_t * ctx = *pctx;
if (ctx->styles != NULL)
{
int ii;
for (ii = 0; ii < ctx->styles_count; ii++)
{
free(ctx->styles[ii].name);
free(ctx->styles[ii].font_name);
}
}
free(ctx->styles);
free(ctx);
*pctx = NULL;
}
#define TX3G_STYLES (HB_STYLE_FLAG_BOLD | \
HB_STYLE_FLAG_ITALIC | \
HB_STYLE_FLAG_UNDERLINE)
static int check_realloc_output(hb_tx3g_output_buf_t * output, int size)
{
if (output->alloc < size)
{
uint8_t * tmp;
output->alloc = size + 1024;
output->size = size;
tmp = realloc(output->buf, output->alloc);
if (tmp == NULL)
{
hb_error("realloc failed!");
free(output->buf);
output->size = 0;
output->alloc = 0;
output->buf = NULL;
return 0;
}
output->buf = tmp;
}
return 1;
}
static int tx3g_update_style_atoms(hb_tx3g_style_context_t *ctx, int stop)
{
uint8_t * style_entry;
uint8_t face = 0;
int font_size;
int pos = 10 + (12 * ctx->style_atom_count);
int size = 10 + (12 * (ctx->style_atom_count + 1));
if (!check_realloc_output(&ctx->style_atoms, size))
{
return 0;
}
style_entry = ctx->style_atoms.buf + pos;
if (ctx->out_style.flags & HB_STYLE_FLAG_BOLD)
face |= 1;
if (ctx->out_style.flags & HB_STYLE_FLAG_ITALIC)
face |= 2;
if (ctx->out_style.flags & HB_STYLE_FLAG_UNDERLINE)
face |= 4;
style_entry[0] = (ctx->style_start >> 8) & 0xff; // startChar
style_entry[1] = ctx->style_start & 0xff;
style_entry[2] = (stop >> 8) & 0xff; // endChar
style_entry[3] = stop & 0xff;
style_entry[4] = 0; // font-ID msb
style_entry[5] = 1; // font-ID lsb
style_entry[6] = face; // face-style-flags
font_size = 0.05 * ctx->height;
if (font_size < 12)
{
font_size = 12;
}
else if (font_size > 255)
{
font_size = 255;
}
style_entry[7] = font_size; // font-size
style_entry[8] = (ctx->out_style.fg_rgb >> 16) & 0xff; // r
style_entry[9] = (ctx->out_style.fg_rgb >> 8) & 0xff; // g
style_entry[10] = (ctx->out_style.fg_rgb) & 0xff; // b
style_entry[11] = ctx->out_style.fg_alpha; // a
ctx->style_atom_count++;
return 1;
}
static int tx3g_update_style(hb_tx3g_style_context_t *ctx, int utf8_end_pos)
{
hb_subtitle_style_t * style = &ctx->in_style->current;
// do we need to add a style atom?
if (((ctx->out_style.flags ^ style->flags) & TX3G_STYLES) ||
ctx->out_style.fg_rgb != style->fg_rgb ||
ctx->out_style.fg_alpha != style->fg_alpha ||
ctx->flush)
{
if (ctx->style_start < utf8_end_pos)
{
if (!tx3g_update_style_atoms(ctx, utf8_end_pos - 1))
{
return 0;
}
ctx->style_start = utf8_end_pos;
}
ctx->out_style = *style;
ctx->flush = 0;
}
return 1;
}
hb_tx3g_style_context_t *
hb_tx3g_style_init(int height, const char * ssa_header)
{
hb_tx3g_style_context_t * ctx;
ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL)
{
return NULL;
}
ctx->in_style = hb_subtitle_style_init(ssa_header);
ctx->height = height;
ctx->style_atoms.buf = NULL;
ctx->style_atoms.size = 0;
ctx->style_atoms.alloc = 0;
ctx->style_atom_count = 0;
ctx->style_start = 0;
ctx->out_style = ctx->in_style->current;
ctx->flush = 1;
return ctx;
}
void hb_tx3g_style_reset(hb_tx3g_style_context_t * ctx)
{
ctx->style_atoms.buf = NULL;
ctx->style_atoms.size = 0;
ctx->style_atoms.alloc = 0;
ctx->style_atom_count = 0;
ctx->style_start = 0;
ctx->out_style = ctx->in_style->current;
ctx->flush = 1;
}
void hb_tx3g_style_close(hb_tx3g_style_context_t ** pctx)
{
if (pctx == NULL || *pctx == NULL)
{
return;
}
hb_tx3g_style_context_t * ctx = *pctx;
hb_subtitle_style_close(&ctx->in_style);
free(ctx);
*pctx = NULL;
}
/*
* Copy the input to output removing markup and adding markup to the style
* atom where appropriate.
*/
void hb_muxmp4_process_subtitle_style(
hb_tx3g_style_context_t * ctx,
uint8_t * input,
uint8_t ** out_buf,
uint8_t ** out_style_atoms,
uint16_t * stylesize)
{
uint16_t utf8_pos = 0;
int consumed, in_pos = 0, out_pos = 0, len;
hb_tx3g_output_buf_t output;
char * text;
const char * ssa_text, * style;
output.buf = NULL;
output.alloc = 0;
output.size = 0;
*out_buf = NULL;
*out_style_atoms = NULL;
*stylesize = 0;
ssa_style_reset(ctx->in_style);
// Skip past the SSA preamble
char ** event = get_fields((char*)input, 9);
if (hb_str_vlen(event) < 9)
{
// Not enough fields
goto fail;
}
style = event[2];
ssa_text = event[8];
ctx->in_style->event_style_default = ssa_style_set(ctx->in_style, style);
hb_tx3g_style_reset(ctx);
in_pos = 0;
// Always allocate enough for empty string
if (!check_realloc_output(&output, 1))
{
goto fail;
}
while (ssa_text[in_pos] != '\0')
{
text = ssa_to_text(ssa_text + in_pos, &consumed, ctx->in_style);
if (text == NULL)
break;
// count UTF8 characters, and get length of text
len = 0;
int ii, n;
for (ii = 0; text[ii] != '\0'; ii += n)
{
int jj;
char c = text[ii];
utf8_pos++;
if ((c & 0x80) == 0x00) n = 1;
else if ((c & 0xE0) == 0xC0) n = 2;
else if ((c & 0xF0) == 0xE0) n = 3;
else if ((c & 0xF8) == 0xF0) n = 4;
else n = 1; // invalid, but must handle
// Prevent skipping null terminator
for (jj = 1; jj < n && text[ii + jj] != '\0'; jj++);
n = jj;
len += n;
}
if (!check_realloc_output(&output, out_pos + len + 1))
{
goto fail;
}
strcpy((char*)output.buf + out_pos, text);
free(text);
out_pos += len;
in_pos += consumed;
if (!tx3g_update_style(ctx, utf8_pos))
{
goto fail;
}
}
// Return to default style at end of line, flushes any pending
// style changes
ctx->flush = 1;
if (!tx3g_update_style(ctx, utf8_pos))
{
goto fail;
}
// null terminate output string
output.buf[out_pos] = 0;
if (ctx->style_atom_count > 0)
{
*stylesize = 10 + (ctx->style_atom_count * 12);
memcpy(ctx->style_atoms.buf + 4, "styl", 4);
ctx->style_atoms.buf[0] = 0;
ctx->style_atoms.buf[1] = 0;
ctx->style_atoms.buf[2] = (*stylesize >> 8) & 0xff;
ctx->style_atoms.buf[3] = *stylesize & 0xff;
ctx->style_atoms.buf[8] = (ctx->style_atom_count >> 8) & 0xff;
ctx->style_atoms.buf[9] = ctx->style_atom_count & 0xff;
*out_style_atoms = ctx->style_atoms.buf;
}
*out_buf = output.buf;
hb_str_vfree(event);
return;
fail:
hb_str_vfree(event);
free(output.buf);
free(ctx->style_atoms.buf);
}