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