Verify error if SMPTE subtitle asset has <Text> but no <LoadFont>.
[libdcp.git] / test / verify_test.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34
35 #include "compose.hpp"
36 #include "cpl.h"
37 #include "dcp.h"
38 #include "file.h"
39 #include "interop_subtitle_asset.h"
40 #include "j2k_transcode.h"
41 #include "mono_picture_asset.h"
42 #include "mono_picture_asset_writer.h"
43 #include "openjpeg_image.h"
44 #include "raw_convert.h"
45 #include "reel.h"
46 #include "reel_interop_closed_caption_asset.h"
47 #include "reel_interop_subtitle_asset.h"
48 #include "reel_markers_asset.h"
49 #include "reel_mono_picture_asset.h"
50 #include "reel_sound_asset.h"
51 #include "reel_stereo_picture_asset.h"
52 #include "reel_smpte_closed_caption_asset.h"
53 #include "reel_smpte_subtitle_asset.h"
54 #include "smpte_subtitle_asset.h"
55 #include "stereo_picture_asset.h"
56 #include "stream_operators.h"
57 #include "test.h"
58 #include "util.h"
59 #include "verify.h"
60 #include "verify_j2k.h"
61 #include <boost/algorithm/string.hpp>
62 #include <boost/random.hpp>
63 #include <boost/test/unit_test.hpp>
64 #include <cstdio>
65 #include <iostream>
66
67
68 using std::list;
69 using std::make_pair;
70 using std::make_shared;
71 using std::pair;
72 using std::shared_ptr;
73 using std::string;
74 using std::vector;
75 using boost::optional;
76 using namespace boost::filesystem;
77
78
79 static list<pair<string, optional<path>>> stages;
80
81 static string filename_to_id(boost::filesystem::path path)
82 {
83         return path.string().substr(4, path.string().length() - 8);
84 }
85
86 static boost::filesystem::path const dcp_test1_pkl = find_file("test/ref/DCP/dcp_test1", "pkl_").filename();
87 static string const dcp_test1_pkl_id = filename_to_id(dcp_test1_pkl);
88
89 static boost::filesystem::path const dcp_test1_cpl = find_file("test/ref/DCP/dcp_test1", "cpl_").filename();
90 static string const dcp_test1_cpl_id = filename_to_id(dcp_test1_cpl);
91
92 static string const dcp_test1_asset_map_id = "017b3de4-6dda-408d-b19b-6711354b0bc3";
93
94 static boost::filesystem::path const encryption_test_cpl = find_file("test/ref/DCP/encryption_test", "cpl_").filename();
95 static string const encryption_test_cpl_id = filename_to_id(encryption_test_cpl);
96
97 static boost::filesystem::path const encryption_test_pkl = find_file("test/ref/DCP/encryption_test", "pkl_").filename();
98 static string const encryption_test_pkl_id = filename_to_id(encryption_test_pkl);
99
100 static void
101 stage (string s, optional<path> p)
102 {
103         stages.push_back (make_pair (s, p));
104 }
105
106 static void
107 progress (float)
108 {
109
110 }
111
112 static void
113 prepare_directory (path path)
114 {
115         using namespace boost::filesystem;
116         remove_all (path);
117         create_directories (path);
118 }
119
120
121 /** Copy dcp_test{reference_number} to build/test/verify_test{verify_test_suffix}
122  *  to make a new sacrificial test DCP.
123  */
124 static path
125 setup (int reference_number, string verify_test_suffix)
126 {
127         auto const dir = dcp::String::compose("build/test/verify_test%1", verify_test_suffix);
128         prepare_directory (dir);
129         for (auto i: directory_iterator(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number))) {
130                 copy_file (i.path(), dir / i.path().filename());
131         }
132
133         return dir;
134 }
135
136
137 static
138 shared_ptr<dcp::CPL>
139 write_dcp_with_single_asset (path dir, shared_ptr<dcp::ReelAsset> reel_asset, dcp::Standard standard = dcp::Standard::SMPTE)
140 {
141         auto reel = make_shared<dcp::Reel>();
142         reel->add (reel_asset);
143         reel->add (simple_markers());
144
145         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, standard);
146         cpl->add (reel);
147         auto dcp = make_shared<dcp::DCP>(dir);
148         dcp->add (cpl);
149         dcp->set_annotation_text("hello");
150         dcp->write_xml ();
151
152         return cpl;
153 }
154
155
156 /** Class that can alter a file by searching and replacing strings within it.
157  *  On destruction modifies the file whose name was given to the constructor.
158  */
159 class Editor
160 {
161 public:
162         Editor (path path)
163                 : _path(path)
164         {
165                 _content = dcp::file_to_string (_path);
166         }
167
168         ~Editor ()
169         {
170                 auto f = fopen(_path.string().c_str(), "w");
171                 BOOST_REQUIRE (f);
172                 fwrite (_content.c_str(), _content.length(), 1, f);
173                 fclose (f);
174         }
175
176         class ChangeChecker
177         {
178         public:
179                 ChangeChecker(Editor* editor)
180                         : _editor(editor)
181                 {
182                         _old_content = _editor->_content;
183                 }
184
185                 ~ChangeChecker()
186                 {
187                         BOOST_REQUIRE(_old_content != _editor->_content);
188                 }
189         private:
190                 Editor* _editor;
191                 std::string _old_content;
192         };
193
194         void replace (string a, string b)
195         {
196                 ChangeChecker cc(this);
197                 boost::algorithm::replace_all (_content, a, b);
198         }
199
200         void delete_first_line_containing (string s)
201         {
202                 ChangeChecker cc(this);
203                 auto lines = as_lines();
204                 _content = "";
205                 bool done = false;
206                 for (auto i: lines) {
207                         if (i.find(s) == string::npos || done) {
208                                 _content += i + "\n";
209                         } else {
210                                 done = true;
211                         }
212                 }
213         }
214
215         void delete_lines (string from, string to)
216         {
217                 ChangeChecker cc(this);
218                 auto lines = as_lines();
219                 bool deleting = false;
220                 _content = "";
221                 for (auto i: lines) {
222                         if (i.find(from) != string::npos) {
223                                 deleting = true;
224                         }
225                         if (!deleting) {
226                                 _content += i + "\n";
227                         }
228                         if (deleting && i.find(to) != string::npos) {
229                                 deleting = false;
230                         }
231                 }
232         }
233
234         void insert (string after, string line)
235         {
236                 ChangeChecker cc(this);
237                 auto lines = as_lines();
238                 _content = "";
239                 bool replaced = false;
240                 for (auto i: lines) {
241                         _content += i + "\n";
242                         if (!replaced && i.find(after) != string::npos) {
243                                 _content += line + "\n";
244                                 replaced = true;
245                         }
246                 }
247         }
248
249         void delete_lines_after(string after, int lines_to_delete)
250         {
251                 ChangeChecker cc(this);
252                 auto lines = as_lines();
253                 _content = "";
254                 auto iter = std::find_if(lines.begin(), lines.end(), [after](string const& line) {
255                         return line.find(after) != string::npos;
256                 });
257                 int to_delete = 0;
258                 for (auto i = lines.begin(); i != lines.end(); ++i) {
259                         if (i == iter) {
260                                 to_delete = lines_to_delete;
261                                 _content += *i + "\n";
262                         } else if (to_delete == 0) {
263                                 _content += *i + "\n";
264                         } else {
265                                 --to_delete;
266                         }
267                 }
268         }
269
270 private:
271         friend class ChangeChecker;
272
273         vector<string> as_lines() const
274         {
275                 vector<string> lines;
276                 boost::algorithm::split(lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
277                 return lines;
278         }
279
280         path _path;
281         std::string _content;
282 };
283
284
285 LIBDCP_DISABLE_WARNINGS
286 static
287 void
288 dump_notes (vector<dcp::VerificationNote> const & notes)
289 {
290         for (auto i: notes) {
291                 std::cout << dcp::note_to_string(i) << "\n";
292         }
293 }
294 LIBDCP_ENABLE_WARNINGS
295
296
297 static
298 void
299 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
300 {
301         auto notes = dcp::verify({dir}, &stage, &progress, {}, xsd_test);
302         std::sort (notes.begin(), notes.end());
303         std::sort (test_notes.begin(), test_notes.end());
304
305         string message = "\nVerification notes from test:\n";
306         for (auto i: notes) {
307                 message += "  " + note_to_string(i) + "\n";
308                 message += dcp::String::compose(
309                         "  [%1 %2 %3 %4 %5]\n",
310                         static_cast<int>(i.type()),
311                         static_cast<int>(i.code()),
312                         i.note().get_value_or("<none>"),
313                         i.file().get_value_or("<none>"),
314                         i.line().get_value_or(0)
315                         );
316         }
317         message += "Expected:\n";
318         for (auto i: test_notes) {
319                 message += "  " + note_to_string(i) + "\n";
320                 message += dcp::String::compose(
321                         "  [%1 %2 %3 %4 %5]\n",
322                         static_cast<int>(i.type()),
323                         static_cast<int>(i.code()),
324                         i.note().get_value_or("<none>"),
325                         i.file().get_value_or("<none>"),
326                         i.line().get_value_or(0)
327                         );
328         }
329
330         BOOST_REQUIRE_MESSAGE (notes == test_notes, message);
331 }
332
333
334 /* Copy dcp_test1 to build/test/verify_test{suffix} then edit a file found by the functor 'file',
335  * replacing from with to.  Verify the resulting DCP and check that the results match the given
336  * list of codes.
337  */
338 static
339 void
340 check_verify_result_after_replace (string suffix, boost::function<path (string)> file, string from, string to, vector<dcp::VerificationNote::Code> codes)
341 {
342         auto dir = setup (1, suffix);
343
344         {
345                 Editor e (file(suffix));
346                 e.replace (from, to);
347         }
348
349         auto notes = dcp::verify({dir}, &stage, &progress, {}, xsd_test);
350
351         BOOST_REQUIRE_EQUAL (notes.size(), codes.size());
352         auto i = notes.begin();
353         auto j = codes.begin();
354         while (i != notes.end()) {
355                 BOOST_CHECK_EQUAL (i->code(), *j);
356                 ++i;
357                 ++j;
358         }
359 }
360
361
362 static
363 void
364 add_font(shared_ptr<dcp::SubtitleAsset> asset)
365 {
366         dcp::ArrayData fake_font(1024);
367         asset->add_font("font", fake_font);
368 }
369
370
371 BOOST_AUTO_TEST_CASE (verify_no_error)
372 {
373         stages.clear ();
374         auto dir = setup (1, "no_error");
375         auto notes = dcp::verify({dir}, &stage, &progress, {}, xsd_test);
376
377         path const cpl_file = dir / dcp_test1_cpl;
378         path const pkl_file = dir / dcp_test1_pkl;
379         path const assetmap_file = dir / "ASSETMAP.xml";
380
381         auto st = stages.begin();
382         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
383         BOOST_REQUIRE (st->second);
384         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
385         ++st;
386         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
387         BOOST_REQUIRE (st->second);
388         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
389         ++st;
390         BOOST_CHECK_EQUAL (st->first, "Checking reel");
391         BOOST_REQUIRE (!st->second);
392         ++st;
393         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
394         BOOST_REQUIRE (st->second);
395         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
396         ++st;
397         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
398         BOOST_REQUIRE (st->second);
399         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
400         ++st;
401         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
402         BOOST_REQUIRE (st->second);
403         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
404         ++st;
405         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
406         BOOST_REQUIRE (st->second);
407         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
408         ++st;
409         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
410         BOOST_REQUIRE (st->second);
411         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
412         ++st;
413         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
414         BOOST_REQUIRE (st->second);
415         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
416         ++st;
417         BOOST_REQUIRE (st == stages.end());
418
419         BOOST_CHECK_EQUAL (notes.size(), 0U);
420 }
421
422
423 BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
424 {
425         using namespace boost::filesystem;
426
427         auto dir = setup (1, "incorrect_picture_sound_hash");
428
429         auto video_path = path(dir / "video.mxf");
430         auto mod = fopen(video_path.string().c_str(), "r+b");
431         BOOST_REQUIRE (mod);
432         fseek (mod, 4096, SEEK_SET);
433         int x = 42;
434         fwrite (&x, sizeof(x), 1, mod);
435         fclose (mod);
436
437         auto audio_path = path(dir / "audio.mxf");
438         mod = fopen(audio_path.string().c_str(), "r+b");
439         BOOST_REQUIRE (mod);
440         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
441         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
442         fclose (mod);
443
444         dcp::ASDCPErrorSuspender sus;
445         check_verify_result (
446                 { dir },
447                 {
448                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
449                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
450                 });
451 }
452
453
454 BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
455 {
456         using namespace boost::filesystem;
457
458         auto dir = setup (1, "mismatched_picture_sound_hashes");
459
460         {
461                 Editor e (dir / dcp_test1_pkl);
462                 e.replace ("<Hash>", "<Hash>x");
463         }
464
465         check_verify_result (
466                 { dir },
467                 {
468                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
469                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
470                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
471                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xKcJb7S2K5cNm8RG4kfQD5FTeS0A=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 28 },
472                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xtfX1mVIKJCVr1m7Y32Nzxf0+Rpw=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 },
473                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xwUmt8G+cFFKMGt0ueS9+F1S4uhc=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 20 },
474                 });
475 }
476
477
478 BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
479 {
480         auto dir = setup (1, "failed_read_content_kind");
481
482         {
483                 Editor e (dir / dcp_test1_cpl);
484                 e.replace ("<ContentKind>", "<ContentKind>x");
485         }
486
487         check_verify_result (
488                 { dir },
489                 {
490                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
491                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("xtrailer") }
492                 });
493 }
494
495
496 static
497 path
498 cpl (string suffix)
499 {
500         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_cpl);
501 }
502
503
504 static
505 path
506 pkl (string suffix)
507 {
508         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_pkl);
509 }
510
511
512 static
513 path
514 asset_map (string suffix)
515 {
516         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", suffix);
517 }
518
519
520 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_rate)
521 {
522         check_verify_result_after_replace (
523                         "invalid_picture_frame_rate", &cpl,
524                         "<FrameRate>24 1", "<FrameRate>99 1",
525                         { dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES,
526                           dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE }
527                         );
528 }
529
530 BOOST_AUTO_TEST_CASE (verify_missing_asset)
531 {
532         auto dir = setup (1, "missing_asset");
533         remove (dir / "video.mxf");
534         check_verify_result (
535                 { dir },
536                 {
537                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_ASSET, canonical(dir) / "video.mxf" }
538                 });
539 }
540
541
542 BOOST_AUTO_TEST_CASE (verify_empty_asset_path)
543 {
544         check_verify_result_after_replace (
545                         "empty_asset_path", &asset_map,
546                         "<Path>video.mxf</Path>", "<Path></Path>",
547                         { dcp::VerificationNote::Code::EMPTY_ASSET_PATH }
548                         );
549 }
550
551
552 BOOST_AUTO_TEST_CASE (verify_mismatched_standard)
553 {
554         check_verify_result_after_replace (
555                         "mismatched_standard", &cpl,
556                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
557                         { dcp::VerificationNote::Code::MISMATCHED_STANDARD,
558                           dcp::VerificationNote::Code::INVALID_XML,
559                           dcp::VerificationNote::Code::INVALID_XML,
560                           dcp::VerificationNote::Code::INVALID_XML,
561                           dcp::VerificationNote::Code::INVALID_XML,
562                           dcp::VerificationNote::Code::INVALID_XML,
563                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
564                         );
565 }
566
567
568 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_id)
569 {
570         /* There's no MISMATCHED_CPL_HASHES error here because it can't find the correct hash by ID (since the ID is wrong) */
571         check_verify_result_after_replace (
572                         "invalid_xml_cpl_id", &cpl,
573                         "<Id>urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab", "<Id>urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358a",
574                         { dcp::VerificationNote::Code::INVALID_XML }
575                         );
576 }
577
578
579 BOOST_AUTO_TEST_CASE (verify_invalid_xml_issue_date)
580 {
581         check_verify_result_after_replace (
582                         "invalid_xml_issue_date", &cpl,
583                         "<IssueDate>", "<IssueDate>x",
584                         { dcp::VerificationNote::Code::INVALID_XML,
585                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
586                         );
587 }
588
589
590 BOOST_AUTO_TEST_CASE (verify_invalid_xml_pkl_id)
591 {
592         check_verify_result_after_replace (
593                 "invalid_xml_pkl_id", &pkl,
594                 "<Id>urn:uuid:" + dcp_test1_pkl_id.substr(0, 3),
595                 "<Id>urn:uuid:x" + dcp_test1_pkl_id.substr(1, 2),
596                 { dcp::VerificationNote::Code::INVALID_XML }
597                 );
598 }
599
600
601 BOOST_AUTO_TEST_CASE (verify_invalid_xml_asset_map_id)
602 {
603         check_verify_result_after_replace (
604                 "invalid_xml_asset_map_id", &asset_map,
605                 "<Id>urn:uuid:" + dcp_test1_asset_map_id.substr(0, 3),
606                 "<Id>urn:uuid:x" + dcp_test1_asset_map_id.substr(1, 2),
607                 { dcp::VerificationNote::Code::INVALID_XML }
608                 );
609 }
610
611
612 BOOST_AUTO_TEST_CASE (verify_invalid_standard)
613 {
614         stages.clear ();
615         auto dir = setup (3, "verify_invalid_standard");
616         auto notes = dcp::verify({dir}, &stage, &progress, {}, xsd_test);
617
618         path const cpl_file = dir / "cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
619         path const pkl_file = dir / "pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
620         path const assetmap_file = dir / "ASSETMAP";
621
622         auto st = stages.begin();
623         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
624         BOOST_REQUIRE (st->second);
625         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
626         ++st;
627         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
628         BOOST_REQUIRE (st->second);
629         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
630         ++st;
631         BOOST_CHECK_EQUAL (st->first, "Checking reel");
632         BOOST_REQUIRE (!st->second);
633         ++st;
634         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
635         BOOST_REQUIRE (st->second);
636         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
637         ++st;
638         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
639         BOOST_REQUIRE (st->second);
640         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
641         ++st;
642         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
643         BOOST_REQUIRE (st->second);
644         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
645         ++st;
646         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
647         BOOST_REQUIRE (st->second);
648         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
649         ++st;
650         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
651         BOOST_REQUIRE (st->second);
652         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
653         ++st;
654         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
655         BOOST_REQUIRE (st->second);
656         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
657         ++st;
658         BOOST_REQUIRE (st == stages.end());
659
660         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
661         auto i = notes.begin ();
662         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
663         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_STANDARD);
664         ++i;
665         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
666         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K);
667 }
668
669 /* DCP with a short asset */
670 BOOST_AUTO_TEST_CASE (verify_invalid_duration)
671 {
672         auto dir = setup (8, "invalid_duration");
673         check_verify_result (
674                 { dir },
675                 {
676                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
677                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
678                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
679                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
680                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
681                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K, string("2") }
682                 });
683 }
684
685
686 static
687 shared_ptr<dcp::CPL>
688 dcp_from_frame (dcp::ArrayData const& frame, path dir)
689 {
690         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
691         create_directories (dir);
692         auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
693         for (int i = 0; i < 24; ++i) {
694                 writer->write (frame.data(), frame.size());
695         }
696         writer->finalize ();
697
698         auto reel_asset = make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
699         return write_dcp_with_single_asset (dir, reel_asset);
700 }
701
702
703 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_size_in_bytes)
704 {
705         int const too_big = 1302083 * 2;
706
707         /* Compress a black image */
708         auto image = black_image ();
709         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
710         BOOST_REQUIRE (frame.size() < too_big);
711
712         /* Place it in a bigger block with some zero padding at the end */
713         dcp::ArrayData oversized_frame(too_big);
714         memcpy (oversized_frame.data(), frame.data(), frame.size());
715         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
716
717         path const dir("build/test/verify_invalid_picture_frame_size_in_bytes");
718         prepare_directory (dir);
719         auto cpl = dcp_from_frame (oversized_frame, dir);
720
721         check_verify_result (
722                 { dir },
723                 {
724                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
725                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
726                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
727                 });
728 }
729
730
731 BOOST_AUTO_TEST_CASE (verify_nearly_invalid_picture_frame_size_in_bytes)
732 {
733         int const nearly_too_big = 1302083 * 0.98;
734
735         /* Compress a black image */
736         auto image = black_image ();
737         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
738         BOOST_REQUIRE (frame.size() < nearly_too_big);
739
740         /* Place it in a bigger block with some zero padding at the end */
741         dcp::ArrayData oversized_frame(nearly_too_big);
742         memcpy (oversized_frame.data(), frame.data(), frame.size());
743         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
744
745         path const dir("build/test/verify_nearly_invalid_picture_frame_size_in_bytes");
746         prepare_directory (dir);
747         auto cpl = dcp_from_frame (oversized_frame, dir);
748
749         check_verify_result (
750                 { dir },
751                 {
752                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
753                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
754                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
755                 });
756 }
757
758
759 BOOST_AUTO_TEST_CASE (verify_valid_picture_frame_size_in_bytes)
760 {
761         /* Compress a black image */
762         auto image = black_image ();
763         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
764         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
765
766         path const dir("build/test/verify_valid_picture_frame_size_in_bytes");
767         prepare_directory (dir);
768         auto cpl = dcp_from_frame (frame, dir);
769
770         check_verify_result ({ dir }, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
771 }
772
773
774 BOOST_AUTO_TEST_CASE (verify_valid_interop_subtitles)
775 {
776         path const dir("build/test/verify_valid_interop_subtitles");
777         prepare_directory (dir);
778         copy_file ("test/data/subs1.xml", dir / "subs.xml");
779         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
780         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
781         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
782
783         check_verify_result (
784                 {dir}, {
785                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
786                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"theFontId"} }
787                 });
788 }
789
790
791 BOOST_AUTO_TEST_CASE (verify_invalid_interop_subtitles)
792 {
793         using namespace boost::filesystem;
794
795         path const dir("build/test/verify_invalid_interop_subtitles");
796         prepare_directory (dir);
797         copy_file ("test/data/subs1.xml", dir / "subs.xml");
798         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
799         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
800         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
801
802         {
803                 Editor e (dir / "subs.xml");
804                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
805         }
806
807         check_verify_result (
808                 { dir },
809                 {
810                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
811                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 5 },
812                         {
813                                 dcp::VerificationNote::Type::ERROR,
814                                 dcp::VerificationNote::Code::INVALID_XML,
815                                 string("element 'Foo' is not allowed for content model '(SubtitleID,MovieTitle,ReelNumber,Language,LoadFont*,Font*,Subtitle*)'"),
816                                 path(),
817                                 29
818                         },
819                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"theFontId"} }
820                 });
821 }
822
823
824 BOOST_AUTO_TEST_CASE(verify_interop_subtitle_asset_with_no_subtitles)
825 {
826         path const dir("build/test/verify_interop_subtitle_asset_with_no_subtitles");
827         prepare_directory(dir);
828         copy_file("test/data/subs4.xml", dir / "subs.xml");
829         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
830         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
831         write_dcp_with_single_asset(dir, reel_asset, dcp::Standard::INTEROP);
832
833         check_verify_result (
834                 { dir },
835                 {
836                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
837                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
838                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"theFontId"} }
839                 });
840
841 }
842
843
844 BOOST_AUTO_TEST_CASE(verify_interop_subtitle_asset_with_single_space_subtitle)
845 {
846         path const dir("build/test/verify_interop_subtitle_asset_with_single_space_subtitle");
847         prepare_directory(dir);
848         copy_file("test/data/subs5.xml", dir / "subs.xml");
849         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
850         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
851         write_dcp_with_single_asset(dir, reel_asset, dcp::Standard::INTEROP);
852
853         check_verify_result (
854                 { dir },
855                 {
856                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
857                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"Arial"} }
858                 });
859
860 }
861
862
863 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
864 {
865         path const dir("build/test/verify_valid_smpte_subtitles");
866         prepare_directory (dir);
867         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
868         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
869         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
870         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
871
872         check_verify_result(
873                 {dir},
874                 {
875                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
876                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2021-04-14T13:19:14.000+02:00"} },
877                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() }
878                 });
879 }
880
881
882 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
883 {
884         using namespace boost::filesystem;
885
886         path const dir("build/test/verify_invalid_smpte_subtitles");
887         prepare_directory (dir);
888         /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
889         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
890         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
891         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
892         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
893
894         check_verify_result (
895                 { dir },
896                 {
897                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
898                         {
899                                 dcp::VerificationNote::Type::ERROR,
900                                 dcp::VerificationNote::Code::INVALID_XML,
901                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
902                                 path(),
903                                 2
904                         },
905                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
906                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
907                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2020-05-09T00:29:21.000+02:00"} },
908                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() }
909                 });
910 }
911
912
913 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles)
914 {
915         path const dir("build/test/verify_empty_text_node_in_subtitles");
916         prepare_directory (dir);
917         copy_file ("test/data/empty_text.mxf", dir / "subs.mxf");
918         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
919         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
920         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
921
922         check_verify_result (
923                 { dir },
924                 {
925                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
926                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
927                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
928                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
929                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2021-08-09T18:34:46.000+02:00"} },
930                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() }
931                 });
932 }
933
934
935 /** A <Text> node with no content except some <Font> nodes, which themselves do have content */
936 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_child_nodes)
937 {
938         path const dir("build/test/verify_empty_text_node_in_subtitles_with_child_nodes");
939         prepare_directory (dir);
940         copy_file ("test/data/empty_but_with_children.xml", dir / "subs.xml");
941         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
942         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
943         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
944
945         check_verify_result (
946                 { dir },
947                 {
948                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
949                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"font0"} }
950                 });
951 }
952
953
954 /** A <Text> node with no content except some <Font> nodes, which themselves also have no content */
955 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_empty_child_nodes)
956 {
957         path const dir("build/test/verify_empty_text_node_in_subtitles_with_empty_child_nodes");
958         prepare_directory (dir);
959         copy_file ("test/data/empty_with_empty_children.xml", dir / "subs.xml");
960         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
961         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
962         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
963
964         check_verify_result (
965                 { dir },
966                 {
967                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
968                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
969                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
970                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_FONT, string{"font0"} },
971                 });
972 }
973
974
975 BOOST_AUTO_TEST_CASE (verify_external_asset)
976 {
977         path const ov_dir("build/test/verify_external_asset");
978         prepare_directory (ov_dir);
979
980         auto image = black_image ();
981         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
982         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
983         dcp_from_frame (frame, ov_dir);
984
985         dcp::DCP ov (ov_dir);
986         ov.read ();
987
988         path const vf_dir("build/test/verify_external_asset_vf");
989         prepare_directory (vf_dir);
990
991         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
992         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
993
994         check_verify_result (
995                 { vf_dir },
996                 {
997                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
998                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
999                 });
1000 }
1001
1002
1003 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
1004 {
1005         path const dir("build/test/verify_valid_cpl_metadata");
1006         prepare_directory (dir);
1007
1008         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1009         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1010         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
1011
1012         auto reel = make_shared<dcp::Reel>();
1013         reel->add (reel_asset);
1014
1015         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
1016         reel->add (simple_markers(16 * 24));
1017
1018         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1019         cpl->add (reel);
1020         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
1021         cpl->set_main_sound_sample_rate (48000);
1022         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1023         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1024         cpl->set_version_number (1);
1025
1026         dcp::DCP dcp (dir);
1027         dcp.add (cpl);
1028         dcp.set_annotation_text("hello");
1029         dcp.write_xml ();
1030 }
1031
1032
1033 path
1034 find_prefix(path dir, string prefix)
1035 {
1036         auto iter = std::find_if(directory_iterator(dir), directory_iterator(), [prefix](path const& p) {
1037                 return boost::starts_with(p.filename().string(), prefix);
1038         });
1039
1040         BOOST_REQUIRE(iter != directory_iterator());
1041         return iter->path();
1042 }
1043
1044
1045 path find_cpl (path dir)
1046 {
1047         return find_prefix(dir, "cpl_");
1048 }
1049
1050
1051 path
1052 find_pkl(path dir)
1053 {
1054         return find_prefix(dir, "pkl_");
1055 }
1056
1057
1058 path
1059 find_asset_map(path dir)
1060 {
1061         return find_prefix(dir, "ASSETMAP");
1062 }
1063
1064
1065 /* DCP with invalid CompositionMetadataAsset */
1066 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
1067 {
1068         using namespace boost::filesystem;
1069
1070         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
1071         prepare_directory (dir);
1072
1073         auto reel = make_shared<dcp::Reel>();
1074         reel->add (black_picture_asset(dir));
1075         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1076         cpl->add (reel);
1077         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
1078         cpl->set_main_sound_sample_rate (48000);
1079         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1080         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1081         cpl->set_version_number (1);
1082
1083         reel->add (simple_markers());
1084
1085         dcp::DCP dcp (dir);
1086         dcp.add (cpl);
1087         dcp.set_annotation_text("hello");
1088         dcp.write_xml();
1089
1090         {
1091                 Editor e (find_cpl(dir));
1092                 e.replace ("MainSound", "MainSoundX");
1093         }
1094
1095         check_verify_result (
1096                 { dir },
1097                 {
1098                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 50 },
1099                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 51 },
1100                         {
1101                                 dcp::VerificationNote::Type::ERROR,
1102                                 dcp::VerificationNote::Code::INVALID_XML,
1103                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
1104                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
1105                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
1106                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
1107                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
1108                                        "ExtensionMetadataList?,)'"),
1109                                 canonical(cpl->file().get()),
1110                                 71
1111                         },
1112                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
1113                 });
1114 }
1115
1116
1117 /* DCP with invalid CompositionMetadataAsset */
1118 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
1119 {
1120         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
1121         prepare_directory (dir);
1122
1123         auto reel = make_shared<dcp::Reel>();
1124         reel->add (black_picture_asset(dir));
1125         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1126         cpl->add (reel);
1127         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
1128         cpl->set_main_sound_sample_rate (48000);
1129         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1130         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1131
1132         dcp::DCP dcp (dir);
1133         dcp.add (cpl);
1134         dcp.set_annotation_text("hello");
1135         dcp.write_xml();
1136
1137         {
1138                 Editor e (find_cpl(dir));
1139                 e.replace ("meta:Width", "meta:WidthX");
1140         }
1141
1142         check_verify_result (
1143                 { dir },
1144                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
1145                 );
1146 }
1147
1148
1149 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
1150 {
1151         path const dir("build/test/verify_invalid_language1");
1152         prepare_directory (dir);
1153         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1154         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1155         asset->_language = "wrong-andbad";
1156         asset->write (dir / "subs.mxf");
1157         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1158         reel_asset->_language = "badlang";
1159         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1160
1161         check_verify_result (
1162                 { dir },
1163                 {
1164                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1165                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1166                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1167                 });
1168 }
1169
1170
1171 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
1172 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
1173 {
1174         path const dir("build/test/verify_invalid_language2");
1175         prepare_directory (dir);
1176         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1177         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1178         asset->_language = "wrong-andbad";
1179         asset->write (dir / "subs.mxf");
1180         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1181         reel_asset->_language = "badlang";
1182         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1183
1184         check_verify_result (
1185                 {dir},
1186                 {
1187                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1188                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1189                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1190                 });
1191 }
1192
1193
1194 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
1195  * the release territory.
1196  */
1197 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
1198 {
1199         path const dir("build/test/verify_invalid_language3");
1200         prepare_directory (dir);
1201
1202         auto picture = simple_picture (dir, "foo");
1203         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1204         auto reel = make_shared<dcp::Reel>();
1205         reel->add (reel_picture);
1206         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
1207         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1208         reel->add (reel_sound);
1209         reel->add (simple_markers());
1210
1211         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1212         cpl->add (reel);
1213         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
1214         cpl->_additional_subtitle_languages.push_back("andso-is-this");
1215         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
1216         cpl->set_main_sound_sample_rate (48000);
1217         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1218         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1219         cpl->set_version_number (1);
1220         cpl->_release_territory = "fred-jim";
1221         auto dcp = make_shared<dcp::DCP>(dir);
1222         dcp->add (cpl);
1223         dcp->set_annotation_text("hello");
1224         dcp->write_xml();
1225
1226         check_verify_result (
1227                 { dir },
1228                 {
1229                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1230                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1231                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1232                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1233                 });
1234 }
1235
1236
1237 static
1238 vector<dcp::VerificationNote>
1239 check_picture_size (int width, int height, int frame_rate, bool three_d)
1240 {
1241         using namespace boost::filesystem;
1242
1243         path dcp_path = "build/test/verify_picture_test";
1244         prepare_directory (dcp_path);
1245
1246         shared_ptr<dcp::PictureAsset> mp;
1247         if (three_d) {
1248                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1249         } else {
1250                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1251         }
1252         auto picture_writer = mp->start_write(dcp_path / "video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
1253
1254         auto image = black_image (dcp::Size(width, height));
1255         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1256         int const length = three_d ? frame_rate * 2 : frame_rate;
1257         for (int i = 0; i < length; ++i) {
1258                 picture_writer->write (j2c.data(), j2c.size());
1259         }
1260         picture_writer->finalize ();
1261
1262         auto d = make_shared<dcp::DCP>(dcp_path);
1263         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1264         cpl->set_annotation_text ("A Test DCP");
1265         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1266         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
1267         cpl->set_main_sound_sample_rate (48000);
1268         cpl->set_main_picture_stored_area(dcp::Size(width, height));
1269         cpl->set_main_picture_active_area(dcp::Size(width, height));
1270         cpl->set_version_number (1);
1271
1272         auto reel = make_shared<dcp::Reel>();
1273
1274         if (three_d) {
1275                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1276         } else {
1277                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1278         }
1279
1280         reel->add (simple_markers(frame_rate));
1281
1282         cpl->add (reel);
1283
1284         d->add (cpl);
1285         d->set_annotation_text("A Test DCP");
1286         d->write_xml();
1287
1288         return dcp::verify({dcp_path}, &stage, &progress, {}, xsd_test);
1289 }
1290
1291
1292 static
1293 void
1294 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1295 {
1296         auto notes = check_picture_size(width, height, frame_rate, three_d);
1297         BOOST_CHECK_EQUAL (notes.size(), 0U);
1298 }
1299
1300
1301 static
1302 void
1303 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1304 {
1305         auto notes = check_picture_size(width, height, frame_rate, three_d);
1306         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1307         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1308         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1309 }
1310
1311
1312 static
1313 void
1314 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1315 {
1316         auto notes = check_picture_size(width, height, frame_rate, three_d);
1317         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1318         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1319         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1320 }
1321
1322
1323 static
1324 void
1325 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1326 {
1327         auto notes = check_picture_size(width, height, frame_rate, three_d);
1328         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1329         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1330         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1331 }
1332
1333
1334 BOOST_AUTO_TEST_CASE (verify_picture_size)
1335 {
1336         using namespace boost::filesystem;
1337
1338         /* 2K scope */
1339         check_picture_size_ok (2048, 858, 24, false);
1340         check_picture_size_ok (2048, 858, 25, false);
1341         check_picture_size_ok (2048, 858, 48, false);
1342         check_picture_size_ok (2048, 858, 24, true);
1343         check_picture_size_ok (2048, 858, 25, true);
1344         check_picture_size_ok (2048, 858, 48, true);
1345
1346         /* 2K flat */
1347         check_picture_size_ok (1998, 1080, 24, false);
1348         check_picture_size_ok (1998, 1080, 25, false);
1349         check_picture_size_ok (1998, 1080, 48, false);
1350         check_picture_size_ok (1998, 1080, 24, true);
1351         check_picture_size_ok (1998, 1080, 25, true);
1352         check_picture_size_ok (1998, 1080, 48, true);
1353
1354         /* 4K scope */
1355         check_picture_size_ok (4096, 1716, 24, false);
1356
1357         /* 4K flat */
1358         check_picture_size_ok (3996, 2160, 24, false);
1359
1360         /* Bad frame size */
1361         check_picture_size_bad_frame_size (2050, 858, 24, false);
1362         check_picture_size_bad_frame_size (2048, 658, 25, false);
1363         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1364         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1365
1366         /* Bad 2K frame rate */
1367         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1368         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1369         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1370
1371         /* Bad 4K frame rate */
1372         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1373         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1374
1375         /* No 4K 3D */
1376         auto notes = check_picture_size(3996, 2160, 24, true);
1377         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1378         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1379         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1380 }
1381
1382
1383 static
1384 void
1385 add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, dcp::VAlign v_align = dcp::VAlign::CENTER, string text = "Hello")
1386 {
1387         asset->add (
1388                 std::make_shared<dcp::SubtitleString>(
1389                         optional<string>(),
1390                         false,
1391                         false,
1392                         false,
1393                         dcp::Colour(),
1394                         42,
1395                         1,
1396                         dcp::Time(start_frame, 24, 24),
1397                         dcp::Time(end_frame, 24, 24),
1398                         0,
1399                         dcp::HAlign::CENTER,
1400                         v_position,
1401                         v_align,
1402                         0,
1403                         dcp::Direction::LTR,
1404                         text,
1405                         dcp::Effect::NONE,
1406                         dcp::Colour(),
1407                         dcp::Time(),
1408                         dcp::Time(),
1409                         0
1410                 )
1411         );
1412 }
1413
1414
1415 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1416 {
1417         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1418         prepare_directory (dir);
1419
1420         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1421         for (int i = 0; i < 2048; ++i) {
1422                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1423         }
1424         add_font(asset);
1425         asset->set_language (dcp::LanguageTag("de-DE"));
1426         asset->write (dir / "subs.mxf");
1427         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1428         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1429
1430         check_verify_result (
1431                 { dir },
1432                 {
1433                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1434                         {
1435                                 dcp::VerificationNote::Type::BV21_ERROR,
1436                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1437                                 string("419371"),
1438                                 canonical(dir / "subs.mxf")
1439                         },
1440                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1441                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1442                 });
1443 }
1444
1445
1446 static
1447 shared_ptr<dcp::SMPTESubtitleAsset>
1448 make_large_subtitle_asset (path font_file)
1449 {
1450         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1451         dcp::ArrayData big_fake_font(1024 * 1024);
1452         big_fake_font.write (font_file);
1453         for (int i = 0; i < 116; ++i) {
1454                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1455         }
1456         return asset;
1457 }
1458
1459
1460 template <class T>
1461 void
1462 verify_timed_text_asset_too_large (string name)
1463 {
1464         auto const dir = path("build/test") / name;
1465         prepare_directory (dir);
1466         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1467         add_test_subtitle (asset, 0, 240);
1468         asset->set_language (dcp::LanguageTag("de-DE"));
1469         asset->write (dir / "subs.mxf");
1470
1471         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1472         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1473
1474         check_verify_result (
1475                 { dir },
1476                 {
1477                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695488"), canonical(dir / "subs.mxf") },
1478                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1479                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1480                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1481                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1482                 });
1483 }
1484
1485
1486 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1487 {
1488         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1489         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1490 }
1491
1492
1493 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1494 {
1495         path dir = "build/test/verify_missing_subtitle_language";
1496         prepare_directory (dir);
1497         auto dcp = make_simple (dir, 1, 106);
1498
1499         string const xml =
1500                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1501                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1502                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1503                 "<ContentTitleText>Content</ContentTitleText>"
1504                 "<AnnotationText>Annotation</AnnotationText>"
1505                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1506                 "<ReelNumber>1</ReelNumber>"
1507                 "<EditRate>24 1</EditRate>"
1508                 "<TimeCodeRate>24</TimeCodeRate>"
1509                 "<StartTime>00:00:00:00</StartTime>"
1510                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1511                 "<SubtitleList>"
1512                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1513                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1514                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1515                 "</Subtitle>"
1516                 "</Font>"
1517                 "</SubtitleList>"
1518                 "</SubtitleReel>";
1519
1520         dcp::File xml_file(dir / "subs.xml", "w");
1521         BOOST_REQUIRE (xml_file);
1522         xml_file.write(xml.c_str(), xml.size(), 1);
1523         xml_file.close();
1524         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1525         subs->write (dir / "subs.mxf");
1526
1527         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1528         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1529         dcp->set_annotation_text("A Test DCP");
1530         dcp->write_xml();
1531
1532         check_verify_result (
1533                 { dir },
1534                 {
1535                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1536                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1537                 });
1538 }
1539
1540
1541 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1542 {
1543         path path ("build/test/verify_mismatched_subtitle_languages");
1544         auto constexpr reel_length = 192;
1545         auto dcp = make_simple (path, 2, reel_length);
1546         auto cpl = dcp->cpls()[0];
1547
1548         {
1549                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1550                 subs->set_language (dcp::LanguageTag("de-DE"));
1551                 subs->add (simple_subtitle());
1552                 add_font(subs);
1553                 subs->write (path / "subs1.mxf");
1554                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1555                 cpl->reels()[0]->add(reel_subs);
1556         }
1557
1558         {
1559                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1560                 subs->set_language (dcp::LanguageTag("en-US"));
1561                 subs->add (simple_subtitle());
1562                 add_font(subs);
1563                 subs->write (path / "subs2.mxf");
1564                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1565                 cpl->reels()[1]->add(reel_subs);
1566         }
1567
1568         dcp->set_annotation_text("A Test DCP");
1569         dcp->write_xml();
1570
1571         check_verify_result (
1572                 { path },
1573                 {
1574                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1575                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1576                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1577                 });
1578 }
1579
1580
1581 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1582 {
1583         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1584         auto constexpr reel_length = 192;
1585         auto dcp = make_simple (path, 2, reel_length);
1586         auto cpl = dcp->cpls()[0];
1587
1588         {
1589                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1590                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1591                 ccaps->add (simple_subtitle());
1592                 add_font(ccaps);
1593                 ccaps->write (path / "subs1.mxf");
1594                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1595                 cpl->reels()[0]->add(reel_ccaps);
1596         }
1597
1598         {
1599                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1600                 ccaps->set_language (dcp::LanguageTag("en-US"));
1601                 ccaps->add (simple_subtitle());
1602                 add_font(ccaps);
1603                 ccaps->write (path / "subs2.mxf");
1604                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1605                 cpl->reels()[1]->add(reel_ccaps);
1606         }
1607
1608         dcp->set_annotation_text("A Test DCP");
1609         dcp->write_xml();
1610
1611         check_verify_result (
1612                 { path },
1613                 {
1614                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1615                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1616                 });
1617 }
1618
1619
1620 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1621 {
1622         path dir = "build/test/verify_missing_subtitle_start_time";
1623         prepare_directory (dir);
1624         auto dcp = make_simple (dir, 1, 106);
1625
1626         string const xml =
1627                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1628                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1629                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1630                 "<ContentTitleText>Content</ContentTitleText>"
1631                 "<AnnotationText>Annotation</AnnotationText>"
1632                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1633                 "<ReelNumber>1</ReelNumber>"
1634                 "<Language>de-DE</Language>"
1635                 "<EditRate>24 1</EditRate>"
1636                 "<TimeCodeRate>24</TimeCodeRate>"
1637                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1638                 "<SubtitleList>"
1639                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1640                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1641                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1642                 "</Subtitle>"
1643                 "</Font>"
1644                 "</SubtitleList>"
1645                 "</SubtitleReel>";
1646
1647         dcp::File xml_file(dir / "subs.xml", "w");
1648         BOOST_REQUIRE (xml_file);
1649         xml_file.write(xml.c_str(), xml.size(), 1);
1650         xml_file.close();
1651         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1652         subs->write (dir / "subs.mxf");
1653
1654         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1655         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1656         dcp->set_annotation_text("A Test DCP");
1657         dcp->write_xml();
1658
1659         check_verify_result (
1660                 { dir },
1661                 {
1662                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1663                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1664                 });
1665 }
1666
1667
1668 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1669 {
1670         path dir = "build/test/verify_invalid_subtitle_start_time";
1671         prepare_directory (dir);
1672         auto dcp = make_simple (dir, 1, 106);
1673
1674         string const xml =
1675                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1676                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1677                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1678                 "<ContentTitleText>Content</ContentTitleText>"
1679                 "<AnnotationText>Annotation</AnnotationText>"
1680                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1681                 "<ReelNumber>1</ReelNumber>"
1682                 "<Language>de-DE</Language>"
1683                 "<EditRate>24 1</EditRate>"
1684                 "<TimeCodeRate>24</TimeCodeRate>"
1685                 "<StartTime>00:00:02:00</StartTime>"
1686                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1687                 "<SubtitleList>"
1688                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1689                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1690                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1691                 "</Subtitle>"
1692                 "</Font>"
1693                 "</SubtitleList>"
1694                 "</SubtitleReel>";
1695
1696         dcp::File xml_file(dir / "subs.xml", "w");
1697         BOOST_REQUIRE (xml_file);
1698         xml_file.write(xml.c_str(), xml.size(), 1);
1699         xml_file.close();
1700         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1701         subs->write (dir / "subs.mxf");
1702
1703         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1704         dcp->cpls().front()->reels().front()->add(reel_subs);
1705         dcp->set_annotation_text("A Test DCP");
1706         dcp->write_xml();
1707
1708         check_verify_result (
1709                 { dir },
1710                 {
1711                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1712                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1713                 });
1714 }
1715
1716
1717 class TestText
1718 {
1719 public:
1720         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1721                 : in(in_)
1722                 , out(out_)
1723                 , v_position(v_position_)
1724                 , v_align(v_align_)
1725                 , text(text_)
1726         {}
1727
1728         int in;
1729         int out;
1730         float v_position;
1731         dcp::VAlign v_align;
1732         string text;
1733 };
1734
1735
1736 template <class T>
1737 shared_ptr<dcp::CPL>
1738 dcp_with_text (path dir, vector<TestText> subs)
1739 {
1740         prepare_directory (dir);
1741         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1742         asset->set_start_time (dcp::Time());
1743         for (auto i: subs) {
1744                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1745         }
1746         asset->set_language (dcp::LanguageTag("de-DE"));
1747         add_font(asset);
1748         asset->write (dir / "subs.mxf");
1749
1750         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1751         return write_dcp_with_single_asset (dir, reel_asset);
1752 }
1753
1754
1755 template <class T>
1756 shared_ptr<dcp::CPL>
1757 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1758 {
1759         prepare_directory (dir);
1760         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1761         asset->set_start_time (dcp::Time());
1762         asset->set_language (dcp::LanguageTag("de-DE"));
1763
1764         auto subs_mxf = dir / "subs.mxf";
1765         asset->write (subs_mxf);
1766
1767         /* The call to write() puts the asset into the DCP correctly but it will have
1768          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1769          * contents.
1770          */
1771         ASDCP::TimedText::MXFWriter writer;
1772         ASDCP::WriterInfo writer_info;
1773         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1774         unsigned int c;
1775         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1776         DCP_ASSERT (c == Kumu::UUID_Length);
1777         ASDCP::TimedText::TimedTextDescriptor descriptor;
1778         descriptor.ContainerDuration = asset->intrinsic_duration();
1779         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1780         DCP_ASSERT (c == Kumu::UUID_Length);
1781         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1782         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1783         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1784         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1785         writer.Finalize ();
1786
1787         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1788         return write_dcp_with_single_asset (dir, reel_asset);
1789 }
1790
1791
1792 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1793 {
1794         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1795         /* Just too early */
1796         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1797         check_verify_result (
1798                 { dir },
1799                 {
1800                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1801                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1802                 });
1803
1804 }
1805
1806
1807 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1808 {
1809         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1810         /* Just late enough */
1811         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1812         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1813 }
1814
1815
1816 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1817 {
1818         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1819         prepare_directory (dir);
1820
1821         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1822         asset1->set_start_time (dcp::Time());
1823         /* Just late enough */
1824         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1825         asset1->set_language (dcp::LanguageTag("de-DE"));
1826         add_font(asset1);
1827         asset1->write (dir / "subs1.mxf");
1828         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1829         auto reel1 = make_shared<dcp::Reel>();
1830         reel1->add (reel_asset1);
1831         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1832         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1833         reel1->add (markers1);
1834
1835         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1836         asset2->set_start_time (dcp::Time());
1837         add_font(asset2);
1838         /* This would be too early on first reel but should be OK on the second */
1839         add_test_subtitle (asset2, 3, 4 * 24);
1840         asset2->set_language (dcp::LanguageTag("de-DE"));
1841         asset2->write (dir / "subs2.mxf");
1842         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1843         auto reel2 = make_shared<dcp::Reel>();
1844         reel2->add (reel_asset2);
1845         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1846         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1847         reel2->add (markers2);
1848
1849         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1850         cpl->add (reel1);
1851         cpl->add (reel2);
1852         auto dcp = make_shared<dcp::DCP>(dir);
1853         dcp->add (cpl);
1854         dcp->set_annotation_text("hello");
1855         dcp->write_xml();
1856
1857         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1858 }
1859
1860
1861 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1862 {
1863         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1864         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1865                 dir,
1866                 {
1867                         { 4 * 24,     5 * 24 },
1868                         { 5 * 24 + 1, 6 * 24 },
1869                 });
1870         check_verify_result (
1871                 {dir},
1872                 {
1873                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1874                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1875                 });
1876 }
1877
1878
1879 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1880 {
1881         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1882         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1883                 dir,
1884                 {
1885                         { 4 * 24,      5 * 24 },
1886                         { 5 * 24 + 16, 8 * 24 },
1887                 });
1888         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1889 }
1890
1891
1892 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1893 {
1894         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1895         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1896         check_verify_result (
1897                 {dir},
1898                 {
1899                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1900                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1901                 });
1902 }
1903
1904
1905 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1906 {
1907         auto const dir = path("build/test/verify_valid_subtitle_duration");
1908         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1909         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1910 }
1911
1912
1913 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1914 {
1915         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1916         prepare_directory (dir);
1917         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1918         asset->set_start_time (dcp::Time());
1919         add_test_subtitle (asset, 0, 4 * 24);
1920         add_font(asset);
1921         asset->set_language (dcp::LanguageTag("de-DE"));
1922         asset->write (dir / "subs.mxf");
1923
1924         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1925         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1926         check_verify_result (
1927                 {dir},
1928                 {
1929                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1930                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1931                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1932                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1933                 });
1934
1935 }
1936
1937
1938 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1939 {
1940         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1941         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1942                 dir,
1943                 {
1944                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1945                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1946                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1947                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1948                 });
1949         check_verify_result (
1950                 {dir},
1951                 {
1952                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1953                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1954                 });
1955 }
1956
1957
1958 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1959 {
1960         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1961         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1962                 dir,
1963                 {
1964                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1965                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1966                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1967                 });
1968         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1969 }
1970
1971
1972 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1973 {
1974         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1975         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1976                 dir,
1977                 {
1978                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1979                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1980                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1981                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1982                 });
1983         check_verify_result (
1984                 {dir},
1985                 {
1986                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1987                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1988                 });
1989 }
1990
1991
1992 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1993 {
1994         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1995         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1996                 dir,
1997                 {
1998                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1999                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2000                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2001                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2002                 });
2003         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2004 }
2005
2006
2007 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
2008 {
2009         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
2010         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
2011                 dir,
2012                 {
2013                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
2014                 });
2015         check_verify_result (
2016                 {dir},
2017                 {
2018                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
2019                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2020                 });
2021 }
2022
2023
2024 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
2025 {
2026         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
2027         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
2028                 dir,
2029                 {
2030                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
2031                 });
2032         check_verify_result (
2033                 {dir},
2034                 {
2035                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
2036                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2037                 });
2038 }
2039
2040
2041 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
2042 {
2043         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
2044         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2045                 dir,
2046                 {
2047                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
2048                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2049                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2050                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
2051                 });
2052         check_verify_result (
2053                 {dir},
2054                 {
2055                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2056                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2057                 });
2058 }
2059
2060
2061 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
2062 {
2063         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
2064         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2065                 dir,
2066                 {
2067                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
2068                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2069                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2070                 });
2071         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2072 }
2073
2074
2075 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
2076 {
2077         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
2078         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2079                 dir,
2080                 {
2081                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2082                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2083                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2084                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
2085                 });
2086         check_verify_result (
2087                 {dir},
2088                 {
2089                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2090                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2091                 });
2092 }
2093
2094
2095 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
2096 {
2097         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
2098         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2099                 dir,
2100                 {
2101                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2102                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2103                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2104                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2105                 });
2106         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2107 }
2108
2109
2110 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
2111 {
2112         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
2113         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2114                 dir,
2115                 {
2116                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
2117                 });
2118         check_verify_result (
2119                 {dir},
2120                 {
2121                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2122                 });
2123 }
2124
2125
2126 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
2127 {
2128         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
2129         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2130                 dir,
2131                 {
2132                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
2133                 });
2134         check_verify_result (
2135                 {dir},
2136                 {
2137                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
2138                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2139                 });
2140 }
2141
2142
2143 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
2144 {
2145         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
2146         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2147                 dir,
2148                 {
2149                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2150                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2151                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2152                 });
2153         check_verify_result (
2154                 {dir},
2155                 {
2156                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2157                 });
2158 }
2159
2160
2161 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2162 {
2163         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2164         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2165                 dir,
2166                 {
2167                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2168                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2169                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2170                 });
2171         check_verify_result (
2172                 {dir},
2173                 {
2174                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2175                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2176                 });
2177 }
2178
2179
2180 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2181 {
2182         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2183         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2184                 dir,
2185                 {
2186                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2187                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2188                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2189                 });
2190         check_verify_result (
2191                 {dir},
2192                 {
2193                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2194                 });
2195 }
2196
2197
2198 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2199 {
2200         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2201         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2202                 dir,
2203                 {
2204                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2205                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2206                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2207                 });
2208         check_verify_result (
2209                 {dir},
2210                 {
2211                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2212                 });
2213 }
2214
2215
2216 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2217 {
2218         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2219         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2220         check_verify_result (
2221                 {dir},
2222                 {
2223                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2224                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2225                 });
2226 }
2227
2228
2229 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2230 {
2231         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2232         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2233         check_verify_result (
2234                 {dir},
2235                 {
2236                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2237                 });
2238 }
2239
2240
2241
2242 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2243 {
2244         path const dir("build/test/verify_invalid_sound_frame_rate");
2245         prepare_directory (dir);
2246
2247         auto picture = simple_picture (dir, "foo");
2248         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2249         auto reel = make_shared<dcp::Reel>();
2250         reel->add (reel_picture);
2251         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2252         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2253         reel->add (reel_sound);
2254         reel->add (simple_markers());
2255         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2256         cpl->add (reel);
2257         auto dcp = make_shared<dcp::DCP>(dir);
2258         dcp->add (cpl);
2259         dcp->set_annotation_text("hello");
2260         dcp->write_xml();
2261
2262         check_verify_result (
2263                 {dir},
2264                 {
2265                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2266                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2267                 });
2268 }
2269
2270
2271 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2272 {
2273         path const dir("build/test/verify_missing_cpl_annotation_text");
2274         auto dcp = make_simple (dir);
2275         dcp->set_annotation_text("A Test DCP");
2276         dcp->write_xml();
2277
2278         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2279
2280         auto const cpl = dcp->cpls()[0];
2281
2282         {
2283                 BOOST_REQUIRE (cpl->file());
2284                 Editor e(cpl->file().get());
2285                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2286         }
2287
2288         check_verify_result (
2289                 {dir},
2290                 {
2291                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2292                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2293                 });
2294 }
2295
2296
2297 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2298 {
2299         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2300         auto dcp = make_simple (dir);
2301         dcp->set_annotation_text("A Test DCP");
2302         dcp->write_xml();
2303
2304         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2305         auto const cpl = dcp->cpls()[0];
2306
2307         {
2308                 BOOST_REQUIRE (cpl->file());
2309                 Editor e(cpl->file().get());
2310                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2311         }
2312
2313         check_verify_result (
2314                 {dir},
2315                 {
2316                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2317                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2318                 });
2319 }
2320
2321
2322 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2323 {
2324         path const dir("build/test/verify_mismatched_asset_duration");
2325         prepare_directory (dir);
2326         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2327         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2328
2329         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2330         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2331
2332         auto reel = make_shared<dcp::Reel>(
2333                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2334                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2335                 );
2336
2337         reel->add (simple_markers());
2338         cpl->add (reel);
2339
2340         dcp->add (cpl);
2341         dcp->set_annotation_text("A Test DCP");
2342         dcp->write_xml();
2343
2344         check_verify_result (
2345                 {dir},
2346                 {
2347                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2348                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2349                 });
2350 }
2351
2352
2353
2354 static
2355 shared_ptr<dcp::CPL>
2356 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2357 {
2358         prepare_directory (dir);
2359         auto dcp = make_shared<dcp::DCP>(dir);
2360         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2361
2362         auto constexpr reel_length = 192;
2363
2364         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2365         subs->set_language (dcp::LanguageTag("de-DE"));
2366         subs->set_start_time (dcp::Time());
2367         subs->add (simple_subtitle());
2368         add_font(subs);
2369         subs->write (dir / "subs.mxf");
2370         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2371
2372         auto reel1 = make_shared<dcp::Reel>(
2373                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2374                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2375                 );
2376
2377         if (add_to_reel1) {
2378                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2379         }
2380
2381         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2382         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2383         reel1->add (markers1);
2384
2385         cpl->add (reel1);
2386
2387         auto reel2 = make_shared<dcp::Reel>(
2388                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2389                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2390                 );
2391
2392         if (add_to_reel2) {
2393                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2394         }
2395
2396         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2397         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2398         reel2->add (markers2);
2399
2400         cpl->add (reel2);
2401
2402         dcp->add (cpl);
2403         dcp->set_annotation_text("A Test DCP");
2404         dcp->write_xml();
2405
2406         return cpl;
2407 }
2408
2409
2410 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2411 {
2412         {
2413                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2414                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2415                 check_verify_result (
2416                         { dir },
2417                         {
2418                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2419                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2420                         });
2421
2422         }
2423
2424         {
2425                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2426                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2427                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2428         }
2429
2430         {
2431                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2432                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2433                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2434         }
2435 }
2436
2437
2438 static
2439 shared_ptr<dcp::CPL>
2440 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2441 {
2442         prepare_directory (dir);
2443         auto dcp = make_shared<dcp::DCP>(dir);
2444         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2445
2446         auto constexpr reel_length = 192;
2447
2448         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2449         subs->set_language (dcp::LanguageTag("de-DE"));
2450         subs->set_start_time (dcp::Time());
2451         subs->add (simple_subtitle());
2452         add_font(subs);
2453         subs->write (dir / "subs.mxf");
2454
2455         auto reel1 = make_shared<dcp::Reel>(
2456                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2457                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2458                 );
2459
2460         for (int i = 0; i < caps_in_reel1; ++i) {
2461                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2462         }
2463
2464         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2465         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2466         reel1->add (markers1);
2467
2468         cpl->add (reel1);
2469
2470         auto reel2 = make_shared<dcp::Reel>(
2471                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2472                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2473                 );
2474
2475         for (int i = 0; i < caps_in_reel2; ++i) {
2476                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2477         }
2478
2479         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2480         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2481         reel2->add (markers2);
2482
2483         cpl->add (reel2);
2484
2485         dcp->add (cpl);
2486         dcp->set_annotation_text("A Test DCP");
2487         dcp->write_xml();
2488
2489         return cpl;
2490 }
2491
2492
2493 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2494 {
2495         {
2496                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2497                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2498                 check_verify_result (
2499                         {dir},
2500                         {
2501                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2502                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2503                         });
2504         }
2505
2506         {
2507                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2508                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2509                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2510         }
2511
2512         {
2513                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2514                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2515                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2516         }
2517 }
2518
2519
2520 template <class T>
2521 void
2522 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2523 {
2524         prepare_directory (dir);
2525         auto dcp = make_shared<dcp::DCP>(dir);
2526         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2527
2528         auto constexpr reel_length = 192;
2529
2530         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2531         subs->set_language (dcp::LanguageTag("de-DE"));
2532         subs->set_start_time (dcp::Time());
2533         subs->add (simple_subtitle());
2534         add_font(subs);
2535         subs->write (dir / "subs.mxf");
2536         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2537         adjust (reel_text);
2538
2539         auto reel = make_shared<dcp::Reel>(
2540                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2541                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2542                 );
2543
2544         reel->add (reel_text);
2545
2546         reel->add (simple_markers(reel_length));
2547
2548         cpl->add (reel);
2549
2550         dcp->add (cpl);
2551         dcp->set_annotation_text("A Test DCP");
2552         dcp->write_xml();
2553
2554         check_verify_result (
2555                 {dir},
2556                 {
2557                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2558                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2559                 });
2560 }
2561
2562
2563 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2564 {
2565         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2566                 "build/test/verify_subtitle_entry_point_must_be_present",
2567                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2568                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2569                         asset->unset_entry_point ();
2570                         }
2571                 );
2572
2573         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2574                 "build/test/verify_subtitle_entry_point_must_be_zero",
2575                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2576                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2577                         asset->set_entry_point (4);
2578                         }
2579                 );
2580
2581         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2582                 "build/test/verify_closed_caption_entry_point_must_be_present",
2583                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2584                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2585                         asset->unset_entry_point ();
2586                         }
2587                 );
2588
2589         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2590                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2591                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2592                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2593                         asset->set_entry_point (9);
2594                         }
2595                 );
2596 }
2597
2598
2599 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2600 {
2601         RNGFixer fix;
2602
2603         path const dir("build/test/verify_missing_hash");
2604         auto dcp = make_simple (dir);
2605         dcp->set_annotation_text("A Test DCP");
2606         dcp->write_xml();
2607
2608         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2609         auto const cpl = dcp->cpls()[0];
2610         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2611         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2612         auto asset_id = cpl->reels()[0]->main_picture()->id();
2613
2614         {
2615                 BOOST_REQUIRE (cpl->file());
2616                 Editor e(cpl->file().get());
2617                 e.delete_first_line_containing("<Hash>");
2618         }
2619
2620         check_verify_result (
2621                 {dir},
2622                 {
2623                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2624                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2625                 });
2626 }
2627
2628
2629 static
2630 void
2631 verify_markers_test (
2632         path dir,
2633         vector<pair<dcp::Marker, dcp::Time>> markers,
2634         vector<dcp::VerificationNote> test_notes
2635         )
2636 {
2637         auto dcp = make_simple (dir);
2638         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2639         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2640         for (auto const& i: markers) {
2641                 markers_asset->set (i.first, i.second);
2642         }
2643         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2644         dcp->set_annotation_text("A Test DCP");
2645         dcp->write_xml();
2646
2647         check_verify_result ({dir}, test_notes);
2648 }
2649
2650
2651 BOOST_AUTO_TEST_CASE (verify_markers)
2652 {
2653         verify_markers_test (
2654                 "build/test/verify_markers_all_correct",
2655                 {
2656                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2657                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2658                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2659                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2660                 },
2661                 {}
2662                 );
2663
2664         verify_markers_test (
2665                 "build/test/verify_markers_missing_ffec",
2666                 {
2667                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2668                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2669                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2670                 },
2671                 {
2672                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2673                 });
2674
2675         verify_markers_test (
2676                 "build/test/verify_markers_missing_ffmc",
2677                 {
2678                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2679                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2680                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2681                 },
2682                 {
2683                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2684                 });
2685
2686         verify_markers_test (
2687                 "build/test/verify_markers_missing_ffoc",
2688                 {
2689                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2690                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2691                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2692                 },
2693                 {
2694                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2695                 });
2696
2697         verify_markers_test (
2698                 "build/test/verify_markers_missing_lfoc",
2699                 {
2700                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2701                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2702                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2703                 },
2704                 {
2705                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2706                 });
2707
2708         verify_markers_test (
2709                 "build/test/verify_markers_incorrect_ffoc",
2710                 {
2711                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2712                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2713                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2714                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2715                 },
2716                 {
2717                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2718                 });
2719
2720         verify_markers_test (
2721                 "build/test/verify_markers_incorrect_lfoc",
2722                 {
2723                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2724                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2725                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2726                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2727                 },
2728                 {
2729                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2730                 });
2731 }
2732
2733
2734 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2735 {
2736         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2737         prepare_directory (dir);
2738         auto dcp = make_simple (dir);
2739         auto cpl = dcp->cpls()[0];
2740         cpl->unset_version_number();
2741         dcp->set_annotation_text("A Test DCP");
2742         dcp->write_xml();
2743
2744         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2745 }
2746
2747
2748 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2749 {
2750         path dir = "build/test/verify_missing_extension_metadata1";
2751         auto dcp = make_simple (dir);
2752         dcp->set_annotation_text("A Test DCP");
2753         dcp->write_xml();
2754
2755         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2756         auto cpl = dcp->cpls()[0];
2757
2758         {
2759                 Editor e (cpl->file().get());
2760                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2761         }
2762
2763         check_verify_result (
2764                 {dir},
2765                 {
2766                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2767                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2768                 });
2769 }
2770
2771
2772 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2773 {
2774         path dir = "build/test/verify_missing_extension_metadata2";
2775         auto dcp = make_simple (dir);
2776         dcp->set_annotation_text("A Test DCP");
2777         dcp->write_xml();
2778
2779         auto cpl = dcp->cpls()[0];
2780
2781         {
2782                 Editor e (cpl->file().get());
2783                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2784         }
2785
2786         check_verify_result (
2787                 {dir},
2788                 {
2789                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2790                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2791                 });
2792 }
2793
2794
2795 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2796 {
2797         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2798         auto dcp = make_simple (dir);
2799         dcp->set_annotation_text("A Test DCP");
2800         dcp->write_xml();
2801
2802         auto const cpl = dcp->cpls()[0];
2803
2804         {
2805                 Editor e (cpl->file().get());
2806                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2807                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2808         }
2809
2810         check_verify_result (
2811                 {dir},
2812                 {
2813                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2814                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
2815                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2816                 });
2817 }
2818
2819
2820 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2821 {
2822         path dir = "build/test/verify_invalid_extension_metadata1";
2823         auto dcp = make_simple (dir);
2824         dcp->set_annotation_text("A Test DCP");
2825         dcp->write_xml();
2826
2827         auto cpl = dcp->cpls()[0];
2828
2829         {
2830                 Editor e (cpl->file().get());
2831                 e.replace ("Application", "Fred");
2832         }
2833
2834         check_verify_result (
2835                 {dir},
2836                 {
2837                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2838                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2839                 });
2840 }
2841
2842
2843 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2844 {
2845         path dir = "build/test/verify_invalid_extension_metadata2";
2846         auto dcp = make_simple (dir);
2847         dcp->set_annotation_text("A Test DCP");
2848         dcp->write_xml();
2849
2850         auto cpl = dcp->cpls()[0];
2851
2852         {
2853                 Editor e (cpl->file().get());
2854                 e.replace ("DCP Constraints Profile", "Fred");
2855         }
2856
2857         check_verify_result (
2858                 {dir},
2859                 {
2860                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2861                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2862                 });
2863 }
2864
2865
2866 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2867 {
2868         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2869         auto dcp = make_simple (dir);
2870         dcp->set_annotation_text("A Test DCP");
2871         dcp->write_xml();
2872
2873         auto const cpl = dcp->cpls()[0];
2874
2875         {
2876                 Editor e (cpl->file().get());
2877                 e.replace ("<meta:Value>", "<meta:ValueX>");
2878                 e.replace ("</meta:Value>", "</meta:ValueX>");
2879         }
2880
2881         check_verify_result (
2882                 {dir},
2883                 {
2884                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2885                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 75 },
2886                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2887                 });
2888 }
2889
2890
2891 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2892 {
2893         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2894         auto dcp = make_simple (dir);
2895         dcp->set_annotation_text("A Test DCP");
2896         dcp->write_xml();
2897
2898         auto const cpl = dcp->cpls()[0];
2899
2900         {
2901                 Editor e (cpl->file().get());
2902                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2903         }
2904
2905         check_verify_result (
2906                 {dir},
2907                 {
2908                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2909                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'"), cpl->file().get() },
2910                 });
2911 }
2912
2913
2914 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2915 {
2916         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2917         auto dcp = make_simple (dir);
2918         dcp->set_annotation_text("A Test DCP");
2919         dcp->write_xml();
2920
2921         auto const cpl = dcp->cpls()[0];
2922
2923         {
2924                 Editor e (cpl->file().get());
2925                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2926                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2927         }
2928
2929         check_verify_result (
2930                 {dir},
2931                 {
2932                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2933                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2934                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2935                 });
2936 }
2937
2938
2939 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2940 {
2941         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2942         auto dcp = make_simple (dir);
2943         dcp->set_annotation_text("A Test DCP");
2944         dcp->write_xml();
2945
2946         auto const cpl = dcp->cpls()[0];
2947
2948         {
2949                 Editor e (cpl->file().get());
2950                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2951                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2952         }
2953
2954         check_verify_result (
2955                 {dir},
2956                 {
2957                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2958                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
2959                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2960                 });
2961 }
2962
2963
2964
2965 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2966 {
2967         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2968         prepare_directory (dir);
2969         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2970                 copy_file (i.path(), dir / i.path().filename());
2971         }
2972
2973         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2974         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2975
2976         {
2977                 Editor e (cpl);
2978                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2979         }
2980
2981         check_verify_result (
2982                 {dir},
2983                 {
2984                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2985                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2986                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2987                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2988                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2989                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2990                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2991                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2992                 });
2993 }
2994
2995
2996 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2997 {
2998         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2999         prepare_directory (dir);
3000         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
3001                 copy_file (i.path(), dir / i.path().filename());
3002         }
3003
3004         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
3005         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
3006         {
3007                 Editor e (pkl);
3008                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
3009         }
3010
3011         check_verify_result (
3012                 {dir},
3013                 {
3014                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
3015                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
3016                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
3017                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
3018                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
3019                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
3020                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
3021                 });
3022 }
3023
3024
3025 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
3026 {
3027         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
3028         prepare_directory (dir);
3029         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
3030                 copy_file (i.path(), dir / i.path().filename());
3031         }
3032
3033         {
3034                 Editor e (dir / dcp_test1_pkl);
3035                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
3036         }
3037
3038         check_verify_result ({dir}, {});
3039 }
3040
3041
3042 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
3043 {
3044         path dir ("build/test/verify_must_not_be_partially_encrypted");
3045         prepare_directory (dir);
3046
3047         dcp::DCP d (dir);
3048
3049         auto signer = make_shared<dcp::CertificateChain>();
3050         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
3051         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
3052         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
3053         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
3054
3055         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3056
3057         dcp::Key key;
3058
3059         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
3060         mp->set_key (key);
3061
3062         auto writer = mp->start_write(dir / "video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
3063         dcp::ArrayData j2c ("test/data/flat_red.j2c");
3064         for (int i = 0; i < 24; ++i) {
3065                 writer->write (j2c.data(), j2c.size());
3066         }
3067         writer->finalize ();
3068
3069         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
3070
3071         auto reel = make_shared<dcp::Reel>(
3072                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3073                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3074                 );
3075
3076         reel->add (simple_markers());
3077
3078         cpl->add (reel);
3079
3080         cpl->set_content_version (
3081                 {"urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00"}
3082                 );
3083         cpl->set_annotation_text ("A Test DCP");
3084         cpl->set_issuer ("OpenDCP 0.0.25");
3085         cpl->set_creator ("OpenDCP 0.0.25");
3086         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
3087         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
3088         cpl->set_main_sound_sample_rate (48000);
3089         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3090         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3091         cpl->set_version_number (1);
3092
3093         d.add (cpl);
3094
3095         d.set_issuer("OpenDCP 0.0.25");
3096         d.set_creator("OpenDCP 0.0.25");
3097         d.set_issue_date("2012-07-17T04:45:18+00:00");
3098         d.set_annotation_text("A Test DCP");
3099         d.write_xml(signer);
3100
3101         check_verify_result (
3102                 {dir},
3103                 {
3104                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3105                 });
3106 }
3107
3108
3109 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3110 {
3111         vector<dcp::VerificationNote> notes;
3112         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV", "j2c.mxf"));
3113         auto reader = picture.start_read ();
3114         auto frame = reader->get_frame (0);
3115         verify_j2k(frame, 0, 24, notes);
3116         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3117 }
3118
3119
3120 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3121 {
3122         vector<dcp::VerificationNote> notes;
3123         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3124         auto reader = picture.start_read ();
3125         auto frame = reader->get_frame (0);
3126         verify_j2k(frame, 0, 24, notes);
3127         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3128 }
3129
3130
3131 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3132 {
3133         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3134         prepare_directory (dir);
3135         auto dcp = make_simple (dir);
3136         dcp->write_xml ();
3137         vector<dcp::VerificationNote> notes;
3138         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3139         auto reader = picture.start_read ();
3140         auto frame = reader->get_frame (0);
3141         verify_j2k(frame, 0, 24, notes);
3142         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3143 }
3144
3145
3146 /** Check that ResourceID and the XML ID being different is spotted */
3147 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3148 {
3149         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3150         prepare_directory (dir);
3151
3152         ASDCP::WriterInfo writer_info;
3153         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3154
3155         unsigned int c;
3156         auto mxf_id = dcp::make_uuid ();
3157         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3158         BOOST_REQUIRE (c == Kumu::UUID_Length);
3159
3160         auto resource_id = dcp::make_uuid ();
3161         ASDCP::TimedText::TimedTextDescriptor descriptor;
3162         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3163         DCP_ASSERT (c == Kumu::UUID_Length);
3164
3165         auto xml_id = dcp::make_uuid ();
3166         ASDCP::TimedText::MXFWriter writer;
3167         auto subs_mxf = dir / "subs.mxf";
3168         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3169         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3170         writer.WriteTimedTextResource (dcp::String::compose(
3171                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3172                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3173                 "<Id>urn:uuid:%1</Id>"
3174                 "<ContentTitleText>Content</ContentTitleText>"
3175                 "<AnnotationText>Annotation</AnnotationText>"
3176                 "<IssueDate>2018-10-02T12:25:14</IssueDate>"
3177                 "<ReelNumber>1</ReelNumber>"
3178                 "<Language>en-US</Language>"
3179                 "<EditRate>25 1</EditRate>"
3180                 "<TimeCodeRate>25</TimeCodeRate>"
3181                 "<StartTime>00:00:00:00</StartTime>"
3182                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
3183                 "<SubtitleList>"
3184                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3185                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3186                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3187                 "</Subtitle>"
3188                 "</Font>"
3189                 "</SubtitleList>"
3190                 "</SubtitleReel>",
3191                 xml_id).c_str());
3192
3193         writer.Finalize();
3194
3195         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3196         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3197
3198         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3199
3200         check_verify_result (
3201                 { dir },
3202                 {
3203                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3204                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3205                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3206                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3207                 });
3208 }
3209
3210
3211 /** Check that ResourceID and the MXF ID being the same is spotted */
3212 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3213 {
3214         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3215         prepare_directory (dir);
3216
3217         ASDCP::WriterInfo writer_info;
3218         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3219
3220         unsigned int c;
3221         auto mxf_id = dcp::make_uuid ();
3222         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3223         BOOST_REQUIRE (c == Kumu::UUID_Length);
3224
3225         auto resource_id = mxf_id;
3226         ASDCP::TimedText::TimedTextDescriptor descriptor;
3227         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3228         DCP_ASSERT (c == Kumu::UUID_Length);
3229
3230         auto xml_id = resource_id;
3231         ASDCP::TimedText::MXFWriter writer;
3232         auto subs_mxf = dir / "subs.mxf";
3233         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3234         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3235         writer.WriteTimedTextResource (dcp::String::compose(
3236                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3237                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3238                 "<Id>urn:uuid:%1</Id>"
3239                 "<ContentTitleText>Content</ContentTitleText>"
3240                 "<AnnotationText>Annotation</AnnotationText>"
3241                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3242                 "<ReelNumber>1</ReelNumber>"
3243                 "<Language>en-US</Language>"
3244                 "<EditRate>25 1</EditRate>"
3245                 "<TimeCodeRate>25</TimeCodeRate>"
3246                 "<StartTime>00:00:00:00</StartTime>"
3247                 "<LoadFont ID=\"font\">urn:uuid:0ce6e0ba-58b9-4344-8929-4d9c959c2d55</LoadFont>"
3248                 "<SubtitleList>"
3249                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3250                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3251                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3252                 "</Subtitle>"
3253                 "</Font>"
3254                 "</SubtitleList>"
3255                 "</SubtitleReel>",
3256                 xml_id).c_str());
3257
3258         writer.Finalize();
3259
3260         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3261         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3262
3263         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3264
3265         check_verify_result (
3266                 { dir },
3267                 {
3268                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3269                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3270                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3271                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
3272                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2018-10-02T12:25:14+02:00"} }
3273                 });
3274 }
3275
3276
3277 /** Check a DCP with a 3D asset marked as 2D */
3278 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3279 {
3280         check_verify_result (
3281                 { private_test / "data" / "xm" },
3282                 {
3283                         {
3284                                 dcp::VerificationNote::Type::WARNING,
3285                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3286                         },
3287                         {
3288                                 dcp::VerificationNote::Type::BV21_ERROR,
3289                                 dcp::VerificationNote::Code::INVALID_STANDARD
3290                         },
3291                 });
3292
3293 }
3294
3295
3296 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3297 {
3298         path dir = "build/test/verify_unexpected_things_in_main_markers";
3299         prepare_directory (dir);
3300         auto dcp = make_simple (dir, 1, 24);
3301         dcp->set_annotation_text("A Test DCP");
3302         dcp->write_xml();
3303
3304         {
3305                 Editor e (find_cpl(dir));
3306                 e.insert(
3307                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3308                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3309                         );
3310         }
3311
3312         dcp::CPL cpl (find_cpl(dir));
3313
3314         check_verify_result (
3315                 { dir },
3316                 {
3317                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3318                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3319                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3320                 });
3321 }
3322
3323
3324 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3325 {
3326         path dir = "build/test/verify_invalid_content_kind";
3327         prepare_directory (dir);
3328         auto dcp = make_simple (dir, 1, 24);
3329         dcp->set_annotation_text("A Test DCP");
3330         dcp->write_xml();
3331
3332         {
3333                 Editor e(find_cpl(dir));
3334                 e.replace("trailer", "trip");
3335         }
3336
3337         dcp::CPL cpl (find_cpl(dir));
3338
3339         check_verify_result (
3340                 { dir },
3341                 {
3342                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3343                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3344                 });
3345
3346 }
3347
3348
3349 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3350 {
3351         path dir = "build/test/verify_valid_content_kind";
3352         prepare_directory (dir);
3353         auto dcp = make_simple (dir, 1, 24);
3354         dcp->set_annotation_text("A Test DCP");
3355         dcp->write_xml();
3356
3357         {
3358                 Editor e(find_cpl(dir));
3359                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3360         }
3361
3362         dcp::CPL cpl (find_cpl(dir));
3363
3364         check_verify_result (
3365                 { dir },
3366                 {
3367                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3368                 });
3369
3370 }
3371
3372
3373 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
3374 {
3375         path dir = "build/test/verify_invalid_main_picture_active_area_1";
3376         prepare_directory(dir);
3377         auto dcp = make_simple(dir, 1, 24);
3378         dcp->write_xml();
3379
3380         auto constexpr area = "<meta:MainPictureActiveArea>";
3381
3382         {
3383                 Editor e(find_cpl(dir));
3384                 e.delete_lines_after(area, 2);
3385                 e.insert(area, "<meta:Height>4080</meta:Height>");
3386                 e.insert(area, "<meta:Width>1997</meta:Width>");
3387         }
3388
3389         dcp::PKL pkl(find_pkl(dir));
3390         dcp::CPL cpl(find_cpl(dir));
3391
3392         check_verify_result(
3393                 { dir },
3394                 {
3395                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3396                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3397                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
3398                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 4080 is bigger than the asset height 1080", canonical(find_cpl(dir)) },
3399                 });
3400 }
3401
3402
3403 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
3404 {
3405         path dir = "build/test/verify_invalid_main_picture_active_area_2";
3406         prepare_directory(dir);
3407         auto dcp = make_simple(dir, 1, 24);
3408         dcp->write_xml();
3409
3410         auto constexpr area = "<meta:MainPictureActiveArea>";
3411
3412         {
3413                 Editor e(find_cpl(dir));
3414                 e.delete_lines_after(area, 2);
3415                 e.insert(area, "<meta:Height>5125</meta:Height>");
3416                 e.insert(area, "<meta:Width>9900</meta:Width>");
3417         }
3418
3419         dcp::PKL pkl(find_pkl(dir));
3420         dcp::CPL cpl(find_cpl(dir));
3421
3422         check_verify_result(
3423                 { dir },
3424                 {
3425                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3426                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3427                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
3428                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 9900 is bigger than the asset width 1998", canonical(find_cpl(dir)) },
3429                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is bigger than the asset height 1080", canonical(find_cpl(dir)) },
3430                 });
3431 }
3432
3433
3434 BOOST_AUTO_TEST_CASE(verify_duplicate_pkl_asset_ids)
3435 {
3436         RNGFixer rg;
3437
3438         path dir = "build/test/verify_duplicate_pkl_asset_ids";
3439         prepare_directory(dir);
3440         auto dcp = make_simple(dir, 1, 24);
3441         dcp->write_xml();
3442
3443         {
3444                 Editor e(find_pkl(dir));
3445                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab");
3446         }
3447
3448         dcp::PKL pkl(find_pkl(dir));
3449
3450         check_verify_result(
3451                 { dir },
3452                 {
3453                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl.id(), canonical(find_pkl(dir)) },
3454                 });
3455 }
3456
3457
3458 BOOST_AUTO_TEST_CASE(verify_duplicate_assetmap_asset_ids)
3459 {
3460         RNGFixer rg;
3461
3462         path dir = "build/test/verify_duplicate_assetmap_asset_ids";
3463         prepare_directory(dir);
3464         auto dcp = make_simple(dir, 1, 24);
3465         dcp->write_xml();
3466
3467         {
3468                 Editor e(find_asset_map(dir));
3469                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa");
3470         }
3471
3472         dcp::PKL pkl(find_pkl(dir));
3473         dcp::AssetMap asset_map(find_asset_map(dir));
3474
3475         check_verify_result(
3476                 { dir },
3477                 {
3478                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3479                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map.id(), canonical(find_asset_map(dir)) },
3480                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, string("5407b210-4441-4e97-8b16-8bdc7c12da54") },
3481                 });
3482 }
3483
3484
3485 BOOST_AUTO_TEST_CASE(verify_mismatched_sound_channel_counts)
3486 {
3487         boost::filesystem::path const path = "build/test/verify_mismatched_sound_channel_counts";
3488
3489         dcp::MXFMetadata mxf_meta;
3490         mxf_meta.company_name = "OpenDCP";
3491         mxf_meta.product_name = "OpenDCP";
3492         mxf_meta.product_version = "0.0.25";
3493
3494         auto constexpr sample_rate = 48000;
3495         auto constexpr frames = 240;
3496
3497         boost::filesystem::remove_all(path);
3498         boost::filesystem::create_directories(path);
3499         auto dcp = make_shared<dcp::DCP>(path);
3500         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3501         cpl->set_annotation_text("hello");
3502         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R"));
3503         cpl->set_main_sound_sample_rate(sample_rate);
3504         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3505         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3506         cpl->set_version_number(1);
3507
3508         {
3509
3510                 /* Reel with 2 channels of audio */
3511
3512                 auto mp = simple_picture(path, "1", frames, {});
3513                 auto ms = simple_sound(path, "1", mxf_meta, "en-US", frames, sample_rate, {}, 2);
3514
3515                 auto reel = make_shared<dcp::Reel>(
3516                         std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3517                         std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3518                         );
3519
3520                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3521                 markers->set(dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
3522                 reel->add(markers);
3523
3524                 cpl->add(reel);
3525         }
3526
3527         {
3528                 /* Reel with 6 channels of audio */
3529
3530                 auto mp = simple_picture(path, "2", frames, {});
3531                 auto ms = simple_sound(path, "2", mxf_meta, "en-US", frames, sample_rate, {}, 6);
3532
3533                 auto reel = make_shared<dcp::Reel>(
3534                         std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3535                         std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3536                         );
3537
3538                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3539                 markers->set(dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
3540                 reel->add(markers);
3541
3542                 cpl->add(reel);
3543         }
3544
3545         dcp->add(cpl);
3546         dcp->set_annotation_text("hello");
3547         dcp->write_xml();
3548
3549         check_verify_result(
3550                 { path },
3551                 {
3552                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, canonical(find_file(path, "audio2")) },
3553                 });
3554 }
3555
3556
3557 BOOST_AUTO_TEST_CASE(verify_invalid_main_sound_configuration)
3558 {
3559         boost::filesystem::path const path = "build/test/verify_invalid_main_sound_configuration";
3560
3561         dcp::MXFMetadata mxf_meta;
3562         mxf_meta.company_name = "OpenDCP";
3563         mxf_meta.product_name = "OpenDCP";
3564         mxf_meta.product_version = "0.0.25";
3565
3566         auto constexpr sample_rate = 48000;
3567         auto constexpr frames = 240;
3568
3569         boost::filesystem::remove_all(path);
3570         boost::filesystem::create_directories(path);
3571         auto dcp = make_shared<dcp::DCP>(path);
3572         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3573         cpl->set_annotation_text("hello");
3574         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
3575         cpl->set_main_sound_sample_rate(sample_rate);
3576         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3577         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3578         cpl->set_version_number(1);
3579
3580         auto mp = simple_picture(path, "1", frames, {});
3581         auto ms = simple_sound(path, "1", mxf_meta, "en-US", frames, sample_rate, {}, 2);
3582
3583         auto reel = make_shared<dcp::Reel>(
3584                 std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3585                 std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3586                 );
3587
3588         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3589         markers->set(dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
3590         markers->set(dcp::Marker::LFOC, dcp::Time(0, 0, 9, 23, 24));
3591         reel->add(markers);
3592
3593         cpl->add(reel);
3594
3595         dcp->add(cpl);
3596         dcp->set_annotation_text("hello");
3597         dcp->write_xml();
3598
3599         check_verify_result(
3600                 { path },
3601                 {
3602                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION, std::string{"MainSoundConfiguration has 6 channels but sound assets have 2"}, canonical(find_cpl(path)) },
3603                 });
3604 }
3605
3606
3607 BOOST_AUTO_TEST_CASE(verify_invalid_tile_part_size)
3608 {
3609         boost::filesystem::path const path = "build/test/verify_invalid_tile_part_size";
3610         auto constexpr video_frames = 24;
3611         auto constexpr sample_rate = 48000;
3612
3613         boost::filesystem::remove_all(path);
3614         boost::filesystem::create_directories(path);
3615
3616         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
3617         auto picture_writer = mp->start_write(path / "video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
3618
3619         dcp::Size const size(1998, 1080);
3620         auto image = make_shared<dcp::OpenJPEGImage>(size);
3621         boost::random::mt19937 rng(1);
3622         boost::random::uniform_int_distribution<> dist(0, 4095);
3623         for (int c = 0; c < 3; ++c) {
3624                 for (int p = 0; p < (1998 * 1080); ++p) {
3625                         image->data(c)[p] = dist(rng);
3626                 }
3627         }
3628         auto j2c = dcp::compress_j2k(image, 750000000, video_frames, false, false);
3629         for (int i = 0; i < 24; ++i) {
3630                 picture_writer->write(j2c.data(), j2c.size());
3631         }
3632         picture_writer->finalize();
3633
3634         auto dcp = make_shared<dcp::DCP>(path);
3635         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3636         cpl->set_content_version(
3637                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
3638                 );
3639         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
3640         cpl->set_main_sound_sample_rate(sample_rate);
3641         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3642         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3643         cpl->set_version_number(1);
3644
3645         auto ms = simple_sound(path, "", dcp::MXFMetadata(), "en-US", video_frames, sample_rate, {});
3646
3647         auto reel = make_shared<dcp::Reel>(
3648                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3649                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3650                 );
3651
3652         cpl->add(reel);
3653         dcp->add(cpl);
3654         dcp->set_annotation_text("A Test DCP");
3655         dcp->write_xml();
3656
3657         check_verify_result(
3658                 { path },
3659                 {
3660                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE).set_frame(0).set_component(0).set_size(1321721),
3661                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(path / "video.mxf") },
3662                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
3663                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
3664                 });
3665 }
3666
3667
3668 BOOST_AUTO_TEST_CASE(verify_too_many_subtitle_namespaces)
3669 {
3670         boost::filesystem::path const dir = "test/ref/DCP/subtitle_namespace_test";
3671         check_verify_result(
3672                 { dir },
3673                 {
3674                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
3675                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
3676                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3677                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(find_file(dir, "sub_")) },
3678                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, "fc815694-7977-4a27-a8b3-32b9d4075e4c", canonical(find_file(dir, "cpl_")) },
3679                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, std::string{"315de731-1173-484c-9a35-bdacf5a9d99d"} }
3680                 });
3681 }
3682
3683
3684 BOOST_AUTO_TEST_CASE(verify_missing_load_font_for_font)
3685 {
3686         path const dir("build/test/verify_missing_load_font");
3687         prepare_directory (dir);
3688         copy_file ("test/data/subs1.xml", dir / "subs.xml");
3689         {
3690                 Editor editor(dir / "subs.xml");
3691                 editor.delete_first_line_containing("LoadFont");
3692         }
3693         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
3694         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
3695         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
3696
3697         check_verify_result (
3698                 {dir}, {
3699                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
3700                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id("theFontId")
3701                 });
3702
3703 }
3704
3705
3706 BOOST_AUTO_TEST_CASE(verify_missing_load_font)
3707 {
3708         boost::filesystem::path const dir = dcp::String::compose("build/test/%1", boost::unit_test::framework::current_test_case().full_name());
3709         prepare_directory(dir);
3710         auto dcp = make_simple (dir, 1, 202);
3711
3712         string const xml =
3713                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3714                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3715                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
3716                 "<ContentTitleText>Content</ContentTitleText>"
3717                 "<AnnotationText>Annotation</AnnotationText>"
3718                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3719                 "<ReelNumber>1</ReelNumber>"
3720                 "<EditRate>24 1</EditRate>"
3721                 "<TimeCodeRate>24</TimeCodeRate>"
3722                 "<StartTime>00:00:00:00</StartTime>"
3723                 "<Language>de-DE</Language>"
3724                 "<SubtitleList>"
3725                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3726                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:06:00\" TimeOut=\"00:00:08:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3727                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3728                 "</Subtitle>"
3729                 "</Font>"
3730                 "</SubtitleList>"
3731                 "</SubtitleReel>";
3732
3733         dcp::File xml_file(dir / "subs.xml", "w");
3734         BOOST_REQUIRE(xml_file);
3735         xml_file.write(xml.c_str(), xml.size(), 1);
3736         xml_file.close();
3737         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
3738         subs->write(dir / "subs.mxf");
3739
3740         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 202, 0);
3741         dcp->cpls()[0]->reels()[0]->add(reel_subs);
3742         dcp->set_annotation_text("A Test DCP");
3743         dcp->write_xml();
3744
3745         check_verify_result (
3746                 { dir },
3747                 {
3748                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_LOAD_FONT).set_id(reel_subs->id())
3749                 });
3750 }
3751