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