X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fsubtitle_asset.cc;h=19f7fa375e10020faf1ada0d2f736151a47d7e47;hb=6b0b06915c254637350dc14646ebc7d641ba1e60;hp=414825a4de8994e1aa0a9ba5d547c76c6747d4c3;hpb=ea629ead13e819b4ff5d5b5eba7b57520e8107e3;p=libdcp.git diff --git a/src/subtitle_asset.cc b/src/subtitle_asset.cc index 414825a4..19f7fa37 100644 --- a/src/subtitle_asset.cc +++ b/src/subtitle_asset.cc @@ -1,50 +1,63 @@ /* - Copyright (C) 2012-2015 Carl Hetherington + Copyright (C) 2012-2016 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 "raw_convert.h" +#include "compose.hpp" #include "subtitle_asset.h" +#include "subtitle_asset_internal.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 +#include #include #include +#include #include #include -#include 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 std::distance; using boost::shared_ptr; using boost::shared_array; using boost::optional; using boost::dynamic_pointer_cast; +using boost::lexical_cast; using namespace dcp; SubtitleAsset::SubtitleAsset () @@ -58,101 +71,284 @@ SubtitleAsset::SubtitleAsset (boost::filesystem::path file) } -void -SubtitleAsset::parse_subtitles (shared_ptr xml, list > font_nodes) +string +string_attribute (xmlpp::Element const * node, string name) { - /* Make Subtitle objects to represent the raw XML nodes in a sane way */ - ParseState parse_state; - examine_font_nodes (xml, font_nodes, parse_state); + xmlpp::Attribute* a = node->get_attribute (name); + if (!a) { + throw XMLError (String::compose ("missing attribute %1", name)); + } + return string (a->get_value ()); } -void -SubtitleAsset::examine_subtitle_nodes ( - shared_ptr xml, - list > const & subtitle_nodes, - ParseState& parse_state - ) +optional +optional_string_attribute (xmlpp::Element const * node, string name) { - for (list >::const_iterator j = subtitle_nodes.begin(); j != 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 (); + xmlpp::Attribute* a = node->get_attribute (name); + if (!a) { + return optional(); } + return string (a->get_value ()); } -void -SubtitleAsset::examine_font_nodes ( - shared_ptr xml, - list > const & font_nodes, - ParseState& parse_state - ) +optional +optional_bool_attribute (xmlpp::Element const * node, string name) { - for (list >::const_iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) { + optional s = optional_string_attribute (node, name); + if (!s) { + return optional (); + } - parse_state.font_nodes.push_back (*i); - maybe_add_subtitle ((*i)->text, parse_state); + return (s.get() == "1" || s.get() == "yes"); +} - examine_subtitle_nodes (xml, (*i)->subtitle_nodes, parse_state); +template +optional +optional_number_attribute (xmlpp::Element const * node, string name) +{ + boost::optional s = optional_string_attribute (node, name); + if (!s) { + return boost::optional (); + } + + std::string t = s.get (); + boost::erase_all (t, " "); + return raw_convert (t); +} - examine_font_nodes (xml, (*i)->font_nodes, parse_state); - examine_text_nodes (xml, (*i)->text_nodes, parse_state); +SubtitleAsset::ParseState +SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const +{ + ParseState ps; - parse_state.font_nodes.pop_back (); + if (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 == INTEROP) { + ps.underline = optional_bool_attribute (node, "Underlined"); + } else { + ps.underline = optional_bool_attribute (node, "Underline"); + } + optional c = optional_string_attribute (node, "Color"); + if (c) { + ps.colour = Colour (c.get ()); + } + optional const e = optional_string_attribute (node, "Effect"); + if (e) { + ps.effect = string_to_effect (e.get ()); + } + c = optional_string_attribute (node, "EffectColor"); + if (c) { + ps.effect_colour = Colour (c.get ()); } + + return ps; +} + +SubtitleAsset::ParseState +SubtitleAsset::text_node_state (xmlpp::Element const * node) const +{ + ParseState ps; + + optional hp = optional_number_attribute (node, "HPosition"); + if (!hp) { + hp = optional_number_attribute (node, "Hposition"); + } + if (hp) { + ps.h_position = hp.get () / 100; + } + + optional ha = optional_string_attribute (node, "HAlign"); + if (!ha) { + ha = optional_string_attribute (node, "Halign"); + } + if (ha) { + ps.h_align = string_to_halign (ha.get ()); + } + + optional vp = optional_number_attribute (node, "VPosition"); + if (!vp) { + vp = optional_number_attribute (node, "Vposition"); + } + if (vp) { + ps.v_position = vp.get () / 100; + } + + optional va = optional_string_attribute (node, "VAlign"); + if (!va) { + va = optional_string_attribute (node, "Valign"); + } + if (va) { + ps.v_align = string_to_valign (va.get ()); + } + + optional d = optional_string_attribute (node, "Direction"); + if (d) { + ps.direction = string_to_direction (d.get ()); + } + + 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 +SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional tcr) const +{ + string const u = optional_string_attribute(node, name).get_value_or (""); + Time t; + + if (u.empty ()) { + t = Time (0, 0, 0, 20, 250); + } else if (u.find (":") != string::npos) { + t = Time (u, tcr); + } else { + t = Time (0, 0, 0, lexical_cast (u), tcr.get_value_or(250)); + } + + if (t > Time (0, 0, 8, 0, 250)) { + t = Time (0, 0, 8, 0, 250); + } + + return t; } void -SubtitleAsset::examine_text_nodes ( - shared_ptr xml, - list > const & text_nodes, - ParseState& parse_state - ) +SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list& state, optional tcr, Standard standard) { - 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 (); + 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 { + 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); + if (v) { + maybe_add_subtitle (v->get_content(), state); + } + xmlpp::Element const * e = dynamic_cast (*i); + if (e) { + parse_subtitles (e, state, tcr, standard); + } + } + + state.pop_back (); } void -SubtitleAsset::maybe_add_subtitle (string text, ParseState const & parse_state) +SubtitleAsset::maybe_add_subtitle (string text, list const & parse_state) { if (empty_or_white_space (text)) { return; } - if (parse_state.text_nodes.empty() || parse_state.subtitle_nodes.empty ()) { - return; + ParseState ps; + BOOST_FOREACH (ParseState 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.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(); + } } - 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 ()); + if (!ps.in || !ps.out) { + /* We're not in a node; just ignore this content */ + return; + } _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, + 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, - 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 + ps.effect.get_value_or (NONE), + ps.effect_colour.get_value_or (dcp::Colour (255, 255, 255)), + ps.fade_up_time.get_value_or(Time()), + ps.fade_down_time.get_value_or(Time()) ) ); } @@ -161,9 +357,9 @@ list SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const { list s; - for (list::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) { - s.push_back (*i); + 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); } } @@ -180,9 +376,9 @@ Time SubtitleAsset::latest_subtitle_out () const { Time t; - for (list::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - if (i->out() > t) { - t = i->out (); + BOOST_FOREACH (SubtitleString const & i, _subtitles) { + if (i.out() > t) { + t = i.out (); } } @@ -209,7 +405,8 @@ SubtitleAsset::equals (shared_ptr other_asset, EqualityOptions opti return true; } -struct SubtitleSorter { +struct SubtitleSorter +{ bool operator() (SubtitleString const & a, SubtitleString const & b) { if (a.in() != b.in()) { return a.in() < b.in(); @@ -218,147 +415,144 @@ struct SubtitleSorter { } }; +void +SubtitleAsset::pull_fonts (shared_ptr part) +{ + if (part->children.empty ()) { + return; + } + + /* Pull up from children */ + BOOST_FOREACH (shared_ptr i, part->children) { + pull_fonts (i); + } + + 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; + BOOST_FOREACH (shared_ptr 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) { + i->font.take_difference (part->font); + } + } + + /* Merge adjacent children with the same font */ + list >::const_iterator i = part->children.begin(); + list > merged; + + while (i != part->children.end()) { + + if ((*i)->font.empty ()) { + merged.push_back (*i); + ++i; + } else { + list >::const_iterator j = i; + ++j; + while (j != part->children.end() && (*i)->font == (*j)->font) { + ++j; + } + if (distance (i, j) == 1) { + merged.push_back (*i); + ++i; + } else { + shared_ptr group (new order::Part (part, (*i)->font)); + for (list >::const_iterator k = i; k != j; ++k) { + (*k)->font.clear (); + group->children.push_back (*k); + } + merged.push_back (group); + i = j; + } + } + } + + 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* root, int time_code_rate, Standard standard) const +SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const { list sorted = _subtitles; sorted.sort (SubtitleSorter ()); - string const xmlns = standard == SMPTE ? "dcst" : ""; + /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing + font information into the bottom level (String) objects. + */ - /* XXX: script, underlined, weight not supported */ + shared_ptr root (new order::Part (shared_ptr ())); + shared_ptr subtitle; + shared_ptr text; - optional 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; + HAlign last_h_align; + float last_h_position; + VAlign last_v_align; + float last_v_position; + Direction last_direction; + + BOOST_FOREACH (SubtitleString const & 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.reset (new order::Subtitle (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 (); + } - xmlpp::Element* font_element = 0; - xmlpp::Element* subtitle_element = 0; + if (!text || + last_h_align != i.h_align() || + fabs(last_h_position - i.h_position()) > ALIGN_EPSILON || + last_v_align != i.v_align() || + fabs(last_v_position - i.v_position()) > ALIGN_EPSILON || + last_direction != i.direction() + ) { + + text.reset (new order::Text (subtitle, i.h_align(), i.h_position(), i.v_align(), i.v_position(), i.direction())); + subtitle->children.push_back (text); + + last_h_align = i.h_align (); + last_h_position = i.h_position (); + last_v_align = i.v_align (); + last_v_position = i.v_position (); + last_direction = i.direction (); + } - for (list::iterator i = sorted.begin(); i != sorted.end(); ++i) { + text->children.push_back (shared_ptr (new order::String (text, order::Font (i, standard), i.text()))); + } - /* We will start a new ... whenever some font property changes. - I suppose we should really make an optimal hierarchy of tags, but - that seems hard. - */ + /* Pull font changes as high up the hierarchy as we can */ - 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 (); - } + pull_fonts (root); - 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 (size)); - if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) { - font_element->set_attribute ("AspectAdjust", raw_convert (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"); - } + /* Write XML */ - 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 (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 (i->fade_up_time().as_editable_units(time_code_rate))); - subtitle_element->set_attribute ("FadeDownTime", raw_convert (i->fade_down_time().as_editable_units(time_code_rate))); - } + order::Context context; + context.time_code_rate = time_code_rate; + context.standard = standard; + context.spot_number = 1; - 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 (i->h_position() * 100, 6)); - } else { - text->set_attribute ("HPosition", raw_convert (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 (i->v_position() * 100, 6)); - } else { - text->set_attribute ("VPosition", raw_convert (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()); - } + root->write_xml (xml_root, context); } map