Pass optimisation setting through to subtitle XML writers.
[libdcp.git] / src / interop_subtitle_asset.cc
index a6bdfcafab41bef83d56a7603097d1d496f65229..0c35abbf3aeb946130d265df4d3dc7620ed06c91 100644 (file)
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
 
-    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 <http://www.gnu.org/licenses/>.
+
+    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 "interop_subtitle_asset.h"
+
+/** @file  src/interop_subtitle_asset.cc
+ *  @brief InteropSubtitleAsset class
+ */
+
+
+#include "compose.hpp"
+#include "dcp_assert.h"
+#include "equality_options.h"
+#include "filesystem.h"
+#include "font_asset.h"
+#include "file.h"
 #include "interop_load_font_node.h"
-#include "xml.h"
+#include "interop_subtitle_asset.h"
 #include "raw_convert.h"
-#include "font_node.h"
+#include "subtitle_asset_internal.h"
+#include "subtitle_image.h"
+#include "util.h"
+#include "warnings.h"
+#include "xml.h"
+LIBDCP_DISABLE_WARNINGS
 #include <libxml++/libxml++.h>
-#include <boost/foreach.hpp>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/weak_ptr.hpp>
 #include <cmath>
+#include <cstdio>
 
-using std::list;
-using std::string;
+
+using std::cerr;
 using std::cout;
-using boost::shared_ptr;
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
 using boost::optional;
-using boost::dynamic_pointer_cast;
 using namespace dcp;
 
+
 InteropSubtitleAsset::InteropSubtitleAsset (boost::filesystem::path file)
        : SubtitleAsset (file)
 {
-       shared_ptr<cxml::Document> xml (new cxml::Document ("DCSubtitle"));
-       xml->read_file (file);
-       _id = xml->string_child ("SubtitleID");
+       _raw_xml = dcp::file_to_string (file);
 
+       auto xml = make_shared<cxml::Document>("DCSubtitle");
+       xml->read_file(dcp::filesystem::fix_long_path(file));
+       _id = xml->string_child ("SubtitleID");
+       _reel_number = xml->string_child ("ReelNumber");
+       _language = xml->string_child ("Language");
        _movie_title = xml->string_child ("MovieTitle");
-       _load_font_nodes = type_children<dcp::InteropLoadFontNode> (xml, "LoadFont");
+       _load_font_nodes = type_children<InteropLoadFontNode> (xml, "LoadFont");
+
+       /* Now we need to drop down to xmlpp */
 
-       list<cxml::NodePtr> f = xml->node_children ("Font");
-       list<shared_ptr<dcp::FontNode> > font_nodes;
-       BOOST_FOREACH (cxml::NodePtr& i, f) {
-               font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, 250)));
+       vector<ParseState> ps;
+       for (auto i: xml->node()->get_children()) {
+               auto e = dynamic_cast<xmlpp::Element const *>(i);
+               if (e && (e->get_name() == "Font" || e->get_name() == "Subtitle")) {
+                       parse_subtitles (e, ps, optional<int>(), Standard::INTEROP);
+               }
        }
 
-       parse_common (xml, font_nodes);
+       for (auto i: _subtitles) {
+               auto si = dynamic_pointer_cast<SubtitleImage>(i);
+               if (si) {
+                       si->read_png_file (file.parent_path() / String::compose("%1.png", si->id()));
+               }
+       }
 }
 
-InteropSubtitleAsset::InteropSubtitleAsset (string movie_title, string language)
-       : _movie_title (movie_title)
+
+InteropSubtitleAsset::InteropSubtitleAsset ()
 {
-       _language = language;
+
 }
 
-struct SubtitleSorter {
-       bool operator() (SubtitleString const & a, SubtitleString const & b) {
-               if (a.in() != b.in()) {
-                       return a.in() < b.in();
-               }
-               return a.v_position() < b.v_position();
-       }
-};
 
