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 i: ids_and_paths) {
178 auto path = i.second;
180 if (path == _directory) {
181 /* I can't see how this is valid, but it's
182 been seen in the wild with a DCP that
183 claims to come from ClipsterDCI 5.10.0.5.
186 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
191 if (!boost::filesystem::exists(path)) {
193 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
198 /* Find the <Type> for this asset from the PKL that contains the asset */
199 optional<string> pkl_type;
200 for (auto j: _pkls) {
201 pkl_type = j->type(i.first);
208 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
209 * need to worry about it.
214 auto remove_parameters = [](string const& n) {
215 return n.substr(0, n.find(";"));
218 /* Remove any optional parameters (after ;) */
219 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
222 pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
223 pkl_type == remove_parameters(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 (cpl->standard() != standard && 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::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 == remove_parameters(PictureAsset::static_pkl_type(standard)) ||
249 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
250 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
251 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
254 bool found_threed_marked_as_twod = false;
255 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
256 if (found_threed_marked_as_twod && notes) {
257 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
259 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
260 other_assets.push_back (make_shared<FontAsset>(i.first, path));
261 } else if (*pkl_type == "image/png") {
262 /* It's an Interop PNG subtitle; let it go */
264 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
268 resolve_refs (other_assets);
270 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
272 for (auto i: cpls()) {
273 for (auto j: i->reel_file_assets()) {
274 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
275 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
284 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
286 for (auto i: cpls()) {
287 i->resolve_refs (assets);
293 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
296 auto b = other.cpls ();
298 if (a.size() != b.size()) {
299 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
307 while (j != b.end() && !(*j)->equals (i, opt, note)) {
321 DCP::add (shared_ptr<CPL> cpl)
323 _cpls.push_back (cpl);
328 DCP::any_encrypted () const
330 for (auto i: cpls()) {
331 if (i->any_encrypted()) {
341 DCP::all_encrypted () const
343 for (auto i: cpls()) {
344 if (!i->all_encrypted()) {
354 DCP::add (DecryptedKDM const & kdm)
356 auto keys = kdm.keys();
357 for (auto cpl: cpls()) {
358 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
365 /** Write the VOLINDEX file.
366 * @param standard DCP standard to use (INTEROP or SMPTE)
369 DCP::write_volindex (Standard standard) const
373 case Standard::INTEROP:
376 case Standard::SMPTE:
384 xmlpp::Element* root;
387 case Standard::INTEROP:
388 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
390 case Standard::SMPTE:
391 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
397 root->add_child("Index")->add_child_text ("1");
398 doc.write_to_file_formatted (p.string (), "UTF-8");
403 DCP::write_xml (shared_ptr<const CertificateChain> signer, NameFormat name_format)
406 throw MiscError ("Cannot write DCP with no CPLs.");
409 auto standard = std::accumulate (
410 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
411 [](Standard s, shared_ptr<CPL> c) {
412 if (s != c->standard()) {
413 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
419 for (auto i: cpls()) {
420 NameFormat::Map values;
422 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
429 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
430 _new_issue_date.get_value_or(LocalTime().as_string()),
431 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
432 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
437 auto pkl = _pkls.front();
439 /* The assets may have changed since we read the PKL, so re-add them */
441 for (auto asset: assets()) {
442 asset->add_to_pkl(pkl, _directory);
445 NameFormat::Map values;
447 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
448 pkl->write_xml (pkl_path, signer);
451 _asset_map = AssetMap(
453 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
454 _new_issue_date.get_value_or(LocalTime().as_string()),
455 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
456 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
460 /* The assets may have changed since we read the asset map, so re-add them */
461 _asset_map->clear_assets();
462 _asset_map->add_asset(pkl->id(), pkl_path, true);
463 for (auto asset: assets()) {
464 asset->add_to_assetmap(*_asset_map, _directory);
467 _asset_map->write_xml(
468 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
471 write_volindex (standard);
475 vector<shared_ptr<CPL>>
482 vector<shared_ptr<Asset>>
483 DCP::assets (bool ignore_unresolved) const
485 vector<shared_ptr<Asset>> assets;
486 for (auto i: cpls()) {
487 assets.push_back (i);
488 for (auto j: i->reel_file_assets()) {
489 if (ignore_unresolved && !j->asset_ref().resolved()) {
493 auto const id = j->asset_ref().id();
494 auto already_got = false;
495 for (auto k: assets) {
502 auto o = j->asset_ref().asset();
503 assets.push_back (o);
504 /* More Interop special-casing */
505 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
507 sub->add_font_assets (assets);
517 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
518 vector<boost::filesystem::path>
519 DCP::directories_from_files (vector<boost::filesystem::path> files)
521 vector<boost::filesystem::path> d;
522 for (auto i: files) {
523 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
524 d.push_back (i.parent_path ());
532 DCP::set_issuer(string issuer)
534 for (auto pkl: _pkls) {
535 pkl->set_issuer(issuer);
538 _asset_map->set_issuer(issuer);
540 _new_issuer = issuer;
545 DCP::set_creator(string creator)
547 for (auto pkl: _pkls) {
548 pkl->set_creator(creator);
551 _asset_map->set_creator(creator);
553 _new_creator = creator;
558 DCP::set_issue_date(string issue_date)
560 for (auto pkl: _pkls) {
561 pkl->set_issue_date(issue_date);
564 _asset_map->set_issue_date(issue_date);
566 _new_issue_date = issue_date;
571 DCP::set_annotation_text(string annotation_text)
573 for (auto pkl: _pkls) {
574 pkl->set_annotation_text(annotation_text);
577 _asset_map->set_annotation_text(annotation_text);
579 _new_annotation_text = annotation_text;