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