Get assetmap and PKL AnnotationText from XMLMetadata.
[libdcp.git] / src / dcp.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/dcp.cc
35  *  @brief DCP class.
36  */
37
38 #include "raw_convert.h"
39 #include "dcp.h"
40 #include "sound_asset.h"
41 #include "picture_asset.h"
42 #include "interop_subtitle_asset.h"
43 #include "smpte_subtitle_asset.h"
44 #include "mono_picture_asset.h"
45 #include "stereo_picture_asset.h"
46 #include "reel_subtitle_asset.h"
47 #include "util.h"
48 #include "metadata.h"
49 #include "exceptions.h"
50 #include "cpl.h"
51 #include "certificate_chain.h"
52 #include "compose.hpp"
53 #include "decrypted_kdm.h"
54 #include "decrypted_kdm_key.h"
55 #include "dcp_assert.h"
56 #include "reel_asset.h"
57 #include "font_asset.h"
58 #include <asdcp/AS_DCP.h>
59 #include <xmlsec/xmldsig.h>
60 #include <xmlsec/app.h>
61 #include <libxml++/libxml++.h>
62 #include <boost/filesystem.hpp>
63 #include <boost/algorithm/string.hpp>
64 #include <boost/foreach.hpp>
65
66 using std::string;
67 using std::list;
68 using std::vector;
69 using std::cout;
70 using std::make_pair;
71 using std::map;
72 using std::cout;
73 using std::cerr;
74 using std::exception;
75 using boost::shared_ptr;
76 using boost::dynamic_pointer_cast;
77 using boost::algorithm::starts_with;
78 using namespace dcp;
79
80 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
81 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
82 static string const pkl_interop_ns      = "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#";
83 static string const pkl_smpte_ns        = "http://www.smpte-ra.org/schemas/429-8/2007/PKL";
84 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
85 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
86
87 DCP::DCP (boost::filesystem::path directory)
88         : _directory (directory)
89 {
90         if (!boost::filesystem::exists (directory)) {
91                 boost::filesystem::create_directories (directory);
92         }
93
94         _directory = boost::filesystem::canonical (_directory);
95 }
96
97 /** Call this instead of throwing an exception if the error can be tolerated */
98 template<class T> void
99 survivable_error (bool keep_going, dcp::DCP::ReadErrors* errors, T const & e)
100 {
101         if (keep_going) {
102                 if (errors) {
103                         errors->push_back (shared_ptr<T> (new T (e)));
104                 }
105         } else {
106                 throw e;
107         }
108 }
109
110 void
111 DCP::read (bool keep_going, ReadErrors* errors, bool ignore_incorrect_picture_mxf_type)
112 {
113         /* Read the ASSETMAP */
114
115         boost::filesystem::path asset_map_file;
116         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
117                 asset_map_file = _directory / "ASSETMAP";
118         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
119                 asset_map_file = _directory / "ASSETMAP.xml";
120         } else {
121                 boost::throw_exception (DCPReadError (String::compose ("could not find AssetMap file in `%1'", _directory.string())));
122         }
123
124         cxml::Document asset_map ("AssetMap");
125
126         asset_map.read_file (asset_map_file);
127         if (asset_map.namespace_uri() == assetmap_interop_ns) {
128                 _standard = INTEROP;
129         } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
130                 _standard = SMPTE;
131         } else {
132                 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
133         }
134
135         list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136         map<string, boost::filesystem::path> paths;
137         BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
138                 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
139                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
140                 }
141                 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
142                 if (starts_with (p, "file://")) {
143                         p = p.substr (7);
144                 }
145                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
146         }
147
148         /* Read all the assets from the asset map */
149         /* XXX: I think we should be looking at the PKL here to decide type, not
150            the extension of the file.
151         */
152
153         /* Make a list of non-CPL assets so that we can resolve the references
154            from the CPLs.
155         */
156         list<shared_ptr<Asset> > other_assets;
157
158         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
159                 boost::filesystem::path path = _directory / i->second;
160
161                 if (!boost::filesystem::exists (path)) {
162                         survivable_error (keep_going, errors, MissingAssetError (path));
163                         continue;
164                 }
165
166                 if (boost::filesystem::extension (path) == ".xml") {
167                         xmlpp::DomParser* p = new xmlpp::DomParser;
168                         try {
169                                 p->parse_file (path.string());
170                         } catch (std::exception& e) {
171                                 delete p;
172                                 continue;
173                         }
174
175                         string const root = p->get_document()->get_root_node()->get_name ();
176                         delete p;
177
178                         if (root == "CompositionPlaylist") {
179                                 shared_ptr<CPL> cpl (new CPL (path));
180                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
181                                         survivable_error (keep_going, errors, MismatchedStandardError ());
182                                 }
183                                 _cpls.push_back (cpl);
184                         } else if (root == "DCSubtitle") {
185                                 if (_standard && _standard.get() == SMPTE) {
186                                         survivable_error (keep_going, errors, MismatchedStandardError ());
187                                 }
188                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
189                         }
190                 } else if (boost::filesystem::extension (path) == ".mxf") {
191
192                         /* XXX: asdcplib does not appear to support discovery of read MXFs standard
193                            (Interop / SMPTE)
194                         */
195
196                         ASDCP::EssenceType_t type;
197                         if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
198                                 throw DCPReadError ("Could not find essence type");
199                         }
200                         switch (type) {
201                                 case ASDCP::ESS_UNKNOWN:
202                                 case ASDCP::ESS_MPEG2_VES:
203                                         throw DCPReadError ("MPEG2 video essences are not supported");
204                                 case ASDCP::ESS_JPEG_2000:
205                                         try {
206                                                 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
207                                         } catch (dcp::MXFFileError& e) {
208                                                 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
209                                                         /* Tried to load it as mono but the error says it's stereo; try that instead */
210                                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
211                                                 } else {
212                                                         throw;
213                                                 }
214                                         }
215                                         break;
216                                 case ASDCP::ESS_PCM_24b_48k:
217                                 case ASDCP::ESS_PCM_24b_96k:
218                                         other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
219                                         break;
220                                 case ASDCP::ESS_JPEG_2000_S:
221                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
222                                         break;
223                                 case ASDCP::ESS_TIMED_TEXT:
224                                         other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
225                                         break;
226                                 default:
227                                         throw DCPReadError ("Unknown MXF essence type");
228                         }
229                 } else if (boost::filesystem::extension (path) == ".ttf") {
230                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
231                 }
232         }
233
234         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
235                 i->resolve_refs (other_assets);
236         }
237 }
238
239 void
240 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
241 {
242         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
243                 i->resolve_refs (assets);
244         }
245 }
246
247 bool
248 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
249 {
250         list<shared_ptr<CPL> > a = cpls ();
251         list<shared_ptr<CPL> > b = other.cpls ();
252
253         if (a.size() != b.size()) {
254                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
255                 return false;
256         }
257
258         bool r = true;
259
260         BOOST_FOREACH (shared_ptr<CPL> i, a) {
261                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
262                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
263                         ++j;
264                 }
265
266                 if (j == b.end ()) {
267                         r = false;
268                 }
269         }
270
271         return r;
272 }
273
274 void
275 DCP::add (boost::shared_ptr<CPL> cpl)
276 {
277         _cpls.push_back (cpl);
278 }
279
280 bool
281 DCP::encrypted () const
282 {
283         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
284                 if (i->encrypted ()) {
285                         return true;
286                 }
287         }
288
289         return false;
290 }
291
292 void
293 DCP::add (DecryptedKDM const & kdm)
294 {
295         list<DecryptedKDMKey> keys = kdm.keys ();
296
297         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
298                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
299                         if (j.cpl_id() == i->id()) {
300                                 i->add (kdm);
301                         }
302                 }
303         }
304 }
305
306 /** @return full pathname of PKL file that was written */
307 boost::filesystem::path
308 DCP::write_pkl (string file, Standard standard, string pkl_uuid, XMLMetadata metadata, shared_ptr<const CertificateChain> signer) const
309 {
310         boost::filesystem::path p = _directory;
311         p /= file;
312
313         xmlpp::Document doc;
314         xmlpp::Element* pkl;
315         if (standard == INTEROP) {
316                 pkl = doc.create_root_node("PackingList", pkl_interop_ns);
317         } else {
318                 pkl = doc.create_root_node("PackingList", pkl_smpte_ns);
319         }
320
321         if (signer) {
322                 pkl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
323         }
324
325         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
326         pkl->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
327         pkl->add_child("IssueDate")->add_child_text (metadata.issue_date);
328         pkl->add_child("Issuer")->add_child_text (metadata.issuer);
329         pkl->add_child("Creator")->add_child_text (metadata.creator);
330
331         xmlpp::Element* asset_list = pkl->add_child("AssetList");
332         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
333                 i->write_to_pkl (asset_list, _directory, standard);
334         }
335
336         if (signer) {
337                 signer->sign (pkl, standard);
338         }
339
340         doc.write_to_file (p.string (), "UTF-8");
341         return p.string ();
342 }
343
344 /** Write the VOLINDEX file.
345  *  @param standard DCP standard to use (INTEROP or SMPTE)
346  */
347 void
348 DCP::write_volindex (Standard standard) const
349 {
350         boost::filesystem::path p = _directory;
351         switch (standard) {
352         case INTEROP:
353                 p /= "VOLINDEX";
354                 break;
355         case SMPTE:
356                 p /= "VOLINDEX.xml";
357                 break;
358         default:
359                 DCP_ASSERT (false);
360         }
361
362         xmlpp::Document doc;
363         xmlpp::Element* root;
364
365         switch (standard) {
366         case INTEROP:
367                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
368                 break;
369         case SMPTE:
370                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
371                 break;
372         default:
373                 DCP_ASSERT (false);
374         }
375
376         root->add_child("Index")->add_child_text ("1");
377         doc.write_to_file (p.string (), "UTF-8");
378 }
379
380 void
381 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
382 {
383         boost::filesystem::path p = _directory;
384
385         switch (standard) {
386         case INTEROP:
387                 p /= "ASSETMAP";
388                 break;
389         case SMPTE:
390                 p /= "ASSETMAP.xml";
391                 break;
392         default:
393                 DCP_ASSERT (false);
394         }
395
396         xmlpp::Document doc;
397         xmlpp::Element* root;
398
399         switch (standard) {
400         case INTEROP:
401                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
402                 break;
403         case SMPTE:
404                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
405                 break;
406         default:
407                 DCP_ASSERT (false);
408         }
409
410         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
411         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
412
413         switch (standard) {
414         case INTEROP:
415                 root->add_child("VolumeCount")->add_child_text ("1");
416                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
417                 root->add_child("Issuer")->add_child_text (metadata.issuer);
418                 root->add_child("Creator")->add_child_text (metadata.creator);
419                 break;
420         case SMPTE:
421                 root->add_child("Creator")->add_child_text (metadata.creator);
422                 root->add_child("VolumeCount")->add_child_text ("1");
423                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
424                 root->add_child("Issuer")->add_child_text (metadata.issuer);
425                 break;
426         default:
427                 DCP_ASSERT (false);
428         }
429
430         xmlpp::Node* asset_list = root->add_child ("AssetList");
431
432         xmlpp::Node* asset = asset_list->add_child ("Asset");
433         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
434         asset->add_child("PackingList")->add_child_text ("true");
435         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
436         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
437         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
438         chunk->add_child("VolumeIndex")->add_child_text ("1");
439         chunk->add_child("Offset")->add_child_text ("0");
440         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
441
442         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
443                 i->write_to_assetmap (asset_list, _directory);
444         }
445
446         /* This must not be the _formatted version otherwise signature digests will be wrong */
447         doc.write_to_file (p.string (), "UTF-8");
448 }
449
450 /** Write all the XML files for this DCP.
451  *  @param standand INTEROP or SMPTE.
452  *  @param metadata Metadata to use for PKL and asset map files.
453  *  @param signer Signer to use, or 0.
454  */
455 void
456 DCP::write_xml (
457         Standard standard,
458         XMLMetadata metadata,
459         shared_ptr<const CertificateChain> signer,
460         NameFormat name_format
461         )
462 {
463         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
464                 NameFormat::Map values;
465                 values['t'] = "cpl";
466                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
467         }
468
469         string const pkl_uuid = make_uuid ();
470         NameFormat::Map values;
471         values['t'] = "pkl";
472         boost::filesystem::path const pkl_path = write_pkl (name_format.get(values, "_" + pkl_uuid + ".xml"), standard, pkl_uuid, metadata, signer);
473
474         write_volindex (standard);
475         write_assetmap (standard, pkl_uuid, pkl_path, metadata);
476 }
477
478 list<shared_ptr<CPL> >
479 DCP::cpls () const
480 {
481         return _cpls;
482 }
483
484 /** @return All assets (including CPLs) */
485 list<shared_ptr<Asset> >
486 DCP::assets () const
487 {
488         list<shared_ptr<Asset> > assets;
489         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
490                 assets.push_back (i);
491                 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
492                         shared_ptr<Asset> o = j->asset_ref().asset ();
493                         assets.push_back (o);
494                         /* More Interop special-casing */
495                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
496                         if (sub) {
497                                 sub->add_font_assets (assets);
498                         }
499                 }
500         }
501
502         return assets;
503 }
504
505 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
506 vector<boost::filesystem::path>
507 DCP::directories_from_files (vector<boost::filesystem::path> files)
508 {
509         vector<boost::filesystem::path> d;
510         BOOST_FOREACH (boost::filesystem::path i, files) {
511                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
512                         d.push_back (i.parent_path ());
513                 }
514         }
515         return d;
516 }