2 Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
38 #include "raw_convert.h"
40 #include "sound_asset.h"
41 #include "atmos_asset.h"
42 #include "picture_asset.h"
43 #include "interop_subtitle_asset.h"
44 #include "smpte_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "stereo_picture_asset.h"
47 #include "reel_subtitle_asset.h"
50 #include "exceptions.h"
52 #include "certificate_chain.h"
53 #include "compose.hpp"
54 #include "decrypted_kdm.h"
55 #include "decrypted_kdm_key.h"
56 #include "dcp_assert.h"
57 #include "reel_asset.h"
58 #include "font_asset.h"
60 #include "asset_factory.h"
62 #include <asdcp/AS_DCP.h>
63 #include <xmlsec/xmldsig.h>
64 #include <xmlsec/app.h>
65 #include <libxml++/libxml++.h>
66 #include <boost/filesystem.hpp>
67 #include <boost/algorithm/string.hpp>
68 #include <boost/foreach.hpp>
78 using boost::shared_ptr;
79 using boost::dynamic_pointer_cast;
80 using boost::optional;
81 using boost::algorithm::starts_with;
84 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
85 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
86 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
87 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
89 DCP::DCP (boost::filesystem::path directory)
90 : _directory (directory)
92 if (!boost::filesystem::exists (directory)) {
93 boost::filesystem::create_directories (directory);
96 _directory = boost::filesystem::canonical (_directory);
99 /** Read a DCP. This method does not do any deep checking of the DCP's validity, but
100 * if it comes across any bad things it will do one of two things.
102 * Errors that are so serious that they prevent the method from working will result
103 * in an exception being thrown. For example, a missing ASSETMAP means that the DCP
104 * can't be read without a lot of guesswork, so this will throw.
106 * Errors that are not fatal will be added to notes, if it's non-0. For example,
107 * if the DCP contains a mixture of Interop and SMPTE elements this will result
108 * in a note being added to the list.
111 DCP::read (list<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
113 /* Read the ASSETMAP and PKL */
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";
121 boost::throw_exception (DCPReadError (String::compose ("could not find ASSETMAP nor ASSETMAP.xml in `%1'", _directory.string())));
124 cxml::Document asset_map ("AssetMap");
126 asset_map.read_file (asset_map_file);
127 if (asset_map.namespace_uri() == assetmap_interop_ns) {
129 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
132 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
135 list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136 map<string, boost::filesystem::path> paths;
137 list<boost::filesystem::path> pkl_paths;
138 BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
139 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
140 boost::throw_exception (XMLError ("unsupported asset chunk count"));
142 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
143 if (starts_with (p, "file://")) {
146 switch (*_standard) {
148 if (i->optional_node_child("PackingList")) {
149 pkl_paths.push_back (p);
151 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
156 optional<string> pkl_bool = i->optional_string_child("PackingList");
157 if (pkl_bool && *pkl_bool == "true") {
158 pkl_paths.push_back (p);
160 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
167 if (pkl_paths.empty()) {
168 boost::throw_exception (XMLError ("No packing lists found in asset map"));
171 BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
172 _pkls.push_back (shared_ptr<PKL>(new PKL(_directory / i)));
176 paths - files in the DCP that are not PKLs.
177 _pkls - PKL objects for each PKL.
179 Read all the assets from the asset map.
182 /* Make a list of non-CPL/PKL assets so that we can resolve the references
185 list<shared_ptr<Asset> > other_assets;
187 for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
188 boost::filesystem::path path = _directory / i->second;
190 if (i->second.empty()) {
191 /* I can't see how this is valid, but it's
192 been seen in the wild with a DCP that
193 claims to come from ClipsterDCI 5.10.0.5.
196 notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
201 if (!boost::filesystem::exists(path)) {
203 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSET));
208 /* Find the <Type> for this asset from the PKL that contains the asset */
209 optional<string> pkl_type;
210 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
211 pkl_type = j->type(i->first);
217 DCP_ASSERT (pkl_type);
219 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
220 xmlpp::DomParser* p = new xmlpp::DomParser;
222 p->parse_file (path.string());
223 } catch (std::exception& e) {
225 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
228 string const root = p->get_document()->get_root_node()->get_name ();
231 if (root == "CompositionPlaylist") {
232 shared_ptr<CPL> cpl (new CPL (path));
233 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
234 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
236 _cpls.push_back (cpl);
237 } else if (root == "DCSubtitle") {
238 if (_standard && _standard.get() == SMPTE) {
239 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
241 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
244 *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
245 *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
246 *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
247 *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
250 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
251 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
252 other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
253 } else if (*pkl_type == "image/png") {
254 /* It's an Interop PNG subtitle; let it go */
256 throw DCPReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
260 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
261 i->resolve_refs (other_assets);
266 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
268 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
269 i->resolve_refs (assets);
274 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
276 list<shared_ptr<CPL> > a = cpls ();
277 list<shared_ptr<CPL> > b = other.cpls ();
279 if (a.size() != b.size()) {
280 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
286 BOOST_FOREACH (shared_ptr<CPL> i, a) {
287 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
288 while (j != b.end() && !(*j)->equals (i, opt, note)) {
301 DCP::add (boost::shared_ptr<CPL> cpl)
303 _cpls.push_back (cpl);
307 DCP::encrypted () const
309 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
310 if (i->encrypted ()) {
318 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
319 * or the KDM you specify will be ignored.
320 * @param kdm KDM to use.
323 DCP::add (DecryptedKDM const & kdm)
325 list<DecryptedKDMKey> keys = kdm.keys ();
327 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
328 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
329 if (j.cpl_id() == i->id()) {
336 /** Write the VOLINDEX file.
337 * @param standard DCP standard to use (INTEROP or SMPTE)
340 DCP::write_volindex (Standard standard) const
342 boost::filesystem::path p = _directory;
355 xmlpp::Element* root;
359 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
362 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
368 root->add_child("Index")->add_child_text ("1");
369 doc.write_to_file_formatted (p.string (), "UTF-8");
373 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
375 boost::filesystem::path p = _directory;
389 xmlpp::Element* root;
393 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
396 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
402 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
403 root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
407 root->add_child("VolumeCount")->add_child_text ("1");
408 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
409 root->add_child("Issuer")->add_child_text (metadata.issuer);
410 root->add_child("Creator")->add_child_text (metadata.creator);
413 root->add_child("Creator")->add_child_text (metadata.creator);
414 root->add_child("VolumeCount")->add_child_text ("1");
415 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
416 root->add_child("Issuer")->add_child_text (metadata.issuer);
422 xmlpp::Node* asset_list = root->add_child ("AssetList");
424 xmlpp::Node* asset = asset_list->add_child ("Asset");
425 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
426 asset->add_child("PackingList")->add_child_text ("true");
427 xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
428 xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
429 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
430 chunk->add_child("VolumeIndex")->add_child_text ("1");
431 chunk->add_child("Offset")->add_child_text ("0");
432 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
434 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
435 i->write_to_assetmap (asset_list, _directory);
438 doc.write_to_file_formatted (p.string (), "UTF-8");
441 /** Write all the XML files for this DCP.
442 * @param standand INTEROP or SMPTE.
443 * @param metadata Metadata to use for PKL and asset map files.
444 * @param signer Signer to use, or 0.
449 XMLMetadata metadata,
450 shared_ptr<const CertificateChain> signer,
451 NameFormat name_format
454 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
455 NameFormat::Map values;
457 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
463 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
464 _pkls.push_back (pkl);
465 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
466 i->add_to_pkl (pkl, _directory);
469 pkl = _pkls.front ();
472 NameFormat::Map values;
474 boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
475 pkl->write (pkl_path, signer);
477 write_volindex (standard);
478 write_assetmap (standard, pkl->id(), pkl_path, metadata);
481 list<shared_ptr<CPL> >
487 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
488 * an exception is thrown if they are found.
489 * @return All assets (including CPLs).
491 list<shared_ptr<Asset> >
492 DCP::assets (bool ignore_unresolved) const
494 list<shared_ptr<Asset> > assets;
495 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
496 assets.push_back (i);
497 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
498 if (ignore_unresolved && !j->asset_ref().resolved()) {
501 shared_ptr<Asset> o = j->asset_ref().asset ();
502 assets.push_back (o);
503 /* More Interop special-casing */
504 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
506 sub->add_font_assets (assets);
514 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
515 vector<boost::filesystem::path>
516 DCP::directories_from_files (vector<boost::filesystem::path> files)
518 vector<boost::filesystem::path> d;
519 BOOST_FOREACH (boost::filesystem::path i, files) {
520 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
521 d.push_back (i.parent_path ());