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