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