2 Copyright (C) 2012-2021 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>
76 using std::make_shared;
78 using std::shared_ptr;
79 using std::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 vector.
111 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
113 /* Read the ASSETMAP and PKL */
115 if (boost::filesystem::exists (_directory / "ASSETMAP")) {
116 _asset_map = _directory / "ASSETMAP";
117 } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
118 _asset_map = _directory / "ASSETMAP.xml";
120 boost::throw_exception (ReadError(String::compose("Could not find ASSETMAP nor ASSETMAP.xml in '%1'", _directory.string())));
123 cxml::Document asset_map ("AssetMap");
125 asset_map.read_file (_asset_map.get());
126 if (asset_map.namespace_uri() == assetmap_interop_ns) {
127 _standard = Standard::INTEROP;
128 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
129 _standard = Standard::SMPTE;
131 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
134 auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
135 map<string, boost::filesystem::path> paths;
136 vector<boost::filesystem::path> pkl_paths;
137 for (auto i: asset_nodes) {
138 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
139 boost::throw_exception (XMLError ("unsupported asset chunk count"));
141 auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
142 if (starts_with (p, "file://")) {
145 switch (*_standard) {
146 case Standard::INTEROP:
147 if (i->optional_node_child("PackingList")) {
148 pkl_paths.push_back (p);
150 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
153 case Standard::SMPTE:
155 auto pkl_bool = i->optional_string_child("PackingList");
156 if (pkl_bool && *pkl_bool == "true") {
157 pkl_paths.push_back (p);
159 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
166 if (pkl_paths.empty()) {
167 boost::throw_exception (XMLError ("No packing lists found in asset map"));
170 for (auto i: pkl_paths) {
171 _pkls.push_back (make_shared<PKL>(_directory / i));
175 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
176 _pkls - PKL objects for each PKL.
178 Read all the assets from the asset map.
181 /* Make a list of non-CPL/PKL assets so that we can resolve the references
184 vector<shared_ptr<Asset>> other_assets;
186 for (auto i: paths) {
187 auto path = _directory / i.second;
189 if (i.second.empty()) {
190 /* I can't see how this is valid, but it's
191 been seen in the wild with a DCP that
192 claims to come from ClipsterDCI 5.10.0.5.
195 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
200 if (!boost::filesystem::exists(path)) {
202 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
207 /* Find the <Type> for this asset from the PKL that contains the asset */
208 optional<string> pkl_type;
209 for (auto j: _pkls) {
210 pkl_type = j->type(i.first);
217 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
218 * need to worry about it.
223 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
224 auto p = new xmlpp::DomParser;
226 p->parse_file (path.string());
227 } catch (std::exception& e) {
229 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
232 auto const root = p->get_document()->get_root_node()->get_name ();
235 if (root == "CompositionPlaylist") {
236 auto cpl = make_shared<CPL>(path);
237 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
238 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
240 _cpls.push_back (cpl);
241 } else if (root == "DCSubtitle") {
242 if (_standard && _standard.get() == Standard::SMPTE && notes) {
243 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
245 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
248 *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
249 *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
250 *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
251 *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
254 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
255 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
256 other_assets.push_back (make_shared<FontAsset>(i.first, path));
257 } else if (*pkl_type == "image/png") {
258 /* It's an Interop PNG subtitle; let it go */
260 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
264 resolve_refs (other_assets);
266 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
268 for (auto i: cpls()) {
269 for (auto j: i->reel_mxfs()) {
270 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
271 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
279 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
281 for (auto i: cpls()) {
282 i->resolve_refs (assets);
287 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
290 auto b = other.cpls ();
292 if (a.size() != b.size()) {
293 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
301 while (j != b.end() && !(*j)->equals (i, opt, note)) {
314 DCP::add (std::shared_ptr<CPL> cpl)
316 _cpls.push_back (cpl);
321 DCP::any_encrypted () const
323 for (auto i: cpls()) {
324 if (i->any_encrypted()) {
334 DCP::all_encrypted () const
336 for (auto i: cpls()) {
337 if (!i->all_encrypted()) {
346 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
347 * or the KDM you specify will be ignored.
348 * @param kdm KDM to use.
351 DCP::add (DecryptedKDM const & kdm)
353 auto keys = kdm.keys ();
355 for (auto i: cpls()) {
356 for (auto const& j: kdm.keys()) {
357 if (j.cpl_id() == i->id()) {
364 /** Write the VOLINDEX file.
365 * @param standard DCP standard to use (INTEROP or SMPTE)
368 DCP::write_volindex (Standard standard) const
372 case Standard::INTEROP:
375 case Standard::SMPTE:
383 xmlpp::Element* root;
386 case Standard::INTEROP:
387 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
389 case Standard::SMPTE:
390 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
396 root->add_child("Index")->add_child_text ("1");
397 doc.write_to_file_formatted (p.string (), "UTF-8");
401 DCP::write_assetmap (
402 Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
403 string issuer, string creator, string issue_date, string annotation_text
409 case Standard::INTEROP:
412 case Standard::SMPTE:
420 xmlpp::Element* root;
423 case Standard::INTEROP:
424 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
426 case Standard::SMPTE:
427 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
433 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
434 root->add_child("AnnotationText")->add_child_text (annotation_text);
437 case Standard::INTEROP:
438 root->add_child("VolumeCount")->add_child_text ("1");
439 root->add_child("IssueDate")->add_child_text (issue_date);
440 root->add_child("Issuer")->add_child_text (issuer);
441 root->add_child("Creator")->add_child_text (creator);
443 case Standard::SMPTE:
444 root->add_child("Creator")->add_child_text (creator);
445 root->add_child("VolumeCount")->add_child_text ("1");
446 root->add_child("IssueDate")->add_child_text (issue_date);
447 root->add_child("Issuer")->add_child_text (issuer);
453 auto asset_list = root->add_child ("AssetList");
455 auto asset = asset_list->add_child ("Asset");
456 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
457 asset->add_child("PackingList")->add_child_text ("true");
458 auto chunk_list = asset->add_child ("ChunkList");
459 auto chunk = chunk_list->add_child ("Chunk");
460 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
461 chunk->add_child("VolumeIndex")->add_child_text ("1");
462 chunk->add_child("Offset")->add_child_text ("0");
463 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
465 for (auto i: assets()) {
466 i->write_to_assetmap (asset_list, _directory);
469 doc.write_to_file_formatted (p.string (), "UTF-8");
473 /** Write all the XML files for this DCP.
474 * @param standand INTEROP or SMPTE.
475 * @param metadata Metadata to use for PKL and asset map files.
476 * @param signer Signer to use, or 0.
484 string annotation_text,
485 shared_ptr<const CertificateChain> signer,
486 NameFormat name_format
489 for (auto i: cpls()) {
490 NameFormat::Map values;
492 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
498 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
499 _pkls.push_back (pkl);
500 for (auto i: assets()) {
501 i->add_to_pkl (pkl, _directory);
504 pkl = _pkls.front ();
507 NameFormat::Map values;
509 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
510 pkl->write (pkl_path, signer);
512 write_volindex (standard);
513 write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
516 vector<shared_ptr<CPL>>
522 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
523 * an exception is thrown if they are found.
524 * @return All assets (including CPLs).
526 vector<shared_ptr<Asset>>
527 DCP::assets (bool ignore_unresolved) const
529 vector<shared_ptr<Asset>> assets;
530 for (auto i: cpls()) {
531 assets.push_back (i);
532 for (auto j: i->reel_mxfs()) {
533 if (ignore_unresolved && !j->asset_ref().resolved()) {
537 auto const id = j->asset_ref().id();
538 auto already_got = false;
539 for (auto k: assets) {
546 auto o = j->asset_ref().asset();
547 assets.push_back (o);
548 /* More Interop special-casing */
549 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
551 sub->add_font_assets (assets);
560 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
561 vector<boost::filesystem::path>
562 DCP::directories_from_files (vector<boost::filesystem::path> files)
564 vector<boost::filesystem::path> d;
565 for (auto i: files) {
566 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
567 d.push_back (i.parent_path ());