/* 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 "handbrake/handbrake.h" #include "handbrake/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); }