Cleanup: move EqualityOptions into its own file.
[libdcp.git] / src / interop_subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34
35 /** @file  src/interop_subtitle_asset.cc
36  *  @brief InteropSubtitleAsset class
37  */
38
39
40 #include "compose.hpp"
41 #include "dcp_assert.h"
42 #include "equality_options.h"
43 #include "font_asset.h"
44 #include "file.h"
45 #include "interop_load_font_node.h"
46 #include "interop_subtitle_asset.h"
47 #include "raw_convert.h"
48 #include "subtitle_asset_internal.h"
49 #include "subtitle_image.h"
50 #include "util.h"
51 #include "warnings.h"
52 #include "xml.h"
53 LIBDCP_DISABLE_WARNINGS
54 #include <libxml++/libxml++.h>
55 LIBDCP_ENABLE_WARNINGS
56 #include <boost/weak_ptr.hpp>
57 #include <cmath>
58 #include <cstdio>
59
60
61 using std::cerr;
62 using std::cout;
63 using std::dynamic_pointer_cast;
64 using std::make_shared;
65 using std::shared_ptr;
66 using std::string;
67 using std::vector;
68 using boost::optional;
69 using namespace dcp;
70
71
72 InteropSubtitleAsset::InteropSubtitleAsset (boost::filesystem::path file)
73         : SubtitleAsset (file)
74 {
75         _raw_xml = dcp::file_to_string (file);
76
77         auto xml = make_shared<cxml::Document>("DCSubtitle");
78         xml->read_file (file);
79         _id = xml->string_child ("SubtitleID");
80         _reel_number = xml->string_child ("ReelNumber");
81         _language = xml->string_child ("Language");
82         _movie_title = xml->string_child ("MovieTitle");
83         _load_font_nodes = type_children<InteropLoadFontNode> (xml, "LoadFont");
84
85         /* Now we need to drop down to xmlpp */
86
87         vector<ParseState> ps;
88         for (auto i: xml->node()->get_children()) {
89                 auto e = dynamic_cast<xmlpp::Element const *>(i);
90                 if (e && (e->get_name() == "Font" || e->get_name() == "Subtitle")) {
91                         parse_subtitles (e, ps, optional<int>(), Standard::INTEROP);
92                 }
93         }
94
95         for (auto i: _subtitles) {
96                 auto si = dynamic_pointer_cast<SubtitleImage>(i);
97                 if (si) {
98                         si->read_png_file (file.parent_path() / String::compose("%1.png", si->id()));
99                 }
100         }
101 }
102
103
104 InteropSubtitleAsset::InteropSubtitleAsset ()
105 {
106
107 }
108
109
110 string
111 InteropSubtitleAsset::xml_as_string () const
112 {
113         xmlpp::Document doc;
114         auto root = doc.create_root_node ("DCSubtitle");
115         root->set_attribute ("Version", "1.0");
116
117         root->add_child("SubtitleID")->add_child_text (_id);
118         root->add_child("MovieTitle")->add_child_text (_movie_title);
119         root->add_child("ReelNumber")->add_child_text (raw_convert<string> (_reel_number));
120         root->add_child("Language")->add_child_text (_language);
121
122         for (auto i: _load_font_nodes) {
123                 auto load_font = root->add_child("LoadFont");
124                 load_font->set_attribute ("Id", i->id);
125                 load_font->set_attribute ("URI", i->uri);
126         }
127
128         subtitles_as_xml (root, 250, Standard::INTEROP);
129
130         return format_xml(doc, {});
131 }
132
133
134 void
135 InteropSubtitleAsset::add_font (string load_id, dcp::ArrayData data)
136 {
137         _fonts.push_back (Font(load_id, make_uuid(), data));
138         auto const uri = String::compose("font_%1.ttf", _load_font_nodes.size());
139         _load_font_nodes.push_back (make_shared<InteropLoadFontNode>(load_id, uri));
140 }
141
142
143 bool
144 InteropSubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
145 {
146         if (!SubtitleAsset::equals (other_asset, options, note)) {
147                 return false;
148         }
149
150         auto other = dynamic_pointer_cast<const InteropSubtitleAsset> (other_asset);
151         if (!other) {
152                 return false;
153         }
154
155         if (!options.load_font_nodes_can_differ) {
156                 auto i = _load_font_nodes.begin();
157                 auto j = other->_load_font_nodes.begin();
158
159                 while (i != _load_font_nodes.end ()) {
160                         if (j == other->_load_font_nodes.end ()) {
161                                 note (NoteType::ERROR, "<LoadFont> nodes differ");
162                                 return false;
163                         }
164
165                         if (**i != **j) {
166                                 note (NoteType::ERROR, "<LoadFont> nodes differ");
167                                 return false;
168                         }
169
170                         ++i;
171                         ++j;
172                 }
173         }
174
175         if (_movie_title != other->_movie_title) {
176                 note (NoteType::ERROR, "Subtitle movie titles differ");
177                 return false;
178         }
179
180         return true;
181 }
182
183
184 vector<shared_ptr<LoadFontNode>>
185 InteropSubtitleAsset::load_font_nodes () const
186 {
187         vector<shared_ptr<LoadFontNode>> lf;
188         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
189         return lf;
190 }
191
192
193 void
194 InteropSubtitleAsset::write (boost::filesystem::path p) const
195 {
196         File f(p, "wb");
197         if (!f) {
198                 throw FileError ("Could not open file for writing", p, -1);
199         }
200
201         _raw_xml = xml_as_string ();
202         /* length() here gives bytes not characters */
203         f.write(_raw_xml->c_str(), 1, _raw_xml->length());
204
205         _file = p;
206
207         /* Image subtitles */
208         for (auto i: _subtitles) {
209                 auto im = dynamic_pointer_cast<dcp::SubtitleImage> (i);
210                 if (im) {
211                         im->write_png_file(p.parent_path() / String::compose("%1.png", im->id()));
212                 }
213         }
214
215         /* Fonts */
216         for (auto i: _load_font_nodes) {
217                 auto file = p.parent_path() / i->uri;
218                 auto font_with_id = std::find_if(_fonts.begin(), _fonts.end(), [i](Font const& font) { return font.load_id == i->id; });
219                 if (font_with_id != _fonts.end()) {
220                         font_with_id->data.write(file);
221                         font_with_id->file = file;
222                 }
223         }
224 }
225
226
227 /** Look at a supplied list of assets and find the fonts.  Then match these
228  *  fonts up with anything requested by a <LoadFont> so that _fonts contains
229  *  a list of font ID, load ID and data.
230  */
231 void
232 InteropSubtitleAsset::resolve_fonts (vector<shared_ptr<Asset>> assets)
233 {
234         for (auto asset: assets) {
235                 auto font = dynamic_pointer_cast<FontAsset>(asset);
236                 if (!font) {
237                         continue;
238                 }
239
240                 DCP_ASSERT(_file);
241
242                 for (auto load_font_node: _load_font_nodes) {
243                         auto const path_in_load_font_node = _file->parent_path() / load_font_node->uri;
244                         if (font->file() && path_in_load_font_node == *font->file()) {
245                                 auto existing = std::find_if(_fonts.begin(), _fonts.end(), [load_font_node](Font const& font) { return font.load_id == load_font_node->id; });
246                                 if (existing != _fonts.end()) {
247                                         *existing = Font(load_font_node->id, asset->id(), font->file().get());
248                                 } else {
249                                         _fonts.push_back(Font(load_font_node->id, asset->id(), font->file().get()));
250                                 }
251                         }
252                 }
253         }
254 }
255
256
257 vector<shared_ptr<Asset>>
258 InteropSubtitleAsset::font_assets()
259 {
260         vector<shared_ptr<Asset>> assets;
261         for (auto const& i: _fonts) {
262                 DCP_ASSERT (i.file);
263                 assets.push_back(make_shared<FontAsset>(i.uuid, i.file.get()));
264         }
265         return assets;
266 }
267
268
269 vector<shared_ptr<const Asset>>
270 InteropSubtitleAsset::font_assets() const
271 {
272         vector<shared_ptr<const Asset>> assets;
273         for (auto const& i: _fonts) {
274                 DCP_ASSERT (i.file);
275                 assets.push_back(make_shared<const FontAsset>(i.uuid, i.file.get()));
276         }
277         return assets;
278 }
279
280
281 void
282 InteropSubtitleAsset::add_to_assetmap (AssetMap& asset_map, boost::filesystem::path root) const
283 {
284         Asset::add_to_assetmap(asset_map, root);
285
286         for (auto i: _subtitles) {
287                 auto im = dynamic_pointer_cast<dcp::SubtitleImage>(i);
288                 if (im) {
289                         DCP_ASSERT(im->file());
290                         add_file_to_assetmap(asset_map, root, im->file().get(), im->id());
291                 }
292         }
293 }
294
295
296 void
297 InteropSubtitleAsset::add_to_pkl (shared_ptr<PKL> pkl, boost::filesystem::path root) const
298 {
299         Asset::add_to_pkl (pkl, root);
300
301         for (auto i: _subtitles) {
302                 auto im = dynamic_pointer_cast<dcp::SubtitleImage> (i);
303                 if (im) {
304                         auto png_image = im->png_image ();
305                         pkl->add_asset(im->id(), optional<string>(), make_digest(png_image), png_image.size(), "image/png", root.filename().string());
306                 }
307         }
308 }
309
310
311 void
312 InteropSubtitleAsset::set_font_file (string load_id, boost::filesystem::path file)
313 {
314         for (auto& i: _fonts) {
315                 if (i.load_id == load_id) {
316                         i.file = file;
317                 }
318         }
319
320         for (auto i: _load_font_nodes) {
321                 if (i->id == load_id) {
322                         i->uri = file.filename().string();
323                 }
324         }
325 }
326
327
328 vector<string>
329 InteropSubtitleAsset::unresolved_fonts() const
330 {
331         vector<string> unresolved;
332         for (auto load_font_node: _load_font_nodes) {
333                 if (std::find_if(_fonts.begin(), _fonts.end(), [load_font_node](Font const& font) { return font.load_id == load_font_node->id; }) == _fonts.end()) {
334                         unresolved.push_back(load_font_node->id);
335                 }
336         }
337         return unresolved;
338 }
339