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/algorithm/string.hpp>
74 #include <boost/filesystem.hpp>
80 using std::dynamic_pointer_cast;
84 using std::make_shared;
86 using std::shared_ptr;
89 using boost::algorithm::starts_with;
90 using boost::optional;
94 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
95 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
98 DCP::DCP (boost::filesystem::path directory)
99 : _directory (directory)
101 if (!boost::filesystem::exists (directory)) {
102 boost::filesystem::create_directories (directory);
105 _directory = boost::filesystem::canonical (_directory);
109 DCP::DCP(DCP&& other)
110 : _directory(std::move(other._directory))
111 , _cpls(std::move(other._cpls))
112 , _pkls(std::move(other._pkls))
113 , _asset_map(std::move(other._asset_map))
114 , _new_issuer(std::move(other._new_issuer))
115 , _new_creator(std::move(other._new_creator))
116 , _new_issue_date(std::move(other._new_issue_date))
117 , _new_annotation_text(std::move(other._new_annotation_text))
124 DCP::operator=(DCP&& other)
126 _directory = std::move(other._directory);
127 _cpls = std::move(other._cpls);
128 _pkls = std::move(other._pkls);
129 _asset_map = std::move(other._asset_map);
130 _new_issuer = std::move(other._new_issuer);
131 _new_creator = std::move(other._new_creator);
132 _new_issue_date = std::move(other._new_issue_date);
133 _new_annotation_text = std::move(other._new_annotation_text);
139 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
141 /* Read the ASSETMAP and PKL */
143 boost::filesystem::path asset_map_path;
144 if (boost::filesystem::exists(_directory / "ASSETMAP")) {
145 asset_map_path = _directory / "ASSETMAP";
146 } else if (boost::filesystem::exists(_directory / "ASSETMAP.xml")) {
147 asset_map_path = _directory / "ASSETMAP.xml";
149 boost::throw_exception(MissingAssetmapError(_directory));
152 _asset_map = AssetMap(asset_map_path);
153 auto const pkl_paths = _asset_map->pkl_paths();
154 auto const standard = _asset_map->standard();
156 if (pkl_paths.empty()) {
157 boost::throw_exception (XMLError ("No packing lists found in asset map"));
160 for (auto i: pkl_paths) {
161 _pkls.push_back(make_shared<PKL>(i));
165 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
166 _pkls - PKL objects for each PKL.
168 Read all the assets from the asset map.
171 /* Make a list of non-CPL/PKL assets so that we can resolve the references
174 vector<shared_ptr<Asset>> other_assets;
176 auto ids_and_paths = _asset_map->asset_ids_and_paths();
177 for (auto id_and_path: ids_and_paths) {
178 auto const id = id_and_path.first;
179 auto const path = id_and_path.second;
181 if (path == _directory) {
182 /* I can't see how this is valid, but it's
183 been seen in the wild with a DCP that
184 claims to come from ClipsterDCI 5.10.0.5.
187 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
192 if (!boost::filesystem::exists(path)) {
194 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
199 /* Find the <Type> for this asset from the PKL that contains the asset */
200 optional<string> pkl_type;
201 for (auto j: _pkls) {
202 pkl_type = j->type(id);
209 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
210 * need to worry about it.
215 auto remove_parameters = [](string const& n) {
216 return n.substr(0, n.find(";"));
219 /* Remove any optional parameters (after ;) */
220 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
223 pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
224 pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(standard))) {
225 auto p = new xmlpp::DomParser;
227 p->parse_file (path.string());
228 } catch (std::exception& e) {
230 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
233 auto const root = p->get_document()->get_root_node()->get_name();
236 if (root == "CompositionPlaylist") {
237 auto cpl = make_shared<CPL>(path);
238 if (cpl->standard() != standard && notes) {
239 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
241 _cpls.push_back (cpl);
242 } else if (root == "DCSubtitle") {
243 if (standard == Standard::SMPTE && notes) {
244 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
246 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
249 *pkl_type == remove_parameters(PictureAsset::static_pkl_type(standard)) ||
250 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
251 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
252 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
255 bool found_threed_marked_as_twod = false;
256 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
257 if (found_threed_marked_as_twod && notes) {
258 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
260 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
261 other_assets.push_back(make_shared<FontAsset>(id, path));
262 } else if (*pkl_type == "image/png") {
263 /* It's an Interop PNG subtitle; let it go */
265 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
269 resolve_refs (other_assets);
271 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
273 for (auto i: cpls()) {
274 for (auto j: i->reel_file_assets()) {
275 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
276 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
285 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
287 for (auto i: cpls()) {
288 i->resolve_refs (assets);
294 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
297 auto b = other.cpls ();
299 if (a.size() != b.size()) {
300 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
308 while (j != b.end() && !(*j)->equals (i, opt, note)) {
322 DCP::add (shared_ptr<CPL> cpl)
324 _cpls.push_back (cpl);
329 DCP::any_encrypted () const
331 for (auto i: cpls()) {
332 if (i->any_encrypted()) {
342 DCP::all_encrypted () const
344 for (auto i: cpls()) {
345 if (!i->all_encrypted()) {
355 DCP::add (DecryptedKDM const & kdm)
357 auto keys = kdm.keys();
358 for (auto cpl: cpls()) {
359 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
366 /** Write the VOLINDEX file.
367 * @param standard DCP standard to use (INTEROP or SMPTE)
370 DCP::write_volindex (Standard standard) const
374 case Standard::INTEROP:
377 case Standard::SMPTE:
385 xmlpp::Element* root;
388 case Standard::INTEROP:
389 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
391 case Standard::SMPTE:
392 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
398 root->add_child("Index")->add_child_text ("1");
399 doc.write_to_file_formatted (p.string (), "UTF-8");
404 DCP::write_xml(shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors, NameFormat name_format)
407 throw MiscError ("Cannot write DCP with no CPLs.");
410 auto standard = std::accumulate (
411 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
412 [](Standard s, shared_ptr<CPL> c) {
413 if (s != c->standard()) {
414 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
420 for (auto i: cpls()) {
421 NameFormat::Map values;
423 i->write_xml(_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer, include_mca_subdescriptors);
430 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
431 _new_issue_date.get_value_or(LocalTime().as_string()),
432 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
433 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
438 auto pkl = _pkls.front();
440 /* The assets may have changed since we read the PKL, so re-add them */
442 for (auto asset: assets()) {
443 asset->add_to_pkl(pkl, _directory);
446 NameFormat::Map values;
448 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
449 pkl->write_xml (pkl_path, signer);
452 _asset_map = AssetMap(
454 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
455 _new_issue_date.get_value_or(LocalTime().as_string()),
456 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
457 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
461 /* The assets may have changed since we read the asset map, so re-add them */
462 _asset_map->clear_assets();
463 _asset_map->add_asset(pkl->id(), pkl_path, true);
464 for (auto asset: assets()) {
465 asset->add_to_assetmap(*_asset_map, _directory);
468 _asset_map->write_xml(
469 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
472 write_volindex (standard);
476 vector<shared_ptr<CPL>>
483 vector<shared_ptr<Asset>>
484 DCP::assets (bool ignore_unresolved) const
486 vector<shared_ptr<Asset>> assets;
487 for (auto i: cpls()) {
488 assets.push_back (i);
489 for (auto j: i->reel_file_assets()) {
490 if (ignore_unresolved && !j->asset_ref().resolved()) {
494 auto const id = j->asset_ref().id();
495 if (std::find_if(assets.begin(), assets.end(), [id](shared_ptr<Asset> asset) { return asset->id() == id; }) == assets.end()) {
496 auto o = j->asset_ref().asset();
497 assets.push_back (o);
498 /* More Interop special-casing */
499 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
501 add_to_container(assets, sub->font_assets());
511 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
512 vector<boost::filesystem::path>
513 DCP::directories_from_files (vector<boost::filesystem::path> files)
515 vector<boost::filesystem::path> d;
516 for (auto i: files) {
517 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
518 d.push_back (i.parent_path ());
526 DCP::set_issuer(string issuer)
528 for (auto pkl: _pkls) {
529 pkl->set_issuer(issuer);
532 _asset_map->set_issuer(issuer);
534 _new_issuer = issuer;
539 DCP::set_creator(string creator)
541 for (auto pkl: _pkls) {
542 pkl->set_creator(creator);
545 _asset_map->set_creator(creator);
547 _new_creator = creator;
552 DCP::set_issue_date(string issue_date)
554 for (auto pkl: _pkls) {
555 pkl->set_issue_date(issue_date);
558 _asset_map->set_issue_date(issue_date);
560 _new_issue_date = issue_date;
565 DCP::set_annotation_text(string annotation_text)
567 for (auto pkl: _pkls) {
568 pkl->set_annotation_text(annotation_text);
571 _asset_map->set_annotation_text(annotation_text);
573 _new_annotation_text = annotation_text;