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 "asset_factory.h"
41 #include "atmos_asset.h"
42 #include "certificate_chain.h"
43 #include "compose.hpp"
46 #include "dcp_assert.h"
47 #include "decrypted_kdm.h"
48 #include "decrypted_kdm_key.h"
49 #include "exceptions.h"
50 #include "font_asset.h"
51 #include "interop_subtitle_asset.h"
53 #include "mono_picture_asset.h"
54 #include "picture_asset.h"
56 #include "raw_convert.h"
57 #include "reel_asset.h"
58 #include "reel_subtitle_asset.h"
59 #include "smpte_subtitle_asset.h"
60 #include "sound_asset.h"
61 #include "stereo_picture_asset.h"
65 LIBDCP_DISABLE_WARNINGS
66 #include <asdcp/AS_DCP.h>
67 LIBDCP_ENABLE_WARNINGS
68 #include <xmlsec/xmldsig.h>
69 #include <xmlsec/app.h>
70 LIBDCP_DISABLE_WARNINGS
71 #include <libxml++/libxml++.h>
72 LIBDCP_ENABLE_WARNINGS
73 #include <boost/filesystem.hpp>
74 #include <boost/algorithm/string.hpp>
85 using std::make_shared;
87 using std::shared_ptr;
88 using std::dynamic_pointer_cast;
89 using boost::optional;
90 using boost::algorithm::starts_with;
94 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
95 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
96 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
97 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
100 DCP::DCP (boost::filesystem::path directory)
101 : _directory (directory)
103 if (!boost::filesystem::exists (directory)) {
104 boost::filesystem::create_directories (directory);
107 _directory = boost::filesystem::canonical (_directory);
112 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
114 /* Read the ASSETMAP and PKL */
116 if (boost::filesystem::exists (_directory / "ASSETMAP")) {
117 _asset_map = _directory / "ASSETMAP";
118 } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
119 _asset_map = _directory / "ASSETMAP.xml";
121 boost::throw_exception (MissingAssetmapError(_directory));
124 cxml::Document asset_map ("AssetMap");
126 asset_map.read_file (_asset_map.get());
127 if (asset_map.namespace_uri() == assetmap_interop_ns) {
128 _standard = Standard::INTEROP;
129 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
130 _standard = Standard::SMPTE;
132 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
135 auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136 map<string, boost::filesystem::path> paths;
137 vector<boost::filesystem::path> pkl_paths;
138 for (auto i: asset_nodes) {
139 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
140 boost::throw_exception (XMLError ("unsupported asset chunk count"));
142 auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
143 if (starts_with (p, "file://")) {
146 switch (*_standard) {
147 case Standard::INTEROP:
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));
154 case Standard::SMPTE:
156 auto 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 for (auto i: pkl_paths) {
172 _pkls.push_back (make_shared<PKL>(_directory / i));
176 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
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 vector<shared_ptr<Asset>> other_assets;
187 for (auto i: paths) {
188 auto 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::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
201 if (!boost::filesystem::exists(path)) {
203 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
208 /* Find the <Type> for this asset from the PKL that contains the asset */
209 optional<string> pkl_type;
210 for (auto j: _pkls) {
211 pkl_type = j->type(i.first);
218 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
219 * need to worry about it.
224 auto remove_parameters = [](string const& n) {
225 return n.substr(0, n.find(";"));
228 /* Remove any optional parameters (after ;) */
229 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
232 pkl_type == remove_parameters(CPL::static_pkl_type(*_standard)) ||
233 pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(*_standard))) {
234 auto p = new xmlpp::DomParser;
236 p->parse_file (path.string());
237 } catch (std::exception& e) {
239 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
242 auto const root = p->get_document()->get_root_node()->get_name();
245 if (root == "CompositionPlaylist") {
246 auto cpl = make_shared<CPL>(path);
247 if (_standard && cpl->standard() != _standard.get() && notes) {
248 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
250 _cpls.push_back (cpl);
251 } else if (root == "DCSubtitle") {
252 if (_standard && _standard.get() == Standard::SMPTE && notes) {
253 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
255 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
258 *pkl_type == remove_parameters(PictureAsset::static_pkl_type(*_standard)) ||
259 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(*_standard)) ||
260 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(*_standard)) ||
261 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(*_standard))
264 bool found_threed_marked_as_twod = false;
265 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
266 if (found_threed_marked_as_twod && notes) {
267 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
269 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(*_standard))) {
270 other_assets.push_back (make_shared<FontAsset>(i.first, path));
271 } else if (*pkl_type == "image/png") {
272 /* It's an Interop PNG subtitle; let it go */
274 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
278 resolve_refs (other_assets);
280 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
282 for (auto i: cpls()) {
283 for (auto j: i->reel_file_assets()) {
284 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
285 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
294 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
296 for (auto i: cpls()) {
297 i->resolve_refs (assets);
303 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
306 auto b = other.cpls ();
308 if (a.size() != b.size()) {
309 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
317 while (j != b.end() && !(*j)->equals (i, opt, note)) {
331 DCP::add (shared_ptr<CPL> cpl)
333 _cpls.push_back (cpl);
338 DCP::any_encrypted () const
340 for (auto i: cpls()) {
341 if (i->any_encrypted()) {
351 DCP::all_encrypted () const
353 for (auto i: cpls()) {
354 if (!i->all_encrypted()) {
364 DCP::add (DecryptedKDM const & kdm)
366 auto keys = kdm.keys();
367 for (auto cpl: cpls()) {
368 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
375 /** Write the VOLINDEX file.
376 * @param standard DCP standard to use (INTEROP or SMPTE)
379 DCP::write_volindex (Standard standard) const
383 case Standard::INTEROP:
386 case Standard::SMPTE:
394 xmlpp::Element* root;
397 case Standard::INTEROP:
398 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
400 case Standard::SMPTE:
401 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
407 root->add_child("Index")->add_child_text ("1");
408 doc.write_to_file_formatted (p.string (), "UTF-8");
413 DCP::write_assetmap (
414 Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
415 string issuer, string creator, string issue_date, string annotation_text
421 case Standard::INTEROP:
424 case Standard::SMPTE:
432 xmlpp::Element* root;
435 case Standard::INTEROP:
436 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
438 case Standard::SMPTE:
439 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
445 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
446 root->add_child("AnnotationText")->add_child_text (annotation_text);
449 case Standard::INTEROP:
450 root->add_child("VolumeCount")->add_child_text ("1");
451 root->add_child("IssueDate")->add_child_text (issue_date);
452 root->add_child("Issuer")->add_child_text (issuer);
453 root->add_child("Creator")->add_child_text (creator);
455 case Standard::SMPTE:
456 root->add_child("Creator")->add_child_text (creator);
457 root->add_child("VolumeCount")->add_child_text ("1");
458 root->add_child("IssueDate")->add_child_text (issue_date);
459 root->add_child("Issuer")->add_child_text (issuer);
465 auto asset_list = root->add_child ("AssetList");
467 auto asset = asset_list->add_child ("Asset");
468 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
469 asset->add_child("PackingList")->add_child_text ("true");
470 auto chunk_list = asset->add_child ("ChunkList");
471 auto chunk = chunk_list->add_child ("Chunk");
472 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
473 chunk->add_child("VolumeIndex")->add_child_text ("1");
474 chunk->add_child("Offset")->add_child_text ("0");
475 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
477 for (auto i: assets()) {
478 i->write_to_assetmap (asset_list, _directory);
481 doc.write_to_file_formatted (p.string (), "UTF-8");
491 string annotation_text,
492 shared_ptr<const CertificateChain> signer,
493 NameFormat name_format
497 throw MiscError ("Cannot write DCP with no CPLs.");
500 auto standard = std::accumulate (
501 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
502 [](Standard s, shared_ptr<CPL> c) {
503 if (s != c->standard()) {
504 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
510 for (auto i: cpls()) {
511 NameFormat::Map values;
513 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
519 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
520 _pkls.push_back (pkl);
521 for (auto i: assets()) {
522 i->add_to_pkl (pkl, _directory);
525 pkl = _pkls.front ();
528 NameFormat::Map values;
530 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
531 pkl->write (pkl_path, signer);
533 write_volindex (standard);
534 write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
538 vector<shared_ptr<CPL>>
545 vector<shared_ptr<Asset>>
546 DCP::assets (bool ignore_unresolved) const
548 vector<shared_ptr<Asset>> assets;
549 for (auto i: cpls()) {
550 assets.push_back (i);
551 for (auto j: i->reel_file_assets()) {
552 if (ignore_unresolved && !j->asset_ref().resolved()) {
556 auto const id = j->asset_ref().id();
557 auto already_got = false;
558 for (auto k: assets) {
565 auto o = j->asset_ref().asset();
566 assets.push_back (o);
567 /* More Interop special-casing */
568 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
570 sub->add_font_assets (assets);
580 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
581 vector<boost::filesystem::path>
582 DCP::directories_from_files (vector<boost::filesystem::path> files)
584 vector<boost::filesystem::path> d;
585 for (auto i: files) {
586 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
587 d.push_back (i.parent_path ());