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