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