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