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