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);
110 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
112 /* Read the ASSETMAP and PKL */
114 boost::filesystem::path asset_map_path;
115 if (boost::filesystem::exists(_directory / "ASSETMAP")) {
116 asset_map_path = _directory / "ASSETMAP";
117 } else if (boost::filesystem::exists(_directory / "ASSETMAP.xml")) {
118 asset_map_path = _directory / "ASSETMAP.xml";
120 boost::throw_exception(MissingAssetmapError(_directory));
123 _asset_map = AssetMap(asset_map_path);
124 auto const pkl_paths = _asset_map->pkl_paths();
125 auto const standard = _asset_map->standard();
127 if (pkl_paths.empty()) {
128 boost::throw_exception (XMLError ("No packing lists found in asset map"));
131 for (auto i: pkl_paths) {
132 _pkls.push_back(make_shared<PKL>(i));
136 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
137 _pkls - PKL objects for each PKL.
139 Read all the assets from the asset map.
142 /* Make a list of non-CPL/PKL assets so that we can resolve the references
145 vector<shared_ptr<Asset>> other_assets;
147 auto ids_and_paths = _asset_map->asset_ids_and_paths();
148 for (auto i: ids_and_paths) {
149 auto path = i.second;
151 if (path == _directory) {
152 /* I can't see how this is valid, but it's
153 been seen in the wild with a DCP that
154 claims to come from ClipsterDCI 5.10.0.5.
157 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
162 if (!boost::filesystem::exists(path)) {
164 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
169 /* Find the <Type> for this asset from the PKL that contains the asset */
170 optional<string> pkl_type;
171 for (auto j: _pkls) {
172 pkl_type = j->type(i.first);
179 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
180 * need to worry about it.
185 auto remove_parameters = [](string const& n) {
186 return n.substr(0, n.find(";"));
189 /* Remove any optional parameters (after ;) */
190 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
193 pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
194 pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(standard))) {
195 auto p = new xmlpp::DomParser;
197 p->parse_file (path.string());
198 } catch (std::exception& e) {
200 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
203 auto const root = p->get_document()->get_root_node()->get_name();
206 if (root == "CompositionPlaylist") {
207 auto cpl = make_shared<CPL>(path);
208 if (cpl->standard() != standard && notes) {
209 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
211 _cpls.push_back (cpl);
212 } else if (root == "DCSubtitle") {
213 if (standard == Standard::SMPTE && notes) {
214 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
216 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
219 *pkl_type == remove_parameters(PictureAsset::static_pkl_type(standard)) ||
220 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
221 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
222 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
225 bool found_threed_marked_as_twod = false;
226 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
227 if (found_threed_marked_as_twod && notes) {
228 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
230 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
231 other_assets.push_back (make_shared<FontAsset>(i.first, path));
232 } else if (*pkl_type == "image/png") {
233 /* It's an Interop PNG subtitle; let it go */
235 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
239 resolve_refs (other_assets);
241 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
243 for (auto i: cpls()) {
244 for (auto j: i->reel_file_assets()) {
245 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
246 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
255 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
257 for (auto i: cpls()) {
258 i->resolve_refs (assets);
264 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
267 auto b = other.cpls ();
269 if (a.size() != b.size()) {
270 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
278 while (j != b.end() && !(*j)->equals (i, opt, note)) {
292 DCP::add (shared_ptr<CPL> cpl)
294 _cpls.push_back (cpl);
299 DCP::any_encrypted () const
301 for (auto i: cpls()) {
302 if (i->any_encrypted()) {
312 DCP::all_encrypted () const
314 for (auto i: cpls()) {
315 if (!i->all_encrypted()) {
325 DCP::add (DecryptedKDM const & kdm)
327 auto keys = kdm.keys();
328 for (auto cpl: cpls()) {
329 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
336 /** Write the VOLINDEX file.
337 * @param standard DCP standard to use (INTEROP or SMPTE)
340 DCP::write_volindex (Standard standard) const
344 case Standard::INTEROP:
347 case Standard::SMPTE:
355 xmlpp::Element* root;
358 case Standard::INTEROP:
359 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
361 case Standard::SMPTE:
362 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
368 root->add_child("Index")->add_child_text ("1");
369 doc.write_to_file_formatted (p.string (), "UTF-8");
374 DCP::write_xml (shared_ptr<const CertificateChain> signer, NameFormat name_format)
377 throw MiscError ("Cannot write DCP with no CPLs.");
380 auto standard = std::accumulate (
381 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
382 [](Standard s, shared_ptr<CPL> c) {
383 if (s != c->standard()) {
384 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
390 for (auto i: cpls()) {
391 NameFormat::Map values;
393 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
400 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
401 _new_issue_date.get_value_or(LocalTime().as_string()),
402 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
403 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
408 auto pkl = _pkls.front();
410 /* The assets may have changed since we read the PKL, so re-add them */
412 for (auto asset: assets()) {
413 asset->add_to_pkl(pkl, _directory);
416 NameFormat::Map values;
418 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
419 pkl->write (pkl_path, signer);
422 _asset_map = AssetMap(
424 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
425 _new_issue_date.get_value_or(LocalTime().as_string()),
426 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
427 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
431 /* The assets may have changed since we read the asset map, so re-add them */
432 _asset_map->clear_assets();
433 _asset_map->add_asset(pkl->id(), pkl_path, true);
434 for (auto asset: assets()) {
435 asset->add_to_assetmap(*_asset_map, _directory);
438 _asset_map->write_xml(
439 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
442 write_volindex (standard);
446 vector<shared_ptr<CPL>>
453 vector<shared_ptr<Asset>>
454 DCP::assets (bool ignore_unresolved) const
456 vector<shared_ptr<Asset>> assets;
457 for (auto i: cpls()) {
458 assets.push_back (i);
459 for (auto j: i->reel_file_assets()) {
460 if (ignore_unresolved && !j->asset_ref().resolved()) {
464 auto const id = j->asset_ref().id();
465 auto already_got = false;
466 for (auto k: assets) {
473 auto o = j->asset_ref().asset();
474 assets.push_back (o);
475 /* More Interop special-casing */
476 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
478 sub->add_font_assets (assets);
488 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
489 vector<boost::filesystem::path>
490 DCP::directories_from_files (vector<boost::filesystem::path> files)
492 vector<boost::filesystem::path> d;
493 for (auto i: files) {
494 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
495 d.push_back (i.parent_path ());
503 DCP::set_issuer(string issuer)
505 for (auto pkl: _pkls) {
506 pkl->set_issuer(issuer);
509 _asset_map->set_issuer(issuer);
511 _new_issuer = issuer;
516 DCP::set_creator(string creator)
518 for (auto pkl: _pkls) {
519 pkl->set_creator(creator);
522 _asset_map->set_creator(creator);
524 _new_creator = creator;
529 DCP::set_issue_date(string issue_date)
531 for (auto pkl: _pkls) {
532 pkl->set_issue_date(issue_date);
535 _asset_map->set_issue_date(issue_date);
537 _new_issue_date = issue_date;
542 DCP::set_annotation_text(string annotation_text)
544 for (auto pkl: _pkls) {
545 pkl->set_annotation_text(annotation_text);
548 _asset_map->set_annotation_text(annotation_text);
550 _new_annotation_text = annotation_text;