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