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