mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-27 04:01:08 +00:00
2059 lines
59 KiB
C
2059 lines
59 KiB
C
/* 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_new0 (TtmlStyleSet, 1);
|
|
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_free (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_free (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_new0 (TtmlElement, 1);
|
|
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_new0 (TtmlScene, 1);
|
|
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);
|
|
|
|
/* Replace 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_new0 (TtmlElement, 1);
|
|
|
|
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_free (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;
|
|
}
|
|
|
|
#define XML_START_TAG "<?xml"
|
|
#define TTML_END_TAG "</tt>"
|
|
|
|
guint
|
|
ttml_parse (const gchar * input, GstClockTime begin, GstClockTime duration,
|
|
GList ** parsed)
|
|
{
|
|
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;
|
|
guint consumed = 0;
|
|
guint start_offset = 0;
|
|
gchar *start_xml, *end_tt;
|
|
|
|
g_return_val_if_fail (parsed != NULL, 0);
|
|
|
|
*parsed = NULL;
|
|
if (!g_utf8_validate (input, -1, NULL)) {
|
|
GST_CAT_ERROR (ttmlparse_debug, "Input isn't valid UTF-8.");
|
|
return 0;
|
|
}
|
|
GST_CAT_LOG (ttmlparse_debug, "Input:\n%s", input);
|
|
|
|
start_xml = g_strstr_len (input, strlen (input), XML_START_TAG);
|
|
end_tt = g_strstr_len (input, strlen (input), TTML_END_TAG);
|
|
|
|
if (!start_xml || !end_tt) {
|
|
GST_CAT_DEBUG (ttmlparse_debug, "Need more data");
|
|
return 0;
|
|
}
|
|
|
|
consumed = end_tt - input + strlen (TTML_END_TAG);
|
|
start_offset = start_xml - 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 (start_xml, consumed - start_offset, "any_doc_name",
|
|
NULL, 0);
|
|
if (!doc) {
|
|
GST_CAT_ERROR (ttmlparse_debug, "Failed to parse document.");
|
|
return 0;
|
|
}
|
|
|
|
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 0;
|
|
}
|
|
|
|
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 0;
|
|
}
|
|
|
|
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);
|
|
|
|
*parsed = output_buffers;
|
|
|
|
return consumed;
|
|
}
|