| /* GStreamer TTML subtitle parser |
| * Copyright (C) <2015> British Broadcasting Corporation |
| * Authors: |
| * Chris Bass <dash@rd.bbc.co.uk> |
| * Peter Taylour <dash@rd.bbc.co.uk> |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public |
| * License along with this library; if not, write to the |
| * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| /* |
| * Parses subtitle files encoded using the EBU-TT-D profile of TTML, as defined |
| * in https://tech.ebu.ch/files/live/sites/tech/files/shared/tech/tech3380.pdf |
| * and http://www.w3.org/TR/ttaf1-dfxp/, respectively. |
| */ |
| |
| #include <glib.h> |
| |
| #include <string.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <math.h> |
| #include <libxml/xmlmemory.h> |
| #include <libxml/parser.h> |
| |
| #include "ttmlparse.h" |
| #include "subtitle.h" |
| #include "subtitlemeta.h" |
| |
| #define DEFAULT_CELLRES_X 32 |
| #define DEFAULT_CELLRES_Y 15 |
| #define MAX_FONT_FAMILY_NAME_LENGTH 128 |
| #define NSECONDS_IN_DAY 24 * 3600 * GST_SECOND |
| |
| #define TTML_CHAR_NULL 0x00 |
| #define TTML_CHAR_SPACE 0x20 |
| #define TTML_CHAR_TAB 0x09 |
| #define TTML_CHAR_LF 0x0A |
| #define TTML_CHAR_CR 0x0D |
| |
| GST_DEBUG_CATEGORY_EXTERN (ttmlparse_debug); |
| #define GST_CAT_DEFAULT ttmlparse_debug |
| |
| static gchar *ttml_get_xml_property (const xmlNode * node, const char *name); |
| static gpointer ttml_copy_tree_element (gconstpointer src, gpointer data); |
| |
| typedef struct _TtmlStyleSet TtmlStyleSet; |
| typedef struct _TtmlElement TtmlElement; |
| typedef struct _TtmlScene TtmlScene; |
| |
| typedef enum |
| { |
| TTML_ELEMENT_TYPE_STYLE, |
| TTML_ELEMENT_TYPE_REGION, |
| TTML_ELEMENT_TYPE_BODY, |
| TTML_ELEMENT_TYPE_DIV, |
| TTML_ELEMENT_TYPE_P, |
| TTML_ELEMENT_TYPE_SPAN, |
| TTML_ELEMENT_TYPE_ANON_SPAN, |
| TTML_ELEMENT_TYPE_BR |
| } TtmlElementType; |
| |
| typedef enum |
| { |
| TTML_WHITESPACE_MODE_NONE, |
| TTML_WHITESPACE_MODE_DEFAULT, |
| TTML_WHITESPACE_MODE_PRESERVE, |
| } TtmlWhitespaceMode; |
| |
| struct _TtmlElement |
| { |
| TtmlElementType type; |
| gchar *id; |
| TtmlWhitespaceMode whitespace_mode; |
| gchar **styles; |
| gchar *region; |
| GstClockTime begin; |
| GstClockTime end; |
| TtmlStyleSet *style_set; |
| gchar *text; |
| }; |
| |
| /* Represents a static scene consisting of one or more trees of elements that |
| * should be visible over a specific period of time. */ |
| struct _TtmlScene |
| { |
| GstClockTime begin; |
| GstClockTime end; |
| GList *trees; |
| GstBuffer *buf; |
| }; |
| |
| struct _TtmlStyleSet |
| { |
| GHashTable *table; |
| }; |
| |
| |
| static TtmlStyleSet * |
| ttml_style_set_new (void) |
| { |
| TtmlStyleSet *ret = g_slice_new0 (TtmlStyleSet); |
| ret->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); |
| return ret; |
| } |
| |
| |
| static void |
| ttml_style_set_delete (TtmlStyleSet * style_set) |
| { |
| if (style_set) { |
| g_hash_table_unref (style_set->table); |
| g_slice_free (TtmlStyleSet, style_set); |
| } |
| } |
| |
| |
| /* If attribute with name @attr_name already exists in @style_set, its value |
| * will be replaced by @attr_value. */ |
| static gboolean |
| ttml_style_set_add_attr (TtmlStyleSet * style_set, const gchar * attr_name, |
| const gchar * attr_value) |
| { |
| return g_hash_table_insert (style_set->table, g_strdup (attr_name), |
| g_strdup (attr_value)); |
| } |
| |
| |
| static gboolean |
| ttml_style_set_contains_attr (TtmlStyleSet * style_set, const gchar * attr_name) |
| { |
| return g_hash_table_contains (style_set->table, attr_name); |
| } |
| |
| |
| static const gchar * |
| ttml_style_set_get_attr (TtmlStyleSet * style_set, const gchar * attr_name) |
| { |
| return g_hash_table_lookup (style_set->table, attr_name); |
| } |
| |
| |
| static guint8 |
| ttml_hex_pair_to_byte (const gchar * hex_pair) |
| { |
| gint hi_digit, lo_digit; |
| |
| hi_digit = g_ascii_xdigit_value (*hex_pair); |
| lo_digit = g_ascii_xdigit_value (*(hex_pair + 1)); |
| return (hi_digit << 4) + lo_digit; |
| } |
| |
| |
| /* Color strings in EBU-TT-D can have the form "#RRBBGG" or "#RRBBGGAA". */ |
| static GstSubtitleColor |
| ttml_parse_colorstring (const gchar * color) |
| { |
| guint length; |
| const gchar *c = NULL; |
| GstSubtitleColor ret = { 0, 0, 0, 0 }; |
| |
| if (!color) |
| return ret; |
| |
| length = strlen (color); |
| if (((length == 7) || (length == 9)) && *color == '#') { |
| c = color + 1; |
| |
| ret.r = ttml_hex_pair_to_byte (c); |
| ret.g = ttml_hex_pair_to_byte (c + 2); |
| ret.b = ttml_hex_pair_to_byte (c + 4); |
| |
| if (length == 7) |
| ret.a = G_MAXUINT8; |
| else |
| ret.a = ttml_hex_pair_to_byte (c + 6); |
| |
| GST_CAT_LOG (ttmlparse_debug, "Returning color - r:%u b:%u g:%u a:%u", |
| ret.r, ret.b, ret.g, ret.a); |
| } else { |
| GST_CAT_ERROR (ttmlparse_debug, "Invalid color string: %s", color); |
| } |
| |
| return ret; |
| } |
| |
| |
| static void |
| ttml_style_set_print (TtmlStyleSet * style_set) |
| { |
| GHashTableIter iter; |
| gpointer attr_name, attr_value; |
| |
| if (!style_set) { |
| GST_CAT_LOG (ttmlparse_debug, "\t\t[NULL]"); |
| return; |
| } |
| |
| g_hash_table_iter_init (&iter, style_set->table); |
| while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) { |
| GST_CAT_LOG (ttmlparse_debug, "\t\t%s: %s", (const gchar *) attr_name, |
| (const gchar *) attr_value); |
| } |
| } |
| |
| |
| static TtmlStyleSet * |
| ttml_parse_style_set (const xmlNode * node) |
| { |
| TtmlStyleSet *s; |
| gchar *value = NULL; |
| xmlAttrPtr attr; |
| |
| value = ttml_get_xml_property (node, "id"); |
| if (!value) { |
| GST_CAT_ERROR (ttmlparse_debug, "styles must have an ID."); |
| return NULL; |
| } |
| g_free (value); |
| |
| s = ttml_style_set_new (); |
| |
| for (attr = node->properties; attr != NULL; attr = attr->next) { |
| if (attr->ns && ((g_strcmp0 ((const gchar *) attr->ns->prefix, "tts") == 0) |
| || (g_strcmp0 ((const gchar *) attr->ns->prefix, "itts") == 0) |
| || (g_strcmp0 ((const gchar *) attr->ns->prefix, "ebutts") == 0))) { |
| ttml_style_set_add_attr (s, (const gchar *) attr->name, |
| (const gchar *) attr->children->content); |
| } |
| } |
| |
| return s; |
| } |
| |
| |
| static void |
| ttml_delete_element (TtmlElement * element) |
| { |
| g_free ((gpointer) element->id); |
| if (element->styles) |
| g_strfreev (element->styles); |
| g_free ((gpointer) element->region); |
| ttml_style_set_delete (element->style_set); |
| g_free ((gpointer) element->text); |
| g_slice_free (TtmlElement, element); |
| } |
| |
| |
| static gchar * |
| ttml_get_xml_property (const xmlNode * node, const char *name) |
| { |
| xmlChar *xml_string = NULL; |
| gchar *gst_string = NULL; |
| |
| g_return_val_if_fail (strlen (name) < 128, NULL); |
| |
| xml_string = xmlGetProp (node, (xmlChar *) name); |
| if (!xml_string) |
| return NULL; |
| gst_string = g_strdup ((gchar *) xml_string); |
| xmlFree (xml_string); |
| return gst_string; |
| } |
| |
| |
| /* EBU-TT-D timecodes have format hours:minutes:seconds[.fraction] */ |
| static GstClockTime |
| ttml_parse_timecode (const gchar * timestring) |
| { |
| gchar **strings; |
| guint64 hours = 0, minutes = 0, seconds = 0, milliseconds = 0; |
| GstClockTime time = GST_CLOCK_TIME_NONE; |
| |
| GST_CAT_LOG (ttmlparse_debug, "time string: %s", timestring); |
| |
| strings = g_strsplit (timestring, ":", 3); |
| if (g_strv_length (strings) != 3U) { |
| GST_CAT_ERROR (ttmlparse_debug, "badly formatted time string: %s", |
| timestring); |
| return time; |
| } |
| |
| hours = g_ascii_strtoull (strings[0], NULL, 10U); |
| minutes = g_ascii_strtoull (strings[1], NULL, 10U); |
| if (g_strstr_len (strings[2], -1, ".")) { |
| guint n_digits; |
| gchar **substrings = g_strsplit (strings[2], ".", 2); |
| seconds = g_ascii_strtoull (substrings[0], NULL, 10U); |
| n_digits = strlen (substrings[1]); |
| milliseconds = g_ascii_strtoull (substrings[1], NULL, 10U); |
| milliseconds = |
| (guint64) (milliseconds * pow (10.0, (3 - (double) n_digits))); |
| g_strfreev (substrings); |
| } else { |
| seconds = g_ascii_strtoull (strings[2], NULL, 10U); |
| } |
| |
| if (minutes > 59 || seconds > 60) { |
| GST_CAT_ERROR (ttmlparse_debug, "invalid time string " |
| "(minutes or seconds out-of-bounds): %s\n", timestring); |
| } |
| |
| g_strfreev (strings); |
| GST_CAT_LOG (ttmlparse_debug, |
| "hours: %" G_GUINT64_FORMAT " minutes: %" G_GUINT64_FORMAT |
| " seconds: %" G_GUINT64_FORMAT " milliseconds: %" G_GUINT64_FORMAT "", |
| hours, minutes, seconds, milliseconds); |
| |
| time = hours * GST_SECOND * 3600 |
| + minutes * GST_SECOND * 60 |
| + seconds * GST_SECOND + milliseconds * GST_MSECOND; |
| |
| return time; |
| } |
| |
| |
| static TtmlElement * |
| ttml_parse_element (const xmlNode * node) |
| { |
| TtmlElement *element; |
| TtmlElementType type; |
| gchar *value; |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "Element name: %s", |
| (const char *) node->name); |
| if ((g_strcmp0 ((const char *) node->name, "style") == 0)) { |
| type = TTML_ELEMENT_TYPE_STYLE; |
| } else if ((g_strcmp0 ((const char *) node->name, "region") == 0)) { |
| type = TTML_ELEMENT_TYPE_REGION; |
| } else if ((g_strcmp0 ((const char *) node->name, "body") == 0)) { |
| type = TTML_ELEMENT_TYPE_BODY; |
| } else if ((g_strcmp0 ((const char *) node->name, "div") == 0)) { |
| type = TTML_ELEMENT_TYPE_DIV; |
| } else if ((g_strcmp0 ((const char *) node->name, "p") == 0)) { |
| type = TTML_ELEMENT_TYPE_P; |
| } else if ((g_strcmp0 ((const char *) node->name, "span") == 0)) { |
| type = TTML_ELEMENT_TYPE_SPAN; |
| } else if ((g_strcmp0 ((const char *) node->name, "text") == 0)) { |
| type = TTML_ELEMENT_TYPE_ANON_SPAN; |
| } else if ((g_strcmp0 ((const char *) node->name, "br") == 0)) { |
| type = TTML_ELEMENT_TYPE_BR; |
| } else { |
| return NULL; |
| } |
| |
| element = g_slice_new0 (TtmlElement); |
| element->type = type; |
| |
| if ((value = ttml_get_xml_property (node, "id"))) { |
| element->id = g_strdup (value); |
| g_free (value); |
| } |
| |
| if ((value = ttml_get_xml_property (node, "style"))) { |
| element->styles = g_strsplit (value, " ", 0); |
| GST_CAT_DEBUG (ttmlparse_debug, "%u style(s) referenced in element.", |
| g_strv_length (element->styles)); |
| g_free (value); |
| } |
| |
| if (element->type == TTML_ELEMENT_TYPE_STYLE |
| || element->type == TTML_ELEMENT_TYPE_REGION) { |
| TtmlStyleSet *ss; |
| ss = ttml_parse_style_set (node); |
| if (ss) |
| element->style_set = ss; |
| else |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Style or Region contains no styling attributes."); |
| } |
| |
| if ((value = ttml_get_xml_property (node, "region"))) { |
| element->region = g_strdup (value); |
| g_free (value); |
| } |
| |
| if ((value = ttml_get_xml_property (node, "begin"))) { |
| element->begin = ttml_parse_timecode (value); |
| g_free (value); |
| } else { |
| element->begin = GST_CLOCK_TIME_NONE; |
| } |
| |
| if ((value = ttml_get_xml_property (node, "end"))) { |
| element->end = ttml_parse_timecode (value); |
| g_free (value); |
| } else { |
| element->end = GST_CLOCK_TIME_NONE; |
| } |
| |
| if (node->content) { |
| GST_CAT_LOG (ttmlparse_debug, "Node content: %s", node->content); |
| element->text = g_strdup ((const gchar *) node->content); |
| } |
| |
| if (element->type == TTML_ELEMENT_TYPE_BR) |
| element->text = g_strdup ("\n"); |
| |
| if ((value = ttml_get_xml_property (node, "space"))) { |
| if (g_strcmp0 (value, "preserve") == 0) |
| element->whitespace_mode = TTML_WHITESPACE_MODE_PRESERVE; |
| else if (g_strcmp0 (value, "default") == 0) |
| element->whitespace_mode = TTML_WHITESPACE_MODE_DEFAULT; |
| g_free (value); |
| } |
| |
| return element; |
| } |
| |
| |
| static GNode * |
| ttml_parse_body (const xmlNode * node) |
| { |
| GNode *ret; |
| TtmlElement *element; |
| |
| GST_CAT_LOG (ttmlparse_debug, "parsing node %s", node->name); |
| element = ttml_parse_element (node); |
| if (element) |
| ret = g_node_new (element); |
| else |
| return NULL; |
| |
| for (node = node->children; node != NULL; node = node->next) { |
| GNode *descendants = NULL; |
| if ((descendants = ttml_parse_body (node))) |
| g_node_append (ret, descendants); |
| } |
| |
| return ret; |
| } |
| |
| |
| /* Update the fields of a GstSubtitleStyleSet, @style_set, according to the |
| * values defined in a TtmlStyleSet, @tss, and a given cell resolution. */ |
| static void |
| ttml_update_style_set (GstSubtitleStyleSet * style_set, TtmlStyleSet * tss, |
| guint cellres_x, guint cellres_y) |
| { |
| const gchar *attr; |
| |
| if ((attr = ttml_style_set_get_attr (tss, "textDirection"))) { |
| if (g_strcmp0 (attr, "rtl") == 0) |
| style_set->text_direction = GST_SUBTITLE_TEXT_DIRECTION_RTL; |
| else |
| style_set->text_direction = GST_SUBTITLE_TEXT_DIRECTION_LTR; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "fontFamily"))) { |
| if (strlen (attr) <= MAX_FONT_FAMILY_NAME_LENGTH) { |
| g_free (style_set->font_family); |
| style_set->font_family = g_strdup (attr); |
| } else { |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Ignoring font family name as it's overly long."); |
| } |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "fontSize"))) { |
| style_set->font_size = g_ascii_strtod (attr, NULL) / 100.0; |
| } |
| style_set->font_size *= (1.0 / cellres_y); |
| |
| if ((attr = ttml_style_set_get_attr (tss, "lineHeight"))) { |
| if (g_strcmp0 (attr, "normal") == 0) |
| style_set->line_height = -1; |
| else |
| style_set->line_height = g_ascii_strtod (attr, NULL) / 100.0; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "textAlign"))) { |
| if (g_strcmp0 (attr, "left") == 0) |
| style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_LEFT; |
| else if (g_strcmp0 (attr, "center") == 0) |
| style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_CENTER; |
| else if (g_strcmp0 (attr, "right") == 0) |
| style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_RIGHT; |
| else if (g_strcmp0 (attr, "end") == 0) |
| style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_END; |
| else |
| style_set->text_align = GST_SUBTITLE_TEXT_ALIGN_START; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "color"))) { |
| style_set->color = ttml_parse_colorstring (attr); |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "backgroundColor"))) { |
| style_set->background_color = ttml_parse_colorstring (attr); |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "fontStyle"))) { |
| if (g_strcmp0 (attr, "italic") == 0) |
| style_set->font_style = GST_SUBTITLE_FONT_STYLE_ITALIC; |
| else |
| style_set->font_style = GST_SUBTITLE_FONT_STYLE_NORMAL; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "fontWeight"))) { |
| if (g_strcmp0 (attr, "bold") == 0) |
| style_set->font_weight = GST_SUBTITLE_FONT_WEIGHT_BOLD; |
| else |
| style_set->font_weight = GST_SUBTITLE_FONT_WEIGHT_NORMAL; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "textDecoration"))) { |
| if (g_strcmp0 (attr, "underline") == 0) |
| style_set->text_decoration = GST_SUBTITLE_TEXT_DECORATION_UNDERLINE; |
| else |
| style_set->text_decoration = GST_SUBTITLE_TEXT_DECORATION_NONE; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "unicodeBidi"))) { |
| if (g_strcmp0 (attr, "embed") == 0) |
| style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_EMBED; |
| else if (g_strcmp0 (attr, "bidiOverride") == 0) |
| style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_OVERRIDE; |
| else |
| style_set->unicode_bidi = GST_SUBTITLE_UNICODE_BIDI_NORMAL; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "wrapOption"))) { |
| if (g_strcmp0 (attr, "noWrap") == 0) |
| style_set->wrap_option = GST_SUBTITLE_WRAPPING_OFF; |
| else |
| style_set->wrap_option = GST_SUBTITLE_WRAPPING_ON; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "multiRowAlign"))) { |
| if (g_strcmp0 (attr, "start") == 0) |
| style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_START; |
| else if (g_strcmp0 (attr, "center") == 0) |
| style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_CENTER; |
| else if (g_strcmp0 (attr, "end") == 0) |
| style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_END; |
| else |
| style_set->multi_row_align = GST_SUBTITLE_MULTI_ROW_ALIGN_AUTO; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "linePadding"))) { |
| style_set->line_padding = g_ascii_strtod (attr, NULL); |
| style_set->line_padding *= (1.0 / cellres_x); |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "origin"))) { |
| gchar *c; |
| style_set->origin_x = g_ascii_strtod (attr, &c) / 100.0; |
| while (!g_ascii_isdigit (*c) && *c != '+' && *c != '-') |
| ++c; |
| style_set->origin_y = g_ascii_strtod (c, NULL) / 100.0; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "extent"))) { |
| gchar *c; |
| style_set->extent_w = g_ascii_strtod (attr, &c) / 100.0; |
| if ((style_set->origin_x + style_set->extent_w) > 1.0) { |
| style_set->extent_w = 1.0 - style_set->origin_x; |
| } |
| while (!g_ascii_isdigit (*c) && *c != '+' && *c != '-') |
| ++c; |
| style_set->extent_h = g_ascii_strtod (c, NULL) / 100.0; |
| if ((style_set->origin_y + style_set->extent_h) > 1.0) { |
| style_set->extent_h = 1.0 - style_set->origin_y; |
| } |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "displayAlign"))) { |
| if (g_strcmp0 (attr, "center") == 0) |
| style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_CENTER; |
| else if (g_strcmp0 (attr, "after") == 0) |
| style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_AFTER; |
| else |
| style_set->display_align = GST_SUBTITLE_DISPLAY_ALIGN_BEFORE; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "padding"))) { |
| gchar **decimals; |
| guint n_decimals; |
| guint i; |
| |
| decimals = g_strsplit (attr, "%", 0); |
| n_decimals = g_strv_length (decimals) - 1; |
| for (i = 0; i < n_decimals; ++i) |
| g_strstrip (decimals[i]); |
| |
| switch (n_decimals) { |
| case 1: |
| style_set->padding_start = style_set->padding_end = |
| style_set->padding_before = style_set->padding_after = |
| g_ascii_strtod (decimals[0], NULL) / 100.0; |
| break; |
| |
| case 2: |
| style_set->padding_before = style_set->padding_after = |
| g_ascii_strtod (decimals[0], NULL) / 100.0; |
| style_set->padding_start = style_set->padding_end = |
| g_ascii_strtod (decimals[1], NULL) / 100.0; |
| break; |
| |
| case 3: |
| style_set->padding_before = g_ascii_strtod (decimals[0], NULL) / 100.0; |
| style_set->padding_start = style_set->padding_end = |
| g_ascii_strtod (decimals[1], NULL) / 100.0; |
| style_set->padding_after = g_ascii_strtod (decimals[2], NULL) / 100.0; |
| break; |
| |
| case 4: |
| style_set->padding_before = g_ascii_strtod (decimals[0], NULL) / 100.0; |
| style_set->padding_end = g_ascii_strtod (decimals[1], NULL) / 100.0; |
| style_set->padding_after = g_ascii_strtod (decimals[2], NULL) / 100.0; |
| style_set->padding_start = g_ascii_strtod (decimals[3], NULL) / 100.0; |
| break; |
| } |
| g_strfreev (decimals); |
| |
| /* Padding values in TTML files are relative to the region width & height; |
| * make them relative to the overall display width & height like all other |
| * dimensions. */ |
| style_set->padding_before *= style_set->extent_h; |
| style_set->padding_after *= style_set->extent_h; |
| style_set->padding_end *= style_set->extent_w; |
| style_set->padding_start *= style_set->extent_w; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "writingMode"))) { |
| if (g_str_has_prefix (attr, "rl")) |
| style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_RLTB; |
| else if ((g_strcmp0 (attr, "tbrl") == 0) |
| || (g_strcmp0 (attr, "tb") == 0)) |
| style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_TBRL; |
| else if (g_strcmp0 (attr, "tblr") == 0) |
| style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_TBLR; |
| else |
| style_set->writing_mode = GST_SUBTITLE_WRITING_MODE_LRTB; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "showBackground"))) { |
| if (g_strcmp0 (attr, "whenActive") == 0) |
| style_set->show_background = GST_SUBTITLE_BACKGROUND_MODE_WHEN_ACTIVE; |
| else |
| style_set->show_background = GST_SUBTITLE_BACKGROUND_MODE_ALWAYS; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "overflow"))) { |
| if (g_strcmp0 (attr, "visible") == 0) |
| style_set->overflow = GST_SUBTITLE_OVERFLOW_MODE_VISIBLE; |
| else |
| style_set->overflow = GST_SUBTITLE_OVERFLOW_MODE_HIDDEN; |
| } |
| |
| if ((attr = ttml_style_set_get_attr (tss, "fillLineGap"))) { |
| if (g_strcmp0 (attr, "true") == 0) |
| style_set->fill_line_gap = TRUE; |
| } |
| } |
| |
| |
| static TtmlStyleSet * |
| ttml_style_set_copy (TtmlStyleSet * style_set) |
| { |
| GHashTableIter iter; |
| gpointer attr_name, attr_value; |
| TtmlStyleSet *ret = ttml_style_set_new (); |
| |
| g_hash_table_iter_init (&iter, style_set->table); |
| while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) { |
| ttml_style_set_add_attr (ret, (const gchar *) attr_name, |
| (const gchar *) attr_value); |
| } |
| |
| return ret; |
| } |
| |
| |
| /* set2 overrides set1. Unlike style inheritance, merging will result in all |
| * values from set1 being merged into set2. */ |
| static TtmlStyleSet * |
| ttml_style_set_merge (TtmlStyleSet * set1, TtmlStyleSet * set2) |
| { |
| TtmlStyleSet *ret = NULL; |
| |
| if (set1) { |
| ret = ttml_style_set_copy (set1); |
| |
| if (set2) { |
| GHashTableIter iter; |
| gpointer attr_name, attr_value; |
| |
| g_hash_table_iter_init (&iter, set2->table); |
| while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) { |
| ttml_style_set_add_attr (ret, (const gchar *) attr_name, |
| (const gchar *) attr_value); |
| } |
| } |
| } else if (set2) { |
| ret = ttml_style_set_copy (set2); |
| } |
| |
| return ret; |
| } |
| |
| |
| static gchar * |
| ttml_get_relative_font_size (const gchar * parent_size, |
| const gchar * child_size) |
| { |
| guint psize = (guint) g_ascii_strtoull (parent_size, NULL, 10U); |
| guint csize = (guint) g_ascii_strtoull (child_size, NULL, 10U); |
| csize = (csize * psize) / 100U; |
| return g_strdup_printf ("%u%%", csize); |
| } |
| |
| |
| static TtmlStyleSet * |
| ttml_style_set_inherit (TtmlStyleSet * parent, TtmlStyleSet * child) |
| { |
| TtmlStyleSet *ret = NULL; |
| GHashTableIter iter; |
| gpointer attr_name, attr_value; |
| |
| if (child) { |
| ret = ttml_style_set_copy (child); |
| } else { |
| ret = ttml_style_set_new (); |
| } |
| |
| if (!parent) |
| return ret; |
| |
| g_hash_table_iter_init (&iter, parent->table); |
| while (g_hash_table_iter_next (&iter, &attr_name, &attr_value)) { |
| /* In TTML, if an element which has a defined fontSize is the child of an |
| * element that also has a defined fontSize, the child's font size is |
| * relative to that of its parent. If its parent doesn't have a defined |
| * fontSize, then the child's fontSize is relative to the document's cell |
| * size. Therefore, if the former is true, we calculate the value of |
| * fontSize based on the parent's fontSize; otherwise, we simply keep |
| * the value defined in the child's style set. */ |
| if (g_strcmp0 ((const gchar *) attr_name, "fontSize") == 0 |
| && ttml_style_set_contains_attr (ret, "fontSize")) { |
| const gchar *original_child_font_size = |
| ttml_style_set_get_attr (ret, "fontSize"); |
| gchar *scaled_child_font_size = |
| ttml_get_relative_font_size ((const gchar *) attr_value, |
| original_child_font_size); |
| GST_CAT_LOG (ttmlparse_debug, "Calculated font size: %s", |
| scaled_child_font_size); |
| ttml_style_set_add_attr (ret, (const gchar *) attr_name, |
| scaled_child_font_size); |
| g_free (scaled_child_font_size); |
| } |
| |
| /* Not all styling attributes are inherited in TTML. */ |
| if (g_strcmp0 ((const gchar *) attr_name, "backgroundColor") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "origin") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "extent") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "displayAlign") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "overflow") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "padding") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "writingMode") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "showBackground") != 0 |
| && g_strcmp0 ((const gchar *) attr_name, "unicodeBidi") != 0) { |
| if (!ttml_style_set_contains_attr (ret, (const gchar *) attr_name)) { |
| ttml_style_set_add_attr (ret, (const gchar *) attr_name, |
| (const gchar *) attr_value); |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| |
| /* |
| * Returns TRUE iff @element1 and @element2 reference the same set of styles. |
| * If neither @element1 nor @element2 reference any styles, they are considered |
| * to have matching styling and, hence, TRUE is returned. |
| */ |
| static gboolean |
| ttml_element_styles_match (TtmlElement * element1, TtmlElement * element2) |
| { |
| const gchar *const *strv; |
| gint i; |
| |
| if (!element1 || !element2 || (!element1->styles && element2->styles) || |
| (element1->styles && !element2->styles)) |
| return FALSE; |
| |
| if (!element1->styles && !element2->styles) |
| return TRUE; |
| |
| strv = (const gchar * const *) element2->styles; |
| |
| if (g_strv_length (element1->styles) != g_strv_length (element2->styles)) |
| return FALSE; |
| |
| for (i = 0; i < g_strv_length (element1->styles); ++i) { |
| if (!g_strv_contains (strv, element1->styles[i])) |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| |
| static gchar * |
| ttml_get_element_type_string (TtmlElement * element) |
| { |
| switch (element->type) { |
| case TTML_ELEMENT_TYPE_STYLE: |
| return g_strdup ("<style>"); |
| break; |
| case TTML_ELEMENT_TYPE_REGION: |
| return g_strdup ("<region>"); |
| break; |
| case TTML_ELEMENT_TYPE_BODY: |
| return g_strdup ("<body>"); |
| break; |
| case TTML_ELEMENT_TYPE_DIV: |
| return g_strdup ("<div>"); |
| break; |
| case TTML_ELEMENT_TYPE_P: |
| return g_strdup ("<p>"); |
| break; |
| case TTML_ELEMENT_TYPE_SPAN: |
| return g_strdup ("<span>"); |
| break; |
| case TTML_ELEMENT_TYPE_ANON_SPAN: |
| return g_strdup ("<anon-span>"); |
| break; |
| case TTML_ELEMENT_TYPE_BR: |
| return g_strdup ("<br>"); |
| break; |
| default: |
| return g_strdup ("Unknown"); |
| break; |
| } |
| } |
| |
| |
| /* Merge styles referenced by an element. */ |
| static gboolean |
| ttml_resolve_styles (GNode * node, gpointer data) |
| { |
| TtmlStyleSet *tmp = NULL; |
| TtmlElement *element, *style; |
| GHashTable *styles_table; |
| gchar *type_string; |
| guint i; |
| |
| styles_table = (GHashTable *) data; |
| element = node->data; |
| |
| type_string = ttml_get_element_type_string (element); |
| GST_CAT_LOG (ttmlparse_debug, "Element type: %s", type_string); |
| g_free (type_string); |
| |
| if (!element->styles) |
| return FALSE; |
| |
| for (i = 0; i < g_strv_length (element->styles); ++i) { |
| tmp = element->style_set; |
| style = g_hash_table_lookup (styles_table, element->styles[i]); |
| if (style) { |
| GST_CAT_LOG (ttmlparse_debug, "Merging style %s...", element->styles[i]); |
| element->style_set = ttml_style_set_merge (element->style_set, |
| style->style_set); |
| ttml_style_set_delete (tmp); |
| } else { |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Element references an unknown style (%s)", element->styles[i]); |
| } |
| } |
| |
| GST_CAT_LOG (ttmlparse_debug, "Style set after merging:"); |
| ttml_style_set_print (element->style_set); |
| |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_resolve_referenced_styles (GList * trees, GHashTable * styles_table) |
| { |
| GList *tree; |
| |
| for (tree = g_list_first (trees); tree; tree = tree->next) { |
| GNode *root = (GNode *) tree->data; |
| g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_resolve_styles, |
| styles_table); |
| } |
| } |
| |
| |
| /* Inherit styling attributes from parent. */ |
| static gboolean |
| ttml_inherit_styles (GNode * node, gpointer data) |
| { |
| TtmlStyleSet *tmp = NULL; |
| TtmlElement *element, *parent; |
| gchar *type_string; |
| |
| element = node->data; |
| |
| type_string = ttml_get_element_type_string (element); |
| GST_CAT_LOG (ttmlparse_debug, "Element type: %s", type_string); |
| g_free (type_string); |
| |
| if (node->parent) { |
| parent = node->parent->data; |
| if (parent->style_set) { |
| tmp = element->style_set; |
| if (element->type == TTML_ELEMENT_TYPE_ANON_SPAN || |
| element->type == TTML_ELEMENT_TYPE_BR) { |
| element->style_set = ttml_style_set_merge (parent->style_set, |
| element->style_set); |
| element->styles = g_strdupv (parent->styles); |
| } else { |
| element->style_set = ttml_style_set_inherit (parent->style_set, |
| element->style_set); |
| } |
| ttml_style_set_delete (tmp); |
| } |
| } |
| |
| GST_CAT_LOG (ttmlparse_debug, "Style set after inheriting:"); |
| ttml_style_set_print (element->style_set); |
| |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_inherit_element_styles (GList * trees) |
| { |
| GList *tree; |
| |
| for (tree = g_list_first (trees); tree; tree = tree->next) { |
| GNode *root = (GNode *) tree->data; |
| g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_inherit_styles, |
| NULL); |
| } |
| } |
| |
| |
| /* If whitespace_mode isn't explicitly set for this element, inherit from its |
| * parent. If this element is the root of the tree, set whitespace_mode to |
| * that of the overall document. */ |
| static gboolean |
| ttml_inherit_element_whitespace_mode (GNode * node, gpointer data) |
| { |
| TtmlWhitespaceMode *doc_mode = (TtmlWhitespaceMode *) data; |
| TtmlElement *element = node->data; |
| TtmlElement *parent; |
| |
| if (element->whitespace_mode != TTML_WHITESPACE_MODE_NONE) |
| return FALSE; |
| |
| if (G_NODE_IS_ROOT (node)) { |
| element->whitespace_mode = *doc_mode; |
| return FALSE; |
| } |
| |
| parent = node->parent->data; |
| element->whitespace_mode = parent->whitespace_mode; |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_inherit_whitespace_mode (GNode * tree, TtmlWhitespaceMode doc_mode) |
| { |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1, |
| ttml_inherit_element_whitespace_mode, &doc_mode); |
| } |
| |
| |
| static gboolean |
| ttml_free_node_data (GNode * node, gpointer data) |
| { |
| TtmlElement *element = node->data; |
| ttml_delete_element (element); |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_delete_tree (GNode * tree) |
| { |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1, ttml_free_node_data, |
| NULL); |
| g_node_destroy (tree); |
| } |
| |
| |
| typedef struct |
| { |
| GstClockTime begin; |
| GstClockTime end; |
| } ClipWindow; |
| |
| static gboolean |
| ttml_clip_element_period (GNode * node, gpointer data) |
| { |
| TtmlElement *element = node->data; |
| ClipWindow *window = data; |
| |
| if (!GST_CLOCK_TIME_IS_VALID (element->begin)) { |
| return FALSE; |
| } |
| |
| if (element->begin > window->end || element->end < window->begin) { |
| ttml_delete_tree (node); |
| node = NULL; |
| return FALSE; |
| } |
| |
| element->begin = MAX (element->begin, window->begin); |
| element->end = MIN (element->end, window->end); |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_apply_time_window (GNode * tree, GstClockTime window_begin, |
| GstClockTime window_end) |
| { |
| ClipWindow window; |
| window.begin = window_begin; |
| window.end = window_end; |
| |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1, |
| ttml_clip_element_period, &window); |
| } |
| |
| |
| static gboolean |
| ttml_resolve_element_timings (GNode * node, gpointer data) |
| { |
| TtmlElement *element, *leaf; |
| |
| leaf = element = node->data; |
| |
| if (GST_CLOCK_TIME_IS_VALID (leaf->begin) |
| && GST_CLOCK_TIME_IS_VALID (leaf->end)) { |
| GST_CAT_LOG (ttmlparse_debug, "Leaf node already has timing."); |
| return FALSE; |
| } |
| |
| /* Inherit timings from ancestor. */ |
| while (node->parent && !GST_CLOCK_TIME_IS_VALID (element->begin)) { |
| node = node->parent; |
| element = node->data; |
| } |
| |
| if (!GST_CLOCK_TIME_IS_VALID (element->begin)) { |
| GST_CAT_WARNING (ttmlparse_debug, |
| "No timing found for element; setting to Root Temporal Extent."); |
| leaf->begin = 0; |
| leaf->end = NSECONDS_IN_DAY; |
| } else { |
| leaf->begin = element->begin; |
| leaf->end = element->end; |
| GST_CAT_LOG (ttmlparse_debug, "Leaf begin: %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (leaf->begin)); |
| GST_CAT_LOG (ttmlparse_debug, "Leaf end: %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (leaf->end)); |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_resolve_timings (GNode * tree) |
| { |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, |
| ttml_resolve_element_timings, NULL); |
| } |
| |
| |
| static gboolean |
| ttml_resolve_leaf_region (GNode * node, gpointer data) |
| { |
| TtmlElement *element, *leaf; |
| leaf = element = node->data; |
| |
| while (node->parent && !element->region) { |
| node = node->parent; |
| element = node->data; |
| } |
| |
| if (element->region) { |
| leaf->region = g_strdup (element->region); |
| GST_CAT_LOG (ttmlparse_debug, "Leaf region: %s", leaf->region); |
| } else { |
| GST_CAT_WARNING (ttmlparse_debug, "No region found above leaf element."); |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_resolve_regions (GNode * tree) |
| { |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, |
| ttml_resolve_leaf_region, NULL); |
| } |
| |
| |
| typedef struct |
| { |
| GstClockTime start_time; |
| GstClockTime next_transition_time; |
| } TrState; |
| |
| |
| static gboolean |
| ttml_update_transition_time (GNode * node, gpointer data) |
| { |
| TtmlElement *element = node->data; |
| TrState *state = (TrState *) data; |
| |
| if ((element->begin < state->next_transition_time) |
| && (!GST_CLOCK_TIME_IS_VALID (state->start_time) |
| || (element->begin > state->start_time))) { |
| state->next_transition_time = element->begin; |
| GST_CAT_LOG (ttmlparse_debug, |
| "Updating next transition time to element begin time (%" |
| GST_TIME_FORMAT ")", GST_TIME_ARGS (state->next_transition_time)); |
| return FALSE; |
| } |
| |
| if ((element->end < state->next_transition_time) |
| && (element->end > state->start_time)) { |
| state->next_transition_time = element->end; |
| GST_CAT_LOG (ttmlparse_debug, |
| "Updating next transition time to element end time (%" |
| GST_TIME_FORMAT ")", GST_TIME_ARGS (state->next_transition_time)); |
| } |
| |
| return FALSE; |
| } |
| |
| |
| /* Return details about the next transition after @time. */ |
| static GstClockTime |
| ttml_find_next_transition (GList * trees, GstClockTime time) |
| { |
| TrState state; |
| state.start_time = time; |
| state.next_transition_time = GST_CLOCK_TIME_NONE; |
| |
| for (trees = g_list_first (trees); trees; trees = trees->next) { |
| GNode *tree = (GNode *) trees->data; |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_ALL, -1, |
| ttml_update_transition_time, &state); |
| } |
| |
| GST_CAT_LOG (ttmlparse_debug, "Next transition is at %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (state.next_transition_time)); |
| |
| return state.next_transition_time; |
| } |
| |
| |
| /* Remove nodes from tree that are not visible at @time. */ |
| static GNode * |
| ttml_remove_nodes_by_time (GNode * node, GstClockTime time) |
| { |
| GNode *child, *next_child; |
| TtmlElement *element; |
| element = node->data; |
| |
| child = node->children; |
| next_child = child ? child->next : NULL; |
| while (child) { |
| ttml_remove_nodes_by_time (child, time); |
| child = next_child; |
| next_child = child ? child->next : NULL; |
| } |
| |
| if (!node->children && ((element->begin > time) || (element->end <= time))) { |
| ttml_delete_tree (node); |
| node = NULL; |
| } |
| |
| return node; |
| } |
| |
| |
| /* Return a list of trees containing the elements and their ancestors that are |
| * visible at @time. */ |
| static GList * |
| ttml_get_active_trees (GList * element_trees, GstClockTime time) |
| { |
| GList *tree; |
| GList *ret = NULL; |
| |
| for (tree = g_list_first (element_trees); tree; tree = tree->next) { |
| GNode *root = g_node_copy_deep ((GNode *) tree->data, |
| ttml_copy_tree_element, NULL); |
| GST_CAT_LOG (ttmlparse_debug, "There are %u nodes in tree.", |
| g_node_n_nodes (root, G_TRAVERSE_ALL)); |
| root = ttml_remove_nodes_by_time (root, time); |
| if (root) { |
| GST_CAT_LOG (ttmlparse_debug, |
| "After filtering there are %u nodes in tree.", g_node_n_nodes (root, |
| G_TRAVERSE_ALL)); |
| |
| ret = g_list_append (ret, root); |
| } else { |
| GST_CAT_LOG (ttmlparse_debug, |
| "All elements have been filtered from tree."); |
| } |
| } |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "There are %u trees in returned list.", |
| g_list_length (ret)); |
| return ret; |
| } |
| |
| |
| static GList * |
| ttml_create_scenes (GList * region_trees) |
| { |
| TtmlScene *cur_scene = NULL; |
| GList *output_scenes = NULL; |
| GList *active_trees = NULL; |
| GstClockTime timestamp = GST_CLOCK_TIME_NONE; |
| |
| while ((timestamp = ttml_find_next_transition (region_trees, timestamp)) |
| != GST_CLOCK_TIME_NONE) { |
| GST_CAT_LOG (ttmlparse_debug, |
| "Next transition found at time %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (timestamp)); |
| if (cur_scene) { |
| cur_scene->end = timestamp; |
| output_scenes = g_list_append (output_scenes, cur_scene); |
| } |
| |
| active_trees = ttml_get_active_trees (region_trees, timestamp); |
| GST_CAT_LOG (ttmlparse_debug, "There will be %u active regions after " |
| "transition", g_list_length (active_trees)); |
| |
| if (active_trees) { |
| cur_scene = g_slice_new0 (TtmlScene); |
| cur_scene->begin = timestamp; |
| cur_scene->trees = active_trees; |
| } else { |
| cur_scene = NULL; |
| } |
| } |
| |
| return output_scenes; |
| } |
| |
| |
| /* Handle element whitespace in accordance with section 7.2.3 of the TTML |
| * specification. Specifically, this function implements the |
| * white-space-collapse="true" and linefeed-treatment="treat-as-space" |
| * behaviours. Note that stripping of whitespace at the start and end of line |
| * areas (suppress-at-line-break="auto" and |
| * white-space-treatment="ignore-if-surrounding-linefeed" behaviours) can only |
| * be done by the renderer once the text from multiple elements has been laid |
| * out in line areas. */ |
| static gboolean |
| ttml_handle_element_whitespace (GNode * node, gpointer data) |
| { |
| TtmlElement *element = node->data; |
| guint space_count = 0; |
| guint textlen; |
| gchar *c; |
| |
| if (!element->text || (element->type == TTML_ELEMENT_TYPE_BR) || |
| (element->whitespace_mode == TTML_WHITESPACE_MODE_PRESERVE)) { |
| return FALSE; |
| } |
| |
| textlen = strlen (element->text); |
| for (c = element->text; TRUE; c = g_utf8_next_char (c)) { |
| |
| gchar buf[6] = { 0 }; |
| gunichar u = g_utf8_get_char (c); |
| gint nbytes = g_unichar_to_utf8 (u, buf); |
| |
| /* Repace each newline or tab with a space. */ |
| if (nbytes == 1 && (buf[0] == TTML_CHAR_LF || buf[0] == TTML_CHAR_TAB)) { |
| *c = ' '; |
| buf[0] = TTML_CHAR_SPACE; |
| } |
| |
| /* Collapse runs of whitespace. */ |
| if (nbytes == 1 && (buf[0] == TTML_CHAR_SPACE || buf[0] == TTML_CHAR_CR)) { |
| ++space_count; |
| } else { |
| if (space_count > 1) { |
| gchar *new_head = c - space_count + 1; |
| g_strlcpy (new_head, c, textlen); |
| c = new_head; |
| } |
| space_count = 0; |
| if (nbytes == 1 && buf[0] == TTML_CHAR_NULL) |
| break; |
| } |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static void |
| ttml_handle_whitespace (GNode * tree) |
| { |
| g_node_traverse (tree, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, |
| ttml_handle_element_whitespace, NULL); |
| } |
| |
| |
| static GNode * |
| ttml_filter_content_nodes (GNode * node) |
| { |
| GNode *child, *next_child; |
| TtmlElement *element = node->data; |
| TtmlElement *parent = node->parent ? node->parent->data : NULL; |
| |
| child = node->children; |
| next_child = child ? child->next : NULL; |
| while (child) { |
| ttml_filter_content_nodes (child); |
| child = next_child; |
| next_child = child ? child->next : NULL; |
| } |
| |
| /* Only text content in <p>s and <span>s is significant. */ |
| if (element->type == TTML_ELEMENT_TYPE_ANON_SPAN |
| && parent->type != TTML_ELEMENT_TYPE_P |
| && parent->type != TTML_ELEMENT_TYPE_SPAN) { |
| ttml_delete_element (element); |
| g_node_destroy (node); |
| node = NULL; |
| } |
| |
| return node; |
| } |
| |
| |
| /* Store in @table child elements of @node with name @element_name. A child |
| * element with the same ID as an existing entry in @table will overwrite the |
| * existing entry. */ |
| static void |
| ttml_store_unique_children (xmlNodePtr node, const gchar * element_name, |
| GHashTable * table) |
| { |
| xmlNodePtr ptr; |
| |
| for (ptr = node->children; ptr; ptr = ptr->next) { |
| if (xmlStrcmp (ptr->name, (const xmlChar *) element_name) == 0) { |
| TtmlElement *element = ttml_parse_element (ptr); |
| gboolean new_key; |
| |
| if (element) { |
| new_key = g_hash_table_insert (table, g_strdup (element->id), element); |
| if (!new_key) |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Document contains two %s elements with the same ID (\"%s\").", |
| element_name, element->id); |
| } |
| } |
| } |
| } |
| |
| |
| /* Parse style and region elements from @head and store in their respective |
| * hash tables for future reference. */ |
| static void |
| ttml_parse_head (xmlNodePtr head, GHashTable * styles_table, |
| GHashTable * regions_table) |
| { |
| xmlNodePtr node; |
| |
| for (node = head->children; node; node = node->next) { |
| if (xmlStrcmp (node->name, (const xmlChar *) "styling") == 0) |
| ttml_store_unique_children (node, "style", styles_table); |
| if (xmlStrcmp (node->name, (const xmlChar *) "layout") == 0) |
| ttml_store_unique_children (node, "region", regions_table); |
| } |
| } |
| |
| |
| /* Remove nodes that do not belong to @region, or are not an ancestor of a node |
| * belonging to @region. */ |
| static GNode * |
| ttml_remove_nodes_by_region (GNode * node, const gchar * region) |
| { |
| GNode *child, *next_child; |
| TtmlElement *element; |
| element = node->data; |
| |
| child = node->children; |
| next_child = child ? child->next : NULL; |
| while (child) { |
| ttml_remove_nodes_by_region (child, region); |
| child = next_child; |
| next_child = child ? child->next : NULL; |
| } |
| |
| if ((element->type == TTML_ELEMENT_TYPE_ANON_SPAN |
| || element->type != TTML_ELEMENT_TYPE_BR) |
| && element->region && (g_strcmp0 (element->region, region) != 0)) { |
| ttml_delete_element (element); |
| g_node_destroy (node); |
| return NULL; |
| } |
| if (element->type != TTML_ELEMENT_TYPE_ANON_SPAN |
| && element->type != TTML_ELEMENT_TYPE_BR && !node->children) { |
| ttml_delete_element (element); |
| g_node_destroy (node); |
| return NULL; |
| } |
| |
| return node; |
| } |
| |
| |
| static TtmlElement * |
| ttml_copy_element (const TtmlElement * element) |
| { |
| TtmlElement *ret = g_slice_new0 (TtmlElement); |
| |
| ret->type = element->type; |
| if (element->id) |
| ret->id = g_strdup (element->id); |
| ret->whitespace_mode = element->whitespace_mode; |
| if (element->styles) |
| ret->styles = g_strdupv (element->styles); |
| if (element->region) |
| ret->region = g_strdup (element->region); |
| ret->begin = element->begin; |
| ret->end = element->end; |
| if (element->style_set) |
| ret->style_set = ttml_style_set_copy (element->style_set); |
| if (element->text) |
| ret->text = g_strdup (element->text); |
| |
| return ret; |
| } |
| |
| |
| static gpointer |
| ttml_copy_tree_element (gconstpointer src, gpointer data) |
| { |
| return ttml_copy_element ((TtmlElement *) src); |
| } |
| |
| |
| /* Split the body tree into a set of trees, each containing only the elements |
| * belonging to a single region. Returns a list of trees, one per region, each |
| * with the corresponding region element at its root. */ |
| static GList * |
| ttml_split_body_by_region (GNode * body, GHashTable * regions) |
| { |
| GHashTableIter iter; |
| gpointer key, value; |
| GList *ret = NULL; |
| |
| g_hash_table_iter_init (&iter, regions); |
| while (g_hash_table_iter_next (&iter, &key, &value)) { |
| gchar *region_name = (gchar *) key; |
| TtmlElement *region = (TtmlElement *) value; |
| GNode *region_node = g_node_new (ttml_copy_element (region)); |
| GNode *body_copy = g_node_copy_deep (body, ttml_copy_tree_element, NULL); |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "Creating tree for region %s", region_name); |
| GST_CAT_LOG (ttmlparse_debug, "Copy of body has %u nodes.", |
| g_node_n_nodes (body_copy, G_TRAVERSE_ALL)); |
| |
| body_copy = ttml_remove_nodes_by_region (body_copy, region_name); |
| if (body_copy) { |
| GST_CAT_LOG (ttmlparse_debug, "Copy of body now has %u nodes.", |
| g_node_n_nodes (body_copy, G_TRAVERSE_ALL)); |
| |
| /* Reparent tree to region node. */ |
| g_node_prepend (region_node, body_copy); |
| } |
| GST_CAT_LOG (ttmlparse_debug, "Final tree has %u nodes.", |
| g_node_n_nodes (region_node, G_TRAVERSE_ALL)); |
| ret = g_list_append (ret, region_node); |
| } |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "Returning %u trees.", g_list_length (ret)); |
| return ret; |
| } |
| |
| |
| static gint |
| ttml_add_text_to_buffer (GstBuffer * buf, const gchar * text) |
| { |
| GstMemory *mem; |
| GstMapInfo map; |
| guint ret; |
| |
| if (gst_buffer_n_memory (buf) == gst_buffer_get_max_memory ()) |
| return -1; |
| |
| mem = gst_allocator_alloc (NULL, strlen (text) + 1, NULL); |
| if (!gst_memory_map (mem, &map, GST_MAP_WRITE)) |
| GST_CAT_ERROR (ttmlparse_debug, "Failed to map memory."); |
| |
| g_strlcpy ((gchar *) map.data, text, map.size); |
| GST_CAT_DEBUG (ttmlparse_debug, "Inserted following text into buffer: \"%s\"", |
| (gchar *) map.data); |
| gst_memory_unmap (mem, &map); |
| |
| ret = gst_buffer_n_memory (buf); |
| gst_buffer_insert_memory (buf, -1, mem); |
| return ret; |
| } |
| |
| |
| /* Create a GstSubtitleElement from @element, add it to @block, and insert its |
| * associated text in @buf. */ |
| static gboolean |
| ttml_add_element (GstSubtitleBlock * block, TtmlElement * element, |
| GstBuffer * buf, guint cellres_x, guint cellres_y) |
| { |
| GstSubtitleStyleSet *element_style = NULL; |
| guint buffer_index; |
| GstSubtitleElement *sub_element = NULL; |
| |
| buffer_index = ttml_add_text_to_buffer (buf, element->text); |
| if (buffer_index == -1) { |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Reached maximum element count for buffer - discarding element."); |
| return FALSE; |
| } |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "Inserted text at index %u in GstBuffer.", |
| buffer_index); |
| |
| element_style = gst_subtitle_style_set_new (); |
| ttml_update_style_set (element_style, element->style_set, |
| cellres_x, cellres_y); |
| sub_element = gst_subtitle_element_new (element_style, buffer_index, |
| (element->whitespace_mode != TTML_WHITESPACE_MODE_PRESERVE)); |
| |
| gst_subtitle_block_add_element (block, sub_element); |
| GST_CAT_DEBUG (ttmlparse_debug, |
| "Added element to block; there are now %u elements in the block.", |
| gst_subtitle_block_get_element_count (block)); |
| return TRUE; |
| } |
| |
| |
| /* Return TRUE if @color is totally transparent. */ |
| static gboolean |
| ttml_color_is_transparent (const GstSubtitleColor * color) |
| { |
| if (!color) |
| return FALSE; |
| else |
| return (color->a == 0); |
| } |
| |
| |
| /* Blend @color2 over @color1 and return the resulting color. This is currently |
| * a dummy implementation that simply returns color2 as long as it's |
| * not fully transparent. */ |
| /* TODO: Implement actual blending of colors. */ |
| static GstSubtitleColor |
| ttml_blend_colors (GstSubtitleColor color1, GstSubtitleColor color2) |
| { |
| if (ttml_color_is_transparent (&color2)) |
| return color1; |
| else |
| return color2; |
| } |
| |
| |
| static void |
| ttml_warn_of_mispositioned_element (TtmlElement * element) |
| { |
| gchar *type = ttml_get_element_type_string (element); |
| GST_CAT_WARNING (ttmlparse_debug, "Ignoring illegally positioned %s element.", |
| type); |
| g_free (type); |
| } |
| |
| |
| /* Create the subtitle region and its child blocks and elements for @tree, |
| * inserting element text in @buf. Ownership of created region is transferred |
| * to caller. */ |
| static GstSubtitleRegion * |
| ttml_create_subtitle_region (GNode * tree, GstBuffer * buf, guint cellres_x, |
| guint cellres_y) |
| { |
| GstSubtitleRegion *region = NULL; |
| GstSubtitleStyleSet *region_style; |
| GstSubtitleColor block_color; |
| TtmlElement *element; |
| GNode *node; |
| |
| element = tree->data; /* Region element */ |
| region_style = gst_subtitle_style_set_new (); |
| ttml_update_style_set (region_style, element->style_set, cellres_x, |
| cellres_y); |
| region = gst_subtitle_region_new (region_style); |
| |
| node = tree->children; |
| if (!node) |
| return region; |
| |
| element = node->data; /* Body element */ |
| block_color = |
| ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set, |
| "backgroundColor")); |
| |
| for (node = node->children; node; node = node->next) { |
| GNode *p_node; |
| GstSubtitleColor div_color; |
| |
| element = node->data; |
| if (element->type != TTML_ELEMENT_TYPE_DIV) { |
| ttml_warn_of_mispositioned_element (element); |
| continue; |
| } |
| div_color = |
| ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set, |
| "backgroundColor")); |
| block_color = ttml_blend_colors (block_color, div_color); |
| |
| for (p_node = node->children; p_node; p_node = p_node->next) { |
| GstSubtitleBlock *block = NULL; |
| GstSubtitleStyleSet *block_style; |
| GNode *content_node; |
| GstSubtitleColor p_color; |
| |
| element = p_node->data; |
| if (element->type != TTML_ELEMENT_TYPE_P) { |
| ttml_warn_of_mispositioned_element (element); |
| continue; |
| } |
| p_color = |
| ttml_parse_colorstring (ttml_style_set_get_attr (element->style_set, |
| "backgroundColor")); |
| block_color = ttml_blend_colors (block_color, p_color); |
| |
| block_style = gst_subtitle_style_set_new (); |
| ttml_update_style_set (block_style, element->style_set, cellres_x, |
| cellres_y); |
| block_style->background_color = block_color; |
| block = gst_subtitle_block_new (block_style); |
| |
| for (content_node = p_node->children; content_node; |
| content_node = content_node->next) { |
| GNode *anon_node; |
| element = content_node->data; |
| |
| if (element->type == TTML_ELEMENT_TYPE_BR |
| || element->type == TTML_ELEMENT_TYPE_ANON_SPAN) { |
| if (!ttml_add_element (block, element, buf, cellres_x, cellres_y)) |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Failed to add element to buffer."); |
| } else if (element->type == TTML_ELEMENT_TYPE_SPAN) { |
| /* Loop through anon-span children of this span. */ |
| for (anon_node = content_node->children; anon_node; |
| anon_node = anon_node->next) { |
| element = anon_node->data; |
| |
| if (element->type == TTML_ELEMENT_TYPE_BR |
| || element->type == TTML_ELEMENT_TYPE_ANON_SPAN) { |
| if (!ttml_add_element (block, element, buf, cellres_x, cellres_y)) |
| GST_CAT_WARNING (ttmlparse_debug, |
| "Failed to add element to buffer."); |
| } else { |
| ttml_warn_of_mispositioned_element (element); |
| } |
| } |
| } else { |
| ttml_warn_of_mispositioned_element (element); |
| } |
| } |
| |
| if (gst_subtitle_block_get_element_count (block) > 0) { |
| gst_subtitle_region_add_block (region, block); |
| GST_CAT_DEBUG (ttmlparse_debug, |
| "Added block to region; there are now %u blocks in the region.", |
| gst_subtitle_region_get_block_count (region)); |
| } else { |
| gst_subtitle_block_unref (block); |
| } |
| } |
| } |
| |
| return region; |
| } |
| |
| |
| /* For each scene, create data objects to describe the layout and styling of |
| * that scene and attach it as metadata to the GstBuffer that will be used to |
| * carry that scene's text. */ |
| static void |
| ttml_attach_scene_metadata (GList * scenes, guint cellres_x, guint cellres_y) |
| { |
| GList *scene_entry; |
| |
| for (scene_entry = g_list_first (scenes); scene_entry; |
| scene_entry = scene_entry->next) { |
| TtmlScene *scene = scene_entry->data; |
| GList *region_tree; |
| GPtrArray *regions = g_ptr_array_new_with_free_func ( |
| (GDestroyNotify) gst_subtitle_region_unref); |
| |
| scene->buf = gst_buffer_new (); |
| GST_BUFFER_PTS (scene->buf) = scene->begin; |
| GST_BUFFER_DURATION (scene->buf) = (scene->end - scene->begin); |
| |
| for (region_tree = g_list_first (scene->trees); region_tree; |
| region_tree = region_tree->next) { |
| GNode *tree = (GNode *) region_tree->data; |
| GstSubtitleRegion *region; |
| |
| region = ttml_create_subtitle_region (tree, scene->buf, cellres_x, |
| cellres_y); |
| if (region) |
| g_ptr_array_add (regions, region); |
| } |
| |
| gst_buffer_add_subtitle_meta (scene->buf, regions); |
| } |
| } |
| |
| |
| static GList * |
| create_buffer_list (GList * scenes) |
| { |
| GList *ret = NULL; |
| |
| while (scenes) { |
| TtmlScene *scene = scenes->data; |
| ret = g_list_prepend (ret, gst_buffer_ref (scene->buf)); |
| scenes = scenes->next; |
| } |
| return g_list_reverse (ret); |
| } |
| |
| |
| static void |
| ttml_delete_scene (TtmlScene * scene) |
| { |
| if (scene->trees) |
| g_list_free_full (scene->trees, (GDestroyNotify) ttml_delete_tree); |
| if (scene->buf) |
| gst_buffer_unref (scene->buf); |
| g_slice_free (TtmlScene, scene); |
| } |
| |
| |
| static void |
| ttml_assign_region_times (GList * region_trees, GstClockTime doc_begin, |
| GstClockTime doc_duration) |
| { |
| GList *tree; |
| |
| for (tree = g_list_first (region_trees); tree; tree = tree->next) { |
| GNode *region_node = (GNode *) tree->data; |
| TtmlElement *region = (TtmlElement *) region_node->data; |
| const gchar *show_background_value = |
| ttml_style_set_get_attr (region->style_set, "showBackground"); |
| gboolean always_visible = |
| (g_strcmp0 (show_background_value, "whenActive") != 0); |
| |
| GstSubtitleColor region_color = { 0, 0, 0, 0 }; |
| if (ttml_style_set_contains_attr (region->style_set, "backgroundColor")) |
| region_color = |
| ttml_parse_colorstring (ttml_style_set_get_attr (region->style_set, |
| "backgroundColor")); |
| |
| if (always_visible && !ttml_color_is_transparent (®ion_color)) { |
| GST_CAT_DEBUG (ttmlparse_debug, "Assigning times to region."); |
| /* If the input XML document was not encapsulated in a container that |
| * provides timing information for the document as a whole (i.e., its |
| * PTS and duration) and the region background should be always visible, |
| * set region start time to 0 and end time to 24 hours. This ensures that |
| * regions with showBackground="always" are visible for the entirety of |
| * any real-world stream. */ |
| region->begin = (doc_begin != GST_CLOCK_TIME_NONE) ? doc_begin : 0; |
| region->end = (doc_duration != GST_CLOCK_TIME_NONE) ? |
| region->begin + doc_duration : NSECONDS_IN_DAY; |
| } |
| } |
| } |
| |
| |
| /* |
| * Promotes @node to the position of its parent, setting the prev, next and |
| * parent pointers of @node to that of its original parent. The replaced parent |
| * is freed. Should be called only on nodes that are the sole child of their |
| * parent, otherwise sibling nodes may be leaked. |
| */ |
| static void |
| ttml_promote_node (GNode * node) |
| { |
| GNode *parent_node = node->parent; |
| TtmlElement *parent_element; |
| |
| if (!parent_node) |
| return; |
| parent_element = (TtmlElement *) parent_node->data; |
| |
| node->prev = parent_node->prev; |
| if (!node->prev) |
| parent_node->parent->children = node; |
| else |
| node->prev->next = node; |
| node->next = parent_node->next; |
| if (node->next) |
| node->next->prev = node; |
| node->parent = parent_node->parent; |
| |
| parent_node->prev = parent_node->next = NULL; |
| parent_node->parent = parent_node->children = NULL; |
| g_node_destroy (parent_node); |
| ttml_delete_element (parent_element); |
| } |
| |
| |
| /* |
| * Returns TRUE if @element is of a type that can be joined with another |
| * joinable element. |
| */ |
| static gboolean |
| ttml_element_is_joinable (TtmlElement * element) |
| { |
| return element->type == TTML_ELEMENT_TYPE_ANON_SPAN || |
| element->type == TTML_ELEMENT_TYPE_BR; |
| } |
| |
| |
| /* Joins adjacent inline element in @tree that have the same styling. */ |
| static void |
| ttml_join_region_tree_inline_elements (GNode * tree) |
| { |
| GNode *n1, *n2; |
| |
| for (n1 = tree; n1; n1 = n1->next) { |
| if (n1->children) { |
| TtmlElement *element = (TtmlElement *) n1->data; |
| ttml_join_region_tree_inline_elements (n1->children); |
| if (element->type == TTML_ELEMENT_TYPE_SPAN && |
| g_node_n_children (n1) == 1) { |
| GNode *child = n1->children; |
| if (n1 == tree) |
| tree = child; |
| ttml_promote_node (child); |
| n1 = child; |
| } |
| } |
| } |
| |
| n1 = tree; |
| n2 = tree->next; |
| |
| while (n1 && n2) { |
| TtmlElement *e1 = (TtmlElement *) n1->data; |
| TtmlElement *e2 = (TtmlElement *) n2->data; |
| |
| if (ttml_element_is_joinable (e1) && |
| ttml_element_is_joinable (e2) && ttml_element_styles_match (e1, e2)) { |
| gchar *tmp = e1->text; |
| GST_CAT_LOG (ttmlparse_debug, |
| "Joining adjacent element text \"%s\" & \"%s\"", e1->text, e2->text); |
| e1->text = g_strconcat (e1->text, e2->text, NULL); |
| e1->type = TTML_ELEMENT_TYPE_ANON_SPAN; |
| g_free (tmp); |
| |
| ttml_delete_element (e2); |
| g_node_destroy (n2); |
| n2 = n1->next; |
| } else { |
| n1 = n2; |
| n2 = n2->next; |
| } |
| } |
| } |
| |
| |
| static void |
| ttml_join_inline_elements (GList * scenes) |
| { |
| GList *scene_entry; |
| |
| for (scene_entry = g_list_first (scenes); scene_entry; |
| scene_entry = scene_entry->next) { |
| TtmlScene *scene = scene_entry->data; |
| GList *region_tree; |
| |
| for (region_tree = g_list_first (scene->trees); region_tree; |
| region_tree = region_tree->next) { |
| GNode *tree = (GNode *) region_tree->data; |
| ttml_join_region_tree_inline_elements (tree); |
| } |
| } |
| } |
| |
| |
| static xmlNodePtr |
| ttml_find_child (xmlNodePtr parent, const gchar * name) |
| { |
| xmlNodePtr child = parent->children; |
| while (child && xmlStrcmp (child->name, (const xmlChar *) name) != 0) |
| child = child->next; |
| return child; |
| } |
| |
| |
| GList * |
| ttml_parse (const gchar * input, GstClockTime begin, GstClockTime duration) |
| { |
| xmlDocPtr doc; |
| xmlNodePtr root_node, head_node, body_node; |
| |
| GHashTable *styles_table, *regions_table; |
| GList *output_buffers = NULL; |
| gchar *value; |
| guint cellres_x, cellres_y; |
| TtmlWhitespaceMode doc_whitespace_mode = TTML_WHITESPACE_MODE_DEFAULT; |
| |
| if (!g_utf8_validate (input, -1, NULL)) { |
| GST_CAT_ERROR (ttmlparse_debug, "Input isn't valid UTF-8."); |
| return NULL; |
| } |
| GST_CAT_LOG (ttmlparse_debug, "Input:\n%s", input); |
| |
| styles_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, |
| (GDestroyNotify) ttml_delete_element); |
| regions_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, |
| (GDestroyNotify) ttml_delete_element); |
| |
| /* Parse input. */ |
| doc = xmlReadMemory (input, strlen (input), "any_doc_name", NULL, 0); |
| if (!doc) { |
| GST_CAT_ERROR (ttmlparse_debug, "Failed to parse document."); |
| return NULL; |
| } |
| root_node = xmlDocGetRootElement (doc); |
| |
| if (xmlStrcmp (root_node->name, (const xmlChar *) "tt") != 0) { |
| GST_CAT_ERROR (ttmlparse_debug, "Root element of document is not tt:tt."); |
| xmlFreeDoc (doc); |
| return NULL; |
| } |
| |
| if ((value = ttml_get_xml_property (root_node, "cellResolution"))) { |
| gchar *ptr = value; |
| cellres_x = (guint) g_ascii_strtoull (ptr, &ptr, 10U); |
| cellres_y = (guint) g_ascii_strtoull (ptr, NULL, 10U); |
| g_free (value); |
| } else { |
| cellres_x = DEFAULT_CELLRES_X; |
| cellres_y = DEFAULT_CELLRES_Y; |
| } |
| |
| GST_CAT_DEBUG (ttmlparse_debug, "cellres_x: %u cellres_y: %u", cellres_x, |
| cellres_y); |
| |
| if ((value = ttml_get_xml_property (root_node, "space"))) { |
| if (g_strcmp0 (value, "preserve") == 0) { |
| GST_CAT_DEBUG (ttmlparse_debug, "Preserving whitespace..."); |
| doc_whitespace_mode = TTML_WHITESPACE_MODE_PRESERVE; |
| } |
| g_free (value); |
| } |
| |
| if (!(head_node = ttml_find_child (root_node, "head"))) { |
| GST_CAT_ERROR (ttmlparse_debug, "No <head> element found."); |
| xmlFreeDoc (doc); |
| return NULL; |
| } |
| ttml_parse_head (head_node, styles_table, regions_table); |
| |
| if ((body_node = ttml_find_child (root_node, "body"))) { |
| GNode *body_tree; |
| GList *region_trees = NULL; |
| GList *scenes = NULL; |
| |
| body_tree = ttml_parse_body (body_node); |
| GST_CAT_LOG (ttmlparse_debug, "body_tree tree contains %u nodes.", |
| g_node_n_nodes (body_tree, G_TRAVERSE_ALL)); |
| GST_CAT_LOG (ttmlparse_debug, "body_tree tree height is %u", |
| g_node_max_height (body_tree)); |
| |
| ttml_inherit_whitespace_mode (body_tree, doc_whitespace_mode); |
| ttml_handle_whitespace (body_tree); |
| ttml_filter_content_nodes (body_tree); |
| if (GST_CLOCK_TIME_IS_VALID (begin) && GST_CLOCK_TIME_IS_VALID (duration)) |
| ttml_apply_time_window (body_tree, begin, begin + duration); |
| ttml_resolve_timings (body_tree); |
| ttml_resolve_regions (body_tree); |
| region_trees = ttml_split_body_by_region (body_tree, regions_table); |
| ttml_resolve_referenced_styles (region_trees, styles_table); |
| ttml_inherit_element_styles (region_trees); |
| ttml_assign_region_times (region_trees, begin, duration); |
| scenes = ttml_create_scenes (region_trees); |
| GST_CAT_LOG (ttmlparse_debug, "There are %u scenes in all.", |
| g_list_length (scenes)); |
| ttml_join_inline_elements (scenes); |
| ttml_attach_scene_metadata (scenes, cellres_x, cellres_y); |
| output_buffers = create_buffer_list (scenes); |
| |
| g_list_free_full (scenes, (GDestroyNotify) ttml_delete_scene); |
| g_list_free_full (region_trees, (GDestroyNotify) ttml_delete_tree); |
| ttml_delete_tree (body_tree); |
| } |
| |
| xmlFreeDoc (doc); |
| g_hash_table_destroy (styles_table); |
| g_hash_table_destroy (regions_table); |
| |
| return output_buffers; |
| } |