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