/*
- Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
*/
+#include "raw_convert.h"
#include "subtitle_asset.h"
+#include "util.h"
+#include "xml.h"
+#include "font_node.h"
+#include "text_node.h"
+#include "subtitle_string.h"
+#include "dcp_assert.h"
+#include "AS_DCP.h"
+#include "KM_util.h"
+#include <libxml++/nodes/element.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/shared_array.hpp>
+#include <boost/foreach.hpp>
+#include <fstream>
-using namespace std;
-using namespace boost;
-using namespace libdcp;
+using std::string;
+using std::list;
+using std::ostream;
+using std::ofstream;
+using std::stringstream;
+using std::cout;
+using std::cerr;
+using std::map;
+using boost::shared_ptr;
+using boost::shared_array;
+using boost::optional;
+using boost::dynamic_pointer_cast;
+using namespace dcp;
-SubtitleAsset::SubtitleAsset (string directory, string xml)
- : Asset (directory, xml)
- , XMLFile (path().string(), "DCSubtitle")
+SubtitleAsset::SubtitleAsset ()
{
- _subtitle_id = string_node ("SubtitleID");
- _movie_title = string_node ("MovieTitle");
- _reel_number = int64_node ("ReelNumber");
- _language = string_node ("Language");
- ignore_node ("LoadFont");
-
- list<shared_ptr<FontNode> > font_nodes = sub_nodes<FontNode> ("Font");
- _load_font_nodes = sub_nodes<LoadFontNode> ("LoadFont");
+}
- /* Now make Subtitle objects to represent the raw XML nodes
- in a sane way.
- */
+SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
+ : Asset (file)
+{
- list<shared_ptr<FontNode> > current_font_nodes;
- for (list<shared_ptr<FontNode> >::iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
- examine_font_node (*i, current_font_nodes);
- }
}
void
-SubtitleAsset::examine_font_node (shared_ptr<FontNode> font_node, list<shared_ptr<FontNode> >& current_font_nodes)
+SubtitleAsset::parse_subtitles (
+ shared_ptr<cxml::Document> xml,
+ list<shared_ptr<dcp::FontNode> > font_nodes,
+ list<shared_ptr<dcp::SubtitleNode> > subtitle_nodes
+ )
{
- current_font_nodes.push_back (font_node);
-
- for (list<shared_ptr<SubtitleNode> >::iterator j = font_node->subtitle_nodes.begin(); j != font_node->subtitle_nodes.end(); ++j) {
- for (list<shared_ptr<TextNode> >::iterator k = (*j)->text_nodes.begin(); k != (*j)->text_nodes.end(); ++k) {
- FontNode effective (current_font_nodes);
- _subtitles.push_back (
- shared_ptr<Subtitle> (
- new Subtitle (
- font_id_to_name (effective.id),
- effective.italic.get(),
- effective.size,
- (*j)->in,
- (*j)->out,
- (*k)->v_position,
- (*k)->text
- )
- )
- );
- }
- }
+ /* Make Subtitle objects to represent the raw XML nodes in a sane way */
+ ParseState parse_state;
+ examine_nodes (xml, font_nodes, parse_state);
+ examine_nodes (xml, subtitle_nodes, parse_state);
+}
- for (list<shared_ptr<FontNode> >::iterator j = font_node->font_nodes.begin(); j != font_node->font_nodes.end(); ++j) {
- examine_font_node (*j, current_font_nodes);
+void
+SubtitleAsset::examine_nodes (
+ shared_ptr<const cxml::Node> xml,
+ list<shared_ptr<dcp::SubtitleNode> > const & subtitle_nodes,
+ ParseState& parse_state
+ )
+{
+ BOOST_FOREACH (shared_ptr<dcp::SubtitleNode> i, subtitle_nodes) {
+ parse_state.subtitle_nodes.push_back (i);
+ examine_nodes (xml, i->text_nodes, parse_state);
+ examine_nodes (xml, i->font_nodes, parse_state);
+ parse_state.subtitle_nodes.pop_back ();
}
-
- current_font_nodes.pop_back ();
}
-FontNode::FontNode (xmlpp::Node const * node)
- : XMLNode (node)
+void
+SubtitleAsset::examine_nodes (
+ shared_ptr<const cxml::Node> xml,
+ list<shared_ptr<dcp::FontNode> > const & font_nodes,
+ ParseState& parse_state
+ )
{
- id = string_attribute ("Id");
- size = optional_int64_attribute ("Size");
- italic = optional_bool_attribute ("Italic");
- subtitle_nodes = sub_nodes<SubtitleNode> ("Subtitle");
- font_nodes = sub_nodes<FontNode> ("Font");
+ BOOST_FOREACH (shared_ptr<dcp::FontNode> i, font_nodes) {
+
+ parse_state.font_nodes.push_back (i);
+ maybe_add_subtitle (i->text, parse_state);
+
+ examine_nodes (xml, i->subtitle_nodes, parse_state);
+ examine_nodes (xml, i->font_nodes, parse_state);
+ examine_nodes (xml, i->text_nodes, parse_state);
+
+ parse_state.font_nodes.pop_back ();
+ }
}
-FontNode::FontNode (list<shared_ptr<FontNode> > const & font_nodes)
- : size (0)
- , italic (false)
+void
+SubtitleAsset::examine_nodes (
+ shared_ptr<const cxml::Node> xml,
+ list<shared_ptr<dcp::TextNode> > const & text_nodes,
+ ParseState& parse_state
+ )
{
- for (list<shared_ptr<FontNode> >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) {
- if (!(*i)->id.empty ()) {
- id = (*i)->id;
- }
- if ((*i)->size != 0) {
- size = (*i)->size;
- }
- if ((*i)->italic) {
- italic = (*i)->italic.get ();
- }
+ BOOST_FOREACH (shared_ptr<dcp::TextNode> i, text_nodes) {
+ parse_state.text_nodes.push_back (i);
+ maybe_add_subtitle (i->text, parse_state);
+ examine_nodes (xml, i->font_nodes, parse_state);
+ parse_state.text_nodes.pop_back ();
}
}
-LoadFontNode::LoadFontNode (xmlpp::Node const * node)
- : XMLNode (node)
+void
+SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state)
{
- id = string_attribute ("Id");
- uri = string_attribute ("URI");
+ if (empty_or_white_space (text)) {
+ return;
+ }
+
+ if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) {
+ return;
+ }
+
+ DCP_ASSERT (!parse_state.text_nodes.empty ());
+ DCP_ASSERT (!parse_state.subtitle_nodes.empty ());
+
+ dcp::FontNode effective_font (parse_state.font_nodes);
+ dcp::TextNode effective_text (*parse_state.text_nodes.back ());
+ dcp::SubtitleNode effective_subtitle (*parse_state.subtitle_nodes.back ());
+
+ _subtitles.push_back (
+ SubtitleString (
+ effective_font.id,
+ effective_font.italic.get_value_or (false),
+ effective_font.colour.get_value_or (dcp::Colour (255, 255, 255)),
+ effective_font.size,
+ effective_font.aspect_adjust.get_value_or (1.0),
+ effective_subtitle.in,
+ effective_subtitle.out,
+ effective_text.h_position,
+ effective_text.h_align,
+ effective_text.v_position,
+ effective_text.v_align,
+ text,
+ effective_font.effect.get_value_or (NONE),
+ effective_font.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
+ effective_subtitle.fade_up_time,
+ effective_subtitle.fade_down_time
+ )
+ );
}
-
-SubtitleNode::SubtitleNode (xmlpp::Node const * node)
- : XMLNode (node)
+list<SubtitleString>
+SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
{
- in = time_attribute ("TimeIn");
- out = time_attribute ("TimeOut");
- text_nodes = sub_nodes<TextNode> ("Text");
+ list<SubtitleString> s;
+ BOOST_FOREACH (SubtitleString const & i, _subtitles) {
+ if ((starting && from <= i.in() && i.in() < to) || (!starting && i.out() >= from && i.in() <= to)) {
+ s.push_back (i);
+ }
+ }
+
+ return s;
}
-TextNode::TextNode (xmlpp::Node const * node)
- : XMLNode (node)
+void
+SubtitleAsset::add (SubtitleString s)
{
- text = content ();
- v_position = float_attribute ("VPosition");
+ _subtitles.push_back (s);
}
-list<shared_ptr<Subtitle> >
-SubtitleAsset::subtitles_at (Time t) const
+Time
+SubtitleAsset::latest_subtitle_out () const
{
- list<shared_ptr<Subtitle> > s;
- for (list<shared_ptr<Subtitle> >::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
- if ((*i)->in() <= t && t <= (*i)->out ()) {
- s.push_back (*i);
+ Time t;
+ BOOST_FOREACH (SubtitleString const & i, _subtitles) {
+ if (i.out() > t) {
+ t = i.out ();
}
}
- return s;
+ return t;
}
-std::string
-SubtitleAsset::font_id_to_name (string id) const
+bool
+SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
{
- list<shared_ptr<LoadFontNode> >::const_iterator i = _load_font_nodes.begin();
- while (i != _load_font_nodes.end() && (*i)->id != id) {
- ++i;
+ if (!Asset::equals (other_asset, options, note)) {
+ return false;
}
- if (i == _load_font_nodes.end ()) {
- return "";
+ shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
+ if (!other) {
+ return false;
}
- if ((*i)->uri == "arial.ttf") {
- return "Arial";
+ if (_subtitles != other->_subtitles) {
+ note (DCP_ERROR, "subtitles differ");
+ return false;
}
- return "";
+ return true;
}
-Subtitle::Subtitle (
- std::string font,
- bool italic,
- int size,
- Time in,
- Time out,
- float v_position,
- std::string text
- )
- : _font (font)
- , _italic (italic)
- , _size (size)
- , _in (in)
- , _out (out)
- , _v_position (v_position)
- , _text (text)
+struct SubtitleSorter {
+ bool operator() (SubtitleString const & a, SubtitleString const & b) {
+ if (a.in() != b.in()) {
+ return a.in() < b.in();
+ }
+ return a.v_position() < b.v_position();
+ }
+};
+
+/** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
+ * class because the differences between the two are fairly subtle.
+ */
+void
+SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, Standard standard) const
{
+ list<SubtitleString> sorted = _subtitles;
+ sorted.sort (SubtitleSorter ());
+
+ string const xmlns = standard == SMPTE ? "dcst" : "";
+
+ /* XXX: script, underlined, weight not supported */
+
+ optional<string> font;
+ bool italic = false;
+ Colour colour;
+ int size = 0;
+ float aspect_adjust = 1.0;
+ Effect effect = NONE;
+ Colour effect_colour;
+ int spot_number = 1;
+ Time last_in;
+ Time last_out;
+ Time last_fade_up_time;
+ Time last_fade_down_time;
+
+ xmlpp::Element* font_element = 0;
+ xmlpp::Element* subtitle_element = 0;
+
+ BOOST_FOREACH (SubtitleString const & i, sorted) {
+
+ /* We will start a new <Font>...</Font> whenever some font property changes.
+ I suppose we should really make an optimal hierarchy of <Font> tags, but
+ that seems hard.
+ */
+ bool const font_changed =
+ font != i.font() ||
+ italic != i.italic() ||
+ colour != i.colour() ||
+ size != i.size() ||
+ fabs (aspect_adjust - i.aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
+ effect != i.effect() ||
+ effect_colour != i.effect_colour();
+
+ if (font_changed) {
+ font = i.font ();
+ italic = i.italic ();
+ colour = i.colour ();
+ size = i.size ();
+ aspect_adjust = i.aspect_adjust ();
+ effect = i.effect ();
+ effect_colour = i.effect_colour ();
+ }
+
+ if (!font_element || font_changed) {
+ font_element = root->add_child ("Font", xmlns);
+ if (font) {
+ if (standard == SMPTE) {
+ font_element->set_attribute ("ID", font.get ());
+ } else {
+ font_element->set_attribute ("Id", font.get ());
+ }
+ }
+ font_element->set_attribute ("Italic", italic ? "yes" : "no");
+ font_element->set_attribute ("Color", colour.to_argb_string());
+ font_element->set_attribute ("Size", raw_convert<string> (size));
+ if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
+ font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
+ }
+ font_element->set_attribute ("Effect", effect_to_string (effect));
+ font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
+ font_element->set_attribute ("Script", "normal");
+ if (standard == SMPTE) {
+ font_element->set_attribute ("Underline", "no");
+ } else {
+ font_element->set_attribute ("Underlined", "no");
+ }
+ font_element->set_attribute ("Weight", "normal");
+ }
+
+ if (!subtitle_element || font_changed ||
+ (last_in != i.in() ||
+ last_out != i.out() ||
+ last_fade_up_time != i.fade_up_time() ||
+ last_fade_down_time != i.fade_down_time()
+ )) {
+
+ subtitle_element = font_element->add_child ("Subtitle", xmlns);
+ subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
+ subtitle_element->set_attribute ("TimeIn", i.in().rebase(time_code_rate).as_string(standard));
+ subtitle_element->set_attribute ("TimeOut", i.out().rebase(time_code_rate).as_string(standard));
+ if (standard == SMPTE) {
+ subtitle_element->set_attribute ("FadeUpTime", i.fade_up_time().rebase(time_code_rate).as_string(standard));
+ subtitle_element->set_attribute ("FadeDownTime", i.fade_down_time().rebase(time_code_rate).as_string(standard));
+ } else {
+ subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i.fade_up_time().as_editable_units(time_code_rate)));
+ subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i.fade_down_time().as_editable_units(time_code_rate)));
+ }
+
+ last_in = i.in ();
+ last_out = i.out ();
+ last_fade_up_time = i.fade_up_time ();
+ last_fade_down_time = i.fade_down_time ();
+ }
+
+ xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns);
+ if (i.h_align() != HALIGN_CENTER) {
+ if (standard == SMPTE) {
+ text->set_attribute ("Halign", halign_to_string (i.h_align ()));
+ } else {
+ text->set_attribute ("HAlign", halign_to_string (i.h_align ()));
+ }
+ }
+ if (i.h_position() > ALIGN_EPSILON) {
+ if (standard == SMPTE) {
+ text->set_attribute ("Hposition", raw_convert<string> (i.h_position() * 100, 6));
+ } else {
+ text->set_attribute ("HPosition", raw_convert<string> (i.h_position() * 100, 6));
+ }
+ }
+ if (standard == SMPTE) {
+ text->set_attribute ("Valign", valign_to_string (i.v_align()));
+ } else {
+ text->set_attribute ("VAlign", valign_to_string (i.v_align()));
+ }
+ if (i.v_position() > ALIGN_EPSILON) {
+ if (standard == SMPTE) {
+ text->set_attribute ("Vposition", raw_convert<string> (i.v_position() * 100, 6));
+ } else {
+ text->set_attribute ("VPosition", raw_convert<string> (i.v_position() * 100, 6));
+ }
+ } else {
+ if (standard == SMPTE) {
+ text->set_attribute ("Vposition", "0");
+ } else {
+ text->set_attribute ("VPosition", "0");
+ }
+ }
+ text->add_child_text (i.text());
+ }
}
-int
-Subtitle::size_in_pixels (int screen_height) const
+map<string, Data>
+SubtitleAsset::fonts_with_load_ids () const
{
- /* Size in the subtitle file is given in points as if the screen
- height is 11 inches, so a 72pt font would be 1/11th of the screen
- height.
- */
-
- return _size * screen_height / (11 * 72);
+ map<string, Data> out;
+ BOOST_FOREACH (Font const & i, _fonts) {
+ out[i.load_id] = i.data;
+ }
+ return out;
}