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