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