/* GStreamer TTML subtitle parser * Copyright (C) <2015> British Broadcasting Corporation * Authors: * Chris Bass * Peter Taylour * * 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 #include #include #include #include #include #include #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, "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; } } 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 ("