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.
40 #include "raw_convert.h"
42 #include "sound_asset.h"
43 #include "atmos_asset.h"
44 #include "picture_asset.h"
45 #include "interop_subtitle_asset.h"
46 #include "smpte_subtitle_asset.h"
47 #include "mono_picture_asset.h"
48 #include "stereo_picture_asset.h"
49 #include "reel_subtitle_asset.h"
52 #include "exceptions.h"
54 #include "certificate_chain.h"
55 #include "compose.hpp"
56 #include "decrypted_kdm.h"
57 #include "decrypted_kdm_key.h"
58 #include "dcp_assert.h"
59 #include "reel_asset.h"
60 #include "font_asset.h"
62 #include "asset_factory.h"
64 #include <asdcp/AS_DCP.h>
65 #include <xmlsec/xmldsig.h>
66 #include <xmlsec/app.h>
67 #include <libxml++/libxml++.h>
68 #include <boost/filesystem.hpp>
69 #include <boost/algorithm/string.hpp>
79 using std::make_shared;
81 using std::shared_ptr;
82 using std::dynamic_pointer_cast;
83 using boost::optional;
84 using boost::algorithm::starts_with;
88 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
89 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
90 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
91 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
94 DCP::DCP (boost::filesystem::path directory)
95 : _directory (directory)
97 if (!boost::filesystem::exists (directory)) {
98 boost::filesystem::create_directories (directory);
101 _directory = boost::filesystem::canonical (_directory);
106 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
108 /* Read the ASSETMAP and PKL */
110 if (boost::filesystem::exists (_directory / "ASSETMAP")) {
111 _asset_map = _directory / "ASSETMAP";
112 } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
113 _asset_map = _directory / "ASSETMAP.xml";
115 boost::throw_exception (ReadError(String::compose("Could not find ASSETMAP nor ASSETMAP.xml in '%1'", _directory.string())));
118 cxml::Document asset_map ("AssetMap");
120 asset_map.read_file (_asset_map.get());
121 if (asset_map.namespace_uri() == assetmap_interop_ns) {
122 _standard = Standard::INTEROP;
123 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
124 _standard = Standard::SMPTE;
126 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
129 auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
130 map<string, boost::filesystem::path> paths;
131 vector<boost::filesystem::path> pkl_paths;
132 for (auto i: asset_nodes) {
133 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
134 boost::throw_exception (XMLError ("unsupported asset chunk count"));
136 auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
137 if (starts_with (p, "file://")) {
140 switch (*_standard) {
141 case Standard::INTEROP:
142 if (i->optional_node_child("PackingList")) {
143 pkl_paths.push_back (p);
145 paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
148 case Standard::SMPTE:
150 auto pkl_bool = i->optional_string_child("PackingList");
151 if (pkl_bool && *pkl_bool == "true") {
152 pkl_paths.push_back (p);
154 paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
161 if (pkl_paths.empty()) {
162 boost::throw_exception (XMLError ("No packing lists found in asset map"));
165 for (auto i: pkl_paths) {
166 _pkls.push_back (make_shared<PKL>(_directory / i));
170 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
171 _pkls - PKL objects for each PKL.
173 Read all the assets from the asset map.
176 /* Make a list of non-CPL/PKL assets so that we can resolve the references
179 vector<shared_ptr<Asset>> other_assets;
181 for (auto i: paths) {
182 auto path = _directory / i.second;
184 if (i.second.empty()) {
185 /* I can't see how this is valid, but it's
186 been seen in the wild with a DCP that
187 claims to come from ClipsterDCI 5.10.0.5.
190 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
195 if (!boost::filesystem::exists(path)) {
197 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
202 /* Find the <Type> for this asset from the PKL that contains the asset */
203 optional<string> pkl_type;
204 for (auto j: _pkls) {
205 pkl_type = j->type(i.first);
212 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
213 * need to worry about it.
218 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
219 auto p = new xmlpp::DomParser;
221 p->parse_file (path.string());
222 } catch (std::exception& e) {
224 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
227 auto const root = p->get_document()->get_root_node()->get_name();
230 if (root == "CompositionPlaylist") {
231 auto cpl = make_shared<CPL>(path);
232 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
233 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
235 _cpls.push_back (cpl);
236 } else if (root == "DCSubtitle") {
237 if (_standard && _standard.get() == Standard::SMPTE && notes) {
238 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
240 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
243 *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
244 *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
245 *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
246 *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
249 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
250 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
251 other_assets.push_back (make_shared<FontAsset>(i.first, path));
252 } else if (*pkl_type == "image/png") {
253 /* It's an Interop PNG subtitle; let it go */
255 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
259 resolve_refs (other_assets);
261 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
263 for (auto i: cpls()) {
264 for (auto j: i->reel_file_assets()) {
265 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
266 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
275 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
277 for (auto i: cpls()) {
278 i->resolve_refs (assets);
284 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
287 auto b = other.cpls ();
289 if (a.size() != b.size()) {
290 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
298 while (j != b.end() && !(*j)->equals (i, opt, note)) {
312 DCP::add (shared_ptr<CPL> cpl)
314 _cpls.push_back (cpl);
319 DCP::any_encrypted () const
321 for (auto i: cpls()) {
322 if (i->any_encrypted()) {
332 DCP::all_encrypted () const
334 for (auto i: cpls()) {
335 if (!i->all_encrypted()) {
345 DCP::add (DecryptedKDM const & kdm)
347 auto keys = kdm.keys ();
349 for (auto i: cpls()) {
350 for (auto const& j: kdm.keys()) {
351 if (j.cpl_id() == i->id()) {
359 /** Write the VOLINDEX file.
360 * @param standard DCP standard to use (INTEROP or SMPTE)
363 DCP::write_volindex (Standard standard) const
367 case Standard::INTEROP:
370 case Standard::SMPTE:
378 xmlpp::Element* root;
381 case Standard::INTEROP:
382 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
384 case Standard::SMPTE:
385 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
391 root->add_child("Index")->add_child_text ("1");
392 doc.write_to_file_formatted (p.string (), "UTF-8");
397 DCP::write_assetmap (
398 Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
399 string issuer, string creator, string issue_date, string annotation_text
405 case Standard::INTEROP:
408 case Standard::SMPTE:
416 xmlpp::Element* root;
419 case Standard::INTEROP:
420 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
422 case Standard::SMPTE:
423 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
429 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
430 root->add_child("AnnotationText")->add_child_text (annotation_text);
433 case Standard::INTEROP:
434 root->add_child("VolumeCount")->add_child_text ("1");
435 root->add_child("IssueDate")->add_child_text (issue_date);
436 root->add_child("Issuer")->add_child_text (issuer);
437 root->add_child("Creator")->add_child_text (creator);
439 case Standard::SMPTE:
440 root->add_child("Creator")->add_child_text (creator);
441 root->add_child("VolumeCount")->add_child_text ("1");
442 root->add_child("IssueDate")->add_child_text (issue_date);
443 root->add_child("Issuer")->add_child_text (issuer);
449 auto asset_list = root->add_child ("AssetList");
451 auto asset = asset_list->add_child ("Asset");
452 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
453 asset->add_child("PackingList")->add_child_text ("true");
454 auto chunk_list = asset->add_child ("ChunkList");
455 auto chunk = chunk_list->add_child ("Chunk");
456 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
457 chunk->add_child("VolumeIndex")->add_child_text ("1");
458 chunk->add_child("Offset")->add_child_text ("0");
459 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
461 for (auto i: assets()) {
462 i->write_to_assetmap (asset_list, _directory);
465 doc.write_to_file_formatted (p.string (), "UTF-8");
476 string annotation_text,
477 shared_ptr<const CertificateChain> signer,
478 NameFormat name_format
481 for (auto i: cpls()) {
482 NameFormat::Map values;
484 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
490 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
491 _pkls.push_back (pkl);
492 for (auto i: assets()) {
493 i->add_to_pkl (pkl, _directory);
496 pkl = _pkls.front ();
499 NameFormat::Map values;
501 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
502 pkl->write (pkl_path, signer);
504 write_volindex (standard);
505 write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
509 vector<shared_ptr<CPL>>
516 vector<shared_ptr<Asset>>
517 DCP::assets (bool ignore_unresolved) const
519 vector<shared_ptr<Asset>> assets;
520 for (auto i: cpls()) {
521 assets.push_back (i);
522 for (auto j: i->reel_file_assets()) {
523 if (ignore_unresolved && !j->asset_ref().resolved()) {
527 auto const id = j->asset_ref().id();
528 auto already_got = false;
529 for (auto k: assets) {
536 auto o = j->asset_ref().asset();
537 assets.push_back (o);
538 /* More Interop special-casing */
539 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
541 sub->add_font_assets (assets);
551 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
552 vector<boost::filesystem::path>
553 DCP::directories_from_files (vector<boost::filesystem::path> files)
555 vector<boost::filesystem::path> d;
556 for (auto i: files) {
557 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
558 d.push_back (i.parent_path ());