Missing initialisattion of DCPReadError::_message.
[libdcp.git] / src / smpte_subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2016 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 /** @file  src/smpte_subtitle_asset.cc
35  *  @brief SMPTESubtitleAsset class.
36  */
37
38 #include "smpte_subtitle_asset.h"
39 #include "smpte_load_font_node.h"
40 #include "exceptions.h"
41 #include "xml.h"
42 #include "raw_convert.h"
43 #include "dcp_assert.h"
44 #include "util.h"
45 #include "compose.hpp"
46 #include "encryption_context.h"
47 #include "decryption_context.h"
48 #include <asdcp/AS_DCP.h>
49 #include <asdcp/KM_util.h>
50 #include <libxml++/libxml++.h>
51 #include <boost/foreach.hpp>
52 #include <boost/algorithm/string.hpp>
53
54 using std::string;
55 using std::list;
56 using std::vector;
57 using std::map;
58 using boost::shared_ptr;
59 using boost::split;
60 using boost::is_any_of;
61 using boost::shared_array;
62 using boost::dynamic_pointer_cast;
63 using namespace dcp;
64
65 SMPTESubtitleAsset::SMPTESubtitleAsset ()
66         : _intrinsic_duration (0)
67         , _edit_rate (24, 1)
68         , _time_code_rate (24)
69         , _xml_id (make_uuid ())
70 {
71
72 }
73
74 /** Construct a SMPTESubtitleAsset by reading an MXF or XML file.
75  *  @param file Filename.
76  */
77 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
78         : SubtitleAsset (file)
79 {
80         shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
81
82         shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
83         Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
84         if (!ASDCP_FAILURE (r)) {
85                 /* MXF-wrapped */
86                 ASDCP::WriterInfo info;
87                 reader->FillWriterInfo (info);
88                 _id = read_writer_info (info);
89                 if (!_key_id) {
90                         /* Not encrypted; read it in now */
91                         string s;
92                         reader->ReadTimedTextResource (s);
93                         xml->read_string (s);
94                         parse_xml (xml);
95                         read_mxf_descriptor (reader, shared_ptr<DecryptionContext> (new DecryptionContext ()));
96                 }
97         } else {
98                 /* Plain XML */
99                 try {
100                         xml.reset (new cxml::Document ("SubtitleReel"));
101                         xml->read_file (file);
102                         parse_xml (xml);
103                         _id = _xml_id = remove_urn_uuid (xml->string_child ("Id"));
104                 } catch (cxml::Error& e) {
105                         boost::throw_exception (
106                                 DCPReadError (
107                                         String::compose (
108                                                 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
109                                                 file, static_cast<int> (r), e.what ()
110                                                 )
111                                         )
112                                 );
113                 }
114         }
115 }
116
117 void
118 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
119 {
120         _xml_id = remove_urn_uuid(xml->string_child("Id"));
121         _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
122
123         _content_title_text = xml->string_child ("ContentTitleText");
124         _annotation_text = xml->optional_string_child ("AnnotationText");
125         _issue_date = LocalTime (xml->string_child ("IssueDate"));
126         _reel_number = xml->optional_number_child<int> ("ReelNumber");
127         _language = xml->optional_string_child ("Language");
128
129         /* This is supposed to be two numbers, but a single number has been seen in the wild */
130         string const er = xml->string_child ("EditRate");
131         vector<string> er_parts;
132         split (er_parts, er, is_any_of (" "));
133         if (er_parts.size() == 1) {
134                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
135         } else if (er_parts.size() == 2) {
136                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
137         } else {
138                 throw XMLError ("malformed EditRate " + er);
139         }
140
141         _time_code_rate = xml->number_child<int> ("TimeCodeRate");
142         if (xml->optional_string_child ("StartTime")) {
143                 _start_time = Time (xml->string_child ("StartTime"), _time_code_rate);
144         }
145
146         /* Now we need to drop down to xmlpp */
147
148         list<ParseState> ps;
149         xmlpp::Node::NodeList c = xml->node()->get_children ();
150         for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
151                 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
152                 if (e && e->get_name() == "SubtitleList") {
153                         parse_subtitles (e, ps, _time_code_rate, SMPTE);
154                 }
155         }
156
157         /* Guess intrinsic duration */
158         _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
159 }
160
161 void
162 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
163 {
164         ASDCP::TimedText::TimedTextDescriptor descriptor;
165         reader->FillTimedTextDescriptor (descriptor);
166
167         /* Load fonts */
168
169         for (
170                 ASDCP::TimedText::ResourceList_t::const_iterator i = descriptor.ResourceList.begin();
171                 i != descriptor.ResourceList.end();
172                 ++i) {
173
174                 if (i->Type == ASDCP::TimedText::MT_OPENTYPE) {
175                         ASDCP::TimedText::FrameBuffer buffer;
176                         buffer.Capacity (10 * 1024 * 1024);
177                         reader->ReadAncillaryResource (i->ResourceID, buffer, dec->decryption());
178
179                         char id[64];
180                         Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof (id));
181
182                         shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
183                         memcpy (data.get(), buffer.RoData(), buffer.Size());
184
185                         list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = _load_font_nodes.begin ();
186                         while (j != _load_font_nodes.end() && (*j)->urn != id) {
187                                 ++j;
188                         }
189
190                         if (j != _load_font_nodes.end ()) {
191                                 _fonts.push_back (Font ((*j)->id, (*j)->urn, Data (data, buffer.Size ())));
192                         }
193                 }
194         }
195
196         /* Get intrinsic duration */
197         _intrinsic_duration = descriptor.ContainerDuration;
198 }
199
200 void
201 SMPTESubtitleAsset::set_key (Key key)
202 {
203         /* See if we already have a key; if we do, and we have a file, we'll already
204            have read that file.
205         */
206         bool const had_key = static_cast<bool> (_key);
207
208         MXF::set_key (key);
209
210         if (!_key_id || !_file || had_key) {
211                 /* Either we don't have any data to read, it wasn't
212                    encrypted, or we've already read it, so we don't
213                    need to do anything else.
214                 */
215                 return;
216         }
217
218         /* Our data was encrypted; now we can decrypt it */
219
220         shared_ptr<ASDCP::TimedText::MXFReader> reader (new ASDCP::TimedText::MXFReader ());
221         Kumu::Result_t r = reader->OpenRead (_file->string().c_str ());
222         if (ASDCP_FAILURE (r)) {
223                 boost::throw_exception (
224                         DCPReadError (
225                                 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
226                                 )
227                         );
228         }
229
230         string s;
231         shared_ptr<DecryptionContext> dec (new DecryptionContext (key));
232         reader->ReadTimedTextResource (s, dec->decryption());
233         shared_ptr<cxml::Document> xml (new cxml::Document ("SubtitleReel"));
234         xml->read_string (s);
235         parse_xml (xml);
236         read_mxf_descriptor (reader, dec);
237 }
238
239 list<shared_ptr<LoadFontNode> >
240 SMPTESubtitleAsset::load_font_nodes () const
241 {
242         list<shared_ptr<LoadFontNode> > lf;
243         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf));
244         return lf;
245 }
246
247 bool
248 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
249 {
250         ASDCP::TimedText::MXFReader reader;
251         Kumu::Result_t r = reader.OpenRead (file.string().c_str ());
252         return !ASDCP_FAILURE (r);
253 }
254
255 string
256 SMPTESubtitleAsset::xml_as_string () const
257 {
258         xmlpp::Document doc;
259         xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel");
260         root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst");
261         root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
262
263         root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
264         root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
265         if (_annotation_text) {
266                 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
267         }
268         root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
269         if (_reel_number) {
270                 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
271         }
272         if (_language) {
273                 root->add_child("Language", "dcst")->add_child_text (_language.get ());
274         }
275         root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
276         root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
277         if (_start_time) {
278                 root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string (SMPTE));
279         }
280
281         BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) {
282                 xmlpp::Element* load_font = root->add_child("LoadFont", "dcst");
283                 load_font->add_child_text ("urn:uuid:" + i->urn);
284                 load_font->set_attribute ("ID", i->id);
285         }
286
287         subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, SMPTE);
288
289         return doc.write_to_string ("UTF-8");
290 }
291
292 /** Write this content to a MXF file */
293 void
294 SMPTESubtitleAsset::write (boost::filesystem::path p) const
295 {
296         EncryptionContext enc (key (), SMPTE);
297
298         ASDCP::WriterInfo writer_info;
299         fill_writer_info (&writer_info, _id, SMPTE);
300
301         ASDCP::TimedText::TimedTextDescriptor descriptor;
302         descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
303         descriptor.EncodingName = "UTF-8";
304
305         BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
306                 list<Font>::const_iterator j = _fonts.begin ();
307                 while (j != _fonts.end() && j->load_id != i->id) {
308                         ++j;
309                 }
310                 if (j != _fonts.end ()) {
311                         ASDCP::TimedText::TimedTextResourceDescriptor res;
312                         unsigned int c;
313                         Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
314                         DCP_ASSERT (c == Kumu::UUID_Length);
315                         res.Type = ASDCP::TimedText::MT_OPENTYPE;
316                         descriptor.ResourceList.push_back (res);
317                 }
318         }
319
320         descriptor.NamespaceName = "dcst";
321         memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen);
322         descriptor.ContainerDuration = _intrinsic_duration;
323
324         ASDCP::TimedText::MXFWriter writer;
325         ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor);
326         if (ASDCP_FAILURE (r)) {
327                 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
328         }
329
330         /* XXX: no encryption */
331         r = writer.WriteTimedTextResource (xml_as_string (), enc.encryption(), enc.hmac());
332         if (ASDCP_FAILURE (r)) {
333                 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
334         }
335
336         BOOST_FOREACH (shared_ptr<dcp::SMPTELoadFontNode> i, _load_font_nodes) {
337                 list<Font>::const_iterator j = _fonts.begin ();
338                 while (j != _fonts.end() && j->load_id != i->id) {
339                         ++j;
340                 }
341                 if (j != _fonts.end ()) {
342                         ASDCP::TimedText::FrameBuffer buffer;
343                         buffer.SetData (j->data.data().get(), j->data.size());
344                         buffer.Size (j->data.size());
345                         r = writer.WriteAncillaryResource (buffer, enc.encryption(), enc.hmac());
346                         if (ASDCP_FAILURE (r)) {
347                                 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
348                         }
349                 }
350         }
351
352         writer.Finalize ();
353
354         _file = p;
355 }
356
357 bool
358 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
359 {
360         if (!SubtitleAsset::equals (other_asset, options, note)) {
361                 return false;
362         }
363
364         shared_ptr<const SMPTESubtitleAsset> other = dynamic_pointer_cast<const SMPTESubtitleAsset> (other_asset);
365         if (!other) {
366                 note (DCP_ERROR, "Subtitles are in different standards");
367                 return false;
368         }
369
370         list<shared_ptr<SMPTELoadFontNode> >::const_iterator i = _load_font_nodes.begin ();
371         list<shared_ptr<SMPTELoadFontNode> >::const_iterator j = other->_load_font_nodes.begin ();
372
373         while (i != _load_font_nodes.end ()) {
374                 if (j == other->_load_font_nodes.end ()) {
375                         note (DCP_ERROR, "<LoadFont> nodes differ");
376                         return false;
377                 }
378
379                 if ((*i)->id != (*j)->id) {
380                         note (DCP_ERROR, "<LoadFont> nodes differ");
381                         return false;
382                 }
383
384                 ++i;
385                 ++j;
386         }
387
388         if (_content_title_text != other->_content_title_text) {
389                 note (DCP_ERROR, "Subtitle content title texts differ");
390                 return false;
391         }
392
393         if (_language != other->_language) {
394                 note (DCP_ERROR, "Subtitle languages differ");
395                 return false;
396         }
397
398         if (_annotation_text != other->_annotation_text) {
399                 note (DCP_ERROR, "Subtitle annotation texts differ");
400                 return false;
401         }
402
403         if (_issue_date != other->_issue_date) {
404                 if (options.issue_dates_can_differ) {
405                         note (DCP_NOTE, "Subtitle issue dates differ");
406                 } else {
407                         note (DCP_ERROR, "Subtitle issue dates differ");
408                         return false;
409                 }
410         }
411
412         if (_reel_number != other->_reel_number) {
413                 note (DCP_ERROR, "Subtitle reel numbers differ");
414                 return false;
415         }
416
417         if (_edit_rate != other->_edit_rate) {
418                 note (DCP_ERROR, "Subtitle edit rates differ");
419                 return false;
420         }
421
422         if (_time_code_rate != other->_time_code_rate) {
423                 note (DCP_ERROR, "Subtitle time code rates differ");
424                 return false;
425         }
426
427         if (_start_time != other->_start_time) {
428                 note (DCP_ERROR, "Subtitle start times differ");
429                 return false;
430         }
431
432         return true;
433 }
434
435 void
436 SMPTESubtitleAsset::add_font (string load_id, boost::filesystem::path file)
437 {
438         string const uuid = make_uuid ();
439         _fonts.push_back (Font (load_id, uuid, file));
440         _load_font_nodes.push_back (shared_ptr<SMPTELoadFontNode> (new SMPTELoadFontNode (load_id, uuid)));
441 }
442
443 void
444 SMPTESubtitleAsset::add (dcp::SubtitleString s)
445 {
446         SubtitleAsset::add (s);
447         _intrinsic_duration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator);
448 }