Cleanup: remove unnecessary initialization.
[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
310         MXF::set_key (key);
311
312         if (!_key_id || !_file || had_key) {
313                 /* Either we don't have any data to read, it wasn't
314                    encrypted, or we've already read it, so we don't
315                    need to do anything else.
316                 */
317                 return;
318         }
319
320         /* Our data was encrypted; now we can decrypt it */
321
322         auto reader = make_shared<ASDCP::TimedText::MXFReader>();
323         auto r = reader->OpenRead(dcp::filesystem::fix_long_path(*_file).string().c_str());
324         if (ASDCP_FAILURE (r)) {
325                 boost::throw_exception (
326                         ReadError (
327                                 String::compose ("Could not read encrypted subtitle MXF (%1)", static_cast<int> (r))
328                                 )
329                         );
330         }
331
332         auto dec = make_shared<DecryptionContext>(key, Standard::SMPTE);
333         string xml_string;
334         reader->ReadTimedTextResource (xml_string, dec->context(), dec->hmac());
335         _raw_xml = xml_string;
336         auto xml = make_shared<cxml::Document>("SubtitleReel");
337         xml->read_string (xml_string);
338         parse_xml (xml);
339         read_mxf_resources (reader, dec);
340 }
341
342
343 vector<shared_ptr<LoadFontNode>>
344 SMPTESubtitleAsset::load_font_nodes () const
345 {
346         vector<shared_ptr<LoadFontNode>> lf;
347         copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter(lf));
348         return lf;
349 }
350
351
352 bool
353 SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file)
354 {
355         ASDCP::TimedText::MXFReader reader;
356         Kumu::DefaultLogSink().UnsetFilterFlag(Kumu::LOG_ALLOW_ALL);
357         auto r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
358         Kumu::DefaultLogSink().SetFilterFlag(Kumu::LOG_ALLOW_ALL);
359         return !ASDCP_FAILURE (r);
360 }
361
362
363 string
364 SMPTESubtitleAsset::xml_as_string () const
365 {
366         xmlpp::Document doc;
367         auto root = doc.create_root_node ("SubtitleReel");
368
369         DCP_ASSERT (_xml_id);
370         root->add_child("Id")->add_child_text("urn:uuid:" + *_xml_id);
371         root->add_child("ContentTitleText")->add_child_text(_content_title_text);
372         if (_annotation_text) {
373                 root->add_child("AnnotationText")->add_child_text(_annotation_text.get());
374         }
375         root->add_child("IssueDate")->add_child_text(_issue_date.as_string(false, false));
376         if (_reel_number) {
377                 root->add_child("ReelNumber")->add_child_text(raw_convert<string>(_reel_number.get()));
378         }
379         if (_language) {
380                 root->add_child("Language")->add_child_text(_language.get());
381         }
382         root->add_child("EditRate")->add_child_text(_edit_rate.as_string());
383         root->add_child("TimeCodeRate")->add_child_text(raw_convert<string>(_time_code_rate));
384         if (_start_time) {
385                 root->add_child("StartTime")->add_child_text(_start_time.get().as_string(Standard::SMPTE));
386         }
387
388         for (auto i: _load_font_nodes) {
389                 auto load_font = root->add_child("LoadFont");
390                 load_font->add_child_text ("urn:uuid:" + i->urn);
391                 load_font->set_attribute ("ID", i->id);
392         }
393
394         subtitles_as_xml (root->add_child("SubtitleList"), _time_code_rate, Standard::SMPTE);
395
396         return format_xml(doc, std::make_pair(string{}, schema_namespace()));
397 }
398
399
400 void
401 SMPTESubtitleAsset::write (boost::filesystem::path p) const
402 {
403         EncryptionContext enc (key(), Standard::SMPTE);
404
405         ASDCP::WriterInfo writer_info;
406         fill_writer_info (&writer_info, _id);
407
408         ASDCP::TimedText::TimedTextDescriptor descriptor;
409         descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator);
410         descriptor.EncodingName = "UTF-8";
411
412         /* Font references */
413
414         for (auto i: _load_font_nodes) {
415                 auto j = _fonts.begin();
416                 while (j != _fonts.end() && j->load_id != i->id) {
417                         ++j;
418                 }
419                 if (j != _fonts.end ()) {
420                         ASDCP::TimedText::TimedTextResourceDescriptor res;
421                         unsigned int c;
422                         Kumu::hex2bin (i->urn.c_str(), res.ResourceID, Kumu::UUID_Length, &c);
423                         DCP_ASSERT (c == Kumu::UUID_Length);
424                         res.Type = ASDCP::TimedText::MT_OPENTYPE;
425                         descriptor.ResourceList.push_back (res);
426                 }
427         }
428
429         /* Image subtitle references */
430
431         for (auto i: _subtitles) {
432                 auto si = dynamic_pointer_cast<SubtitleImage>(i);
433                 if (si) {
434                         ASDCP::TimedText::TimedTextResourceDescriptor res;
435                         unsigned int c;
436                         Kumu::hex2bin (si->id().c_str(), res.ResourceID, Kumu::UUID_Length, &c);
437                         DCP_ASSERT (c == Kumu::UUID_Length);
438                         res.Type = ASDCP::TimedText::MT_PNG;
439                         descriptor.ResourceList.push_back (res);
440                 }
441         }
442
443         descriptor.NamespaceName = schema_namespace();
444         unsigned int c;
445         DCP_ASSERT (_xml_id);
446         Kumu::hex2bin (_xml_id->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
447         DCP_ASSERT (c == Kumu::UUID_Length);
448         descriptor.ContainerDuration = _intrinsic_duration;
449
450         ASDCP::TimedText::MXFWriter writer;
451         /* This header size is a guess.  Empirically it seems that each subtitle reference is 90 bytes, and we need some extra.
452            The default size is not enough for some feature-length PNG sub projects (see DCP-o-matic #1561).
453         */
454         ASDCP::Result_t r = writer.OpenWrite(dcp::filesystem::fix_long_path(p).string().c_str(), writer_info, descriptor, _subtitles.size() * 90 + 16384);
455         if (ASDCP_FAILURE (r)) {
456                 boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r));
457         }
458
459         _raw_xml = xml_as_string ();
460
461         r = writer.WriteTimedTextResource (*_raw_xml, enc.context(), enc.hmac());
462         if (ASDCP_FAILURE (r)) {
463                 boost::throw_exception (MXFFileError ("could not write XML to timed text resource", p.string(), r));
464         }
465
466         /* Font payload */
467
468         for (auto i: _load_font_nodes) {
469                 auto j = _fonts.begin();
470                 while (j != _fonts.end() && j->load_id != i->id) {
471                         ++j;
472                 }
473                 if (j != _fonts.end ()) {
474                         ASDCP::TimedText::FrameBuffer buffer;
475                         ArrayData data_copy(j->data);
476                         buffer.SetData (data_copy.data(), data_copy.size());
477                         buffer.Size (j->data.size());
478                         r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
479                         if (ASDCP_FAILURE(r)) {
480                                 boost::throw_exception (MXFFileError ("could not write font to timed text resource", p.string(), r));
481                         }
482                 }
483         }
484
485         /* Image subtitle payload */
486
487         for (auto i: _subtitles) {
488                 auto si = dynamic_pointer_cast<SubtitleImage>(i);
489                 if (si) {
490                         ASDCP::TimedText::FrameBuffer buffer;
491                         buffer.SetData (si->png_image().data(), si->png_image().size());
492                         buffer.Size (si->png_image().size());
493                         r = writer.WriteAncillaryResource (buffer, enc.context(), enc.hmac());
494                         if (ASDCP_FAILURE(r)) {
495                                 boost::throw_exception (MXFFileError ("could not write PNG data to timed text resource", p.string(), r));
496                         }
497                 }
498         }
499
500         writer.Finalize ();
501
502         _file = p;
503 }
504
505 bool
506 SMPTESubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
507 {
508         if (!SubtitleAsset::equals (other_asset, options, note)) {
509                 return false;
510         }
511
512         auto other = dynamic_pointer_cast<const SMPTESubtitleAsset>(other_asset);
513         if (!other) {
514                 note (NoteType::ERROR, "Subtitles are in different standards");
515                 return false;
516         }
517
518         auto i = _load_font_nodes.begin();
519         auto j = other->_load_font_nodes.begin();
520
521         while (i != _load_font_nodes.end ()) {
522                 if (j == other->_load_font_nodes.end ()) {
523                         note (NoteType::ERROR, "<LoadFont> nodes differ");
524                         return false;
525                 }
526
527                 if ((*i)->id != (*j)->id) {
528                         note (NoteType::ERROR, "<LoadFont> nodes differ");
529                         return false;
530                 }
531
532                 ++i;
533                 ++j;
534         }
535
536         if (_content_title_text != other->_content_title_text) {
537                 note (NoteType::ERROR, "Subtitle content title texts differ");
538                 return false;
539         }
540
541         if (_language != other->_language) {
542                 note (NoteType::ERROR, String::compose("Subtitle languages differ (`%1' vs `%2')", _language.get_value_or("[none]"), other->_language.get_value_or("[none]")));
543                 return false;
544         }
545
546         if (_annotation_text != other->_annotation_text) {
547                 note (NoteType::ERROR, "Subtitle annotation texts differ");
548                 return false;
549         }
550
551         if (_issue_date != other->_issue_date) {
552                 if (options.issue_dates_can_differ) {
553                         note (NoteType::NOTE, "Subtitle issue dates differ");
554                 } else {
555                         note (NoteType::ERROR, "Subtitle issue dates differ");
556                         return false;
557                 }
558         }
559
560         if (_reel_number != other->_reel_number) {
561                 note (NoteType::ERROR, "Subtitle reel numbers differ");
562                 return false;
563         }
564
565         if (_edit_rate != other->_edit_rate) {
566                 note (NoteType::ERROR, "Subtitle edit rates differ");
567                 return false;
568         }
569
570         if (_time_code_rate != other->_time_code_rate) {
571                 note (NoteType::ERROR, "Subtitle time code rates differ");
572                 return false;
573         }
574
575         if (_start_time != other->_start_time) {
576                 note (NoteType::ERROR, "Subtitle start times differ");
577                 return false;
578         }
579
580         return true;
581 }
582
583
584 void
585 SMPTESubtitleAsset::add_font (string load_id, dcp::ArrayData data)
586 {
587         string const uuid = make_uuid ();
588         _fonts.push_back (Font(load_id, uuid, data));
589         _load_font_nodes.push_back (make_shared<SMPTELoadFontNode>(load_id, uuid));
590 }
591
592
593 void
594 SMPTESubtitleAsset::add (shared_ptr<Subtitle> s)
595 {
596         SubtitleAsset::add (s);
597         _intrinsic_duration = latest_subtitle_out().as_editable_units_ceil(_edit_rate.numerator / _edit_rate.denominator);
598 }
599
600
601 string
602 SMPTESubtitleAsset::schema_namespace() const
603 {
604         switch (_subtitle_standard) {
605         case SubtitleStandard::SMPTE_2007:
606                 return subtitle_smpte_ns_2007;
607         case SubtitleStandard::SMPTE_2010:
608                 return subtitle_smpte_ns_2010;
609         case SubtitleStandard::SMPTE_2014:
610                 return subtitle_smpte_ns_2014;
611         default:
612                 DCP_ASSERT(false);
613         }
614
615         DCP_ASSERT(false);
616 }