Run premultiply filter on still images that have alpha channels (more of #2681).
[dcpomatic.git] / test / map_cli_test.cc
1 /*
2     Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "lib/config.h"
23 #include "lib/content.h"
24 #include "lib/dcp_content.h"
25 #include "lib/content_factory.h"
26 #include "lib/film.h"
27 #include "lib/map_cli.h"
28 #include "lib/text_content.h"
29 #include "test.h"
30 #include <dcp/cpl.h>
31 #include <dcp/dcp.h>
32 #include <dcp/reel.h>
33 #include <dcp/reel_picture_asset.h>
34 #include <dcp/reel_sound_asset.h>
35 #include <boost/algorithm/string.hpp>
36 #include <boost/filesystem.hpp>
37 #include <boost/optional.hpp>
38 #include <boost/test/unit_test.hpp>
39
40
41 using std::dynamic_pointer_cast;
42 using std::make_shared;
43 using std::shared_ptr;
44 using std::string;
45 using std::vector;
46 using boost::optional;
47
48
49 static
50 optional<string>
51 run(vector<string> const& args, vector<string>& output)
52 {
53         vector<char*> argv(args.size() + 1);
54         for (auto i = 0U; i < args.size(); ++i) {
55                 argv[i] = const_cast<char*>(args[i].c_str());
56         }
57         argv[args.size()] = nullptr;
58
59         auto error = map_cli(args.size(), argv.data(), [&output](string s) { output.push_back(s); });
60         if (error) {
61                 std::cout << *error << "\n";
62         }
63
64         return error;
65 }
66
67
68 static
69 boost::filesystem::path
70 find_prefix(boost::filesystem::path dir, string prefix)
71 {
72         auto iter = std::find_if(boost::filesystem::directory_iterator(dir), boost::filesystem::directory_iterator(), [prefix](boost::filesystem::path const& p) {
73                 return boost::starts_with(p.filename().string(), prefix);
74         });
75
76         BOOST_REQUIRE(iter != boost::filesystem::directory_iterator());
77         return iter->path();
78 }
79
80
81 static
82 boost::filesystem::path
83 find_cpl(boost::filesystem::path dir)
84 {
85         return find_prefix(dir, "cpl_");
86 }
87
88
89 /** Map a single DCP into a new DCP */
90 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy)
91 {
92         string const name = "map_simple_dcp_copy";
93         string const out = String::compose("build/test/%1_out", name);
94
95         auto content = content_factory("test/data/flat_red.png");
96         auto film = new_test_film2(name + "_in", content);
97         make_and_verify_dcp(film);
98
99         vector<string> const args = {
100                 "map_cli",
101                 "-o", out,
102                 "-d", film->dir(film->dcp_name()).string(),
103                 find_cpl(film->dir(film->dcp_name())).string()
104         };
105
106         boost::filesystem::remove_all(out);
107
108         vector<string> output_messages;
109         auto error = run(args, output_messages);
110         BOOST_CHECK(!error);
111
112         verify_dcp(out, {});
113
114         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "j2c_")));
115         BOOST_CHECK(boost::filesystem::is_regular_file(find_prefix(out, "pcm_")));
116 }
117
118
119 /** Map a single DCP into a new DCP using the symlink option */
120 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_symlinks)
121 {
122         string const name = "map_simple_dcp_copy_with_symlinks";
123         string const out = String::compose("build/test/%1_out", name);
124
125         auto content = content_factory("test/data/flat_red.png");
126         auto film = new_test_film2(name + "_in", content);
127         make_and_verify_dcp(film);
128
129         vector<string> const args = {
130                 "map_cli",
131                 "-o", out,
132                 "-d", film->dir(film->dcp_name()).string(),
133                 "-s",
134                 find_cpl(film->dir(film->dcp_name())).string()
135         };
136
137         boost::filesystem::remove_all(out);
138
139         vector<string> output_messages;
140         auto error = run(args, output_messages);
141         BOOST_CHECK(!error);
142
143         /* We can't verify this DCP because the symlinks will make it fail
144          * (as it should be, I think).
145          */
146
147         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "j2c_")));
148         BOOST_CHECK(boost::filesystem::is_symlink(find_prefix(out, "pcm_")));
149 }
150
151
152 /** Map a single DCP into a new DCP using the hardlink option */
153 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_hardlinks)
154 {
155         string const name = "map_simple_dcp_copy_with_hardlinks";
156         string const out = String::compose("build/test/%1_out", name);
157
158         auto content = content_factory("test/data/flat_red.png");
159         auto film = new_test_film2(name + "_in", content);
160         make_and_verify_dcp(film);
161
162         vector<string> const args = {
163                 "map_cli",
164                 "-o", out,
165                 "-d", film->dir(film->dcp_name()).string(),
166                 "-l",
167                 find_cpl(film->dir(film->dcp_name())).string()
168         };
169
170         boost::filesystem::remove_all(out);
171
172         vector<string> output_messages;
173         auto error = run(args, output_messages);
174         BOOST_CHECK(!error);
175
176         verify_dcp(out, {});
177
178         /* The video file will have 3 links because DoM also makes a link into the video directory */
179         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "j2c_")), 3U);
180         BOOST_CHECK_EQUAL(boost::filesystem::hard_link_count(find_prefix(out, "pcm_")), 2U);
181 }
182
183
184 /** Map a single Interop DCP with subs into a new DCP */
185 BOOST_AUTO_TEST_CASE(map_simple_interop_dcp_with_subs)
186 {
187         string const name = "map_simple_interop_dcp_with_subs";
188         string const out = String::compose("build/test/%1_out", name);
189
190         auto picture = content_factory("test/data/flat_red.png").front();
191         auto subs = content_factory("test/data/15s.srt").front();
192         auto film = new_test_film2(name + "_in", { picture, subs });
193         film->set_interop(true);
194         make_and_verify_dcp(film, {dcp::VerificationNote::Code::INVALID_STANDARD});
195
196         vector<string> const args = {
197                 "map_cli",
198                 "-o", out,
199                 "-d", film->dir(film->dcp_name()).string(),
200                 find_cpl(film->dir(film->dcp_name())).string()
201         };
202
203         boost::filesystem::remove_all(out);
204
205         vector<string> output_messages;
206         auto error = run(args, output_messages);
207         BOOST_CHECK(!error);
208
209         verify_dcp(out, {dcp::VerificationNote::Code::INVALID_STANDARD});
210 }
211
212
213 static
214 void
215 test_map_ov_vf_copy(vector<string> extra_args = {})
216 {
217         string const name = "map_ov_vf_copy";
218         string const out = String::compose("build/test/%1_out", name);
219
220         auto ov_content = content_factory("test/data/flat_red.png");
221         auto ov_film = new_test_film2(name + "_ov", ov_content);
222         make_and_verify_dcp(ov_film);
223
224         auto const ov_dir = ov_film->dir(ov_film->dcp_name());
225
226         auto vf_ov = make_shared<DCPContent>(ov_dir);
227         auto vf_sound = content_factory("test/data/sine_440.wav").front();
228         auto vf_film = new_test_film2(name + "_vf", { vf_ov, vf_sound });
229         vf_ov->set_reference_video(true);
230         make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET});
231
232         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
233
234         vector<string> args = {
235                 "map_cli",
236                 "-o", out,
237                 "-d", ov_dir.string(),
238                 "-d", vf_dir.string(),
239                 find_cpl(vf_dir).string()
240         };
241
242         args.insert(std::end(args), std::begin(extra_args), std::end(extra_args));
243
244         boost::filesystem::remove_all(out);
245
246         vector<string> output_messages;
247         auto error = run(args, output_messages);
248         BOOST_CHECK(!error);
249
250         verify_dcp(out, {});
251
252         check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
253         check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
254         check_file(find_file(out, "pcm_"), find_file(vf_dir, "pcm_"));
255 }
256
257
258 /** Map an OV and a VF into a single DCP */
259 BOOST_AUTO_TEST_CASE(map_ov_vf_copy)
260 {
261         test_map_ov_vf_copy();
262         test_map_ov_vf_copy({"-l"});
263 }
264
265
266 /** Map an OV and VF into a single DCP, where the VF refers to the OV's assets multiple times */
267 BOOST_AUTO_TEST_CASE(map_ov_vf_copy_multiple_reference)
268 {
269         string const name = "map_ov_vf_copy_multiple_reference";
270         string const out = String::compose("build/test/%1_out", name);
271
272         auto ov_content = content_factory("test/data/flat_red.png");
273         auto ov_film = new_test_film2(name + "_ov", ov_content);
274         make_and_verify_dcp(ov_film);
275
276         auto const ov_dir = ov_film->dir(ov_film->dcp_name());
277
278         auto vf_ov1 = make_shared<DCPContent>(ov_dir);
279         auto vf_ov2 = make_shared<DCPContent>(ov_dir);
280         auto vf_sound = content_factory("test/data/sine_440.wav").front();
281         auto vf_film = new_test_film2(name + "_vf", { vf_ov1, vf_ov2, vf_sound });
282         vf_film->set_reel_type(ReelType::BY_VIDEO_CONTENT);
283         vf_ov2->set_position(vf_film, vf_ov1->end(vf_film));
284         vf_ov1->set_reference_video(true);
285         vf_ov2->set_reference_video(true);
286         make_and_verify_dcp(vf_film, {dcp::VerificationNote::Code::EXTERNAL_ASSET});
287
288         auto const vf_dir = vf_film->dir(vf_film->dcp_name());
289
290         vector<string> const args = {
291                 "map_cli",
292                 "-o", out,
293                 "-d", ov_dir.string(),
294                 "-d", vf_dir.string(),
295                 "-l",
296                 find_cpl(vf_dir).string()
297         };
298
299         boost::filesystem::remove_all(out);
300
301         vector<string> output_messages;
302         auto error = run(args, output_messages);
303         BOOST_CHECK(!error);
304
305         verify_dcp(out, {});
306
307         check_file(find_file(out, "cpl_"), find_file(vf_dir, "cpl_"));
308         check_file(find_file(out, "j2c_"), find_file(ov_dir, "j2c_"));
309 }
310
311
312 /** Map a single DCP into a new DCP using the rename option */
313 BOOST_AUTO_TEST_CASE(map_simple_dcp_copy_with_rename)
314 {
315         ConfigRestorer cr;
316         Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("hello%c"));
317         string const name = "map_simple_dcp_copy_with_rename";
318         string const out = String::compose("build/test/%1_out", name);
319
320         auto content = content_factory("test/data/flat_red.png");
321         auto film = new_test_film2(name + "_in", content);
322         make_and_verify_dcp(film);
323
324         vector<string> const args = {
325                 "map_cli",
326                 "-o", out,
327                 "-d", film->dir(film->dcp_name()).string(),
328                 "-r",
329                 find_cpl(film->dir(film->dcp_name())).string()
330         };
331
332         boost::filesystem::remove_all(out);
333
334         vector<string> output_messages;
335         auto error = run(args, output_messages);
336         BOOST_CHECK(!error);
337
338         verify_dcp(out, {});
339
340         dcp::DCP out_dcp(out);
341         out_dcp.read();
342
343         BOOST_REQUIRE_EQUAL(out_dcp.cpls().size(), 1U);
344         auto const cpl = out_dcp.cpls()[0];
345         BOOST_REQUIRE_EQUAL(cpl->reels().size(), 1U);
346         auto const reel = cpl->reels()[0];
347         BOOST_REQUIRE(reel->main_picture());
348         BOOST_REQUIRE(reel->main_sound());
349         auto const picture = reel->main_picture()->asset();
350         BOOST_REQUIRE(picture);
351         auto const sound = reel->main_sound()->asset();
352         BOOST_REQUIRE(sound);
353
354         BOOST_REQUIRE(picture->file());
355         BOOST_CHECK(picture->file().get().filename() == picture->id() + ".mxf");
356
357         BOOST_REQUIRE(sound->file());
358         BOOST_CHECK(sound->file().get().filename() == sound->id() + ".mxf");
359 }
360
361
362 static
363 void
364 test_two_cpls_each_with_subs(string name, bool interop)
365 {
366         string const out = String::compose("build/test/%1_out", name);
367
368         vector<dcp::VerificationNote::Code> acceptable_errors;
369         if (interop) {
370                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_STANDARD);
371         } else {
372                 acceptable_errors.push_back(dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE);
373                 acceptable_errors.push_back(dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
374         }
375
376         shared_ptr<Film> films[2];
377         for (auto i = 0; i < 2; ++i) {
378                 auto picture = content_factory("test/data/flat_red.png").front();
379                 auto subs = content_factory("test/data/15s.srt").front();
380                 films[i] = new_test_film2(String::compose("%1_%2_in", name, i), { picture, subs });
381                 films[i]->set_interop(interop);
382                 make_and_verify_dcp(films[i], acceptable_errors);
383         }
384
385         vector<string> const args = {
386                 "map_cli",
387                 "-o", out,
388                 "-d", films[0]->dir(films[0]->dcp_name()).string(),
389                 "-d", films[1]->dir(films[1]->dcp_name()).string(),
390                 find_cpl(films[0]->dir(films[0]->dcp_name())).string(),
391                 find_cpl(films[1]->dir(films[1]->dcp_name())).string()
392         };
393
394         boost::filesystem::remove_all(out);
395
396         vector<string> output_messages;
397         auto error = run(args, output_messages);
398         BOOST_CHECK(!error);
399
400         verify_dcp(out, acceptable_errors);
401 }
402
403
404 BOOST_AUTO_TEST_CASE(map_two_interop_cpls_each_with_subs)
405 {
406         test_two_cpls_each_with_subs("map_two_interop_cpls_each_with_subs", true);
407 }
408
409
410 BOOST_AUTO_TEST_CASE(map_two_smpte_cpls_each_with_subs)
411 {
412         test_two_cpls_each_with_subs("map_two_smpte_cpls_each_with_subs", false);
413 }
414
415
416 BOOST_AUTO_TEST_CASE(map_with_given_config)
417 {
418         ConfigRestorer cr;
419
420         string const name = "map_with_given_config";
421         string const out = String::compose("build/test/%1_out", name);
422
423         auto content = content_factory("test/data/flat_red.png");
424         auto film = new_test_film2(name + "_in", content);
425         make_and_verify_dcp(film);
426
427         vector<string> const args = {
428                 "map_cli",
429                 "-o", out,
430                 "-d", film->dir(film->dcp_name()).string(),
431                 "--config", "test/data/map_with_given_config",
432                 find_cpl(film->dir(film->dcp_name())).string()
433         };
434
435         boost::filesystem::remove_all(out);
436
437         Config::instance()->drop();
438         vector<string> output_messages;
439         auto error = run(args, output_messages);
440         BOOST_CHECK(!error);
441
442         /* It should be signed by the key in test/data/map_with_given_config, not the one in test/data/signer_key */
443         BOOST_CHECK(dcp::file_to_string(find_file(out, "cpl_")).find("dnQualifier=\\+uOcNN2lPuxpxgd/5vNkkBER0GE=,CN=CS.dcpomatic.smpte-430-2.LEAF,OU=dcpomatic.com,O=dcpomatic.com") != std::string::npos);
444 }
445
446
447 BOOST_AUTO_TEST_CASE(map_multireel_interop_ov_and_vf_adding_ccaps)
448 {
449         string const name = "map_multireel_interop_ov_and_vf_adding_ccaps";
450         string const out = String::compose("build/test/%1_out", name);
451
452         vector<shared_ptr<Content>> video = {
453                 content_factory("test/data/flat_red.png")[0],
454                 content_factory("test/data/flat_red.png")[0],
455                 content_factory("test/data/flat_red.png")[0]
456         };
457
458         auto ov = new_test_film2(name + "_ov", { video[0], video[1], video[2] });
459         ov->set_reel_type(ReelType::BY_VIDEO_CONTENT);
460         ov->set_interop(true);
461         make_and_verify_dcp(ov, { dcp::VerificationNote::Code::INVALID_STANDARD });
462
463         auto ov_dcp = make_shared<DCPContent>(ov->dir(ov->dcp_name()));
464
465         vector<shared_ptr<Content>> ccap = {
466                 content_factory("test/data/short.srt")[0],
467                 content_factory("test/data/short.srt")[0],
468                 content_factory("test/data/short.srt")[0]
469         };
470
471         auto vf = new_test_film2(name + "_vf", { ov_dcp, ccap[0], ccap[1], ccap[2] });
472         vf->set_interop(true);
473         vf->set_reel_type(ReelType::BY_VIDEO_CONTENT);
474         ov_dcp->set_reference_video(true);
475         ov_dcp->set_reference_audio(true);
476         for (auto i = 0; i < 3; ++i) {
477                 ccap[i]->text[0]->set_use(true);
478                 ccap[i]->text[0]->set_type(TextType::CLOSED_CAPTION);
479         }
480         make_and_verify_dcp(
481                 vf,
482                 {
483                         dcp::VerificationNote::Code::INVALID_STANDARD,
484                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
485                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
486                         dcp::VerificationNote::Code::EXTERNAL_ASSET
487                 });
488
489         vector<string> const args = {
490                 "map_cli",
491                 "-o", out,
492                 "-d", ov->dir(ov->dcp_name()).string(),
493                 "-d", vf->dir(vf->dcp_name()).string(),
494                 find_cpl(vf->dir(vf->dcp_name())).string()
495         };
496
497         boost::filesystem::remove_all(out);
498
499         vector<string> output_messages;
500         auto error = run(args, output_messages);
501         BOOST_CHECK(!error);
502
503         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
504 }
505
506
507 BOOST_AUTO_TEST_CASE(map_uses_config_for_issuer_and_creator)
508 {
509         ConfigRestorer cr;
510
511         Config::instance()->set_dcp_issuer("ostrabagalous");
512         Config::instance()->set_dcp_creator("Fred");
513
514         string const name = "map_uses_config_for_issuer_and_creator";
515         string const out = String::compose("build/test/%1_out", name);
516
517         auto content = content_factory("test/data/flat_red.png");
518         auto film = new_test_film2(name + "_in", content);
519         make_and_verify_dcp(film);
520
521         vector<string> const args = {
522                 "map_cli",
523                 "-o", out,
524                 "-d", film->dir(film->dcp_name()).string(),
525                 find_cpl(film->dir(film->dcp_name())).string()
526         };
527
528         boost::filesystem::remove_all(out);
529
530         vector<string> output_messages;
531         auto error = run(args, output_messages);
532         BOOST_CHECK(!error);
533
534         cxml::Document assetmap("AssetMap");
535         assetmap.read_file(film->dir(film->dcp_name()) / "ASSETMAP.xml");
536         BOOST_CHECK(assetmap.string_child("Issuer") == "ostrabagalous");
537         BOOST_CHECK(assetmap.string_child("Creator") == "Fred");
538
539         cxml::Document pkl("PackingList");
540         pkl.read_file(find_prefix(out, "pkl_"));
541         BOOST_CHECK(pkl.string_child("Issuer") == "ostrabagalous");
542         BOOST_CHECK(pkl.string_child("Creator") == "Fred");
543 }
544
545
546 BOOST_AUTO_TEST_CASE(map_handles_interop_png_subs)
547 {
548         string const name = "map_handles_interop_png_subs";
549         auto arrietty = content_factory(TestPaths::private_data() / "arrietty_JP-EN.mkv")[0];
550         auto film = new_test_film2(name + "_input", { arrietty });
551         film->set_interop(true);
552         arrietty->set_trim_end(dcpomatic::ContentTime::from_seconds(110));
553         arrietty->text[0]->set_use(true);
554         make_and_verify_dcp(
555                 film,
556                 {
557                         dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME,
558                         dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE,
559                         dcp::VerificationNote::Code::INVALID_STANDARD
560                 });
561
562         auto const out = boost::filesystem::path("build") / "test" / (name + "_output");
563
564         vector<string> const args = {
565                 "map_cli",
566                 "-o", out.string(),
567                 "-d", film->dir(film->dcp_name()).string(),
568                 find_cpl(film->dir(film->dcp_name())).string()
569         };
570
571         boost::filesystem::remove_all(out);
572
573         vector<string> output_messages;
574         auto error = run(args, output_messages);
575         BOOST_CHECK(!error);
576
577         verify_dcp(out, { dcp::VerificationNote::Code::INVALID_STANDARD });
578 }
579