2 Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic 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 DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "compose.hpp"
27 #include <dcp/interop_subtitle_asset.h>
28 #include <dcp/filesystem.h>
29 #include <dcp/font_asset.h>
30 #include <dcp/mono_picture_asset.h>
32 #include <dcp/reel_atmos_asset.h>
33 #include <dcp/reel_closed_caption_asset.h>
34 #include <dcp/reel_file_asset.h>
35 #include <dcp/reel_picture_asset.h>
36 #include <dcp/reel_sound_asset.h>
37 #include <dcp/reel_subtitle_asset.h>
38 #include <dcp/smpte_subtitle_asset.h>
39 #include <dcp/sound_asset.h>
40 #include <dcp/stereo_picture_asset.h>
41 #include <boost/optional.hpp>
48 using std::dynamic_pointer_cast;
49 using std::shared_ptr;
51 using std::make_shared;
53 using boost::optional;
57 help(std::function<void (string)> out)
59 out(String::compose("Syntax: %1 [OPTION} <cpl-file|ID> [<cpl-file|ID> ... ]", program_name));
60 out(" -V, --version show libdcp version");
61 out(" -h, --help show this help");
62 out(" -o, --output output directory");
63 out(" -l, --hard-link using hard links instead of copying");
64 out(" -s, --soft-link using soft links instead of copying");
65 out(" -d, --assets-dir look in this directory for assets (can be given more than once)");
66 out(" -r, --rename rename all files to <uuid>.<mxf|xml>");
67 out(" --config <dir> directory containing config.xml and cinemas.xml");
72 map_cli(int argc, char* argv[], std::function<void (string)> out)
74 optional<boost::filesystem::path> output_dir;
75 bool hard_link = false;
76 bool soft_link = false;
78 vector<boost::filesystem::path> assets_dir;
79 optional<boost::filesystem::path> config_dir;
81 /* This makes it possible to call getopt several times in the same executable, for tests */
86 static struct option long_options[] = {
87 { "help", no_argument, 0, 'h' },
88 { "output", required_argument, 0, 'o' },
89 { "hard-link", no_argument, 0, 'l' },
90 { "soft-link", no_argument, 0, 's' },
91 { "assets-dir", required_argument, 0, 'd' },
92 { "rename", no_argument, 0, 'r' },
93 { "config", required_argument, 0, 'c' },
97 int c = getopt_long(argc, argv, "ho:lsd:rc:", long_options, &option_index);
101 } else if (c == '?' || c == ':') {
119 assets_dir.push_back(optarg);
130 program_name = argv[0];
132 if (argc <= optind) {
138 State::override_path = *config_dir;
141 vector<string> cpl_filenames_or_ids;
142 for (int i = optind; i < argc; ++i) {
143 cpl_filenames_or_ids.push_back(argv[i]);
146 if (cpl_filenames_or_ids.empty()) {
147 return string{"No CPL specified."};
151 return string{"Missing -o or --output"};
154 if (dcp::filesystem::exists(*output_dir)) {
155 return String::compose("Output directory %1 already exists.", *output_dir);
158 if (hard_link && soft_link) {
159 return string{"Specify either -s,--soft-link or -l,--hard-link, not both."};
162 boost::system::error_code ec;
163 dcp::filesystem::create_directory(*output_dir, ec);
165 return String::compose("Could not create output directory %1: %2", *output_dir, ec.message());
168 /* Find all the assets in the asset directories. This assumes that the asset directories are in fact
169 * DCPs (with AssetMaps and so on). We could search for assets ourselves here but interop fonts are
170 * a little tricky because they don't contain their own UUID within the DCP.
172 vector<shared_ptr<dcp::Asset>> assets;
173 for (auto dir: assets_dir) {
176 auto dcp_assets = dcp.assets(true);
177 std::copy(dcp_assets.begin(), dcp_assets.end(), back_inserter(assets));
180 dcp::DCP dcp(*output_dir);
182 /* Find all the CPLs */
183 vector<shared_ptr<dcp::CPL>> cpls;
184 for (auto filename_or_id: cpl_filenames_or_ids) {
185 if (boost::filesystem::exists(filename_or_id)) {
187 auto cpl = make_shared<dcp::CPL>(filename_or_id);
188 cpl->resolve_refs(assets);
190 } catch (std::exception& e) {
191 return String::compose("Could not read CPL %1: %2", filename_or_id, e.what());
194 auto cpl_iter = std::find_if(assets.begin(), assets.end(), [filename_or_id](shared_ptr<dcp::Asset> asset) {
195 return asset->id() == filename_or_id;
197 if (cpl_iter == assets.end()) {
198 return String::compose("Could not find CPL with ID %1", filename_or_id);
200 if (auto cpl = dynamic_pointer_cast<dcp::CPL>(*cpl_iter)) {
201 cpl->resolve_refs(assets);
204 return String::compose("Could not find CPL with ID %1", filename_or_id);
209 class CopyError : public std::runtime_error
212 CopyError(std::string message) : std::runtime_error(message) {}
215 vector<string> already_copied;
218 boost::filesystem::path input_path,
219 boost::filesystem::path output_path,
223 dcp::filesystem::create_directories(output_path.parent_path());
225 boost::system::error_code ec;
227 dcp::filesystem::create_hard_link(input_path, output_path, ec);
229 throw CopyError(String::compose("Could not hard-link asset %1: %2", input_path.string(), ec.message()));
231 } else if (soft_link) {
232 dcp::filesystem::create_symlink(input_path, output_path, ec);
234 throw CopyError(String::compose("Could not soft-link asset %1: %2", input_path.string(), ec.message()));
237 dcp::filesystem::copy_file(input_path, output_path, ec);
239 throw CopyError(String::compose("Could not copy asset %1: %2", input_path.string(), ec.message()));
244 auto maybe_copy = [&assets, &already_copied, output_dir, copy](
249 boost::optional<boost::filesystem::path> extra = boost::none
252 if (std::find(already_copied.begin(), already_copied.end(), asset_id) != already_copied.end()) {
256 auto iter = std::find_if(assets.begin(), assets.end(), [asset_id](shared_ptr<const dcp::Asset> a) { return a->id() == asset_id; });
257 if (iter != assets.end()) {
258 DCP_ASSERT((*iter)->file());
260 auto const input_path = (*iter)->file().get();
261 boost::filesystem::path output_path = *output_dir;
263 output_path /= *extra;
267 output_path /= String::compose("%1%2", (*iter)->id(), dcp::filesystem::extension((*iter)->file().get()));
268 (*iter)->rename_file(output_path);
270 output_path /= (*iter)->file()->filename();
273 copy(input_path, output_path, hard_link, soft_link);
274 (*iter)->set_file_preserving_hash(output_path);
275 already_copied.push_back(asset_id);
277 boost::system::error_code ec;
278 dcp::filesystem::remove_all(*output_dir, ec);
279 throw CopyError(String::compose("Could not find required asset %1", asset_id));
283 auto maybe_copy_from_reel = [output_dir, &maybe_copy](
284 shared_ptr<dcp::ReelFileAsset> asset,
288 boost::optional<boost::filesystem::path> extra = boost::none
290 if (asset && asset->asset_ref().resolved()) {
291 maybe_copy(asset->asset_ref().id(), rename, hard_link, soft_link, extra);
295 auto maybe_copy_font_and_images = [&maybe_copy, output_dir, copy](shared_ptr<const dcp::SubtitleAsset> asset, bool rename, bool hard_link, bool soft_link) {
296 auto interop = dynamic_pointer_cast<const dcp::InteropSubtitleAsset>(asset);
297 boost::optional<boost::filesystem::path> extra;
299 extra = interop->id();
300 for (auto font_asset: interop->font_assets()) {
301 maybe_copy(font_asset->id(), rename, hard_link, soft_link, extra);
303 for (auto subtitle: interop->subtitles()) {
304 if (auto image = dynamic_pointer_cast<const dcp::SubtitleImage>(subtitle)) {
305 auto const output_path = *output_dir / asset->id() / image->file()->filename();
306 copy(*image->file(), output_path, hard_link, soft_link);
313 /* Copy assets that the CPLs need */
315 for (auto cpl: cpls) {
316 for (auto reel: cpl->reels()) {
317 maybe_copy_from_reel(reel->main_picture(), rename, hard_link, soft_link);
318 maybe_copy_from_reel(reel->main_sound(), rename, hard_link, soft_link);
319 if (reel->main_subtitle()) {
320 auto extra = maybe_copy_font_and_images(reel->main_subtitle()->asset(), rename, hard_link, soft_link);
321 maybe_copy_from_reel(reel->main_subtitle(), rename, hard_link, soft_link, extra);
323 for (auto ccap: reel->closed_captions()) {
324 auto extra = maybe_copy_font_and_images(ccap->asset(), rename, hard_link, soft_link);
325 maybe_copy_from_reel(ccap, rename, hard_link, soft_link, extra);
327 maybe_copy_from_reel(reel->atmos(), rename, hard_link, soft_link);
332 } catch (CopyError& e) {
333 return string{e.what()};
336 dcp.resolve_refs(assets);
337 dcp.set_annotation_text(cpls[0]->annotation_text().get_value_or(""));
339 dcp.set_creator(Config::instance()->dcp_creator());
340 dcp.set_issuer(Config::instance()->dcp_issuer());
341 dcp.write_xml(Config::instance()->signer_chain());
342 } catch (dcp::UnresolvedRefError& e) {
343 return String::compose("%1\nPerhaps you need to give a -d parameter to say where this asset is located.", e.what());