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=bb7b0b1ccf4f535d964ee72f2d23d221778b36f0;hpb=62c7d0c33b06ffadc09fb6c25d8dba673c384c47;p=libdcp.git diff --git a/src/subtitle_asset.cc b/src/subtitle_asset.cc index bb7b0b1c..4baa7b06 100644 --- a/src/subtitle_asset.cc +++ b/src/subtitle_asset.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2018 Carl Hetherington + Copyright (C) 2012-2021 Carl Hetherington This file is part of libdcp. @@ -31,6 +31,12 @@ files in the program, then also delete it here. */ + +/** @file src/subtitle_asset.cc + * @brief SubtitleAsset class + */ + + #include "raw_convert.h" #include "compose.hpp" #include "subtitle_asset.h" @@ -40,73 +46,83 @@ #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 +#include + -using std::string; -using std::list; -using std::cout; using std::cerr; +using std::cout; +using std::dynamic_pointer_cast; +using std::make_shared; using std::map; -using boost::shared_ptr; -using boost::shared_array; -using boost::optional; -using boost::dynamic_pointer_cast; +using std::pair; +using std::shared_ptr; +using std::string; +using std::vector; using boost::lexical_cast; +using boost::optional; using namespace dcp; + SubtitleAsset::SubtitleAsset () { } + SubtitleAsset::SubtitleAsset (boost::filesystem::path file) : Asset (file) { } + string string_attribute (xmlpp::Element const * node, string name) { - xmlpp::Attribute* a = node->get_attribute (name); + auto a = node->get_attribute (name); if (!a) { throw XMLError (String::compose ("missing attribute %1", name)); } return string (a->get_value ()); } + optional optional_string_attribute (xmlpp::Element const * node, string name) { - xmlpp::Attribute* a = node->get_attribute (name); + auto a = node->get_attribute (name); if (!a) { - return optional(); + return {}; } return string (a->get_value ()); } + optional optional_bool_attribute (xmlpp::Element const * node, string name) { - optional s = optional_string_attribute (node, name); + auto s = optional_string_attribute (node, name); if (!s) { - return optional (); + return {}; } return (s.get() == "1" || s.get() == "yes"); } + template optional optional_number_attribute (xmlpp::Element const * node, string name) { - boost::optional s = optional_string_attribute (node, name); + auto s = optional_string_attribute (node, name); if (!s) { return boost::optional (); } @@ -116,12 +132,13 @@ optional_number_attribute (xmlpp::Element const * node, string name) return raw_convert (t); } + SubtitleAsset::ParseState SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const { ParseState ps; - if (standard == INTEROP) { + if (standard == Standard::INTEROP) { ps.font_id = optional_string_attribute (node, "Id"); } else { ps.font_id = optional_string_attribute (node, "ID"); @@ -130,16 +147,16 @@ SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) 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 == INTEROP) { + if (standard == Standard::INTEROP) { ps.underline = optional_bool_attribute (node, "Underlined"); } else { ps.underline = optional_bool_attribute (node, "Underline"); } - optional c = optional_string_attribute (node, "Color"); + auto c = optional_string_attribute (node, "Color"); if (c) { ps.colour = Colour (c.get ()); } - optional const e = optional_string_attribute (node, "Effect"); + auto const e = optional_string_attribute (node, "Effect"); if (e) { ps.effect = string_to_effect (e.get ()); } @@ -154,7 +171,7 @@ SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) void SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const { - optional hp = optional_number_attribute (node, "HPosition"); + auto hp = optional_number_attribute (node, "HPosition"); if (!hp) { hp = optional_number_attribute (node, "Hposition"); } @@ -162,7 +179,7 @@ SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element con ps.h_position = hp.get () / 100; } - optional ha = optional_string_attribute (node, "HAlign"); + auto ha = optional_string_attribute (node, "HAlign"); if (!ha) { ha = optional_string_attribute (node, "Halign"); } @@ -170,7 +187,7 @@ SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element con ps.h_align = string_to_halign (ha.get ()); } - optional vp = optional_number_attribute (node, "VPosition"); + auto vp = optional_number_attribute (node, "VPosition"); if (!vp) { vp = optional_number_attribute (node, "Vposition"); } @@ -178,7 +195,7 @@ SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element con ps.v_position = vp.get () / 100; } - optional va = optional_string_attribute (node, "VAlign"); + auto va = optional_string_attribute (node, "VAlign"); if (!va) { va = optional_string_attribute (node, "Valign"); } @@ -186,8 +203,13 @@ SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element con ps.v_align = string_to_valign (va.get ()); } + auto zp = optional_number_attribute(node, "Zposition"); + if (zp) { + ps.z_position = zp.get() / 100; + } } + SubtitleAsset::ParseState SubtitleAsset::text_node_state (xmlpp::Element const * node) const { @@ -195,16 +217,17 @@ SubtitleAsset::text_node_state (xmlpp::Element const * node) const position_align (ps, node); - optional d = optional_string_attribute (node, "Direction"); + auto d = optional_string_attribute (node, "Direction"); if (d) { ps.direction = string_to_direction (d.get ()); } - ps.type = ParseState::TEXT; + ps.type = ParseState::Type::TEXT; return ps; } + SubtitleAsset::ParseState SubtitleAsset::image_node_state (xmlpp::Element const * node) const { @@ -212,11 +235,12 @@ SubtitleAsset::image_node_state (xmlpp::Element const * node) const position_align (ps, node); - ps.type = ParseState::IMAGE; + ps.type = ParseState::Type::IMAGE; return ps; } + SubtitleAsset::ParseState SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional tcr) const { @@ -228,10 +252,11 @@ SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional t return ps; } + Time SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional tcr) const { - string const u = optional_string_attribute(node, name).get_value_or (""); + auto const u = optional_string_attribute(node, name).get_value_or (""); Time t; if (u.empty ()) { @@ -249,8 +274,9 @@ SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional& state, optional tcr, Standard standard) +SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector& state, optional tcr, Standard standard) { if (node->get_name() == "Font") { state.push_back (font_node_state (node, standard)); @@ -266,30 +292,48 @@ SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list& s throw XMLError ("unexpected node " + node->get_name()); } - xmlpp::Node::NodeList c = node->get_children (); - for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) { - xmlpp::ContentNode const * v = dynamic_cast (*i); + float space_before = 0; + + for (auto i: node->get_children()) { + auto const v = dynamic_cast(i); if (v) { - maybe_add_subtitle (v->get_content(), state, standard); + maybe_add_subtitle (v->get_content(), state, space_before, standard); + space_before = 0; } - xmlpp::Element const * e = dynamic_cast (*i); + auto const e = dynamic_cast(i); if (e) { - parse_subtitles (e, state, tcr, standard); + 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); + } } } state.pop_back (); } + void -SubtitleAsset::maybe_add_subtitle (string text, list const & parse_state, Standard standard) +SubtitleAsset::maybe_add_subtitle (string text, vector const & parse_state, float space_before, Standard standard) { - if (empty_or_white_space (text)) { + auto wanted = [](ParseState const& ps) { + return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE); + }; + + if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) { return; } ParseState ps; - BOOST_FOREACH (ParseState const & i, parse_state) { + for (auto const& i: parse_state) { if (i.font_id) { ps.font_id = i.font_id.get(); } @@ -329,6 +373,9 @@ SubtitleAsset::maybe_add_subtitle (string text, list const & parse_s 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(); } @@ -357,60 +404,92 @@ SubtitleAsset::maybe_add_subtitle (string text, list const & parse_s DCP_ASSERT (ps.type); switch (ps.type.get()) { - case ParseState::TEXT: + case ParseState::Type::TEXT: _subtitles.push_back ( - shared_ptr ( - new SubtitleString ( - 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.direction.get_value_or (DIRECTION_LTR), - text, - ps.effect.get_value_or (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()) - ) + 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::IMAGE: + 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 ( - shared_ptr ( - new SubtitleImage ( - Data (), - standard == INTEROP ? text.substr(0, text.size() - 4) : 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.fade_up_time.get_value_or(Time()), - ps.fade_down_time.get_value_or(Time()) - ) + 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; } + } +} + + +vector> +SubtitleAsset::subtitles () const +{ + vector> s; + for (auto i: _subtitles) { + s.push_back (i); + } + return s; } -list > + +vector> SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const { - list > s; - BOOST_FOREACH (shared_ptr i, _subtitles) { + 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); } @@ -419,17 +498,39 @@ SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const return s; } + +/* 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); + } + + return corrected; +} + + void SubtitleAsset::add (shared_ptr s) { _subtitles.push_back (s); } + Time SubtitleAsset::latest_subtitle_out () const { Time t; - BOOST_FOREACH (shared_ptr i, _subtitles) { + for (auto i: _subtitles) { if (i->out() > t) { t = i->out (); } @@ -438,6 +539,7 @@ SubtitleAsset::latest_subtitle_out () const return t; } + bool SubtitleAsset::equals (shared_ptr other_asset, EqualityOptions options, NoteHandler note) const { @@ -445,37 +547,35 @@ SubtitleAsset::equals (shared_ptr other_asset, EqualityOptions opti return false; } - shared_ptr other = dynamic_pointer_cast (other_asset); + auto other = dynamic_pointer_cast (other_asset); if (!other) { return false; } if (_subtitles.size() != other->_subtitles.size()) { - note (DCP_ERROR, "subtitles differ"); + note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size())); return false; } - list >::const_iterator i = _subtitles.begin (); - list >::const_iterator j = other->_subtitles.begin (); + auto i = _subtitles.begin(); + auto j = other->_subtitles.begin(); while (i != _subtitles.end()) { - shared_ptr string_i = dynamic_pointer_cast (*i); - shared_ptr string_j = dynamic_pointer_cast (*j); - shared_ptr image_i = dynamic_pointer_cast (*i); - shared_ptr image_j = dynamic_pointer_cast (*j); + 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 (DCP_ERROR, "subtitles differ"); + note (NoteType::ERROR, "subtitles differ: string vs. image"); return false; } - if (string_i && *string_i != *string_j) { - note (DCP_ERROR, "subtitles differ"); + if (string_i && !string_i->equals(string_j, options, note)) { return false; } - if (image_i && *image_i != *image_j) { - note (DCP_ERROR, "subtitles differ"); + if (image_i && !image_i->equals(image_j, options, note)) { return false; } @@ -486,16 +586,21 @@ SubtitleAsset::equals (shared_ptr other_asset, EqualityOptions opti return true; } + 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::pull_fonts (shared_ptr part) { @@ -504,7 +609,7 @@ SubtitleAsset::pull_fonts (shared_ptr part) } /* Pull up from children */ - BOOST_FOREACH (shared_ptr i, part->children) { + for (auto i: part->children) { pull_fonts (i); } @@ -513,19 +618,19 @@ SubtitleAsset::pull_fonts (shared_ptr part) these features go into part's font. */ part->font = part->children.front()->font; - BOOST_FOREACH (shared_ptr i, part->children) { + for (auto i: part->children) { part->font.take_intersection (i->font); } /* Remove common values from part's children's fonts */ - BOOST_FOREACH (shared_ptr i, part->children) { + for (auto i: part->children) { i->font.take_difference (part->font); } } /* Merge adjacent children with the same font */ - list >::const_iterator i = part->children.begin(); - list > merged; + auto i = part->children.begin(); + vector> merged; while (i != part->children.end()) { @@ -533,7 +638,7 @@ SubtitleAsset::pull_fonts (shared_ptr part) merged.push_back (*i); ++i; } else { - list >::const_iterator j = i; + auto j = i; ++j; while (j != part->children.end() && (*i)->font == (*j)->font) { ++j; @@ -543,7 +648,7 @@ SubtitleAsset::pull_fonts (shared_ptr part) ++i; } else { shared_ptr group (new order::Part (part, (*i)->font)); - for (list >::const_iterator k = i; k != j; ++k) { + for (auto k = i; k != j; ++k) { (*k)->font.clear (); group->children.push_back (*k); } @@ -556,20 +661,21 @@ SubtitleAsset::pull_fonts (shared_ptr part) 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 { - list > sorted = _subtitles; - sorted.sort (SubtitleSorter ()); + 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. */ - shared_ptr root (new order::Part (shared_ptr ())); + auto root = make_shared(shared_ptr()); shared_ptr subtitle; shared_ptr text; @@ -581,9 +687,10 @@ SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, S float last_h_position; VAlign last_v_align; float last_v_position; + float last_z_position; Direction last_direction; - BOOST_FOREACH (shared_ptr i, sorted) { + for (auto i: sorted) { if (!subtitle || (last_in != i->in() || last_out != i->out() || @@ -591,7 +698,7 @@ SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, S last_fade_down_time != i->fade_down_time()) ) { - subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_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 (); @@ -601,33 +708,35 @@ SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, S text.reset (); } - shared_ptr is = dynamic_pointer_cast(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.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), 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 (); } - text->children.push_back (shared_ptr (new order::String (text, order::Font (is, standard), is->text()))); + text->children.push_back (make_shared(text, order::Font (is, standard), is->text(), is->space_before())); } - shared_ptr ii = dynamic_pointer_cast(i); + auto ii = dynamic_pointer_cast(i); if (ii) { text.reset (); subtitle->children.push_back ( - shared_ptr (new order::Image (subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position())) + make_shared(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position(), ii->z_position()) ); } } @@ -646,12 +755,176 @@ SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, S root->write_xml (xml_root, context); } -map -SubtitleAsset::fonts_with_load_ids () const + +map +SubtitleAsset::font_data () const { - map out; - BOOST_FOREACH (Font const & i, _fonts) { + 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; + } + + string const empty_id = unique_string (ids, "font"); + + 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; +}; + +} + + +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; + } +} + + +/** 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::format_xml (xmlpp::Document const& document, vector> const& namespaces) +{ + 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; +} +