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