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