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 <asdcp/AS_DCP.h>
61 #include <xmlsec/xmldsig.h>
62 #include <xmlsec/app.h>
63 #include <libxml++/libxml++.h>
64 #include <boost/filesystem.hpp>
65 #include <boost/algorithm/string.hpp>
66 #include <boost/foreach.hpp>
76 using boost::shared_ptr;
77 using boost::dynamic_pointer_cast;
78 using boost::optional;
79 using boost::algorithm::starts_with;
82 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
83 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
84 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
85 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
87 DCP::DCP (boost::filesystem::path directory)
88 : _directory (directory)
90 if (!boost::filesystem::exists (directory)) {
91 boost::filesystem::create_directories (directory);
94 _directory = boost::filesystem::canonical (_directory);
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)
103 errors->push_back (shared_ptr<T> (new T (e)));
111 DCP::read (bool keep_going, ReadErrors* errors, 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 file 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)));
175 /* Read all the assets from the asset map */
177 /* Make a list of non-CPL/PKL assets so that we can resolve the references
180 list<shared_ptr<Asset> > other_assets;
182 for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
183 boost::filesystem::path path = _directory / i->second;
185 if (!boost::filesystem::exists (path)) {
186 survivable_error (keep_going, errors, MissingAssetError (path));
190 optional<string> pkl_type;
191 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
192 pkl_type = j->type(i->first);
198 DCP_ASSERT (pkl_type);
200 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
201 xmlpp::DomParser* p = new xmlpp::DomParser;
203 p->parse_file (path.string());
204 } catch (std::exception& e) {
206 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
209 string const root = p->get_document()->get_root_node()->get_name ();
212 if (root == "CompositionPlaylist") {
213 shared_ptr<CPL> cpl (new CPL (path));
214 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
215 survivable_error (keep_going, errors, MismatchedStandardError ());
217 _cpls.push_back (cpl);
218 } else if (root == "DCSubtitle") {
219 if (_standard && _standard.get() == SMPTE) {
220 survivable_error (keep_going, errors, MismatchedStandardError ());
222 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
225 *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
226 *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
227 *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
228 *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
231 /* XXX: asdcplib does not appear to support discovery of read MXFs standard
235 ASDCP::EssenceType_t type;
236 if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
237 throw DCPReadError ("Could not find essence type");
240 case ASDCP::ESS_UNKNOWN:
241 case ASDCP::ESS_MPEG2_VES:
242 throw DCPReadError ("MPEG2 video essences are not supported");
243 case ASDCP::ESS_JPEG_2000:
245 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
246 } catch (dcp::MXFFileError& e) {
247 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
248 /* Tried to load it as mono but the error says it's stereo; try that instead */
249 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
255 case ASDCP::ESS_PCM_24b_48k:
256 case ASDCP::ESS_PCM_24b_96k:
257 other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
259 case ASDCP::ESS_JPEG_2000_S:
260 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
262 case ASDCP::ESS_TIMED_TEXT:
263 other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
265 case ASDCP::ESS_DCDATA_DOLBY_ATMOS:
266 other_assets.push_back (shared_ptr<AtmosAsset> (new AtmosAsset (path)));
269 throw DCPReadError (String::compose ("Unknown MXF essence type %1 in %2", int(type), path.string()));
271 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
272 other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
273 } else if (*pkl_type == "image/png") {
274 /* It's an Interop PNG subtitle; let it go */
276 throw DCPReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
280 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
281 i->resolve_refs (other_assets);
286 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
288 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
289 i->resolve_refs (assets);
294 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
296 list<shared_ptr<CPL> > a = cpls ();
297 list<shared_ptr<CPL> > b = other.cpls ();
299 if (a.size() != b.size()) {
300 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
306 BOOST_FOREACH (shared_ptr<CPL> i, a) {
307 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
308 while (j != b.end() && !(*j)->equals (i, opt, note)) {
321 DCP::add (boost::shared_ptr<CPL> cpl)
323 _cpls.push_back (cpl);
327 DCP::encrypted () const
329 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
330 if (i->encrypted ()) {
338 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
339 * or the KDM you specify will be ignored.
340 * @param kdm KDM to use.
343 DCP::add (DecryptedKDM const & kdm)
345 list<DecryptedKDMKey> keys = kdm.keys ();
347 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
348 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
349 if (j.cpl_id() == i->id()) {
356 /** Write the VOLINDEX file.
357 * @param standard DCP standard to use (INTEROP or SMPTE)
360 DCP::write_volindex (Standard standard) const
362 boost::filesystem::path p = _directory;
375 xmlpp::Element* root;
379 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
382 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
388 root->add_child("Index")->add_child_text ("1");
389 doc.write_to_file (p.string (), "UTF-8");
393 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
395 boost::filesystem::path p = _directory;
409 xmlpp::Element* root;
413 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
416 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
422 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
423 root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
427 root->add_child("VolumeCount")->add_child_text ("1");
428 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
429 root->add_child("Issuer")->add_child_text (metadata.issuer);
430 root->add_child("Creator")->add_child_text (metadata.creator);
433 root->add_child("Creator")->add_child_text (metadata.creator);
434 root->add_child("VolumeCount")->add_child_text ("1");
435 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
436 root->add_child("Issuer")->add_child_text (metadata.issuer);
442 xmlpp::Node* asset_list = root->add_child ("AssetList");
444 xmlpp::Node* asset = asset_list->add_child ("Asset");
445 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
446 asset->add_child("PackingList")->add_child_text ("true");
447 xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
448 xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
449 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
450 chunk->add_child("VolumeIndex")->add_child_text ("1");
451 chunk->add_child("Offset")->add_child_text ("0");
452 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
454 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
455 i->write_to_assetmap (asset_list, _directory);
458 /* This must not be the _formatted version otherwise signature digests will be wrong */
459 doc.write_to_file (p.string (), "UTF-8");
462 /** Write all the XML files for this DCP.
463 * @param standand INTEROP or SMPTE.
464 * @param metadata Metadata to use for PKL and asset map files.
465 * @param signer Signer to use, or 0.
470 XMLMetadata metadata,
471 shared_ptr<const CertificateChain> signer,
472 NameFormat name_format
475 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
476 NameFormat::Map values;
478 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
484 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
485 _pkls.push_back (pkl);
486 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
487 i->add_to_pkl (pkl, _directory);
490 pkl = _pkls.front ();
493 NameFormat::Map values;
495 boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
496 pkl->write (pkl_path, signer);
498 write_volindex (standard);
499 write_assetmap (standard, pkl->id(), pkl_path, metadata);
502 list<shared_ptr<CPL> >
508 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
509 * an exception is thrown if they are found.
510 * @return All assets (including CPLs).
512 list<shared_ptr<Asset> >
513 DCP::assets (bool ignore_unresolved) const
515 list<shared_ptr<Asset> > assets;
516 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
517 assets.push_back (i);
518 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
519 if (ignore_unresolved && !j->asset_ref().resolved()) {
522 shared_ptr<Asset> o = j->asset_ref().asset ();
523 assets.push_back (o);
524 /* More Interop special-casing */
525 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
527 sub->add_font_assets (assets);
535 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
536 vector<boost::filesystem::path>
537 DCP::directories_from_files (vector<boost::filesystem::path> files)
539 vector<boost::filesystem::path> d;
540 BOOST_FOREACH (boost::filesystem::path i, files) {
541 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
542 d.push_back (i.parent_path ());