X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fsubtitle_asset.cc;h=4baa7b06e8bf1a83ac5a89fd432315f23707c38d;hb=refs%2Ftags%2Fv1.8.63;hp=1eae1fcc07c0a4ec53ef28091cd34ffca9bb2015;hpb=797916ae28d976f3c5be62d37b45864219af6098;p=libdcp.git diff --git a/src/subtitle_asset.cc b/src/subtitle_asset.cc index 1eae1fcc..4baa7b06 100644 --- a/src/subtitle_asset.cc +++ b/src/subtitle_asset.cc @@ -1,533 +1,930 @@ /* - Copyright (C) 2012 Carl Hetherington + Copyright (C) 2012-2021 Carl Hetherington - This program is free software; you can redistribute it and/or modify + This file is part of libdcp. + + libdcp is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, + libdcp 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 General Public License for more details. You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - + along with libdcp. If not, see . + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations + including the two. + + You must obey the GNU General Public License in all respects + for all of the code used other than OpenSSL. If you modify + file(s) with this exception, you may extend this exception to your + version of the file(s), but you are not obligated to do so. If you + do not wish to do so, delete this exception statement from your + version. If you delete this exception statement from all source + files in the program, then also delete it here. */ -#include -#include -#include + +/** @file src/subtitle_asset.cc + * @brief SubtitleAsset class + */ + + +#include "raw_convert.h" +#include "compose.hpp" #include "subtitle_asset.h" +#include "subtitle_asset_internal.h" #include "util.h" #include "xml.h" +#include "subtitle_string.h" +#include "subtitle_image.h" +#include "dcp_assert.h" +#include "load_font_node.h" +#include "reel_asset.h" +#include +#include +#include +#include +#include +#include +#include + +using std::cerr; +using std::cout; +using std::dynamic_pointer_cast; +using std::make_shared; +using std::map; +using std::pair; +using std::shared_ptr; using std::string; -using std::list; -using std::ostream; -using std::ofstream; -using std::stringstream; -using boost::shared_ptr; +using std::vector; using boost::lexical_cast; using boost::optional; -using namespace libdcp; +using namespace dcp; -SubtitleAsset::SubtitleAsset (string directory, string xml_file) - : Asset (directory, xml_file) - , _need_sort (false) -{ - read_xml (path().string()); -} -SubtitleAsset::SubtitleAsset (string directory, string movie_title, string language) - : Asset (directory) - , _movie_title (movie_title) - , _reel_number ("1") - , _language (language) - , _need_sort (false) +SubtitleAsset::SubtitleAsset () { } -void -SubtitleAsset::read_xml (string xml_file) -{ - shared_ptr xml (new cxml::File (xml_file, "DCSubtitle")); - - _uuid = xml->string_child ("SubtitleID"); - _movie_title = xml->string_child ("MovieTitle"); - _reel_number = xml->string_child ("ReelNumber"); - _language = xml->string_child ("Language"); - xml->ignore_child ("LoadFont"); - - list > font_nodes = type_children (xml, "Font"); - _load_font_nodes = type_children (xml, "LoadFont"); - - /* Now make Subtitle objects to represent the raw XML nodes - in a sane way. - */ +SubtitleAsset::SubtitleAsset (boost::filesystem::path file) + : Asset (file) +{ - ParseState parse_state; - examine_font_nodes (xml, font_nodes, parse_state); } -void -SubtitleAsset::examine_font_nodes ( - shared_ptr xml, - list > const & font_nodes, - ParseState& parse_state - ) + +string +string_attribute (xmlpp::Element const * node, string name) { - for (list >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) { + auto a = node->get_attribute (name); + if (!a) { + throw XMLError (String::compose ("missing attribute %1", name)); + } + return string (a->get_value ()); +} - parse_state.font_nodes.push_back (*i); - maybe_add_subtitle ((*i)->text, parse_state); - for (list >::iterator j = (*i)->subtitle_nodes.begin(); j != (*i)->subtitle_nodes.end(); ++j) { - parse_state.subtitle_nodes.push_back (*j); - examine_text_nodes (xml, (*j)->text_nodes, parse_state); - examine_font_nodes (xml, (*j)->font_nodes, parse_state); - parse_state.subtitle_nodes.pop_back (); - } - - examine_font_nodes (xml, (*i)->font_nodes, parse_state); - examine_text_nodes (xml, (*i)->text_nodes, parse_state); - - parse_state.font_nodes.pop_back (); +optional +optional_string_attribute (xmlpp::Element const * node, string name) +{ + auto a = node->get_attribute (name); + if (!a) { + return {}; } + return string (a->get_value ()); } -void -SubtitleAsset::examine_text_nodes ( - shared_ptr xml, - list > const & text_nodes, - ParseState& parse_state - ) + +optional +optional_bool_attribute (xmlpp::Element const * node, string name) { - for (list >::const_iterator i = text_nodes.begin(); i != text_nodes.end(); ++i) { - parse_state.text_nodes.push_back (*i); - maybe_add_subtitle ((*i)->text, parse_state); - examine_font_nodes (xml, (*i)->font_nodes, parse_state); - parse_state.text_nodes.pop_back (); + auto s = optional_string_attribute (node, name); + if (!s) { + return {}; } + + return (s.get() == "1" || s.get() == "yes"); } -void -SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state) + +template +optional +optional_number_attribute (xmlpp::Element const * node, string name) { - if (empty_or_white_space (text)) { - return; - } - - if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) { - return; + auto s = optional_string_attribute (node, name); + if (!s) { + return boost::optional (); } - assert (!parse_state.text_nodes.empty ()); - assert (!parse_state.subtitle_nodes.empty ()); - - FontNode effective_font (parse_state.font_nodes); - TextNode effective_text (*parse_state.text_nodes.back ()); - SubtitleNode effective_subtitle (*parse_state.subtitle_nodes.back ()); - - _subtitles.push_back ( - shared_ptr ( - new Subtitle ( - font_id_to_name (effective_font.id), - effective_font.italic.get(), - effective_font.color.get(), - effective_font.size, - effective_subtitle.in, - effective_subtitle.out, - effective_text.v_position, - effective_text.v_align, - text, - effective_font.effect ? effective_font.effect.get() : NONE, - effective_font.effect_color.get(), - effective_subtitle.fade_up_time, - effective_subtitle.fade_down_time - ) - ) - ); + std::string t = s.get (); + boost::erase_all (t, " "); + return raw_convert (t); } -FontNode::FontNode (shared_ptr node) + +SubtitleAsset::ParseState +SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const { - text = node->content (); - - id = node->optional_string_attribute ("Id").get_value_or (""); - size = node->optional_number_attribute ("Size").get_value_or (0); - italic = node->optional_bool_attribute ("Italic").get_value_or (false); - optional c = node->optional_string_attribute ("Color"); + ParseState ps; + + if (standard == Standard::INTEROP) { + ps.font_id = optional_string_attribute (node, "Id"); + } else { + ps.font_id = optional_string_attribute (node, "ID"); + } + ps.size = optional_number_attribute (node, "Size"); + ps.aspect_adjust = optional_number_attribute (node, "AspectAdjust"); + ps.italic = optional_bool_attribute (node, "Italic"); + ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold"; + if (standard == Standard::INTEROP) { + ps.underline = optional_bool_attribute (node, "Underlined"); + } else { + ps.underline = optional_bool_attribute (node, "Underline"); + } + auto c = optional_string_attribute (node, "Color"); if (c) { - color = Color (c.get ()); + ps.colour = Colour (c.get ()); } - optional const e = node->optional_string_attribute ("Effect"); + auto const e = optional_string_attribute (node, "Effect"); if (e) { - effect = string_to_effect (e.get ()); + ps.effect = string_to_effect (e.get ()); } - c = node->optional_string_attribute ( "EffectColor"); + c = optional_string_attribute (node, "EffectColor"); if (c) { - effect_color = Color (c.get ()); + ps.effect_colour = Colour (c.get ()); } - subtitle_nodes = type_children (node, "Subtitle"); - font_nodes = type_children (node, "Font"); - text_nodes = type_children (node, "Text"); + + return ps; } -FontNode::FontNode (list > const & font_nodes) - : size (0) - , italic (false) - , color ("FFFFFFFF") - , effect_color ("FFFFFFFF") +void +SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const { - for (list >::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 (); - } - if ((*i)->color) { - color = (*i)->color.get (); - } - if ((*i)->effect) { - effect = (*i)->effect.get (); - } - if ((*i)->effect_color) { - effect_color = (*i)->effect_color.get (); - } + auto hp = optional_number_attribute (node, "HPosition"); + if (!hp) { + hp = optional_number_attribute (node, "Hposition"); + } + if (hp) { + ps.h_position = hp.get () / 100; + } + + auto ha = optional_string_attribute (node, "HAlign"); + if (!ha) { + ha = optional_string_attribute (node, "Halign"); + } + if (ha) { + ps.h_align = string_to_halign (ha.get ()); + } + + auto vp = optional_number_attribute (node, "VPosition"); + if (!vp) { + vp = optional_number_attribute (node, "Vposition"); + } + if (vp) { + ps.v_position = vp.get () / 100; + } + + auto va = optional_string_attribute (node, "VAlign"); + if (!va) { + va = optional_string_attribute (node, "Valign"); + } + if (va) { + ps.v_align = string_to_valign (va.get ()); + } + + auto zp = optional_number_attribute(node, "Zposition"); + if (zp) { + ps.z_position = zp.get() / 100; } } -LoadFontNode::LoadFontNode (shared_ptr node) + +SubtitleAsset::ParseState +SubtitleAsset::text_node_state (xmlpp::Element const * node) const { - id = node->string_attribute ("Id"); - uri = node->string_attribute ("URI"); + ParseState ps; + + position_align (ps, node); + + auto d = optional_string_attribute (node, "Direction"); + if (d) { + ps.direction = string_to_direction (d.get ()); + } + + ps.type = ParseState::Type::TEXT; + + return ps; } - -SubtitleNode::SubtitleNode (shared_ptr node) + +SubtitleAsset::ParseState +SubtitleAsset::image_node_state (xmlpp::Element const * node) const { - in = Time (node->string_attribute ("TimeIn")); - out = Time (node->string_attribute ("TimeOut")); - font_nodes = type_children (node, "Font"); - text_nodes = type_children (node, "Text"); - fade_up_time = fade_time (node, "FadeUpTime"); - fade_down_time = fade_time (node, "FadeDownTime"); + ParseState ps; + + position_align (ps, node); + + ps.type = ParseState::Type::IMAGE; + + return ps; } + +SubtitleAsset::ParseState +SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional tcr) const +{ + ParseState ps; + ps.in = Time (string_attribute(node, "TimeIn"), tcr); + ps.out = Time (string_attribute(node, "TimeOut"), tcr); + ps.fade_up_time = fade_time (node, "FadeUpTime", tcr); + ps.fade_down_time = fade_time (node, "FadeDownTime", tcr); + return ps; +} + + Time -SubtitleNode::fade_time (shared_ptr node, string name) +SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional tcr) const { - string const u = node->optional_string_attribute (name).get_value_or (""); + auto const u = optional_string_attribute(node, name).get_value_or (""); Time t; - + if (u.empty ()) { - t = Time (0, 0, 0, 20); + t = Time (0, 0, 0, 20, 250); } else if (u.find (":") != string::npos) { - t = Time (u); + t = Time (u, tcr); } else { - t = Time (0, 0, 0, lexical_cast (u)); + t = Time (0, 0, 0, lexical_cast (u), tcr.get_value_or(250)); } - if (t > Time (0, 0, 8, 0)) { - t = Time (0, 0, 8, 0); + if (t > Time (0, 0, 8, 0, 250)) { + t = Time (0, 0, 8, 0, 250); } return t; } -TextNode::TextNode (shared_ptr node) - : v_align (CENTER) + +void +SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector& state, optional tcr, Standard standard) { - text = node->content (); - v_position = node->number_attribute ("VPosition"); - optional v = node->optional_string_attribute ("VAlign"); - if (v) { - v_align = string_to_valign (v.get ()); + if (node->get_name() == "Font") { + state.push_back (font_node_state (node, standard)); + } else if (node->get_name() == "Subtitle") { + state.push_back (subtitle_node_state (node, tcr)); + } else if (node->get_name() == "Text") { + state.push_back (text_node_state (node)); + } else if (node->get_name() == "SubtitleList") { + state.push_back (ParseState ()); + } else if (node->get_name() == "Image") { + state.push_back (image_node_state (node)); + } else { + throw XMLError ("unexpected node " + node->get_name()); } - font_nodes = type_children (node, "Font"); -} + float space_before = 0; -list > -SubtitleAsset::subtitles_at (Time t) const -{ - list > s; - for (list >::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - if ((*i)->in() <= t && t <= (*i)->out ()) { - s.push_back (*i); + for (auto i: node->get_children()) { + auto const v = dynamic_cast(i); + if (v) { + maybe_add_subtitle (v->get_content(), state, space_before, standard); + space_before = 0; + } + auto const e = dynamic_cast(i); + if (e) { + if (e->get_name() == "Space") { + if (node->get_name() != "Text") { + throw XMLError ("Space node found outside Text"); + } + auto size = optional_string_attribute(e, "Size").get_value_or("0.5"); + if (standard == dcp::Standard::INTEROP) { + boost::replace_all(size, "em", ""); + } + space_before += raw_convert(size); + } else { + parse_subtitles (e, state, tcr, standard); + } } } - return s; + state.pop_back (); } -std::string -SubtitleAsset::font_id_to_name (string id) const + +void +SubtitleAsset::maybe_add_subtitle (string text, vector const & parse_state, float space_before, Standard standard) { - list >::const_iterator i = _load_font_nodes.begin(); - while (i != _load_font_nodes.end() && (*i)->id != id) { - ++i; - } + auto wanted = [](ParseState const& ps) { + return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE); + }; - if (i == _load_font_nodes.end ()) { - return ""; + if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) { + return; } - if ((*i)->uri == "arial.ttf") { - return "Arial"; + ParseState ps; + for (auto const& i: parse_state) { + if (i.font_id) { + ps.font_id = i.font_id.get(); + } + if (i.size) { + ps.size = i.size.get(); + } + if (i.aspect_adjust) { + ps.aspect_adjust = i.aspect_adjust.get(); + } + if (i.italic) { + ps.italic = i.italic.get(); + } + if (i.bold) { + ps.bold = i.bold.get(); + } + if (i.underline) { + ps.underline = i.underline.get(); + } + if (i.colour) { + ps.colour = i.colour.get(); + } + if (i.effect) { + ps.effect = i.effect.get(); + } + if (i.effect_colour) { + ps.effect_colour = i.effect_colour.get(); + } + if (i.h_position) { + ps.h_position = i.h_position.get(); + } + if (i.h_align) { + ps.h_align = i.h_align.get(); + } + if (i.v_position) { + ps.v_position = i.v_position.get(); + } + if (i.v_align) { + ps.v_align = i.v_align.get(); + } + if (i.z_position) { + ps.z_position = i.z_position.get(); + } + if (i.direction) { + ps.direction = i.direction.get(); + } + if (i.in) { + ps.in = i.in.get(); + } + if (i.out) { + ps.out = i.out.get(); + } + if (i.fade_up_time) { + ps.fade_up_time = i.fade_up_time.get(); + } + if (i.fade_down_time) { + ps.fade_down_time = i.fade_down_time.get(); + } + if (i.type) { + ps.type = i.type.get(); + } } - return ""; -} + if (!ps.in || !ps.out) { + /* We're not in a node; just ignore this content */ + return; + } -Subtitle::Subtitle ( - string font, - bool italic, - Color color, - int size, - Time in, - Time out, - float v_position, - VAlign v_align, - string text, - Effect effect, - Color effect_color, - Time fade_up_time, - Time fade_down_time - ) - : _font (font) - , _italic (italic) - , _color (color) - , _size (size) - , _in (in) - , _out (out) - , _v_position (v_position) - , _v_align (v_align) - , _text (text) - , _effect (effect) - , _effect_color (effect_color) - , _fade_up_time (fade_up_time) - , _fade_down_time (fade_down_time) -{ + DCP_ASSERT (ps.type); + + switch (ps.type.get()) { + case ParseState::Type::TEXT: + _subtitles.push_back ( + make_shared( + ps.font_id, + ps.italic.get_value_or (false), + ps.bold.get_value_or (false), + ps.underline.get_value_or (false), + ps.colour.get_value_or (dcp::Colour (255, 255, 255)), + ps.size.get_value_or (42), + ps.aspect_adjust.get_value_or (1.0), + ps.in.get(), + ps.out.get(), + ps.h_position.get_value_or(0), + ps.h_align.get_value_or(HAlign::CENTER), + ps.v_position.get_value_or(0), + ps.v_align.get_value_or(VAlign::CENTER), + ps.z_position.get_value_or(0), + ps.direction.get_value_or (Direction::LTR), + text, + ps.effect.get_value_or (Effect::NONE), + ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)), + ps.fade_up_time.get_value_or(Time()), + ps.fade_down_time.get_value_or(Time()), + space_before + ) + ); + break; + case ParseState::Type::IMAGE: + { + switch (standard) { + case Standard::INTEROP: + if (text.size() >= 4) { + /* Remove file extension */ + text = text.substr(0, text.size() - 4); + } + break; + case Standard::SMPTE: + /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it) + * until around 2.15.140 so I guess either: + * a) it is not (always) used in the field, or + * b) nobody noticed / complained. + */ + if (text.substr(0, 9) == "urn:uuid:") { + text = text.substr(9); + } + break; + } + /* Add a subtitle with no image data and we'll fill that in later */ + _subtitles.push_back ( + make_shared( + ArrayData(), + text, + ps.in.get(), + ps.out.get(), + ps.h_position.get_value_or(0), + ps.h_align.get_value_or(HAlign::CENTER), + ps.v_position.get_value_or(0), + ps.v_align.get_value_or(VAlign::CENTER), + ps.z_position.get_value_or(0), + ps.fade_up_time.get_value_or(Time()), + ps.fade_down_time.get_value_or(Time()) + ) + ); + break; + } + } } -int -Subtitle::size_in_pixels (int screen_height) const + +vector> +SubtitleAsset::subtitles () 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); + vector> s; + for (auto i: _subtitles) { + s.push_back (i); + } + return s; } -bool -libdcp::operator== (Subtitle const & a, Subtitle const & b) + +vector> +SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const { - return ( - a.font() == b.font() && - a.italic() == b.italic() && - a.color() == b.color() && - a.size() == b.size() && - a.in() == b.in() && - a.out() == b.out() && - a.v_position() == b.v_position() && - a.v_align() == b.v_align() && - a.text() == b.text() && - a.effect() == b.effect() && - a.effect_color() == b.effect_color() && - a.fade_up_time() == b.fade_up_time() && - a.fade_down_time() == b.fade_down_time() - ); + vector> s; + for (auto i: _subtitles) { + if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) { + s.push_back (i); + } + } + + return s; } -ostream& -libdcp::operator<< (ostream& s, Subtitle const & sub) -{ - s << "\n`" << sub.text() << "' from " << sub.in() << " to " << sub.out() << ";\n" - << "fade up " << sub.fade_up_time() << ", fade down " << sub.fade_down_time() << ";\n" - << "font " << sub.font() << ", "; - if (sub.italic()) { - s << "italic"; - } else { - s << "non-italic"; +/* XXX: this needs a test */ +vector> +SubtitleAsset::subtitles_in_reel (shared_ptr asset) const +{ + auto frame_rate = asset->edit_rate().as_float(); + auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate()); + auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false); + + vector> corrected; + for (auto i: during) { + auto c = make_shared(*i); + c->set_in (c->in() - start); + c->set_out (c->out() - start); + corrected.push_back (c); } - - s << ", size " << sub.size() << ", color " << sub.color() << ", vpos " << sub.v_position() << ", valign " << ((int) sub.v_align()) << ";\n" - << "effect " << ((int) sub.effect()) << ", effect color " << sub.effect_color(); - return s; + return corrected; } + void SubtitleAsset::add (shared_ptr s) { _subtitles.push_back (s); - _need_sort = true; } -void -SubtitleAsset::write_to_cpl (ostream& s) const + +Time +SubtitleAsset::latest_subtitle_out () const { - /* XXX: should EditRate, Duration and IntrinsicDuration be in here? */ - - s << " \n" - << " urn:uuid:" << _uuid << "\n" - << " " << _file_name << "\n" - << " 0\n" - << " \n"; + Time t; + for (auto i: _subtitles) { + if (i->out() > t) { + t = i->out (); + } + } + + return t; +} + + +bool +SubtitleAsset::equals (shared_ptr other_asset, EqualityOptions options, NoteHandler note) const +{ + if (!Asset::equals (other_asset, options, note)) { + return false; + } + + auto other = dynamic_pointer_cast (other_asset); + if (!other) { + return false; + } + + if (_subtitles.size() != other->_subtitles.size()) { + note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size())); + return false; + } + + auto i = _subtitles.begin(); + auto j = other->_subtitles.begin(); + + while (i != _subtitles.end()) { + auto string_i = dynamic_pointer_cast (*i); + auto string_j = dynamic_pointer_cast (*j); + auto image_i = dynamic_pointer_cast (*i); + auto image_j = dynamic_pointer_cast (*j); + + if ((string_i && !string_j) || (image_i && !image_j)) { + note (NoteType::ERROR, "subtitles differ: string vs. image"); + return false; + } + + if (string_i && !string_i->equals(string_j, options, note)) { + return false; + } + + if (image_i && !image_i->equals(image_j, options, note)) { + return false; + } + + ++i; + ++j; + } + + return true; } -struct SubtitleSorter { + +struct SubtitleSorter +{ bool operator() (shared_ptr a, shared_ptr b) { if (a->in() != b->in()) { return a->in() < b->in(); } + if (a->v_align() == VAlign::BOTTOM) { + return a->v_position() > b->v_position(); + } return a->v_position() < b->v_position(); } }; -void -SubtitleAsset::write_xml () const -{ - ofstream f (path().string().c_str()); - write_xml (f); -} void -SubtitleAsset::write_xml (ostream& s) const +SubtitleAsset::pull_fonts (shared_ptr part) { - s << "\n" - << "\n" - << " " << _uuid << "\n" - << " " << _movie_title << "\n" - << " " << _reel_number << "\n" - << " " << _language << "\n"; + if (part->children.empty ()) { + return; + } - if (_load_font_nodes.size() > 1) { - boost::throw_exception (MiscError ("multiple LoadFont nodes not supported")); + /* Pull up from children */ + for (auto i: part->children) { + pull_fonts (i); } - if (!_load_font_nodes.empty ()) { - s << " id << "\" URI=\"" << _load_font_nodes.front()->uri << "\"/>\n"; + if (part->parent) { + /* Establish the common font features that each of part's children have; + these features go into part's font. + */ + part->font = part->children.front()->font; + for (auto i: part->children) { + part->font.take_intersection (i->font); + } + + /* Remove common values from part's children's fonts */ + for (auto i: part->children) { + i->font.take_difference (part->font); + } } - list > sorted = _subtitles; - if (_need_sort) { - sorted.sort (SubtitleSorter ()); + /* Merge adjacent children with the same font */ + auto i = part->children.begin(); + vector> merged; + + while (i != part->children.end()) { + + if ((*i)->font.empty ()) { + merged.push_back (*i); + ++i; + } else { + auto j = i; + ++j; + while (j != part->children.end() && (*i)->font == (*j)->font) { + ++j; + } + if (std::distance (i, j) == 1) { + merged.push_back (*i); + ++i; + } else { + shared_ptr group (new order::Part (part, (*i)->font)); + for (auto k = i; k != j; ++k) { + (*k)->font.clear (); + group->children.push_back (*k); + } + merged.push_back (group); + i = j; + } + } } - /* XXX: multiple fonts not supported */ - /* XXX: script, underlined, weight not supported */ + part->children = merged; +} + + +/** @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* xml_root, int time_code_rate, Standard standard) const +{ + auto sorted = _subtitles; + std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter()); + + /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing + font information into the bottom level (String) objects. + */ + + auto root = make_shared(shared_ptr()); + shared_ptr subtitle; + shared_ptr text; - bool first = true; - bool italic = false; - Color color; - int size = 0; - Effect effect = NONE; - Color effect_color; - int spot_number = 1; Time last_in; Time last_out; Time last_fade_up_time; Time last_fade_down_time; + HAlign last_h_align; + float last_h_position; + VAlign last_v_align; + float last_v_position; + float last_z_position; + Direction last_direction; + + for (auto i: sorted) { + if (!subtitle || + (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 = make_shared(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()); + root->children.push_back (subtitle); + + last_in = i->in (); + last_out = i->out (); + last_fade_up_time = i->fade_up_time (); + last_fade_down_time = i->fade_down_time (); + text.reset (); + } - for (list >::iterator i = sorted.begin(); i != sorted.end(); ++i) { + auto is = dynamic_pointer_cast(i); + if (is) { + if (!text || + last_h_align != is->h_align() || + fabs(last_h_position - is->h_position()) > ALIGN_EPSILON || + last_v_align != is->v_align() || + fabs(last_v_position - is->v_position()) > ALIGN_EPSILON || + fabs(last_z_position - is->z_position()) > ALIGN_EPSILON || + last_direction != is->direction() + ) { + text = make_shared(subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->z_position(), is->direction()); + subtitle->children.push_back (text); + + last_h_align = is->h_align (); + last_h_position = is->h_position (); + last_v_align = is->v_align (); + last_v_position = is->v_position (); + last_z_position = is->z_position(); + last_direction = is->direction (); + } - /* We will start a new ... whenever some font property changes. - I suppose should really make an optimal hierarchy of tags, but - that seems hard. - */ + text->children.push_back (make_shared(text, order::Font (is, standard), is->text(), is->space_before())); + } - bool const font_changed = first || - italic != (*i)->italic() || - color != (*i)->color() || - size != (*i)->size() || - effect != (*i)->effect() || - effect_color != (*i)->effect_color(); - - stringstream a; - if (font_changed) { - italic = (*i)->italic (); - a << "Italic=\"" << (italic ? "yes" : "no") << "\" "; - color = (*i)->color (); - a << "Color=\"" << color.to_argb_string() << "\" "; - size = (*i)->size (); - a << "Size=\"" << size << "\" "; - effect = (*i)->effect (); - a << "Effect=\"" << effect_to_string(effect) << "\" "; - effect_color = (*i)->effect_color (); - a << "EffectColor=\"" << effect_color.to_argb_string() << "\" "; - a << "Script=\"normal\" Underlined=\"no\" Weight=\"normal\""; - } - - if (first || 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() - )) { - - if (!first) { - s << " \n"; - } + auto ii = dynamic_pointer_cast(i); + if (ii) { + text.reset (); + subtitle->children.push_back ( + make_shared(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position(), ii->z_position()) + ); + } + } - if (font_changed) { - if (!first) { - s << " \n"; - } + /* Pull font changes as high up the hierarchy as we can */ - string id = "theFontId"; - if (!_load_font_nodes.empty()) { - id = _load_font_nodes.front()->id; - } - - s << " \n"; - } + pull_fonts (root); + + /* Write XML */ + + order::Context context; + context.time_code_rate = time_code_rate; + context.standard = standard; + context.spot_number = 1; + + root->write_xml (xml_root, context); +} - s << " in().to_string() << "\" " - << "TimeOut=\"" << (*i)->out().to_string() << "\" " - << "FadeUpTime=\"" << (*i)->fade_up_time().to_ticks() << "\" " - << "FadeDownTime=\"" << (*i)->fade_down_time().to_ticks() << "\"" - << ">\n"; - last_in = (*i)->in (); - last_out = (*i)->out (); - last_fade_up_time = (*i)->fade_up_time (); - last_fade_down_time = (*i)->fade_down_time (); +map +SubtitleAsset::font_data () const +{ + map out; + for (auto const& i: _fonts) { + out[i.load_id] = i.data; + } + return out; +} + + +map +SubtitleAsset::font_filenames () const +{ + map out; + for (auto const& i: _fonts) { + if (i.file) { + out[i.load_id] = *i.file; + } + } + return out; +} + + +/** Replace empty IDs in any and tags with + * a dummy string. Some systems give errors with empty font IDs + * (see DCP-o-matic bug #1689). + */ +void +SubtitleAsset::fix_empty_font_ids () +{ + bool have_empty = false; + vector ids; + for (auto i: load_font_nodes()) { + if (i->id == "") { + have_empty = true; + } else { + ids.push_back (i->id); } + } + + if (!have_empty) { + return; + } - s << " v_align()) << "\" " - << "VPosition=\"" << (*i)->v_position() << "\"" - << ">" << escape ((*i)->text()) << "\n"; + string const empty_id = unique_string (ids, "font"); - first = false; + for (auto i: load_font_nodes()) { + if (i->id == "") { + i->id = empty_id; + } + } + + for (auto i: _subtitles) { + auto j = dynamic_pointer_cast (i); + if (j && j->font() && j->font().get() == "") { + j->set_font (empty_id); + } } +} + + +namespace { + +struct State +{ + int indent; + string xml; + int disable_formatting; +}; + +} - s << " \n"; - s << " \n"; - s << "\n"; + +static +void +format_xml_node (xmlpp::Node const* node, State& state) +{ + if (auto text_node = dynamic_cast(node)) { + string content = text_node->get_content(); + boost::replace_all(content, "&", "&"); + boost::replace_all(content, "<", "<"); + boost::replace_all(content, ">", ">"); + state.xml += content; + } else if (auto element = dynamic_cast(node)) { + ++state.indent; + + auto children = element->get_children(); + auto const should_disable_formatting = + std::any_of( + children.begin(), children.end(), + [](xmlpp::Node const* node) { return static_cast(dynamic_cast(node)); } + ) || element->get_name() == "Text"; + + if (!state.disable_formatting) { + state.xml += "\n" + string(state.indent * 2, ' '); + } + + state.xml += "<" + element->get_name(); + + for (auto attribute: element->get_attributes()) { + state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw()); + } + + if (children.empty()) { + state.xml += "/>"; + } else { + state.xml += ">"; + + if (should_disable_formatting) { + ++state.disable_formatting; + } + + for (auto child: children) { + format_xml_node(child, state); + } + + if (!state.disable_formatting) { + state.xml += "\n" + string(state.indent * 2, ' '); + } + + state.xml += String::compose("", element->get_name().raw()); + + if (should_disable_formatting) { + --state.disable_formatting; + } + } + + --state.indent; + } } -/** XXX: Another reason why we should be writing with libxml++ */ + +/** Format XML much as write_to_string_formatted() would do, except without adding any white space + * to nodes. This is an attempt to avoid changing what is actually displayed as subtitles + * while also formatting the XML in such a way as to avoid DoM bug 2205. + * + * namespace is a list of namespaces for the root node; it would be nicer to set these up with + * set_namespace_declaration in the caller and then to extract them here but I couldn't find a way + * to get all namespaces with the libxml++ API. + */ string -SubtitleAsset::escape (string s) const +SubtitleAsset::format_xml (xmlpp::Document const& document, vector> const& namespaces) { - boost::replace_all (s, "&", "&"); - return s; + auto root = document.get_root_node(); + + State state = {}; + state.xml = "\n<" + root->get_name(); + + for (auto const& ns: namespaces) { + if (ns.first.empty()) { + state.xml += String::compose(" xmlns=\"%1\"", ns.second); + } else { + state.xml += String::compose(" xmlns:%1=\"%2\"", ns.first, ns.second); + } + } + + for (auto attribute: root->get_attributes()) { + state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw()); + } + + state.xml += ">"; + + for (auto child: document.get_root_node()->get_children()) { + format_xml_node(child, state); + } + + state.xml += String::compose("\n\n", root->get_name().raw()); + + return state.xml; } +