Add dcpomatic2_map tool (#2445).
authorCarl Hetherington <cth@carlh.net>
Fri, 17 Feb 2023 23:15:30 +0000 (00:15 +0100)
committerCarl Hetherington <cth@carlh.net>
Sat, 13 May 2023 06:54:58 +0000 (08:54 +0200)
platform/osx/make_dmg.sh
platform/windows/wscript
run/dcpomatic_map [new file with mode: 0755]
src/lib/map_cli.cc [new file with mode: 0644]
src/lib/map_cli.h [new file with mode: 0644]
src/lib/wscript
src/tools/dcpomatic_map.cc [new file with mode: 0644]
src/tools/wscript
test/map_cli_test.cc [new file with mode: 0644]
test/test.cc
test/wscript

index 7b2f330a235b3a31b74fad48aad40bbe73a26827..3775508915399f525786faa2c6a63fcd5230f345 100644 (file)
@@ -487,6 +487,7 @@ if [[ "$BUILD" == *main* ]]; then
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2 "$approot/MacOS"
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_cli "$approot/MacOS"
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_create "$approot/MacOS"
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2 "$approot/MacOS"
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_cli "$approot/MacOS"
        copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_create "$approot/MacOS"
+       copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_map "$approot/MacOS"
        copy $ROOT bin/ffprobe "$approot/MacOS"
        copy $ROOT src/openssl/apps/openssl "$approot/MacOS"
        copy_verify
        copy $ROOT bin/ffprobe "$approot/MacOS"
        copy $ROOT src/openssl/apps/openssl "$approot/MacOS"
        copy_verify
index b873446465b8cfd8459555463f5201c2d1362dc0..e544554270a6868848a91445291da25615db6de8 100644 (file)
@@ -13,6 +13,7 @@ def write_installer(bits, dcpomatic_version, debug, disk):
         ('playlist', 'Playlist Editor'),
         ('combiner', 'Combiner'),
         ('editor', 'Editor'),
         ('playlist', 'Playlist Editor'),
         ('combiner', 'Combiner'),
         ('editor', 'Editor'),
+        ('map', 'Map'),
     ]
 
     if disk:
     ]
 
     if disk:
