Read the bits we can from the MXF header even when it is encrypted.
[libdcp.git] / src / smpte_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/smpte_subtitle_asset.cc
36  *  @brief SMPTESubtitleAsset class
37  */
38
39
40 #include "compose.hpp"
41 #include "crypto_context.h"
42 #include "dcp_assert.h"
43 #include "exceptions.h"
44 #include "raw_convert.h"
45 #include "smpte_load_font_node.h"
46 #include "smpte_subtitle_asset.h"
47 #include "subtitle_image.h"
48 #include "util.h"
49 #include "warnings.h"
50 #include "xml.h"
51 LIBDCP_DISABLE_WARNINGS
52 #include <asdcp/AS_DCP.h>
53 #include <asdcp/KM_util.h>
54 #include <asdcp/KM_log.h>
55 #include <libxml++/libxml++.h>
56 LIBDCP_ENABLE_WARNINGS
57 #include <boost/algorithm/string.hpp>
58
59
60 using std::string;
61 using std::list;
62 using std::vector;
63 using std::map;
64 using std::shared_ptr;
65 using std::dynamic_pointer_cast;
66 using std::make_shared;
67 using boost::split;
68 using boost::is_any_of;
69 using boost::shared_array;
70 using boost::optional;
71 using boost::starts_with;
72 using namespace dcp;
73
74
75 static string const subtitle_smpte_ns = "http://www.smpte-ra.org/schemas/428-7/2010/DCST";
76
77
78 SMPTESubtitleAsset::SMPTESubtitleAsset ()
79         : MXF (Standard::SMPTE)
80         , _edit_rate (24, 1)
81         , _time_code_rate (24)
82         , _xml_id (make_uuid())
83 {
84
85 }
86
87
88 SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file)
89         : SubtitleAsset (file)
90 {
91         auto xml = make_shared<cxml::Document>("SubtitleReel");
92
93         auto reader = make_shared<ASDCP::TimedText::MXFReader>();
94         auto r = Kumu::RESULT_OK;
95         {
96                 ASDCPErrorSuspender sus;
97                 r = reader->OpenRead (_file->string().c_str ());
98         }
99         if (!ASDCP_FAILURE(r)) {
100                 /* MXF-wrapped */
101                 ASDCP::WriterInfo info;
102                 reader->FillWriterInfo (info);
103                 _id = read_writer_info (info);
104                 if (!_key_id) {
105                         /* Not encrypted; read it in now */
106                         reader->ReadTimedTextResource (_raw_xml);
107                         xml->read_string (_raw_xml);
108                         parse_xml (xml);
109                         read_mxf_descriptor (reader);
110                         read_mxf_resources (reader, make_shared<DecryptionContext>(optional<Key>(), Standard::SMPTE));
111                 } else {
112                         read_mxf_descriptor (reader);
113                 }
114         } else {
115                 /* Plain XML */
116                 try {
117                         _raw_xml = dcp::file_to_string (file);
118                         xml = make_shared<cxml::Document>("SubtitleReel");
119                         xml->read_file (file);
120                         parse_xml (xml);
121                 } catch (cxml::Error& e) {
122                         boost::throw_exception (
123                                 ReadError (
124                                         String::compose (
125                                                 "Failed to read subtitle file %1; MXF failed with %2, XML failed with %3",
126                                                 file, static_cast<int>(r), e.what()
127                                                 )
128                                         )
129                                 );
130                 }
131
132                 /* Try to read PNG files from the same folder that the XML is in; the wisdom of this is
133                    debatable, at best...
134                 */
135                 for (auto i: _subtitles) {
136                         auto im = dynamic_pointer_cast<SubtitleImage>(i);
137                         if (im && im->png_image().size() == 0) {
138                                 /* Even more dubious; allow <id>.png or urn:uuid:<id>.png */
139                                 auto p = file.parent_path() / String::compose("%1.png", im->id());
140                                 if (boost::filesystem::is_regular_file(p)) {
141                                         im->read_png_file (p);
142                                 } else if (starts_with (im->id(), "urn:uuid:")) {
143                                         p = file.parent_path() / String::compose("%1.png", remove_urn_uuid(im->id()));
144                                         if (boost::filesystem::is_regular_file(p)) {
145                                                 im->read_png_file (p);
146                                         }
147                                 }
148                         }
149                 }
150                 _standard = Standard::SMPTE;
151         }
152
153         /* Check that all required image data have been found */
154         for (auto i: _subtitles) {
155                 auto im = dynamic_pointer_cast<SubtitleImage>(i);
156                 if (im && im->png_image().size() == 0) {
157                         throw MissingSubtitleImageError (im->id());
158                 }
159         }
160 }
161
162
163 void
164 SMPTESubtitleAsset::parse_xml (shared_ptr<cxml::Document> xml)
165 {
166         _xml_id = remove_urn_uuid(xml->string_child("Id"));
167         _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont");
168
169         _content_title_text = xml->string_child ("ContentTitleText");
170         _annotation_text = xml->optional_string_child ("AnnotationText");
171         _issue_date = LocalTime (xml->string_child ("IssueDate"));
172         _reel_number = xml->optional_number_child<int> ("ReelNumber");
173         _language = xml->optional_string_child ("Language");
174
175         /* This is supposed to be two numbers, but a single number has been seen in the wild */
176         auto const er = xml->string_child ("EditRate");
177         vector<string> er_parts;
178         split (er_parts, er, is_any_of (" "));
179         if (er_parts.size() == 1) {
180                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1);
181         } else if (er_parts.size() == 2) {
182                 _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1]));
183         } else {
184                 throw XMLError ("malformed EditRate " + er);
185         }
186
187         _time_code_rate = xml->number_child<int> ("TimeCodeRate");
188         if (xml->optional_string_child ("StartTime")) {
189                 _start_time = Time (xml->string_child("StartTime"), _time_code_rate);
190         }
191
192         /* Now we need to drop down to xmlpp */
193
194         vector<ParseState> ps;
195         for (auto i: xml->node()->get_children()) {
196                 auto const e = dynamic_cast<xmlpp::Element const *>(i);
197                 if (e && e->get_name() == "SubtitleList") {
198                         parse_subtitles (e, ps, _time_code_rate, Standard::SMPTE);
199                 }
200         }
201
202         /* Guess intrinsic duration */
203         _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
204 }
205
206
207 void
208 SMPTESubtitleAsset::read_mxf_resources (shared_ptr<ASDCP::TimedText::MXFReader> reader, shared_ptr<DecryptionContext> dec)
209 {
210         ASDCP::TimedText::TimedTextDescriptor descriptor;
211         reader->FillTimedTextDescriptor (descriptor);
212
213         /* Load fonts and images */
214
215         for (
216                 auto i = descriptor.ResourceList.begin();
217                 i != descriptor.ResourceList.end();
218                 ++i) {
219
220                 ASDCP::TimedText::FrameBuffer buffer;
221                 buffer.Capacity (10 * 1024 * 1024);
222                 reader->ReadAncillaryResource (i->ResourceID, buffer, dec->context(), dec->hmac());
223
224                 char id[64];
225                 Kumu::bin2UUIDhex (i->ResourceID, ASDCP::UUIDlen, id, sizeof(id));
226
227                 shared_array<uint8_t> data (new uint8_t[buffer.Size()]);
228                 memcpy (data.get(), buffer.RoData(), buffer.Size());
229
230                 switch (i->Type) {
231                 case ASDCP::TimedText::MT_OPENTYPE:
232                 {
233                         auto j = _load_font_nodes.begin();
234                         while (j != _load_font_nodes.end() && (*j)->urn != id) {
235                                 ++j;
236                         }
237
238                         if (j != _load_font_nodes.end ()) {
239                                 _fonts.push_back (Font ((*j)->id, (*j)->urn, ArrayData (data, buffer.Size ())));
240                         }
241                         break;
242                 }
243                 case ASDCP::TimedText::MT_PNG:
244                 {
245                         auto j = _subtitles.begin();
246                         while (j != _subtitles.end() && ((!dynamic_pointer_cast<SubtitleImage>(*j)) || dynamic_pointer_cast<SubtitleImage>(*j)->id() != id)) {
247                                 ++j;
248                         }
249
250                         if (j != _subtitles.end()) {
251                                 dynamic_pointer_cast<SubtitleImage>(*j)->set_png_image (ArrayData(data, buffer.Size()));
252                         }
253                         break;
254                 }
255                 default:
256                         break;
257                 }
258         }
259 }
260
261
262 void
263 SMPTESubtitleAsset::read_mxf_descriptor (shared_ptr<ASDCP::TimedText::MXFReader> reader)
264 {
265         ASDCP::TimedText::TimedTextDescriptor descriptor;
266         reader->FillTimedTextDescriptor (descriptor);
267
268         _intrinsic_duration = descriptor.ContainerDuration;
269         /* The thing which is called AssetID in the descriptor is also known as the
270          * ResourceID of the MXF.  We store that, at present just for verification
271          * purposes.
272          */
273         char id[64];
274         Kumu::bin2UUIDhex (descriptor.AssetID, ASDCP::UUIDlen, id, sizeof(id));
275         _resource_id = id;
276 }
277
278
279 void
280 SMPTESubtitleAsset::set_key (Key key)
281 {
282         /* See if we already have a key; if we do, and we have a file, we'll already
283            have read that file.
284         */
285         auto const had_key = static_cast<bool>(_key);
286
287         MXF::set_key (key);
288
289         if (!_key_id || !_file || had_key) {
290                 /* Either we don't have any data to read, it wasn't
291                    encrypted, or we've already read it, so we don't
292                    need to do anything else.
293                 */
294                 return;
295         }
296
297         /* Our data was encrypted; now we can decrypt it */
298
299         auto reader = make_shared<ASDCP::TimedText::MXFReader>();
300         auto r = reader->OpenRead (_file->string().c_str ());
301         if (ASDCP_FAILURE (r)) {
302                 boost::throw_exception (
303                         ReadError (
304                                 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
305                                 )
306                         );
307         }
308
309         auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
310         reader->ReadTimedTextResource (_raw_xml, dec->context(), dec->hmac());
311         auto xml = make_shared<cxml::Document>("SubtitleReel");
312         xml->read_string (_raw_xml);
313         parse_xml (xml);
314         read_mxf_resources (reader, dec);
315 }
316
317
318 vector<shared_ptr<LoadFontNode>>
319 SMPTESubtitleAsset::load_font_nodes () const
320 {
321         vector<shared_ptr<LoadFontNode>> lf;
322         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
323         return lf;
324 }
325
326
327 bool
328 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
329 {
330         ASDCP::TimedText::MXFReader reader;
331         Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
332         auto r = reader.OpenRead (file.string().c_str ());
333         Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
334         return !ASDCP_FAILURE (r);
335 }
336
337
338 string
339 SMPTESubtitleAsset::xml_as_string () const
340 {
341         xmlpp::Document doc;
342         auto root = doc.create_root_node ("dcst:SubtitleReel");
343         root->set_namespace_declaration (subtitle_smpte_ns, "dcst");
344         root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs");
345
346         root->add_child("Id", "dcst")->add_child_text ("urn:uuid:" + _xml_id);
347         root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text);
348         if (_annotation_text) {
349                 root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ());
350         }
351         root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true));
352         if (_reel_number) {
353                 root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ()));
354         }
355         if (_language) {
356                 root->add_child("Language", "dcst")->add_child_text (_language.get ());
357         }
358         root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ());
359         root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate));
360         if (_start_time) {
361                 root->add_child("StartTime", "dcst")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
362         }
363
364         for (auto i: _load_font_nodes) {
365                 auto load_font = root->add_child("LoadFont", "dcst");
366                 load_font->add_child_text ("urn:uuid:" + i->urn);
367                 load_font->set_attribute ("ID", i->id);
368         }
369
370         subtitles_as_xml (root->add_child("SubtitleList", "dcst"), _time_code_rate, Standard::SMPTE);
371
372         return doc.write_to_string ("UTF-8");
373 }
374
375
376 void
377 SMPTESubtitleAsset::write (boost::filesystem::path p) const
378 {
379         EncryptionContext enc (key(), Standard::SMPTE);
380
381         ASDCP::WriterInfo writer_info;
382         fill_writer_info (&writer_info, _id);
383
384         ASDCP::TimedText::TimedTextDescriptor descriptor;
385         descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
386         descriptor.EncodingName = "UTF-8";
387
388         /* Font references */
389
390         for (auto i: _load_font_nodes) {
391                 auto j = _fonts.begin();
392                 while (j != _fonts.end() && j->load_id != i->id) {
393                         ++j;
394                 }
395                 if (j != _fonts.end ()) {
396                         ASDCP::TimedText::TimedTextResourceDescriptor res;
397                         unsigned int c;
398                         Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
399                         DCP_ASSERT (c == Kumu::UUID_Length);
400                         res.Type = ASDCP::TimedText::MT_OPENTYPE;
401                         descriptor.ResourceList.push_back (res);
402                 }
403         }
404
405         /* Image subtitle references */
406
407         for (auto i: _subtitles) {
408                 auto si = dynamic_pointer_cast<SubtitleImage>(i);
409                 if (si) {
410                         ASDCP::TimedText::TimedTextResourceDescriptor res;
411                         unsigned int c;
412                         Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
413                         DCP_ASSERT (c == Kumu::UUID_Length);
414                         res.Type = ASDCP::TimedText::MT_PNG;
415                         descriptor.ResourceList.push_back (res);
416                 }
417         }
418
419         descriptor.NamespaceName = subtitle_smpte_ns;
420         unsigned int c;
421         Kumu::hex2bin (_xml_id.c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
422         DCP_ASSERT (c == Kumu::UUID_Length);
423         descriptor.ContainerDuration = _intrinsic_duration;
424
425         ASDCP::TimedText::MXFWriter writer;
426         /* This header size is a guess.  Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
427            The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
428         */
429         ASDCP::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
430         if (ASDCP_FAILURE (r)) {
431                 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
432         }
433
434         r = writer.WriteTimedTextResource (xml_as_string (), enc.context(), enc.hmac());
435         if (ASDCP_FAILURE (r)) {
436                 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
437         }
438
439         /* Font payload */
440
441         for (auto i: _load_font_nodes) {
442                 auto j = _fonts.begin();
443                 while (j != _fonts.end() && j->load_id != i->id) {
444                         ++j;
445                 }
446                 if (j != _fonts.end ()) {
447                         ASDCP::TimedText::FrameBuffer buffer;
448                         ArrayData data_copy(j->data);
449                         buffer.SetData (data_copy.data(), data_copy.size());
450                         buffer.Size (j->data.size());
451                         r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
452                         if (ASDCP_FAILURE(r)) {
453                                 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
454                         }
455                 }
456         }
457
458         /* Image subtitle payload */
459
460         for (auto i: _subtitles) {
461                 auto si = dynamic_pointer_cast<SubtitleImage>(i);
462                 if (si) {
463                         ASDCP::TimedText::FrameBuffer buffer;
464                         buffer.SetData (si->png_image().data(), si->png_image().size());
465                         buffer.Size (si->png_image().size());
466                         r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
467                         if (ASDCP_FAILURE(r)) {
468                                 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
469                         }
470                 }
471         }
472
473         writer.Finalize ();
474
475         _file = p;
476 }
477
478 bool
479 SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
480 {
481         if (!SubtitleAsset::equals (other_asset, options, note)) {
482                 return false;
483         }
484
485         auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
486         if (!other) {
487                 note (NoteType::ERROR, "Subtitles are in different standards");
488                 return false;
489         }
490
491         auto i = _load_font_nodes.begin();
492         auto j = other->_load_font_nodes.begin();
493
494         while (i != _load_font_nodes.end ()) {
495                 if (j == other->_load_font_nodes.end ()) {
496                         note (NoteType::ERROR, "<LoadFont> nodes differ");
497                         return false;
498                 }
499
500                 if ((*i)->id != (*j)->id) {
501                         note (NoteType::ERROR, "<LoadFont> nodes differ");
502                         return false;
503                 }
504
505                 ++i;
506                 ++j;
507         }
508
509         if (_content_title_text != other->_content_title_text) {
510                 note (NoteType::ERROR, "Subtitle content title texts differ");
511                 return false;
512         }
513
514         if (_language != other->_language) {
515                 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
516                 return false;
517         }
518
519         if (_annotation_text != other->_annotation_text) {
520                 note (NoteType::ERROR, "Subtitle annotation texts differ");
521                 return false;
522         }
523
524         if (_issue_date != other->_issue_date) {
525                 if (options.issue_dates_can_differ) {
526                         note (NoteType::NOTE, "Subtitle issue dates differ");
527                 } else {
528                         note (NoteType::ERROR, "Subtitle issue dates differ");
529                         return false;
530                 }
531         }
532
533         if (_reel_number != other->_reel_number) {
534                 note (NoteType::ERROR, "Subtitle reel numbers differ");
535                 return false;
536         }
537
538         if (_edit_rate != other->_edit_rate) {
539                 note (NoteType::ERROR, "Subtitle edit rates differ");
540                 return false;
541         }
542
543         if (_time_code_rate != other->_time_code_rate) {
544                 note (NoteType::ERROR, "Subtitle time code rates differ");
545                 return false;
546         }
547
548         if (_start_time != other->_start_time) {
549                 note (NoteType::ERROR, "Subtitle start times differ");
550                 return false;
551         }
552
553         return true;
554 }
555
556
557 void
558 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
559 {
560         string const uuid = make_uuid ();
561         _fonts.push_back (Font(load_id, uuid, data));
562         _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
563 }
564
565
566 void
567 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
568 {
569         SubtitleAsset::add (s);
570         _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
571 }