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