diff --git a/run/dcpomatic_map b/run/dcpomatic_map
new file mode 100755 (executable)
index 0000000..f81be30
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source $DIR/environment
+
+if [ "$1" == "--debug" ]; then
+    shift
+    gdb --args build/src/tools/dcpomatic2_map "$@"
+elif [ "$1" == "--valgrind" ]; then
+    shift
+    valgrind --tool="memcheck" --leak-check=full --show-reachable=yes build/src/tools/dcpomatic2_map "$@"
+else
+    build/src/tools/dcpomatic2_map "$@"
+fi
diff --git a/src/lib/map_cli.cc b/src/lib/map_cli.cc
new file mode 100644 (file)
index 0000000..1b99afa
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "compose.hpp"
+#include "config.h"
+#include "util.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/interop_subtitle_asset.h>
+#include <dcp/font_asset.h>
+#include <dcp/mono_picture_asset.h>
+#include <dcp/reel.h>
+#include <dcp/reel_atmos_asset.h>
+#include <dcp/reel_closed_caption_asset.h>
+#include <dcp/reel_file_asset.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <dcp/reel_subtitle_asset.h>
+#include <dcp/smpte_subtitle_asset.h>
+#include <dcp/sound_asset.h>
+#include <dcp/stereo_picture_asset.h>
+#include <boost/optional.hpp>
+#include <getopt.h>
+#include <algorithm>
+#include <memory>
+#include <string>
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::string;
+using std::make_shared;
+using std::vector;
+using boost::optional;
+
+
+static void
+help(std::function<void (string)> out)
+{
+       out(String::compose("Syntax: %1 [OPTION} <cpl-file> [<cpl-file> ... ]", program_name));
+       out("  -V, --version    show libdcp version");
+       out("  -h, --help       show this help");
+       out("  -o, --output     output directory");
+       out("  -l, --hard-link  using hard links instead of copying");
+       out("  -s, --soft-link  using soft links instead of copying");
+       out("  -d, --assets-dir look in this directory for assets (can be given more than once)");
+       out("  -r, --rename     rename all files to <uuid>.<mxf|xml>");
+}
+
+
+optional<string>
+map_cli(int argc, char* argv[], std::function<void (string)> out)
+{
+       optional<boost::filesystem::path> output_dir;
+       bool hard_link = false;
+       bool soft_link = false;
+       bool rename = false;
+       vector<boost::filesystem::path> assets_dir;
+
+       /* This makes it possible to call getopt several times in the same executable, for tests */
+       optind = 0;
+
+       int option_index = 0;
+       while (true) {
+               static struct option long_options[] = {
+                       { "help", no_argument, 0, 'h' },
+                       { "output", required_argument, 0, 'o' },
+                       { "hard-link", no_argument, 0, 'l' },
+                       { "soft-link", no_argument, 0, 's' },
+                       { "assets-dir", required_argument, 0, 'd' },
+                       { "rename", no_argument, 0, 'r' },
+                       { 0, 0, 0, 0 }
+               };
+
+               int c = getopt_long(argc, argv, "ho:lsd:r", long_options, &option_index);
+
+               if (c == -1) {
+                       break;
+               } else if (c == '?' || c == ':') {
+                       exit(EXIT_FAILURE);
+               }
+
+               switch (c) {
+               case 'h':
+                       help(out);
+                       exit(EXIT_SUCCESS);
+               case 'o':
+                       output_dir = optarg;
+                       break;
+               case 'l':
+                       hard_link = true;
+                       break;
+               case 's':
+                       soft_link = true;
+                       break;
+               case 'd':
+                       assets_dir.push_back(optarg);
+                       break;
+               case 'r':
+                       rename = true;
+                       break;
+               }
+       }
+
+       program_name = argv[0];
+
+       if (argc <= optind) {
+               help(out);
+               exit(EXIT_FAILURE);
+       }
+
+       vector<boost::filesystem::path> cpl_filenames;
+       for (int i = optind; i < argc; ++i) {
+               cpl_filenames.push_back(argv[i]);
+       }
+
+       if (cpl_filenames.empty()) {
+               return string{"No CPL specified."};
+       }
+
+       if (!output_dir) {
+               return string{"Missing -o or --output"};
+       }
+
+       if (boost::filesystem::exists(*output_dir)) {
+               return String::compose("Output directory %1 already exists.", *output_dir);
+       }
+
+       if (hard_link && soft_link) {
+               return string{"Specify either -s,--soft-link or -l,--hard-link, not both."};
+       }
+
+       boost::system::error_code ec;
+       boost::filesystem::create_directory(*output_dir, ec);
+       if (ec) {
+               return String::compose("Could not create output directory %1: %2", *output_dir, ec.message());
+       }
+
+       /* Find all the assets in the asset directories.  This assumes that the asset directories are in fact
+        * DCPs (with AssetMaps and so on).  We could search for assets ourselves here but interop fonts are
+        * a little tricky because they don't contain their own UUID within the DCP.
+        */
+       vector<shared_ptr<dcp::Asset>> assets;
+       for (auto dir: assets_dir) {
+               dcp::DCP dcp(dir);
+               dcp.read();
+               auto dcp_assets = dcp.assets(true);
+               std::copy(dcp_assets.begin(), dcp_assets.end(), back_inserter(assets));
+       }
+
+       dcp::DCP dcp(*output_dir);
+
+       /* Find all the CPLs */
+       vector<shared_ptr<dcp::CPL>> cpls;
+       for (auto filename: cpl_filenames) {
+               try {
+                       auto cpl = make_shared<dcp::CPL>(filename);
+                       cpl->resolve_refs(assets);
+                       cpls.push_back(cpl);
+               } catch (std::exception& e) {
+                       return String::compose("Could not read CPL %1: %2", filename, e.what());
+               }
+       }
+
+       class CopyError : public std::runtime_error
+       {
+       public:
+               CopyError(std::string message) : std::runtime_error(message) {}
+       };
+
+       auto maybe_copy = [&assets, output_dir](
+               string asset_id,
+               bool rename,
+               bool hard_link,
+               bool soft_link,
+               boost::optional<boost::filesystem::path> extra = boost::none
+               ) {
+               auto iter = std::find_if(assets.begin(), assets.end(), [asset_id](shared_ptr<const dcp::Asset> a) { return a->id() == asset_id; });
+               if (iter != assets.end()) {
+                       DCP_ASSERT((*iter)->file());
+
+                       auto const input_path = (*iter)->file().get();
+                       boost::filesystem::path output_path = *output_dir;
+                       if (extra) {
+                               output_path /= *extra;
+                       }
+
+                       if (rename) {
+                               output_path /= String::compose("%1%2", (*iter)->id(), boost::filesystem::extension((*iter)->file().get()));
+                               (*iter)->rename_file(output_path);
+                       } else {
+                               output_path /= (*iter)->file()->filename();
+                       }
+
+                       boost::filesystem::create_directories(output_path.parent_path());
+
+                       boost::system::error_code ec;
+                       if (hard_link) {
+                               boost::filesystem::create_hard_link(input_path, output_path, ec);
+                               if (ec) {
+                                       throw CopyError(String::compose("Could not hard-link asset %1: %2", input_path.string(), ec.message()));
+                               }
+                       } else if (soft_link) {
+                               boost::filesystem::create_symlink(input_path, output_path, ec);
+                               if (ec) {
+                                       throw CopyError(String::compose("Could not soft-link asset %1: %2", input_path.string(), ec.message()));
+                               }
+                       } else {
+                               boost::filesystem::copy_file(input_path, output_path, ec);
+                               if (ec) {
+                                       throw CopyError(String::compose("Could not copy asset %1: %2", input_path.string(), ec.message()));
+                               }
+                       }
+                       (*iter)->set_file(output_path);
+               } else {
+                       boost::system::error_code ec;
+                       boost::filesystem::remove_all(*output_dir, ec);
+                       throw CopyError(String::compose("Could not find required asset %1", asset_id));
+               }
+       };
+
+       auto maybe_copy_from_reel = [output_dir, &maybe_copy](
+               shared_ptr<dcp::ReelFileAsset> asset,
+               bool rename,
+               bool hard_link,
+               bool soft_link,
+               boost::optional<boost::filesystem::path> extra = boost::none
+               ) {
+               if (asset && asset->asset_ref().resolved()) {
+                       maybe_copy(asset->asset_ref().id(), rename, hard_link, soft_link, extra);
+               }
+       };
+
+       /* Copy assets that the CPLs need */
+       try {
+               for (auto cpl: cpls) {
+                       for (auto reel: cpl->reels()) {
+                               maybe_copy_from_reel(reel->main_picture(), rename, hard_link, soft_link);
+                               maybe_copy_from_reel(reel->main_sound(), rename, hard_link, soft_link);
+                               boost::optional<boost::filesystem::path> extra;
+                               if (reel->main_subtitle()) {
+                                       auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(reel->main_subtitle()->asset());
+                                       if (interop) {
+                                               extra = interop->id();
+                                               for (auto font_asset: interop->font_assets()) {
+                                                       maybe_copy(font_asset->id(), rename, hard_link, soft_link, extra);
+                                               }
+                                       }
+                               }
+                               maybe_copy_from_reel(reel->main_subtitle(), rename, hard_link, soft_link, extra);
+                               for (auto ccap: reel->closed_captions()) {
+                                       maybe_copy_from_reel(ccap, rename, hard_link, soft_link);
+                               }
+                               maybe_copy_from_reel(reel->atmos(), rename, hard_link, soft_link);
+                       }
+
+                       dcp.add(cpl);
+               }
+       } catch (CopyError& e) {
+               return string{e.what()};
+       }
+
+       dcp.resolve_refs(assets);
+       dcp.set_annotation_text(cpls[0]->annotation_text().get_value_or(""));
+       dcp.write_xml(Config::instance()->signer_chain());
+
+       return {};
+}
+
diff --git a/src/lib/map_cli.h b/src/lib/map_cli.h
new file mode 100644 (file)
index 0000000..b260545
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <boost/optional.hpp>
+#include <string>
+
+
+extern boost::optional<std::string> map_cli(int argc, char* argv[], std::function<void (std::string)> out);
+
index de7b947c22e5c202f44797accf563b5aa3b8c7a0..00b6b7ed8180943f7ee4f77b6613889aec4088dd 100644 (file)
@@ -145,6 +145,7 @@ sources = """
           log.cc
           log_entry.cc
           make_dcp.cc
           log.cc
           log_entry.cc
           make_dcp.cc
+          map_cli.cc
           maths_util.cc
           memory_util.cc
           mid_side_decoder.cc
           maths_util.cc
           memory_util.cc
           mid_side_decoder.cc
diff --git a/src/tools/dcpomatic_map.cc b/src/tools/dcpomatic_map.cc
new file mode 100644 (file)
index 0000000..2817fdd
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+/** @file  src/tools/dcpomatic_map.cc
+ *  @brief Command-line program to assemble new DCPs from CPLs and assets.
+ */
+
+
+#include "lib/cross.h"
+#include "lib/map_cli.h"
+#include "lib/util.h"
+#include <iostream>
+
+
+int
+main(int argc, char* argv[])
+{
+       ArgFixer fixer(argc, argv);
+
+       dcpomatic_setup_path_encoding();
+       dcpomatic_setup();
+
+       auto error = map_cli(fixer.argc(), fixer.argv(), [](std::string s) { std::cout << s << "\n"; });
+       if (error) {
+               std::cerr << *error << "\n";
+               exit (EXIT_FAILURE);
+       }
+
+       return 0;
+}
+
index 9a3b95a795cef863a8444eef18d229e9054e59d1..c3b2b5fe03b850228ec3bf74f07724c9858a71f7 100644 (file)
@@ -42,7 +42,7 @@ def build(bld):
     if bld.env.TARGET_LINUX:
         uselib += 'DL '
 
     if bld.env.TARGET_LINUX:
         uselib += 'DL '
 
-    cli_tools = ['dcpomatic_cli', 'dcpomatic_server_cli', 'server_test', 'dcpomatic_kdm_cli', 'dcpomatic_create']
+    cli_tools = ['dcpomatic_cli', 'dcpomatic_server_cli', 'server_test', 'dcpomatic_kdm_cli', 'dcpomatic_create', 'dcpomatic_map']
     if bld.env.ENABLE_DISK and not bld.env.DISABLE_GUI:
         cli_tools.append('dcpomatic_disk_writer')
 
     if bld.env.ENABLE_DISK and not bld.env.DISABLE_GUI:
         cli_tools.append('dcpomatic_disk_writer')
 
diff --git a/test/map_cli_test.cc b/test/map_cli_test.cc
new file mode 100644 (file)
index 0000000..3e6abc0
--- /dev/null
@@ -0,0 +1,355 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/content.h"
+#include "lib/dcp_content.h"
+#include "lib/content_factory.h"
+#include "lib/film.h"
+#include "lib/map_cli.h"
+#include "test.h"
+#include <dcp/cpl.h>
+#include <dcp/dcp.h>
+#include <dcp/reel.h>
+#include <dcp/reel_picture_asset.h>
+#include <dcp/reel_sound_asset.h>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+#include <boost/test/unit_test.hpp>
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+static
+optional<string>
+run(vector<string> const& args, vector<string>& output)
+{
+       vector<char*> argv(args.size() + 1);
+       for (auto i = 0U; i < args.size(); ++i) {
+               argv[i] = const_cast<char*>(args[i].c_str());
+       }
+       argv[args.size()] = nullptr;
+
+       auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
+       if (error) {
+               std::cout << *error << "\n";
+       }
+
+       return error;
+}
+
+
+static
+boost::filesystem::path
+find_prefix(boost::filesystem::path dir, string prefix)
+{
+        auto iter = std::find_if(boost::filesystem::directory_iterator(dir), boost::filesystem::directory_iterator(), [prefix](boost::filesystem::path const& p) {
+               return boost::starts_with(p.filename().string(), prefix);
+        });
+
+        BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
+        return iter->path();
+}
+
+
+static
+boost::filesystem::path
+find_cpl(boost::filesystem::path dir)
+{
+        return find_prefix(dir, "cpl_");
+}
+
+
+/** Map a single DCP into a new DCP */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
+{
+       string const name = "map_simple_dcp_copy";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto content = content_factory("test/data/flat_red.png");
+       auto film = new_test_film2(name + "_in", content);
+       make_and_verify_dcp(film);
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", film->dir(film->dcp_name()).string(),
+               find_cpl(film->dir(film->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, {});
+
+       BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
+       BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
+}
+
+
+/** Map a single DCP into a new DCP using the symlink option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
+{
+       string const name = "map_simple_dcp_copy_with_symlinks";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto content = content_factory("test/data/flat_red.png");
+       auto film = new_test_film2(name + "_in", content);
+       make_and_verify_dcp(film);
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", film->dir(film->dcp_name()).string(),
+               "-s",
+               find_cpl(film->dir(film->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       /* We can't verify this DCP because the symlinks will make it fail
+        * (as it should be, I think).
+        */
+
+       BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
+       BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
+}
+
+
+/** Map a single DCP into a new DCP using the hardlink option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
+{
+       string const name = "map_simple_dcp_copy_with_hardlinks";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto content = content_factory("test/data/flat_red.png");
+       auto film = new_test_film2(name + "_in", content);
+       make_and_verify_dcp(film);
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", film->dir(film->dcp_name()).string(),
+               "-l",
+               find_cpl(film->dir(film->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, {});
+
+       /* The video file will have 3 links because DoM also makes a link into the video directory */
+       BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 3U);
+       BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
+}
+
+
+/** Map a single Interop DCP with subs into a new DCP */
+BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
+{
+       string const name = "map_simple_interop_dcp_with_subs";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto picture = content_factory("test/data/flat_red.png").front();
+       auto subs = content_factory("test/data/15s.srt").front();
+       auto film = new_test_film2(name + "_in", { picture, subs });
+       film->set_interop(true);
+       make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", film->dir(film->dcp_name()).string(),
+               find_cpl(film->dir(film->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
+}
+
+
+/** Map an OV and a VF into a single DCP */
+BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
+{
+       string const name = "map_ov_vf_copy";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto ov_content = content_factory("test/data/flat_red.png");
+       auto ov_film = new_test_film2(name + "_ov", ov_content);
+       make_and_verify_dcp(ov_film);
+
+       auto const ov_dir = ov_film->dir(ov_film->dcp_name());
+
+       auto vf_ov = make_shared<DCPContent>(ov_dir);
+       auto vf_sound = content_factory("test/data/sine_440.wav").front();
+       auto vf_film = new_test_film2(name + "_vf", { vf_ov, vf_sound });
+       vf_ov->set_reference_video(true);
+       make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET});
+
+       auto const vf_dir = vf_film->dir(vf_film->dcp_name());
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", ov_dir.string(),
+               "-d", vf_dir.string(),
+               find_cpl(vf_dir).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, {});
+
+       check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
+       check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
+       check_file(find_file(out, "pcm_"), find_file(vf_dir, "pcm_"));
+}
+
+
+/** Map a single DCP into a new DCP using the rename option */
+BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
+{
+       ConfigRestorer cr;
+       Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("hello%c"));
+       string const name = "map_simple_dcp_copy_with_rename";
+       string const out = String::compose("build/test/%1_out", name);
+
+       auto content = content_factory("test/data/flat_red.png");
+       auto film = new_test_film2(name + "_in", content);
+       make_and_verify_dcp(film);
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", film->dir(film->dcp_name()).string(),
+               "-r",
+               find_cpl(film->dir(film->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, {});
+
+       dcp::DCP out_dcp(out);
+       out_dcp.read();
+
+       BOOST_REQUIRE_EQUAL(out_dcp.cpls().size(), 1U);
+       auto const cpl = out_dcp.cpls()[0];
+       BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
+       auto const reel = cpl->reels()[0];
+       BOOST_REQUIRE(reel->main_picture());
+       BOOST_REQUIRE(reel->main_sound());
+       auto const picture = reel->main_picture()->asset();
+       BOOST_REQUIRE(picture);
+       auto const sound = reel->main_sound()->asset();
+       BOOST_REQUIRE(sound);
+
+       BOOST_REQUIRE(picture->file());
+       BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
+
+       BOOST_REQUIRE(sound->file());
+       BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
+}
+
+
+static
+void
+test_two_cpls_each_with_subs(string name, bool interop)
+{
+       string const out = String::compose("build/test/%1_out", name);
+
+       vector<dcp::VerificationNote::Code> acceptable_errors;
+       if (interop) {
+               acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
+       } else {
+               acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
+               acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
+       }
+
+       shared_ptr<Film> films[2];
+       for (auto i = 0; i < 2; ++i) {
+               auto picture = content_factory("test/data/flat_red.png").front();
+               auto subs = content_factory("test/data/15s.srt").front();
+               films[i] = new_test_film2(String::compose("%1_%2_in", name, i), { picture, subs });
+               films[i]->set_interop(interop);
+               make_and_verify_dcp(films[i], acceptable_errors);
+       }
+
+       vector<string> const args = {
+               "map_cli",
+               "-o", out,
+               "-d", films[0]->dir(films[0]->dcp_name()).string(),
+               "-d", films[1]->dir(films[1]->dcp_name()).string(),
+               find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
+               find_cpl(films[1]->dir(films[1]->dcp_name())).string()
+       };
+
+       boost::filesystem::remove_all(out);
+
+       vector<string> output_messages;
+       auto error = run(args, output_messages);
+       BOOST_CHECK(!error);
+
+       verify_dcp(out, acceptable_errors);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
+{
+       test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
+}
+
+
+BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
+{
+       test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
+}
index 354a79e26cb60a8dc9df1ee4ba4043feed9cb487..06ec23dbcb6c4daadd5399cba6058fcb87197ed4 100644 (file)
@@ -132,6 +132,7 @@ setup_test_config ()
        auto decryption = make_shared<dcp::CertificateChain>(dcp::file_to_string("test/data/decryption_chain"));
        decryption->set_key(dcp::file_to_string("test/data/decryption_key"));
        Config::instance()->set_decryption_chain (decryption);
        auto decryption = make_shared<dcp::CertificateChain>(dcp::file_to_string("test/data/decryption_chain"));
        decryption->set_key(dcp::file_to_string("test/data/decryption_key"));
        Config::instance()->set_decryption_chain (decryption);
+       Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("%t"));
 }
 
 
 }
 
 
index 827a3feb854d4644bc481013f8c032c5eca4bcb9..88cee9b076d3ea61e34a9bc24b339ee7662ccf66 100644 (file)
@@ -113,6 +113,7 @@ def build(bld):
                  kdm_util_test.cc
                  low_bitrate_test.cc
                  markers_test.cc
                  kdm_util_test.cc
                  low_bitrate_test.cc
                  markers_test.cc
+                 map_cli_test.cc
                  mca_subdescriptors_test.cc
                  no_use_video_test.cc
                  optimise_stills_test.cc
                  mca_subdescriptors_test.cc
                  no_use_video_test.cc
                  optimise_stills_test.cc