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>
80 using std::make_shared;
82 using std::shared_ptr;
83 using std::dynamic_pointer_cast;
84 using boost::optional;
85 using boost::algorithm::starts_with;
89 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
90 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
91 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
92 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
95 DCP::DCP (boost::filesystem::path directory)
96 : _directory (directory)
98 if (!boost::filesystem::exists (directory)) {
99 boost::filesystem::create_directories (directory);
102 _directory = boost::filesystem::canonical (_directory);
107 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
109 /* Read the ASSETMAP and PKL */
111 if (boost::filesystem::exists (_directory / "ASSETMAP")) {
112 _asset_map = _directory / "ASSETMAP";
113 } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
114 _asset_map = _directory / "ASSETMAP.xml";
116 boost::throw_exception (MissingAssetmapError(_directory));
119 cxml::Document asset_map ("AssetMap");
121 asset_map.read_file (_asset_map.get());
122 if (asset_map.namespace_uri() == assetmap_interop_ns) {
123 _standard = Standard::INTEROP;
124 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
125 _standard = Standard::SMPTE;
127 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
130 auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
131 map<string, boost::filesystem::path> paths;
132 vector<boost::filesystem::path> pkl_paths;
133 for (auto i: asset_nodes) {
134 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
135 boost::throw_exception (XMLError ("unsupported asset chunk count"));
137 auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
138 if (starts_with (p, "file://")) {
141 switch (*_standard) {
142 case Standard::INTEROP:
143 if (i->optional_node_child("PackingList")) {
144 pkl_paths.push_back (p);
146 paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
149 case Standard::SMPTE:
151 auto pkl_bool = i->optional_string_child("PackingList");
152 if (pkl_bool && *pkl_bool == "true") {
153 pkl_paths.push_back (p);
155 paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
162 if (pkl_paths.empty()) {
163 boost::throw_exception (XMLError ("No packing lists found in asset map"));
166 for (auto i: pkl_paths) {
167 _pkls.push_back (make_shared<PKL>(_directory / i));
171 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
172 _pkls - PKL objects for each PKL.
174 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 vector<shared_ptr<Asset>> other_assets;
182 for (auto i: paths) {
183 auto path = _directory / i.second;
185 if (i.second.empty()) {
186 /* I can't see how this is valid, but it's
187 been seen in the wild with a DCP that
188 claims to come from ClipsterDCI 5.10.0.5.
191 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
196 if (!boost::filesystem::exists(path)) {
198 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
203 /* Find the <Type> for this asset from the PKL that contains the asset */
204 optional<string> pkl_type;
205 for (auto j: _pkls) {
206 pkl_type = j->type(i.first);
213 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
214 * need to worry about it.
219 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
220 auto p = new xmlpp::DomParser;
222 p->parse_file (path.string());
223 } catch (std::exception& e) {
225 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
228 auto const root = p->get_document()->get_root_node()->get_name();
231 if (root == "CompositionPlaylist") {
232 auto cpl = make_shared<CPL>(path);
233 if (_standard && cpl->standard() != _standard.get() && notes) {
234 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
236 _cpls.push_back (cpl);
237 } else if (root == "DCSubtitle") {
238 if (_standard && _standard.get() == Standard::SMPTE && notes) {
239 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
241 other_assets.push_back (make_shared<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 (make_shared<FontAsset>(i.first, path));
253 } else if (*pkl_type == "image/png") {
254 /* It's an Interop PNG subtitle; let it go */
256 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
260 resolve_refs (other_assets);
262 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
264 for (auto i: cpls()) {
265 for (auto j: i->reel_file_assets()) {
266 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
267 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
276 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
278 for (auto i: cpls()) {
279 i->resolve_refs (assets);
285 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
288 auto b = other.cpls ();
290 if (a.size() != b.size()) {
291 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
299 while (j != b.end() && !(*j)->equals (i, opt, note)) {
313 DCP::add (shared_ptr<CPL> cpl)
315 _cpls.push_back (cpl);
320 DCP::any_encrypted () const
322 for (auto i: cpls()) {
323 if (i->any_encrypted()) {
333 DCP::all_encrypted () const
335 for (auto i: cpls()) {
336 if (!i->all_encrypted()) {
346 DCP::add (DecryptedKDM const & kdm)
348 auto keys = kdm.keys ();
350 for (auto i: cpls()) {
351 for (auto const& j: kdm.keys()) {
352 if (j.cpl_id() == i->id()) {
360 /** Write the VOLINDEX file.
361 * @param standard DCP standard to use (INTEROP or SMPTE)
364 DCP::write_volindex (Standard standard) const
368 case Standard::INTEROP:
371 case Standard::SMPTE:
379 xmlpp::Element* root;
382 case Standard::INTEROP:
383 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
385 case Standard::SMPTE:
386 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
392 root->add_child("Index")->add_child_text ("1");
393 doc.write_to_file_formatted (p.string (), "UTF-8");
398 DCP::write_assetmap (
399 Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
400 string issuer, string creator, string issue_date, string annotation_text
406 case Standard::INTEROP:
409 case Standard::SMPTE:
417 xmlpp::Element* root;
420 case Standard::INTEROP:
421 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
423 case Standard::SMPTE:
424 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
430 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
431 root->add_child("AnnotationText")->add_child_text (annotation_text);
434 case Standard::INTEROP:
435 root->add_child("VolumeCount")->add_child_text ("1");
436 root->add_child("IssueDate")->add_child_text (issue_date);
437 root->add_child("Issuer")->add_child_text (issuer);
438 root->add_child("Creator")->add_child_text (creator);
440 case Standard::SMPTE:
441 root->add_child("Creator")->add_child_text (creator);
442 root->add_child("VolumeCount")->add_child_text ("1");
443 root->add_child("IssueDate")->add_child_text (issue_date);
444 root->add_child("Issuer")->add_child_text (issuer);
450 auto asset_list = root->add_child ("AssetList");
452 auto asset = asset_list->add_child ("Asset");
453 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
454 asset->add_child("PackingList")->add_child_text ("true");
455 auto chunk_list = asset->add_child ("ChunkList");
456 auto chunk = chunk_list->add_child ("Chunk");
457 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
458 chunk->add_child("VolumeIndex")->add_child_text ("1");
459 chunk->add_child("Offset")->add_child_text ("0");
460 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
462 for (auto i: assets()) {
463 i->write_to_assetmap (asset_list, _directory);
466 doc.write_to_file_formatted (p.string (), "UTF-8");
476 string annotation_text,
477 shared_ptr<const CertificateChain> signer,
478 NameFormat name_format
482 throw MiscError ("Cannot write DCP with no CPLs.");
485 auto standard = std::accumulate (
486 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
487 [](Standard s, shared_ptr<CPL> c) {
488 if (s != c->standard()) {
489 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
495 for (auto i: cpls()) {
496 NameFormat::Map values;
498 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
504 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
505 _pkls.push_back (pkl);
506 for (auto i: assets()) {
507 i->add_to_pkl (pkl, _directory);
510 pkl = _pkls.front ();
513 NameFormat::Map values;
515 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
516 pkl->write (pkl_path, signer);
518 write_volindex (standard);
519 write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
523 vector<shared_ptr<CPL>>
530 vector<shared_ptr<Asset>>
531 DCP::assets (bool ignore_unresolved) const
533 vector<shared_ptr<Asset>> assets;
534 for (auto i: cpls()) {
535 assets.push_back (i);
536 for (auto j: i->reel_file_assets()) {
537 if (ignore_unresolved && !j->asset_ref().resolved()) {
541 auto const id = j->asset_ref().id();
542 auto already_got = false;
543 for (auto k: assets) {
550 auto o = j->asset_ref().asset();
551 assets.push_back (o);
552 /* More Interop special-casing */
553 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
555 sub->add_font_assets (assets);
565 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
566 vector<boost::filesystem::path>
567 DCP::directories_from_files (vector<boost::filesystem::path> files)
569 vector<boost::filesystem::path> d;
570 for (auto i: files) {
571 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
572 d.push_back (i.parent_path ());