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