-Glib::ustring
-InteropSubtitleAsset::xml_as_string () const
+string
+InteropSubtitleAsset::xml_as_string(SubtitleOptimisation optimisation) const
 {
        xmlpp::Document doc;
-       xmlpp::Element* root = doc.create_root_node ("DCSubtitle");
+       auto root = doc.create_root_node ("DCSubtitle");
        root->set_attribute ("Version", "1.0");
 
        root->add_child("SubtitleID")->add_child_text (_id);
@@ -80,160 +120,221 @@ InteropSubtitleAsset::xml_as_string () const
        root->add_child("ReelNumber")->add_child_text (raw_convert<string> (_reel_number));
        root->add_child("Language")->add_child_text (_language);
 
-       for (list<shared_ptr<InteropLoadFontNode> >::const_iterator i = _load_font_nodes.begin(); i != _load_font_nodes.end(); ++i) {
-               xmlpp::Element* load_font = root->add_child("LoadFont");
-               load_font->set_attribute ("Id", (*i)->id);
-               load_font->set_attribute ("URI", (*i)->uri);
-       }
-
-       list<SubtitleString> sorted = _subtitles;
-       sorted.sort (SubtitleSorter ());
-
-       /* XXX: script, underlined, weight not supported */
-
-       optional<string> font;
-       bool italic = false;
-       Colour colour;
-       int size = 0;
-       float aspect_adjust = 1.0;
-       Effect effect = NONE;
-       Colour effect_colour;
-       int spot_number = 1;
-       Time last_in;
-       Time last_out;
-       Time last_fade_up_time;
-       Time last_fade_down_time;
-
-       xmlpp::Element* font_element = 0;
-       xmlpp::Element* subtitle_element = 0;
-
-       for (list<SubtitleString>::iterator i = sorted.begin(); i != sorted.end(); ++i) {
-
-               /* We will start a new <Font>...</Font> whenever some font property changes.
-                  I suppose we should really make an optimal hierarchy of <Font> tags, but
-                  that seems hard.
-               */
-
-               bool const font_changed =
-                       font          != i->font()          ||
-                       italic        != i->italic()        ||
-                       colour        != i->colour()        ||
-                       size          != i->size()          ||
-                       fabs (aspect_adjust - i->aspect_adjust()) > ASPECT_ADJUST_EPSILON ||
-                       effect        != i->effect()        ||
-                       effect_colour != i->effect_colour();
-
-               if (font_changed) {
-                       font = i->font ();
-                       italic = i->italic ();
-                       colour = i->colour ();
-                       size = i->size ();
-                       aspect_adjust = i->aspect_adjust ();
-                       effect = i->effect ();
-                       effect_colour = i->effect_colour ();
-               }
-
-               if (!font_element || font_changed) {
-                       font_element = root->add_child ("Font");
-                       if (font) {
-                               font_element->set_attribute ("Id", font.get ());
-                       }
-                       font_element->set_attribute ("Italic", italic ? "yes" : "no");
-                       font_element->set_attribute ("Color", colour.to_argb_string());
-                       font_element->set_attribute ("Size", raw_convert<string> (size));
-                       if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) {
-                               font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust));
-                       }
-                       font_element->set_attribute ("Effect", effect_to_string (effect));
-                       font_element->set_attribute ("EffectColor", effect_colour.to_argb_string());
-                       font_element->set_attribute ("Script", "normal");
-                       font_element->set_attribute ("Underlined", "no");
-                       font_element->set_attribute ("Weight", "normal");
-               }
-
-               if (!subtitle_element || font_changed ||
-                   (last_in != i->in() ||
-                    last_out != i->out() ||
-                    last_fade_up_time != i->fade_up_time() ||
-                    last_fade_down_time != i->fade_down_time()
-                           )) {
-
-                       subtitle_element = font_element->add_child ("Subtitle");
-                       subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++));
-                       subtitle_element->set_attribute ("TimeIn", i->in().as_string());
-                       subtitle_element->set_attribute ("TimeOut", i->out().as_string());
-                       subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i->fade_up_time().as_editable_units(250)));
-                       subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i->fade_down_time().as_editable_units(250)));
-
-                       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");
-               if (i->h_align() != HALIGN_CENTER) {
-                       text->set_attribute ("HAlign", halign_to_string (i->h_align ()));
-               }
-               if (i->h_position() > ALIGN_EPSILON) {
-                       text->set_attribute ("HPosition", raw_convert<string> (i->h_position() * 100, 6));
-               }
-               text->set_attribute ("VAlign", valign_to_string (i->v_align()));                
-               text->set_attribute ("VPosition", raw_convert<string> (i->v_position() * 100, 6));
-               text->add_child_text (i->text());
+       for (auto i: _load_font_nodes) {
+               auto load_font = root->add_child("LoadFont");
+               load_font->set_attribute ("Id", i->id);
+               load_font->set_attribute ("URI", i->uri);
        }
 
