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