X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Fsubtitle_asset.cc;h=083a8040d8dce5f8c533dbea43ca5420f2d7866f;hb=ada2065df461ae9e56165e93e02b901c67a8bea3;hp=2b200a2f319cb58be7d37a3256edfb23039722ed;hpb=b56b008e2ad86bd2c29a42390891a32ae658d6c4;p=libdcp.git diff --git a/src/subtitle_asset.cc b/src/subtitle_asset.cc index 2b200a2f..083a8040 100644 --- a/src/subtitle_asset.cc +++ b/src/subtitle_asset.cc @@ -1,212 +1,695 @@ /* - Copyright (C) 2012 Carl Hetherington + Copyright (C) 2012-2019 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 "subtitle_string.h" +#include "subtitle_image.h" +#include "dcp_assert.h" +#include "load_font_node.h" +#include +#include +#include +#include +#include +#include +#include + +using std::string; +using std::list; +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 boost::lexical_cast; +using namespace dcp; + +SubtitleAsset::SubtitleAsset () +{ -using namespace std; -using namespace boost; -using namespace libdcp; +} -SubtitleAsset::SubtitleAsset (string directory, string xml) - : Asset (directory, xml) - , XMLFile (path().string(), "DCSubtitle") +SubtitleAsset::SubtitleAsset (boost::filesystem::path file) + : Asset (file) { - _subtitle_id = string_node ("SubtitleID"); - _movie_title = string_node ("MovieTitle"); - _reel_number = int64_node ("ReelNumber"); - _language = string_node ("Language"); - - ignore_node ("LoadFont"); - list > font_nodes = sub_nodes ("Font"); - _load_font_nodes = sub_nodes ("LoadFont"); +} - /* Now make Subtitle objects to represent the raw XML nodes - in a sane way. - */ +string +string_attribute (xmlpp::Element const * node, string name) +{ + xmlpp::Attribute* a = node->get_attribute (name); + if (!a) { + throw XMLError (String::compose ("missing attribute %1", name)); + } + return string (a->get_value ()); +} - list > current_font_nodes; - for (list >::iterator i = font_nodes.begin(); i != font_nodes.end(); ++i) { - examine_font_node (*i, current_font_nodes); +optional +optional_string_attribute (xmlpp::Element const * node, string name) +{ + xmlpp::Attribute* a = node->get_attribute (name); + if (!a) { + return optional(); } + return string (a->get_value ()); } -void -SubtitleAsset::examine_font_node (shared_ptr font_node, list >& current_font_nodes) -{ - current_font_nodes.push_back (font_node); - - for (list >::iterator j = font_node->subtitle_nodes.begin(); j != font_node->subtitle_nodes.end(); ++j) { - for (list >::iterator k = (*j)->text_nodes.begin(); k != (*j)->text_nodes.end(); ++k) { - _subtitles.push_back ( - shared_ptr ( - new Subtitle ( - font_id_to_name (id_from_font_nodes (current_font_nodes)), - italic_from_font_nodes (current_font_nodes), - size_from_font_nodes (current_font_nodes), - (*j)->in, - (*j)->out, - (*k)->v_position, - (*k)->text - ) - ) - ); - } +optional +optional_bool_attribute (xmlpp::Element const * node, string name) +{ + optional s = optional_string_attribute (node, name); + if (!s) { + return optional (); } - for (list >::iterator j = font_node->font_nodes.begin(); j != font_node->font_nodes.end(); ++j) { - examine_font_node (*j, current_font_nodes); + 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); + if (!s) { + return boost::optional (); } - current_font_nodes.pop_back (); + std::string t = s.get (); + boost::erase_all (t, " "); + return raw_convert (t); } -string -SubtitleAsset::id_from_font_nodes (list > const & font_nodes) const +SubtitleAsset::ParseState +SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const { - for (list >::const_reverse_iterator i = font_nodes.rbegin(); i != font_nodes.rend(); ++i) { - if (!(*i)->id.empty ()) { - return (*i)->id; - } + ParseState ps; + + 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 ""; + return ps; } -int -SubtitleAsset::size_from_font_nodes (list > const & font_nodes) const +void +SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const { - for (list >::const_reverse_iterator i = font_nodes.rbegin(); i != font_nodes.rend(); ++i) { - if ((*i)->size != 0) { - return (*i)->size; - } + 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; } - return 0; + 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 ()); + } } -bool -SubtitleAsset::italic_from_font_nodes (list > const & font_nodes) const +SubtitleAsset::ParseState +SubtitleAsset::text_node_state (xmlpp::Element const * node) const { - for (list >::const_reverse_iterator i = font_nodes.rbegin(); i != font_nodes.rend(); ++i) { - if ((*i)->italic) { - return (*i)->italic.get (); - } + ParseState ps; + + position_align (ps, node); + + optional d = optional_string_attribute (node, "Direction"); + if (d) { + ps.direction = string_to_direction (d.get ()); } - return false; + ps.type = ParseState::TEXT; + return ps; } -FontNode::FontNode (xmlpp::Node const * node) - : XMLNode (node) +SubtitleAsset::ParseState +SubtitleAsset::image_node_state (xmlpp::Element const * node) const { - id = string_attribute ("Id"); - size = optional_int64_attribute ("Size"); - italic = optional_bool_attribute ("Italic"); - subtitle_nodes = sub_nodes ("Subtitle"); - font_nodes = sub_nodes ("Font"); + ParseState ps; + + position_align (ps, node); + + ps.type = ParseState::IMAGE; + + return ps; } -LoadFontNode::LoadFontNode (xmlpp::Node const * node) - : XMLNode (node) +SubtitleAsset::ParseState +SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional tcr) const { - id = string_attribute ("Id"); - uri = string_attribute ("URI"); + 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; } - -SubtitleNode::SubtitleNode (xmlpp::Node const * node) - : XMLNode (node) +Time +SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional tcr) const { - in = time_attribute ("TimeIn"); - out = time_attribute ("TimeOut"); - text_nodes = sub_nodes ("Text"); + 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; } -TextNode::TextNode (xmlpp::Node const * node) - : XMLNode (node) +void +SubtitleAsset::parse_subtitles (xmlpp::Element const * node, list& state, optional tcr, Standard standard) { - text = content (); - v_position = float_attribute ("VPosition"); + 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()); + } + + 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, standard); + } + 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, list const & parse_state, Standard standard) +{ + if (empty_or_white_space (text)) { + 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(); + } + if (i.type) { + ps.type = i.type.get(); + } + } + + if (!ps.in || !ps.out) { + /* We're not in a node; just ignore this content */ + return; + } + + DCP_ASSERT (ps.type); + + switch (ps.type.get()) { + case ParseState::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()) + ) + ) + ); + break; + case ParseState::IMAGE: + /* 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()) + ) + ) + ); + break; + } } list > -SubtitleAsset::subtitles_at (Time t) const +SubtitleAsset::subtitles_during (Time from, Time to, bool starting) 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); + BOOST_FOREACH (shared_ptr i, _subtitles) { + if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) { + s.push_back (i); } } return s; } -std::string -SubtitleAsset::font_id_to_name (string id) const +void +SubtitleAsset::add (shared_ptr s) { - list >::const_iterator i = _load_font_nodes.begin(); - while (i != _load_font_nodes.end() && (*i)->id != id) { - ++i; + _subtitles.push_back (s); +} + +Time +SubtitleAsset::latest_subtitle_out () const +{ + Time t; + BOOST_FOREACH (shared_ptr 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; + } + + shared_ptr other = dynamic_pointer_cast (other_asset); + if (!other) { + return false; } - if (i == _load_font_nodes.end ()) { - return ""; + if (_subtitles.size() != other->_subtitles.size()) { + note (DCP_ERROR, "subtitles differ"); + return false; } - if ((*i)->uri == "arial.ttf") { - return "Arial"; + list >::const_iterator i = _subtitles.begin (); + list >::const_iterator 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); + + if ((string_i && !string_j) || (image_i && !image_j)) { + note (DCP_ERROR, "subtitles differ"); + return false; + } + + if (string_i && *string_i != *string_j) { + note (DCP_ERROR, "subtitles differ"); + return false; + } + + if (image_i && *image_i != *image_j) { + note (DCP_ERROR, "subtitles differ"); + return false; + } + + ++i; + ++j; } - 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() (shared_ptr a, shared_ptr b) { + if (a->in() != b->in()) { + return a->in() < b->in(); + } + return a->v_position() < b->v_position(); + } +}; + +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 (std::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; } -int -Subtitle::size_in_pixels (int screen_height) const +/** @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 { - /* 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. + list > sorted = _subtitles; + sorted.sort (SubtitleSorter ()); + + /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing + font information into the bottom level (String) objects. */ - - return _size * screen_height / (11 * 72); + + shared_ptr root (new order::Part (shared_ptr ())); + shared_ptr subtitle; + shared_ptr text; + + 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 (shared_ptr 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 (); + } + + shared_ptr 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 || + last_direction != is->direction() + ) { + text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_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_direction = is->direction (); + } + + text->children.push_back (shared_ptr (new order::String (text, order::Font (is, standard), is->text()))); + } + + shared_ptr 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())) + ); + } + } + + /* Pull font changes as high up the hierarchy as we can */ + + 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); +} + +map +SubtitleAsset::fonts_with_load_ids () const +{ + map out; + BOOST_FOREACH (Font const & i, _fonts) { + out[i.load_id] = i.data; + } + 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; + list ids; + BOOST_FOREACH (shared_ptr 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"); + + BOOST_FOREACH (shared_ptr i, load_font_nodes()) { + if (i->id == "") { + i->id = empty_id; + } + } + + BOOST_FOREACH (shared_ptr i, _subtitles) { + shared_ptr j = dynamic_pointer_cast (i); + if (j && j->font() && j->font().get() == "") { + j->set_font (empty_id); + } + } }