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