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