-       return doc.write_to_string_formatted ("UTF-8");
+       subtitles_as_xml(root, 250, Standard::INTEROP, optimisation);
+
+       return format_xml(doc, {});
 }
 
+
 void
-InteropSubtitleAsset::add_font (string id, string uri)
+InteropSubtitleAsset::add_font (string load_id, dcp::ArrayData data)
 {
-       _load_font_nodes.push_back (shared_ptr<InteropLoadFontNode> (new InteropLoadFontNode (id, uri)));
+       _fonts.push_back (Font(load_id, make_uuid(), data));
+       auto const uri = String::compose("font_%1.ttf", _load_font_nodes.size());
+       _load_font_nodes.push_back (make_shared<InteropLoadFontNode>(load_id, uri));
 }
 
+
 bool
-InteropSubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
+InteropSubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
 {
        if (!SubtitleAsset::equals (other_asset, options, note)) {
                return false;
        }
-       
-       shared_ptr<const InteropSubtitleAsset> other = dynamic_pointer_cast<const InteropSubtitleAsset> (other_asset);
+
+       auto other = dynamic_pointer_cast<const InteropSubtitleAsset> (other_asset);
        if (!other) {
                return false;
        }
 
-       list<shared_ptr<InteropLoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
-       list<shared_ptr<InteropLoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
+       if (!options.load_font_nodes_can_differ) {
+               auto i = _load_font_nodes.begin();
+               auto j = other->_load_font_nodes.begin();
 
-       while (i != _load_font_nodes.end ()) {
-               if (j == other->_load_font_nodes.end ()) {
-                       note (DCP_ERROR, "<LoadFont> nodes differ");
-                       return false;
-               }
+               while (i != _load_font_nodes.end ()) {
+                       if (j == other->_load_font_nodes.end ()) {
+                               note (NoteType::ERROR, "<LoadFont> nodes differ");
+                               return false;
+                       }
 
-               if (**i != **j) {
-                       note (DCP_ERROR, "<LoadFont> nodes differ");
-                       return false;
-               }
+                       if (**i != **j) {
+                               note (NoteType::ERROR, "<LoadFont> nodes differ");
+                               return false;
+                       }
 
-               ++i;
-               ++j;
+                       ++i;
+                       ++j;
+               }
        }
 
        if (_movie_title != other->_movie_title) {
-               note (DCP_ERROR, "Subtitle movie titles differ");
+               note (NoteType::ERROR, "Subtitle movie titles differ");
                return false;
        }
 
        return true;
 }
 
-list<shared_ptr<LoadFontNode> >
+
+vector<shared_ptr<LoadFontNode>>
 InteropSubtitleAsset::load_font_nodes () const
 {
-       list<shared_ptr<LoadFontNode> > lf;
+       vector<shared_ptr<LoadFontNode>> lf;
        copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
        return lf;
 }
+
+
+void
+InteropSubtitleAsset::write(boost::filesystem::path path, SubtitleOptimisation optimisation) const
+{
+       File file(path, "wb");
+       if (!file) {
+               throw FileError("Could not open file for writing", path, -1);
+       }
+
+       _raw_xml = xml_as_string(optimisation);
+       /* length() here gives bytes not characters */
+       file.write(_raw_xml->c_str(), 1, _raw_xml->length());
+
+       _file = path;
+
+       /* Image subtitles */
+       for (auto i: _subtitles) {
+               auto im = dynamic_pointer_cast<dcp::SubtitleImage> (i);
+               if (im) {
+                       im->write_png_file(path.parent_path() / String::compose("%1.png", im->id()));
+               }
+       }
+
+       /* Fonts */
+       for (auto i: _load_font_nodes) {
+               auto file = path.parent_path() / i->uri;
+               auto font_with_id = std::find_if(_fonts.begin(), _fonts.end(), [i](Font const& font) { return font.load_id == i->id; });
+               if (font_with_id != _fonts.end()) {
+                       font_with_id->data.write(file);
+                       font_with_id->file = file;
+               }
+       }
+}
+
+
+/** Look at a supplied list of assets and find the fonts.  Then match these
+ *  fonts up with anything requested by a <LoadFont> so that _fonts contains
+ *  a list of font ID, load ID and data.
+ */
+void
+InteropSubtitleAsset::resolve_fonts (vector<shared_ptr<Asset>> assets)
+{
+       for (auto asset: assets) {
+               auto font = dynamic_pointer_cast<FontAsset>(asset);
+               if (!font) {
+                       continue;
+               }
+
+               DCP_ASSERT(_file);
+
+               for (auto load_font_node: _load_font_nodes) {
+                       auto const path_in_load_font_node = _file->parent_path() / load_font_node->uri;
+                       if (font->file() && path_in_load_font_node == *font->file()) {
+                               auto existing = std::find_if(_fonts.begin(), _fonts.end(), [load_font_node](Font const& font) { return font.load_id == load_font_node->id; });
+                               if (existing != _fonts.end()) {
+                                       *existing = Font(load_font_node->id, asset->id(), font->file().get());
+                               } else {
+                                       _fonts.push_back(Font(load_font_node->id, asset->id(), font->file().get()));
+                               }
+                       }
+               }
+       }
+}
+
+
+vector<shared_ptr<Asset>>
+InteropSubtitleAsset::font_assets()
+{
+       vector<shared_ptr<Asset>> assets;
+       for (auto const& i: _fonts) {
+               DCP_ASSERT (i.file);
+               assets.push_back(make_shared<FontAsset>(i.uuid, i.file.get()));
+       }
+       return assets;
+}
+
+
+vector<shared_ptr<const Asset>>
+InteropSubtitleAsset::font_assets() const
+{
+       vector<shared_ptr<const Asset>> assets;
+       for (auto const& i: _fonts) {
+               DCP_ASSERT (i.file);
+               assets.push_back(make_shared<const FontAsset>(i.uuid, i.file.get()));
+       }
+       return assets;
+}
+
+
+void
+InteropSubtitleAsset::add_to_assetmap (AssetMap& asset_map, boost::filesystem::path root) const
+{
+       Asset::add_to_assetmap(asset_map, root);
+
+       for (auto i: _subtitles) {
+               auto im = dynamic_pointer_cast<dcp::SubtitleImage>(i);
+               if (im) {
+                       DCP_ASSERT(im->file());
+                       add_file_to_assetmap(asset_map, root, im->file().get(), im->id());
+               }
+       }
+}
+
+
+void
+InteropSubtitleAsset::add_to_pkl (shared_ptr<PKL> pkl, boost::filesystem::path root) const
+{
+       Asset::add_to_pkl (pkl, root);
+
+       for (auto i: _subtitles) {
+               auto im = dynamic_pointer_cast<dcp::SubtitleImage> (i);
+               if (im) {
+                       auto png_image = im->png_image ();
+                       pkl->add_asset(im->id(), optional<string>(), make_digest(png_image), png_image.size(), "image/png", root.filename().string());
+               }
+       }
+}
+
+
+void
+InteropSubtitleAsset::set_font_file (string load_id, boost::filesystem::path file)
+{
+       for (auto& i: _fonts) {
+               if (i.load_id == load_id) {
+                       i.file = file;
+               }
+       }
+
+       for (auto i: _load_font_nodes) {
+               if (i->id == load_id) {
+                       i->uri = file.filename().string();
+               }
+       }
+}
+
+
+vector<string>
+InteropSubtitleAsset::unresolved_fonts() const
+{
+       vector<string> unresolved;
+       for (auto load_font_node: _load_font_nodes) {
+               if (std::find_if(_fonts.begin(), _fonts.end(), [load_font_node](Font const& font) { return font.load_id == load_font_node->id; }) == _fonts.end()) {
+                       unresolved.push_back(load_font_node->id);
+               }
+       }
+       return unresolved;
+}
+