Add check for Bv2.1 10.4.3 - container duration vs reel duration in timed text.
[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 #include "compose.hpp"
35 #include "cpl.h"
36 #include "dcp.h"
37 #include "interop_subtitle_asset.h"
38 #include "j2k_transcode.h"
39 #include "mono_picture_asset.h"
40 #include "mono_picture_asset_writer.h"
41 #include "openjpeg_image.h"
42 #include "raw_convert.h"
43 #include "reel.h"
44 #include "reel_closed_caption_asset.h"
45 #include "reel_markers_asset.h"
46 #include "reel_mono_picture_asset.h"
47 #include "reel_sound_asset.h"
48 #include "reel_stereo_picture_asset.h"
49 #include "reel_subtitle_asset.h"
50 #include "smpte_subtitle_asset.h"
51 #include "stereo_picture_asset.h"
52 #include "stream_operators.h"
53 #include "test.h"
54 #include "util.h"
55 #include "verify.h"
56 #include "verify_j2k.h"
57 #include <boost/test/unit_test.hpp>
58 #include <boost/algorithm/string.hpp>
59 #include <cstdio>
60 #include <iostream>
61
62 using std::list;
63 using std::pair;
64 using std::string;
65 using std::vector;
66 using std::make_pair;
67 using std::make_shared;
68 using boost::optional;
69 using namespace boost::filesystem;
70 using std::shared_ptr;
71
72
73 static list<pair<string, optional<path>>> stages;
74 static string const dcp_test1_pkl_id = "6af1e0c1-c441-47f8-a502-3efd46b1fa4f";
75 static string const dcp_test1_pkl = "pkl_" + dcp_test1_pkl_id + ".xml";
76 static string const dcp_test1_cpl_id = "81fb54df-e1bf-4647-8788-ea7ba154375b";
77 static string const dcp_test1_cpl = "cpl_" + dcp_test1_cpl_id + ".xml";
78 static string const dcp_test1_asset_map_id = "5d51e8a1-b2a5-4da6-9b66-4615c3609440";
79 static string const encryption_test_cpl_id = "81fb54df-e1bf-4647-8788-ea7ba154375b";
80 static string const encryption_test_pkl_id = "627ad740-ae36-4c49-92bb-553a9f09c4f8";
81
82 static void
83 stage (string s, optional<path> p)
84 {
85         stages.push_back (make_pair (s, p));
86 }
87
88 static void
89 progress (float)
90 {
91
92 }
93
94 static void
95 prepare_directory (path path)
96 {
97         using namespace boost::filesystem;
98         remove_all (path);
99         create_directories (path);
100 }
101
102
103 static path
104 setup (int reference_number, string verify_test_suffix)
105 {
106         auto const dir = dcp::String::compose("build/test/verify_test%1", verify_test_suffix);
107         prepare_directory (dir);
108         for (auto i: directory_iterator(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number))) {
109                 copy_file (i.path(), dir / i.path().filename());
110         }
111
112         return dir;
113 }
114
115
116 static
117 shared_ptr<dcp::CPL>
118 write_dcp_with_single_asset (path dir, shared_ptr<dcp::ReelAsset> reel_asset, dcp::Standard standard = dcp::Standard::SMPTE)
119 {
120         auto reel = make_shared<dcp::Reel>();
121         reel->add (reel_asset);
122         reel->add (simple_markers());
123
124         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
125         cpl->add (reel);
126         auto dcp = make_shared<dcp::DCP>(dir);
127         dcp->add (cpl);
128         dcp->write_xml (
129                 standard,
130                 dcp::String::compose("libdcp %1", dcp::version),
131                 dcp::String::compose("libdcp %1", dcp::version),
132                 dcp::LocalTime().as_string(),
133                 "hello"
134                 );
135
136         return cpl;
137 }
138
139
140 /** Class that can alter a file by searching and replacing strings within it.
141  *  On destruction modifies the file whose name was given to the constructor.
142  */
143 class Editor
144 {
145 public:
146         Editor (path path)
147                 : _path(path)
148         {
149                 _content = dcp::file_to_string (_path);
150         }
151
152         ~Editor ()
153         {
154                 auto f = fopen(_path.string().c_str(), "w");
155                 BOOST_REQUIRE (f);
156                 fwrite (_content.c_str(), _content.length(), 1, f);
157                 fclose (f);
158         }
159
160         void replace (string a, string b)
161         {
162                 auto old_content = _content;
163                 boost::algorithm::replace_all (_content, a, b);
164                 BOOST_REQUIRE (_content != old_content);
165         }
166
167         void delete_lines (string from, string to)
168         {
169                 vector<string> lines;
170                 boost::algorithm::split (lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
171                 bool deleting = false;
172                 auto old_content = _content;
173                 _content = "";
174                 for (auto i: lines) {
175                         if (i.find(from) != string::npos) {
176                                 deleting = true;
177                         }
178                         if (!deleting) {
179                                 _content += i + "\n";
180                         }
181                         if (deleting && i.find(to) != string::npos) {
182                                 deleting = false;
183                         }
184                 }
185                 BOOST_REQUIRE (_content != old_content);
186         }
187
188 private:
189         path _path;
190         std::string _content;
191 };
192
193
194 #if 0
195 static
196 void
197 dump_notes (vector<dcp::VerificationNote> const & notes)
198 {
199         for (auto i: notes) {
200                 std::cout << dcp::note_to_string(i) << "\n";
201         }
202 }
203 #endif
204
205
206 static
207 void
208 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
209 {
210         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
211         BOOST_REQUIRE_EQUAL (notes.size(), test_notes.size());
212         for (auto i = 0U; i < notes.size(); ++i) {
213                 BOOST_REQUIRE_EQUAL (notes[i], test_notes[i]);
214         }
215 }
216
217
218 static
219 void
220 check_verify_result_after_replace (string suffix, boost::function<path (string)> file, string from, string to, vector<dcp::VerificationNote::Code> codes)
221 {
222         auto dir = setup (1, suffix);
223
224         {
225                 Editor e (file(suffix));
226                 e.replace (from, to);
227         }
228
229         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
230
231         BOOST_REQUIRE_EQUAL (notes.size(), codes.size());
232         auto i = notes.begin();
233         auto j = codes.begin();
234         while (i != notes.end()) {
235                 BOOST_CHECK_EQUAL (i->code(), *j);
236                 ++i;
237                 ++j;
238         }
239 }
240
241
242 BOOST_AUTO_TEST_CASE (verify_no_error)
243 {
244         stages.clear ();
245         auto dir = setup (1, "no_error");
246         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
247
248         path const cpl_file = dir / dcp_test1_cpl;
249         path const pkl_file = dir / dcp_test1_pkl;
250         path const assetmap_file = dir / "ASSETMAP.xml";
251
252         auto st = stages.begin();
253         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
254         BOOST_REQUIRE (st->second);
255         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
256         ++st;
257         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
258         BOOST_REQUIRE (st->second);
259         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
260         ++st;
261         BOOST_CHECK_EQUAL (st->first, "Checking reel");
262         BOOST_REQUIRE (!st->second);
263         ++st;
264         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
265         BOOST_REQUIRE (st->second);
266         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
267         ++st;
268         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
269         BOOST_REQUIRE (st->second);
270         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
271         ++st;
272         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
273         BOOST_REQUIRE (st->second);
274         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
275         ++st;
276         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
277         BOOST_REQUIRE (st->second);
278         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
279         ++st;
280         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
281         BOOST_REQUIRE (st->second);
282         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
283         ++st;
284         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
285         BOOST_REQUIRE (st->second);
286         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
287         ++st;
288         BOOST_REQUIRE (st == stages.end());
289
290         BOOST_CHECK_EQUAL (notes.size(), 0);
291 }
292
293
294 BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
295 {
296         using namespace boost::filesystem;
297
298         auto dir = setup (1, "incorrect_picture_sound_hash");
299
300         auto video_path = path(dir / "video.mxf");
301         auto mod = fopen(video_path.string().c_str(), "r+b");
302         BOOST_REQUIRE (mod);
303         fseek (mod, 4096, SEEK_SET);
304         int x = 42;
305         fwrite (&x, sizeof(x), 1, mod);
306         fclose (mod);
307
308         auto audio_path = path(dir / "audio.mxf");
309         mod = fopen(audio_path.string().c_str(), "r+b");
310         BOOST_REQUIRE (mod);
311         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
312         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
313         fclose (mod);
314
315         dcp::ASDCPErrorSuspender sus;
316         check_verify_result (
317                 { dir },
318                 {
319                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
320                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
321                 });
322 }
323
324
325 BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
326 {
327         using namespace boost::filesystem;
328
329         auto dir = setup (1, "mismatched_picture_sound_hashes");
330
331         {
332                 Editor e (dir / dcp_test1_pkl);
333                 e.replace ("<Hash>", "<Hash>x");
334         }
335
336         check_verify_result (
337                 { dir },
338                 {
339                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
340                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
341                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
342                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xSEEi70vx1WQs67bmu2zKvzIkXvY=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 },
343                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xaddO7je2lZSNQp55qjCWo5DLKFQ=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 19 },
344                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xWU0/u1wM17y7Kriq06+65/ViX1o=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 26 }
345                 });
346 }
347
348
349 BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
350 {
351         auto dir = setup (1, "failed_read_content_kind");
352
353         {
354                 Editor e (dir / dcp_test1_cpl);
355                 e.replace ("<ContentKind>", "<ContentKind>x");
356         }
357
358         check_verify_result (
359                 { dir },
360                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("Bad content kind 'xtrailer'")}}
361                 );
362 }
363
364
365 static
366 path
367 cpl (string suffix)
368 {
369         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_cpl);
370 }
371
372
373 static
374 path
375 pkl (string suffix)
376 {
377         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_pkl);
378 }
379
380
381 static
382 path
383 asset_map (string suffix)
384 {
385         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", suffix);
386 }
387
388
389 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_rate)
390 {
391         check_verify_result_after_replace (
392                         "invalid_picture_frame_rate", &cpl,
393                         "<FrameRate>24 1", "<FrameRate>99 1",
394                         { dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES,
395                           dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE }
396                         );
397 }
398
399 BOOST_AUTO_TEST_CASE (verify_missing_asset)
400 {
401         auto dir = setup (1, "missing_asset");
402         remove (dir / "video.mxf");
403         check_verify_result (
404                 { dir },
405                 {
406                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_ASSET, canonical(dir) / "video.mxf" }
407                 });
408 }
409
410
411 BOOST_AUTO_TEST_CASE (verify_empty_asset_path)
412 {
413         check_verify_result_after_replace (
414                         "empty_asset_path", &asset_map,
415                         "<Path>video.mxf</Path>", "<Path></Path>",
416                         { dcp::VerificationNote::Code::EMPTY_ASSET_PATH }
417                         );
418 }
419
420
421 BOOST_AUTO_TEST_CASE (verify_mismatched_standard)
422 {
423         check_verify_result_after_replace (
424                         "mismatched_standard", &cpl,
425                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
426                         { dcp::VerificationNote::Code::MISMATCHED_STANDARD,
427                           dcp::VerificationNote::Code::INVALID_XML,
428                           dcp::VerificationNote::Code::INVALID_XML,
429                           dcp::VerificationNote::Code::INVALID_XML,
430                           dcp::VerificationNote::Code::INVALID_XML,
431                           dcp::VerificationNote::Code::INVALID_XML,
432                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
433                         );
434 }
435
436
437 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_id)
438 {
439         /* There's no MISMATCHED_CPL_HASHES error here because it can't find the correct hash by ID (since the ID is wrong) */
440         check_verify_result_after_replace (
441                         "invalid_xml_cpl_id", &cpl,
442                         "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375b", "<Id>urn:uuid:81fb54df-e1bf-4647-8788-ea7ba154375",
443                         { dcp::VerificationNote::Code::INVALID_XML }
444                         );
445 }
446
447
448 BOOST_AUTO_TEST_CASE (verify_invalid_xml_issue_date)
449 {
450         check_verify_result_after_replace (
451                         "invalid_xml_issue_date", &cpl,
452                         "<IssueDate>", "<IssueDate>x",
453                         { dcp::VerificationNote::Code::INVALID_XML,
454                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
455                         );
456 }
457
458
459 BOOST_AUTO_TEST_CASE (verify_invalid_xml_pkl_id)
460 {
461         check_verify_result_after_replace (
462                 "invalid_xml_pkl_id", &pkl,
463                 "<Id>urn:uuid:" + dcp_test1_pkl_id.substr(0, 3),
464                 "<Id>urn:uuid:x" + dcp_test1_pkl_id.substr(1, 2),
465                 { dcp::VerificationNote::Code::INVALID_XML }
466                 );
467 }
468
469
470 BOOST_AUTO_TEST_CASE (verify_invalid_xml_asset_map_id)
471 {
472         check_verify_result_after_replace (
473                 "invalix_xml_asset_map_id", &asset_map,
474                 "<Id>urn:uuid:" + dcp_test1_asset_map_id.substr(0, 3),
475                 "<Id>urn:uuid:x" + dcp_test1_asset_map_id.substr(1, 2),
476                 { dcp::VerificationNote::Code::INVALID_XML }
477                 );
478 }
479
480
481 BOOST_AUTO_TEST_CASE (verify_invalid_standard)
482 {
483         stages.clear ();
484         auto dir = setup (3, "verify_invalid_standard");
485         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
486
487         path const cpl_file = dir / "cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
488         path const pkl_file = dir / "pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
489         path const assetmap_file = dir / "ASSETMAP";
490
491         auto st = stages.begin();
492         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
493         BOOST_REQUIRE (st->second);
494         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
495         ++st;
496         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
497         BOOST_REQUIRE (st->second);
498         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
499         ++st;
500         BOOST_CHECK_EQUAL (st->first, "Checking reel");
501         BOOST_REQUIRE (!st->second);
502         ++st;
503         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
504         BOOST_REQUIRE (st->second);
505         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
506         ++st;
507         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
508         BOOST_REQUIRE (st->second);
509         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
510         ++st;
511         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
512         BOOST_REQUIRE (st->second);
513         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
514         ++st;
515         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
516         BOOST_REQUIRE (st->second);
517         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
518         ++st;
519         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
520         BOOST_REQUIRE (st->second);
521         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
522         ++st;
523         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
524         BOOST_REQUIRE (st->second);
525         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
526         ++st;
527         BOOST_REQUIRE (st == stages.end());
528
529         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
530         auto i = notes.begin ();
531         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
532         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_STANDARD);
533         ++i;
534         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
535         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K);
536 }
537
538 /* DCP with a short asset */
539 BOOST_AUTO_TEST_CASE (verify_invalid_duration)
540 {
541         auto dir = setup (8, "invalid_duration");
542         check_verify_result (
543                 { dir },
544                 {
545                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
546                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
547                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
548                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
549                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
550                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K, string("2") }
551                 });
552 }
553
554
555 static
556 shared_ptr<dcp::CPL>
557 dcp_from_frame (dcp::ArrayData const& frame, path dir)
558 {
559         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
560         create_directories (dir);
561         auto writer = asset->start_write (dir / "pic.mxf", true);
562         for (int i = 0; i < 24; ++i) {
563                 writer->write (frame.data(), frame.size());
564         }
565         writer->finalize ();
566
567         auto reel_asset = make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
568         return write_dcp_with_single_asset (dir, reel_asset);
569 }
570
571
572 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_size_in_bytes)
573 {
574         int const too_big = 1302083 * 2;
575
576         /* Compress a black image */
577         auto image = black_image ();
578         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
579         BOOST_REQUIRE (frame.size() < too_big);
580
581         /* Place it in a bigger block with some zero padding at the end */
582         dcp::ArrayData oversized_frame(too_big);
583         memcpy (oversized_frame.data(), frame.data(), frame.size());
584         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
585
586         path const dir("build/test/verify_invalid_picture_frame_size_in_bytes");
587         prepare_directory (dir);
588         auto cpl = dcp_from_frame (oversized_frame, dir);
589
590         check_verify_result (
591                 { dir },
592                 {
593                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
594                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
595                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
596                 });
597 }
598
599
600 BOOST_AUTO_TEST_CASE (verify_nearly_invalid_picture_frame_size_in_bytes)
601 {
602         int const nearly_too_big = 1302083 * 0.98;
603
604         /* Compress a black image */
605         auto image = black_image ();
606         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
607         BOOST_REQUIRE (frame.size() < nearly_too_big);
608
609         /* Place it in a bigger block with some zero padding at the end */
610         dcp::ArrayData oversized_frame(nearly_too_big);
611         memcpy (oversized_frame.data(), frame.data(), frame.size());
612         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
613
614         path const dir("build/test/verify_nearly_invalid_picture_frame_size_in_bytes");
615         prepare_directory (dir);
616         auto cpl = dcp_from_frame (oversized_frame, dir);
617
618         check_verify_result (
619                 { dir },
620                 {
621                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
622                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
623                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
624                 });
625 }
626
627
628 BOOST_AUTO_TEST_CASE (verify_valid_picture_frame_size_in_bytes)
629 {
630         /* Compress a black image */
631         auto image = black_image ();
632         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
633         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
634
635         path const dir("build/test/verify_valid_picture_frame_size_in_bytes");
636         prepare_directory (dir);
637         auto cpl = dcp_from_frame (frame, dir);
638
639         check_verify_result ({ dir }, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
640 }
641
642
643 BOOST_AUTO_TEST_CASE (verify_valid_interop_subtitles)
644 {
645         path const dir("build/test/verify_valid_interop_subtitles");
646         prepare_directory (dir);
647         copy_file ("test/data/subs1.xml", dir / "subs.xml");
648         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
649         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
650         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
651
652         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD }});
653 }
654
655
656 BOOST_AUTO_TEST_CASE (verify_invalid_interop_subtitles)
657 {
658         using namespace boost::filesystem;
659
660         path const dir("build/test/verify_invalid_interop_subtitles");
661         prepare_directory (dir);
662         copy_file ("test/data/subs1.xml", dir / "subs.xml");
663         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
664         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
665         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
666
667         {
668                 Editor e (dir / "subs.xml");
669                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
670         }
671
672         check_verify_result (
673                 { dir },
674                 {
675                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
676                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 5 },
677                         {
678                                 dcp::VerificationNote::Type::ERROR,
679                                 dcp::VerificationNote::Code::INVALID_XML,
680                                 string("element 'Foo' is not allowed for content model '(SubtitleID,MovieTitle,ReelNumber,Language,LoadFont*,Font*,Subtitle*)'"),
681                                 path(),
682                                 29
683                         }
684                 });
685 }
686
687
688 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
689 {
690         path const dir("build/test/verify_valid_smpte_subtitles");
691         prepare_directory (dir);
692         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
693         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
694         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
695         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
696
697         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
698 }
699
700
701 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
702 {
703         using namespace boost::filesystem;
704
705         path const dir("build/test/verify_invalid_smpte_subtitles");
706         prepare_directory (dir);
707         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
708         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
709         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
710         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
711
712         check_verify_result (
713                 { dir },
714                 {
715                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
716                         {
717                                 dcp::VerificationNote::Type::ERROR,
718                                 dcp::VerificationNote::Code::INVALID_XML,
719                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
720                                 path(),
721                                 2
722                         },
723                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
724                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
725                 });
726 }
727
728
729 BOOST_AUTO_TEST_CASE (verify_external_asset)
730 {
731         path const ov_dir("build/test/verify_external_asset");
732         prepare_directory (ov_dir);
733
734         auto image = black_image ();
735         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
736         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
737         dcp_from_frame (frame, ov_dir);
738
739         dcp::DCP ov (ov_dir);
740         ov.read ();
741
742         path const vf_dir("build/test/verify_external_asset_vf");
743         prepare_directory (vf_dir);
744
745         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
746         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
747
748         check_verify_result (
749                 { vf_dir },
750                 {
751                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
752                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
753                 });
754 }
755
756
757 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
758 {
759         path const dir("build/test/verify_valid_cpl_metadata");
760         prepare_directory (dir);
761
762         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
763         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
764         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
765
766         auto reel = make_shared<dcp::Reel>();
767         reel->add (reel_asset);
768
769         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
770         reel->add (simple_markers(16 * 24));
771
772         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
773         cpl->add (reel);
774         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
775         cpl->set_main_sound_sample_rate (48000);
776         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
777         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
778         cpl->set_version_number (1);
779
780         dcp::DCP dcp (dir);
781         dcp.add (cpl);
782         dcp.write_xml (
783                 dcp::Standard::SMPTE,
784                 dcp::String::compose("libdcp %1", dcp::version),
785                 dcp::String::compose("libdcp %1", dcp::version),
786                 dcp::LocalTime().as_string(),
787                 "hello"
788                 );
789 }
790
791
792 path find_cpl (path dir)
793 {
794         for (auto i: directory_iterator(dir)) {
795                 if (boost::starts_with(i.path().filename().string(), "cpl_")) {
796                         return i.path();
797                 }
798         }
799
800         BOOST_REQUIRE (false);
801         return {};
802 }
803
804
805 /* DCP with invalid CompositionMetadataAsset */
806 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
807 {
808         using namespace boost::filesystem;
809
810         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
811         prepare_directory (dir);
812
813         auto reel = make_shared<dcp::Reel>();
814         reel->add (black_picture_asset(dir));
815         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
816         cpl->add (reel);
817         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
818         cpl->set_main_sound_sample_rate (48000);
819         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
820         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
821         cpl->set_version_number (1);
822
823         reel->add (simple_markers());
824
825         dcp::DCP dcp (dir);
826         dcp.add (cpl);
827         dcp.write_xml (
828                 dcp::Standard::SMPTE,
829                 dcp::String::compose("libdcp %1", dcp::version),
830                 dcp::String::compose("libdcp %1", dcp::version),
831                 dcp::LocalTime().as_string(),
832                 "hello"
833                 );
834
835         {
836                 Editor e (find_cpl(dir));
837                 e.replace ("MainSound", "MainSoundX");
838         }
839
840         check_verify_result (
841                 { dir },
842                 {
843                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 54 },
844                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 55 },
845                         {
846                                 dcp::VerificationNote::Type::ERROR,
847                                 dcp::VerificationNote::Code::INVALID_XML,
848                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
849                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
850                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
851                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
852                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
853                                        "ExtensionMetadataList?,)'"),
854                                 canonical(cpl->file().get()),
855                                 75
856                         },
857                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
858                 });
859 }
860
861
862 /* DCP with invalid CompositionMetadataAsset */
863 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
864 {
865         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
866         prepare_directory (dir);
867
868         auto reel = make_shared<dcp::Reel>();
869         reel->add (black_picture_asset(dir));
870         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
871         cpl->add (reel);
872         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
873         cpl->set_main_sound_sample_rate (48000);
874         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
875         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
876
877         dcp::DCP dcp (dir);
878         dcp.add (cpl);
879         dcp.write_xml (
880                 dcp::Standard::SMPTE,
881                 dcp::String::compose("libdcp %1", dcp::version),
882                 dcp::String::compose("libdcp %1", dcp::version),
883                 dcp::LocalTime().as_string(),
884                 "hello"
885                 );
886
887         {
888                 Editor e (find_cpl(dir));
889                 e.replace ("meta:Width", "meta:WidthX");
890         }
891
892         check_verify_result (
893                 { dir },
894                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
895                 );
896 }
897
898
899 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
900 {
901         path const dir("build/test/verify_invalid_language1");
902         prepare_directory (dir);
903         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
904         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
905         asset->_language = "wrong-andbad";
906         asset->write (dir / "subs.mxf");
907         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
908         reel_asset->_language = "badlang";
909         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
910
911         check_verify_result (
912                 { dir },
913                 {
914                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
915                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
916                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
917                 });
918 }
919
920
921 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
922 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
923 {
924         path const dir("build/test/verify_invalid_language2");
925         prepare_directory (dir);
926         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
927         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
928         asset->_language = "wrong-andbad";
929         asset->write (dir / "subs.mxf");
930         auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
931         reel_asset->_language = "badlang";
932         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
933
934         check_verify_result (
935                 {dir},
936                 {
937                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
938                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
939                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
940                 });
941 }
942
943
944 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
945  * the release territory.
946  */
947 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
948 {
949         path const dir("build/test/verify_invalid_language3");
950         prepare_directory (dir);
951
952         auto picture = simple_picture (dir, "foo");
953         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
954         auto reel = make_shared<dcp::Reel>();
955         reel->add (reel_picture);
956         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
957         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
958         reel->add (reel_sound);
959         reel->add (simple_markers());
960
961         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
962         cpl->add (reel);
963         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
964         cpl->_additional_subtitle_languages.push_back("andso-is-this");
965         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
966         cpl->set_main_sound_sample_rate (48000);
967         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
968         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
969         cpl->set_version_number (1);
970         cpl->_release_territory = "fred-jim";
971         auto dcp = make_shared<dcp::DCP>(dir);
972         dcp->add (cpl);
973         dcp->write_xml (
974                 dcp::Standard::SMPTE,
975                 dcp::String::compose("libdcp %1", dcp::version),
976                 dcp::String::compose("libdcp %1", dcp::version),
977                 dcp::LocalTime().as_string(),
978                 "hello"
979                 );
980
981         check_verify_result (
982                 { dir },
983                 {
984                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
985                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
986                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
987                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
988                 });
989 }
990
991
992 static
993 vector<dcp::VerificationNote>
994 check_picture_size (int width, int height, int frame_rate, bool three_d)
995 {
996         using namespace boost::filesystem;
997
998         path dcp_path = "build/test/verify_picture_test";
999         prepare_directory (dcp_path);
1000
1001         shared_ptr<dcp::PictureAsset> mp;
1002         if (three_d) {
1003                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1004         } else {
1005                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1006         }
1007         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1008
1009         auto image = black_image (dcp::Size(width, height));
1010         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1011         int const length = three_d ? frame_rate * 2 : frame_rate;
1012         for (int i = 0; i < length; ++i) {
1013                 picture_writer->write (j2c.data(), j2c.size());
1014         }
1015         picture_writer->finalize ();
1016
1017         auto d = make_shared<dcp::DCP>(dcp_path);
1018         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER);
1019         cpl->set_annotation_text ("A Test DCP");
1020         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1021         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1022         cpl->set_main_sound_sample_rate (48000);
1023         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1024         cpl->set_main_picture_active_area (dcp::Size(1998, 1080));
1025         cpl->set_version_number (1);
1026
1027         auto reel = make_shared<dcp::Reel>();
1028
1029         if (three_d) {
1030                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1031         } else {
1032                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1033         }
1034
1035         reel->add (simple_markers(frame_rate));
1036
1037         cpl->add (reel);
1038
1039         d->add (cpl);
1040         d->write_xml (
1041                 dcp::Standard::SMPTE,
1042                 dcp::String::compose("libdcp %1", dcp::version),
1043                 dcp::String::compose("libdcp %1", dcp::version),
1044                 dcp::LocalTime().as_string(),
1045                 "A Test DCP"
1046                 );
1047
1048         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1049 }
1050
1051
1052 static
1053 void
1054 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1055 {
1056         auto notes = check_picture_size(width, height, frame_rate, three_d);
1057         BOOST_CHECK_EQUAL (notes.size(), 0U);
1058 }
1059
1060
1061 static
1062 void
1063 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1064 {
1065         auto notes = check_picture_size(width, height, frame_rate, three_d);
1066         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1067         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1068         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1069 }
1070
1071
1072 static
1073 void
1074 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1075 {
1076         auto notes = check_picture_size(width, height, frame_rate, three_d);
1077         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1078         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1079         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1080 }
1081
1082
1083 static
1084 void
1085 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1086 {
1087         auto notes = check_picture_size(width, height, frame_rate, three_d);
1088         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1089         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1090         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1091 }
1092
1093
1094 BOOST_AUTO_TEST_CASE (verify_picture_size)
1095 {
1096         using namespace boost::filesystem;
1097
1098         /* 2K scope */
1099         check_picture_size_ok (2048, 858, 24, false);
1100         check_picture_size_ok (2048, 858, 25, false);
1101         check_picture_size_ok (2048, 858, 48, false);
1102         check_picture_size_ok (2048, 858, 24, true);
1103         check_picture_size_ok (2048, 858, 25, true);
1104         check_picture_size_ok (2048, 858, 48, true);
1105
1106         /* 2K flat */
1107         check_picture_size_ok (1998, 1080, 24, false);
1108         check_picture_size_ok (1998, 1080, 25, false);
1109         check_picture_size_ok (1998, 1080, 48, false);
1110         check_picture_size_ok (1998, 1080, 24, true);
1111         check_picture_size_ok (1998, 1080, 25, true);
1112         check_picture_size_ok (1998, 1080, 48, true);
1113
1114         /* 4K scope */
1115         check_picture_size_ok (4096, 1716, 24, false);
1116
1117         /* 4K flat */
1118         check_picture_size_ok (3996, 2160, 24, false);
1119
1120         /* Bad frame size */
1121         check_picture_size_bad_frame_size (2050, 858, 24, false);
1122         check_picture_size_bad_frame_size (2048, 658, 25, false);
1123         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1124         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1125
1126         /* Bad 2K frame rate */
1127         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1128         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1129         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1130
1131         /* Bad 4K frame rate */
1132         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1133         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1134
1135         /* No 4K 3D */
1136         auto notes = check_picture_size(3996, 2160, 24, true);
1137         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1138         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1139         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1140 }
1141
1142
1143 static
1144 void
1145 add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, string text = "Hello")
1146 {
1147         asset->add (
1148                 make_shared<dcp::SubtitleString>(
1149                         optional<string>(),
1150                         false,
1151                         false,
1152                         false,
1153                         dcp::Colour(),
1154                         42,
1155                         1,
1156                         dcp::Time(start_frame, 24, 24),
1157                         dcp::Time(end_frame, 24, 24),
1158                         0,
1159                         dcp::HAlign::CENTER,
1160                         v_position,
1161                         dcp::VAlign::CENTER,
1162                         dcp::Direction::LTR,
1163                         text,
1164                         dcp::Effect::NONE,
1165                         dcp::Colour(),
1166                         dcp::Time(),
1167                         dcp::Time()
1168                 )
1169         );
1170 }
1171
1172
1173 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1174 {
1175         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1176         prepare_directory (dir);
1177
1178         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1179         for (int i = 0; i < 2048; ++i) {
1180                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1181         }
1182         asset->set_language (dcp::LanguageTag("de-DE"));
1183         asset->write (dir / "subs.mxf");
1184         auto reel_asset = make_shared<dcp::ReelClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1185         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1186
1187         check_verify_result (
1188                 { dir },
1189                 {
1190                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1191                         {
1192                                 dcp::VerificationNote::Type::BV21_ERROR,
1193                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1194                                 string("413262"),
1195                                 canonical(dir / "subs.mxf")
1196                         },
1197                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1198                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1199                 });
1200 }
1201
1202
1203 static
1204 shared_ptr<dcp::SMPTESubtitleAsset>
1205 make_large_subtitle_asset (path font_file)
1206 {
1207         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1208         dcp::ArrayData big_fake_font(1024 * 1024);
1209         big_fake_font.write (font_file);
1210         for (int i = 0; i < 116; ++i) {
1211                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1212         }
1213         return asset;
1214 }
1215
1216
1217 template <class T>
1218 void
1219 verify_timed_text_asset_too_large (string name)
1220 {
1221         auto const dir = path("build/test") / name;
1222         prepare_directory (dir);
1223         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1224         add_test_subtitle (asset, 0, 240);
1225         asset->set_language (dcp::LanguageTag("de-DE"));
1226         asset->write (dir / "subs.mxf");
1227
1228         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1229         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1230
1231         check_verify_result (
1232                 { dir },
1233                 {
1234                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121696411"), canonical(dir / "subs.mxf") },
1235                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1236                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1237                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1238                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1239                 });
1240 }
1241
1242
1243 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1244 {
1245         verify_timed_text_asset_too_large<dcp::ReelSubtitleAsset>("verify_subtitle_asset_too_large");
1246         verify_timed_text_asset_too_large<dcp::ReelClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1247 }
1248
1249
1250 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1251 {
1252         path dir = "build/test/verify_missing_subtitle_language";
1253         prepare_directory (dir);
1254         auto dcp = make_simple (dir, 1, 106);
1255
1256         string const xml =
1257                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1258                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1259                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1260                 "<ContentTitleText>Content</ContentTitleText>"
1261                 "<AnnotationText>Annotation</AnnotationText>"
1262                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1263                 "<ReelNumber>1</ReelNumber>"
1264                 "<EditRate>24 1</EditRate>"
1265                 "<TimeCodeRate>24</TimeCodeRate>"
1266                 "<StartTime>00:00:00:00</StartTime>"
1267                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1268                 "<SubtitleList>"
1269                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1270                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1271                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1272                 "</Subtitle>"
1273                 "</Font>"
1274                 "</SubtitleList>"
1275                 "</SubtitleReel>";
1276
1277         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1278         BOOST_REQUIRE (xml_file);
1279         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1280         fclose (xml_file);
1281         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1282         subs->write (dir / "subs.mxf");
1283
1284         auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1285         dcp->cpls().front()->reels().front()->add(reel_subs);
1286         dcp->write_xml (
1287                 dcp::Standard::SMPTE,
1288                 dcp::String::compose("libdcp %1", dcp::version),
1289                 dcp::String::compose("libdcp %1", dcp::version),
1290                 dcp::LocalTime().as_string(),
1291                 "A Test DCP"
1292                 );
1293
1294         check_verify_result (
1295                 { dir },
1296                 {
1297                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1298                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1299                 });
1300 }
1301
1302
1303 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1304 {
1305         path path ("build/test/verify_mismatched_subtitle_languages");
1306         auto constexpr reel_length = 192;
1307         auto dcp = make_simple (path, 2, reel_length);
1308         auto cpl = dcp->cpls()[0];
1309
1310         {
1311                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1312                 subs->set_language (dcp::LanguageTag("de-DE"));
1313                 subs->add (simple_subtitle());
1314                 subs->write (path / "subs1.mxf");
1315                 auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1316                 cpl->reels()[0]->add(reel_subs);
1317         }
1318
1319         {
1320                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1321                 subs->set_language (dcp::LanguageTag("en-US"));
1322                 subs->add (simple_subtitle());
1323                 subs->write (path / "subs2.mxf");
1324                 auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1325                 cpl->reels()[1]->add(reel_subs);
1326         }
1327
1328         dcp->write_xml (
1329                 dcp::Standard::SMPTE,
1330                 dcp::String::compose("libdcp %1", dcp::version),
1331                 dcp::String::compose("libdcp %1", dcp::version),
1332                 dcp::LocalTime().as_string(),
1333                 "A Test DCP"
1334                 );
1335
1336         check_verify_result (
1337                 { path },
1338                 {
1339                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1340                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1341                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1342                 });
1343 }
1344
1345
1346 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1347 {
1348         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1349         auto constexpr reel_length = 192;
1350         auto dcp = make_simple (path, 2, reel_length);
1351         auto cpl = dcp->cpls()[0];
1352
1353         {
1354                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1355                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1356                 ccaps->add (simple_subtitle());
1357                 ccaps->write (path / "subs1.mxf");
1358                 auto reel_ccaps = make_shared<dcp::ReelClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1359                 cpl->reels()[0]->add(reel_ccaps);
1360         }
1361
1362         {
1363                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1364                 ccaps->set_language (dcp::LanguageTag("en-US"));
1365                 ccaps->add (simple_subtitle());
1366                 ccaps->write (path / "subs2.mxf");
1367                 auto reel_ccaps = make_shared<dcp::ReelClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1368                 cpl->reels()[1]->add(reel_ccaps);
1369         }
1370
1371         dcp->write_xml (
1372                 dcp::Standard::SMPTE,
1373                 dcp::String::compose("libdcp %1", dcp::version),
1374                 dcp::String::compose("libdcp %1", dcp::version),
1375                 dcp::LocalTime().as_string(),
1376                 "A Test DCP"
1377                 );
1378
1379         check_verify_result (
1380                 { path },
1381                 {
1382                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1383                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1384                 });
1385 }
1386
1387
1388 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1389 {
1390         path dir = "build/test/verify_missing_subtitle_start_time";
1391         prepare_directory (dir);
1392         auto dcp = make_simple (dir, 1, 106);
1393
1394         string const xml =
1395                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1396                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1397                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1398                 "<ContentTitleText>Content</ContentTitleText>"
1399                 "<AnnotationText>Annotation</AnnotationText>"
1400                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1401                 "<ReelNumber>1</ReelNumber>"
1402                 "<Language>de-DE</Language>"
1403                 "<EditRate>24 1</EditRate>"
1404                 "<TimeCodeRate>24</TimeCodeRate>"
1405                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1406                 "<SubtitleList>"
1407                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1408                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1409                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1410                 "</Subtitle>"
1411                 "</Font>"
1412                 "</SubtitleList>"
1413                 "</SubtitleReel>";
1414
1415         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1416         BOOST_REQUIRE (xml_file);
1417         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1418         fclose (xml_file);
1419         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1420         subs->write (dir / "subs.mxf");
1421
1422         auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1423         dcp->cpls().front()->reels().front()->add(reel_subs);
1424         dcp->write_xml (
1425                 dcp::Standard::SMPTE,
1426                 dcp::String::compose("libdcp %1", dcp::version),
1427                 dcp::String::compose("libdcp %1", dcp::version),
1428                 dcp::LocalTime().as_string(),
1429                 "A Test DCP"
1430                 );
1431
1432         check_verify_result (
1433                 { dir },
1434                 {
1435                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1436                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1437                 });
1438 }
1439
1440
1441 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1442 {
1443         path dir = "build/test/verify_invalid_subtitle_start_time";
1444         prepare_directory (dir);
1445         auto dcp = make_simple (dir, 1, 106);
1446
1447         string const xml =
1448                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1449                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1450                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1451                 "<ContentTitleText>Content</ContentTitleText>"
1452                 "<AnnotationText>Annotation</AnnotationText>"
1453                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1454                 "<ReelNumber>1</ReelNumber>"
1455                 "<Language>de-DE</Language>"
1456                 "<EditRate>24 1</EditRate>"
1457                 "<TimeCodeRate>24</TimeCodeRate>"
1458                 "<StartTime>00:00:02:00</StartTime>"
1459                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1460                 "<SubtitleList>"
1461                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1462                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1463                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1464                 "</Subtitle>"
1465                 "</Font>"
1466                 "</SubtitleList>"
1467                 "</SubtitleReel>";
1468
1469         auto xml_file = dcp::fopen_boost (dir / "subs.xml", "w");
1470         BOOST_REQUIRE (xml_file);
1471         fwrite (xml.c_str(), xml.size(), 1, xml_file);
1472         fclose (xml_file);
1473         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1474         subs->write (dir / "subs.mxf");
1475
1476         auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1477         dcp->cpls().front()->reels().front()->add(reel_subs);
1478         dcp->write_xml (
1479                 dcp::Standard::SMPTE,
1480                 dcp::String::compose("libdcp %1", dcp::version),
1481                 dcp::String::compose("libdcp %1", dcp::version),
1482                 dcp::LocalTime().as_string(),
1483                 "A Test DCP"
1484                 );
1485
1486         check_verify_result (
1487                 { dir },
1488                 {
1489                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1490                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1491                 });
1492 }
1493
1494
1495 class TestText
1496 {
1497 public:
1498         TestText (int in_, int out_, float v_position_ = 0, string text_ = "Hello")
1499                 : in(in_)
1500                 , out(out_)
1501                 , v_position(v_position_)
1502                 , text(text_)
1503         {}
1504
1505         int in;
1506         int out;
1507         float v_position;
1508         string text;
1509 };
1510
1511
1512 template <class T>
1513 shared_ptr<dcp::CPL>
1514 dcp_with_text (path dir, vector<TestText> subs)
1515 {
1516         prepare_directory (dir);
1517         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1518         asset->set_start_time (dcp::Time());
1519         for (auto i: subs) {
1520                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.text);
1521         }
1522         asset->set_language (dcp::LanguageTag("de-DE"));
1523         asset->write (dir / "subs.mxf");
1524
1525         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1526         return write_dcp_with_single_asset (dir, reel_asset);
1527 }
1528
1529
1530 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1531 {
1532         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1533         /* Just too early */
1534         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1535         check_verify_result (
1536                 { dir },
1537                 {
1538                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1539                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1540                 });
1541
1542 }
1543
1544
1545 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1546 {
1547         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1548         /* Just late enough */
1549         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1550         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1551 }
1552
1553
1554 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1555 {
1556         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1557         prepare_directory (dir);
1558
1559         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1560         asset1->set_start_time (dcp::Time());
1561         /* Just late enough */
1562         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1563         asset1->set_language (dcp::LanguageTag("de-DE"));
1564         asset1->write (dir / "subs1.mxf");
1565         auto reel_asset1 = make_shared<dcp::ReelSubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1566         auto reel1 = make_shared<dcp::Reel>();
1567         reel1->add (reel_asset1);
1568         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24, 0);
1569         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1570         reel1->add (markers1);
1571
1572         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1573         asset2->set_start_time (dcp::Time());
1574         /* This would be too early on first reel but should be OK on the second */
1575         add_test_subtitle (asset2, 3, 4 * 24);
1576         asset2->set_language (dcp::LanguageTag("de-DE"));
1577         asset2->write (dir / "subs2.mxf");
1578         auto reel_asset2 = make_shared<dcp::ReelSubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1579         auto reel2 = make_shared<dcp::Reel>();
1580         reel2->add (reel_asset2);
1581         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24, 0);
1582         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1583         reel2->add (markers2);
1584
1585         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
1586         cpl->add (reel1);
1587         cpl->add (reel2);
1588         auto dcp = make_shared<dcp::DCP>(dir);
1589         dcp->add (cpl);
1590         dcp->write_xml (
1591                 dcp::Standard::SMPTE,
1592                 dcp::String::compose("libdcp %1", dcp::version),
1593                 dcp::String::compose("libdcp %1", dcp::version),
1594                 dcp::LocalTime().as_string(),
1595                 "hello"
1596                 );
1597
1598
1599         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1600 }
1601
1602
1603 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1604 {
1605         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1606         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1607                 dir,
1608                 {
1609                         { 4 * 24,     5 * 24 },
1610                         { 5 * 24 + 1, 6 * 24 },
1611                 });
1612         check_verify_result (
1613                 {dir},
1614                 {
1615                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1616                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1617                 });
1618 }
1619
1620
1621 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1622 {
1623         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1624         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1625                 dir,
1626                 {
1627                         { 4 * 24,      5 * 24 },
1628                         { 5 * 24 + 16, 8 * 24 },
1629                 });
1630         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1631 }
1632
1633
1634 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1635 {
1636         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1637         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1638         check_verify_result (
1639                 {dir},
1640                 {
1641                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1642                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1643                 });
1644 }
1645
1646
1647 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1648 {
1649         auto const dir = path("build/test/verify_valid_subtitle_duration");
1650         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1651         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1652 }
1653
1654
1655 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1656 {
1657         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1658         prepare_directory (dir);
1659         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1660         asset->set_start_time (dcp::Time());
1661         add_test_subtitle (asset, 0, 4 * 24);
1662         asset->set_language (dcp::LanguageTag("de-DE"));
1663         asset->write (dir / "subs.mxf");
1664
1665         auto reel_asset = make_shared<dcp::ReelSubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1666         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1667         check_verify_result (
1668                 {dir},
1669                 {
1670                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1671                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1672                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1673                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1674                 });
1675
1676 }
1677
1678
1679 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1680 {
1681         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1682         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1683                 dir,
1684                 {
1685                         { 96, 200, 0.0, "We" },
1686                         { 96, 200, 0.1, "have" },
1687                         { 96, 200, 0.2, "four" },
1688                         { 96, 200, 0.3, "lines" }
1689                 });
1690         check_verify_result (
1691                 {dir},
1692                 {
1693                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1694                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1695                 });
1696 }
1697
1698
1699 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1700 {
1701         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1702         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1703                 dir,
1704                 {
1705                         { 96, 200, 0.0, "We" },
1706                         { 96, 200, 0.1, "have" },
1707                         { 96, 200, 0.2, "four" },
1708                 });
1709         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1710 }
1711
1712
1713 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1714 {
1715         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1716         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1717                 dir,
1718                 {
1719                         { 96, 300, 0.0, "We" },
1720                         { 96, 300, 0.1, "have" },
1721                         { 150, 180, 0.2, "four" },
1722                         { 150, 180, 0.3, "lines" }
1723                 });
1724         check_verify_result (
1725                 {dir},
1726                 {
1727                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1728                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1729                 });
1730 }
1731
1732
1733 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1734 {
1735         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1736         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1737                 dir,
1738                 {
1739                         { 96, 300, 0.0, "We" },
1740                         { 96, 300, 0.1, "have" },
1741                         { 150, 180, 0.2, "four" },
1742                         { 190, 250, 0.3, "lines" }
1743                 });
1744         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1745 }
1746
1747
1748 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1749 {
1750         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1751         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1752                 dir,
1753                 {
1754                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123" }
1755                 });
1756         check_verify_result (
1757                 {dir},
1758                 {
1759                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1760                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1761                 });
1762 }
1763
1764
1765 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1766 {
1767         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1768         auto cpl = dcp_with_text<dcp::ReelSubtitleAsset> (
1769                 dir,
1770                 {
1771                         { 96, 300, 0.0, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1772                 });
1773         check_verify_result (
1774                 {dir},
1775                 {
1776                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1777                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1778                 });
1779 }
1780
1781
1782 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1783 {
1784         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1785         auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
1786                 dir,
1787                 {
1788                         { 96, 200, 0.0, "We" },
1789                         { 96, 200, 0.1, "have" },
1790                         { 96, 200, 0.2, "four" },
1791                         { 96, 200, 0.3, "lines" }
1792                 });
1793         check_verify_result (
1794                 {dir},
1795                 {
1796                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1797                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1798                 });
1799 }
1800
1801
1802 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1803 {
1804         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1805         auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
1806                 dir,
1807                 {
1808                         { 96, 200, 0.0, "We" },
1809                         { 96, 200, 0.1, "have" },
1810                         { 96, 200, 0.2, "four" },
1811                 });
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_invalid_closed_caption_line_count3)
1817 {
1818         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1819         auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
1820                 dir,
1821                 {
1822                         { 96, 300, 0.0, "We" },
1823                         { 96, 300, 0.1, "have" },
1824                         { 150, 180, 0.2, "four" },
1825                         { 150, 180, 0.3, "lines" }
1826                 });
1827         check_verify_result (
1828                 {dir},
1829                 {
1830                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1831                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1832                 });
1833 }
1834
1835
1836 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1837 {
1838         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1839         auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
1840                 dir,
1841                 {
1842                         { 96, 300, 0.0, "We" },
1843                         { 96, 300, 0.1, "have" },
1844                         { 150, 180, 0.2, "four" },
1845                         { 190, 250, 0.3, "lines" }
1846                 });
1847         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1848 }
1849
1850
1851 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1852 {
1853         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1854         auto cpl = dcp_with_text<dcp::ReelClosedCaptionAsset> (
1855                 dir,
1856                 {
1857                         { 96, 300, 0.0, "0123456789012345678901234567890123" }
1858                 });
1859         check_verify_result (
1860                 {dir},
1861                 {
1862                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1863                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1864                 });
1865 }
1866
1867
1868 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
1869 {
1870         path const dir("build/test/verify_invalid_sound_frame_rate");
1871         prepare_directory (dir);
1872
1873         auto picture = simple_picture (dir, "foo");
1874         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1875         auto reel = make_shared<dcp::Reel>();
1876         reel->add (reel_picture);
1877         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000);
1878         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1879         reel->add (reel_sound);
1880         reel->add (simple_markers());
1881         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER);
1882         cpl->add (reel);
1883         auto dcp = make_shared<dcp::DCP>(dir);
1884         dcp->add (cpl);
1885         dcp->write_xml (
1886                 dcp::Standard::SMPTE,
1887                 dcp::String::compose("libdcp %1", dcp::version),
1888                 dcp::String::compose("libdcp %1", dcp::version),
1889                 dcp::LocalTime().as_string(),
1890                 "hello"
1891                 );
1892
1893         check_verify_result (
1894                 {dir},
1895                 {
1896                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
1897                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1898                 });
1899 }
1900
1901
1902 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
1903 {
1904         path const dir("build/test/verify_missing_cpl_annotation_text");
1905         auto dcp = make_simple (dir);
1906         dcp->write_xml (
1907                 dcp::Standard::SMPTE,
1908                 dcp::String::compose("libdcp %1", dcp::version),
1909                 dcp::String::compose("libdcp %1", dcp::version),
1910                 dcp::LocalTime().as_string(),
1911                 "A Test DCP"
1912                 );
1913
1914         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
1915
1916         auto const cpl = dcp->cpls()[0];
1917
1918         {
1919                 BOOST_REQUIRE (cpl->file());
1920                 Editor e(cpl->file().get());
1921                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
1922         }
1923
1924         check_verify_result (
1925                 {dir},
1926                 {
1927                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
1928                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
1929                 });
1930 }
1931
1932
1933 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
1934 {
1935         path const dir("build/test/verify_mismatched_cpl_annotation_text");
1936         auto dcp = make_simple (dir);
1937         dcp->write_xml (
1938                 dcp::Standard::SMPTE,
1939                 dcp::String::compose("libdcp %1", dcp::version),
1940                 dcp::String::compose("libdcp %1", dcp::version),
1941                 dcp::LocalTime().as_string(),
1942                 "A Test DCP"
1943                 );
1944
1945         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
1946         auto const cpl = dcp->cpls()[0];
1947
1948         {
1949                 BOOST_REQUIRE (cpl->file());
1950                 Editor e(cpl->file().get());
1951                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
1952         }
1953
1954         check_verify_result (
1955                 {dir},
1956                 {
1957                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
1958                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
1959                 });
1960 }
1961
1962
1963 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
1964 {
1965         path const dir("build/test/verify_mismatched_asset_duration");
1966         prepare_directory (dir);
1967         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
1968         shared_ptr<dcp::CPL> cpl (new dcp::CPL("A Test DCP", dcp::ContentKind::TRAILER));
1969
1970         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
1971         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
1972
1973         auto reel = make_shared<dcp::Reel>(
1974                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
1975                 make_shared<dcp::ReelSoundAsset>(ms, 0)
1976                 );
1977
1978         reel->add (simple_markers());
1979         cpl->add (reel);
1980
1981         dcp->add (cpl);
1982         dcp->write_xml (
1983                 dcp::Standard::SMPTE,
1984                 dcp::String::compose("libdcp %1", dcp::version),
1985                 dcp::String::compose("libdcp %1", dcp::version),
1986                 dcp::LocalTime().as_string(),
1987                 "A Test DCP"
1988                 );
1989
1990         check_verify_result (
1991                 {dir},
1992                 {
1993                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
1994                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
1995                 });
1996 }
1997
1998
1999
2000 static
2001 shared_ptr<dcp::CPL>
2002 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2003 {
2004         prepare_directory (dir);
2005         auto dcp = make_shared<dcp::DCP>(dir);
2006         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER);
2007
2008         auto constexpr reel_length = 192;
2009
2010         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2011         subs->set_language (dcp::LanguageTag("de-DE"));
2012         subs->set_start_time (dcp::Time());
2013         subs->add (simple_subtitle());
2014         subs->write (dir / "subs.mxf");
2015         auto reel_subs = make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2016
2017         auto reel1 = make_shared<dcp::Reel>(
2018                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2019                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2020                 );
2021
2022         if (add_to_reel1) {
2023                 reel1->add (make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2024         }
2025
2026         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2027         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2028         reel1->add (markers1);
2029
2030         cpl->add (reel1);
2031
2032         auto reel2 = make_shared<dcp::Reel>(
2033                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2034                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2035                 );
2036
2037         if (add_to_reel2) {
2038                 reel2->add (make_shared<dcp::ReelSubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2039         }
2040
2041         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2042         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2043         reel2->add (markers2);
2044
2045         cpl->add (reel2);
2046
2047         dcp->add (cpl);
2048         dcp->write_xml (
2049                 dcp::Standard::SMPTE,
2050                 dcp::String::compose("libdcp %1", dcp::version),
2051                 dcp::String::compose("libdcp %1", dcp::version),
2052                 dcp::LocalTime().as_string(),
2053                 "A Test DCP"
2054                 );
2055
2056         return cpl;
2057 }
2058
2059
2060 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2061 {
2062         {
2063                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2064                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2065                 check_verify_result (
2066                         { dir },
2067                         {
2068                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2069                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2070                         });
2071
2072         }
2073
2074         {
2075                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2076                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2077                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2078         }
2079
2080         {
2081                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2082                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2083                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2084         }
2085 }
2086
2087
2088 static
2089 shared_ptr<dcp::CPL>
2090 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2091 {
2092         prepare_directory (dir);
2093         auto dcp = make_shared<dcp::DCP>(dir);
2094         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER);
2095
2096         auto constexpr reel_length = 192;
2097
2098         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2099         subs->set_language (dcp::LanguageTag("de-DE"));
2100         subs->set_start_time (dcp::Time());
2101         subs->add (simple_subtitle());
2102         subs->write (dir / "subs.mxf");
2103
2104         auto reel1 = make_shared<dcp::Reel>(
2105                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2106                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2107                 );
2108
2109         for (int i = 0; i < caps_in_reel1; ++i) {
2110                 reel1->add (make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2111         }
2112
2113         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2114         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2115         reel1->add (markers1);
2116
2117         cpl->add (reel1);
2118
2119         auto reel2 = make_shared<dcp::Reel>(
2120                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2121                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2122                 );
2123
2124         for (int i = 0; i < caps_in_reel2; ++i) {
2125                 reel2->add (make_shared<dcp::ReelClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2126         }
2127
2128         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2129         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2130         reel2->add (markers2);
2131
2132         cpl->add (reel2);
2133
2134         dcp->add (cpl);
2135         dcp->write_xml (
2136                 dcp::Standard::SMPTE,
2137                 dcp::String::compose("libdcp %1", dcp::version),
2138                 dcp::String::compose("libdcp %1", dcp::version),
2139                 dcp::LocalTime().as_string(),
2140                 "A Test DCP"
2141                 );
2142
2143         return cpl;
2144 }
2145
2146
2147 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2148 {
2149         {
2150                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2151                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2152                 check_verify_result (
2153                         {dir},
2154                         {
2155                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2156                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2157                         });
2158         }
2159
2160         {
2161                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2162                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2163                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2164         }
2165
2166         {
2167                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2168                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2169                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2170         }
2171 }
2172
2173
2174 template <class T>
2175 void
2176 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2177 {
2178         prepare_directory (dir);
2179         auto dcp = make_shared<dcp::DCP>(dir);
2180         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER);
2181
2182         auto constexpr reel_length = 192;
2183
2184         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2185         subs->set_language (dcp::LanguageTag("de-DE"));
2186         subs->set_start_time (dcp::Time());
2187         subs->add (simple_subtitle());
2188         subs->write (dir / "subs.mxf");
2189         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2190         adjust (reel_text);
2191
2192         auto reel = make_shared<dcp::Reel>(
2193                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2194                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2195                 );
2196
2197         reel->add (reel_text);
2198
2199         reel->add (simple_markers(reel_length));
2200
2201         cpl->add (reel);
2202
2203         dcp->add (cpl);
2204         dcp->write_xml (
2205                 dcp::Standard::SMPTE,
2206                 dcp::String::compose("libdcp %1", dcp::version),
2207                 dcp::String::compose("libdcp %1", dcp::version),
2208                 dcp::LocalTime().as_string(),
2209                 "A Test DCP"
2210                 );
2211
2212         check_verify_result (
2213                 {dir},
2214                 {
2215                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2216                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2217                 });
2218 }
2219
2220
2221 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2222 {
2223         verify_text_entry_point_check<dcp::ReelSubtitleAsset> (
2224                 "build/test/verify_subtitle_entry_point_must_be_present",
2225                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2226                 [](shared_ptr<dcp::ReelSubtitleAsset> asset) {
2227                         asset->unset_entry_point ();
2228                         }
2229                 );
2230
2231         verify_text_entry_point_check<dcp::ReelSubtitleAsset> (
2232                 "build/test/verify_subtitle_entry_point_must_be_zero",
2233                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2234                 [](shared_ptr<dcp::ReelSubtitleAsset> asset) {
2235                         asset->set_entry_point (4);
2236                         }
2237                 );
2238
2239         verify_text_entry_point_check<dcp::ReelClosedCaptionAsset> (
2240                 "build/test/verify_closed_caption_entry_point_must_be_present",
2241                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2242                 [](shared_ptr<dcp::ReelClosedCaptionAsset> asset) {
2243                         asset->unset_entry_point ();
2244                         }
2245                 );
2246
2247         verify_text_entry_point_check<dcp::ReelClosedCaptionAsset> (
2248                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2249                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2250                 [](shared_ptr<dcp::ReelClosedCaptionAsset> asset) {
2251                         asset->set_entry_point (9);
2252                         }
2253                 );
2254 }
2255
2256
2257 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2258 {
2259         RNGFixer fix;
2260
2261         path const dir("build/test/verify_missing_hash");
2262         auto dcp = make_simple (dir);
2263         dcp->write_xml (
2264                 dcp::Standard::SMPTE,
2265                 dcp::String::compose("libdcp %1", dcp::version),
2266                 dcp::String::compose("libdcp %1", dcp::version),
2267                 dcp::LocalTime().as_string(),
2268                 "A Test DCP"
2269                 );
2270
2271         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2272         auto const cpl = dcp->cpls()[0];
2273
2274         {
2275                 BOOST_REQUIRE (cpl->file());
2276                 Editor e(cpl->file().get());
2277                 e.replace("<Hash>addO7je2lZSNQp55qjCWo5DLKFQ=</Hash>", "");
2278         }
2279
2280         check_verify_result (
2281                 {dir},
2282                 {
2283                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2284                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, string("1fab8bb0-cfaf-4225-ad6d-01768bc10470") }
2285                 });
2286 }
2287
2288
2289 static
2290 void
2291 verify_markers_test (
2292         path dir,
2293         vector<pair<dcp::Marker, dcp::Time>> markers,
2294         vector<dcp::VerificationNote> test_notes
2295         )
2296 {
2297         auto dcp = make_simple (dir);
2298         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2299         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24, 0);
2300         for (auto const& i: markers) {
2301                 markers_asset->set (i.first, i.second);
2302         }
2303         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2304         dcp->write_xml (
2305                 dcp::Standard::SMPTE,
2306                 dcp::String::compose("libdcp %1", dcp::version),
2307                 dcp::String::compose("libdcp %1", dcp::version),
2308                 dcp::LocalTime().as_string(),
2309                 "A Test DCP"
2310                 );
2311
2312         check_verify_result ({dir}, test_notes);
2313 }
2314
2315
2316 BOOST_AUTO_TEST_CASE (verify_markers)
2317 {
2318         verify_markers_test (
2319                 "build/test/verify_markers_all_correct",
2320                 {
2321                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2322                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2323                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2324                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2325                 },
2326                 {}
2327                 );
2328
2329         verify_markers_test (
2330                 "build/test/verify_markers_missing_ffec",
2331                 {
2332                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2333                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2334                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2335                 },
2336                 {
2337                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2338                 });
2339
2340         verify_markers_test (
2341                 "build/test/verify_markers_missing_ffmc",
2342                 {
2343                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2344                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2345                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2346                 },
2347                 {
2348                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2349                 });
2350
2351         verify_markers_test (
2352                 "build/test/verify_markers_missing_ffoc",
2353                 {
2354                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2355                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2356                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2357                 },
2358                 {
2359                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2360                 });
2361
2362         verify_markers_test (
2363                 "build/test/verify_markers_missing_lfoc",
2364                 {
2365                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2366                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2367                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2368                 },
2369                 {
2370                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2371                 });
2372
2373         verify_markers_test (
2374                 "build/test/verify_markers_incorrect_ffoc",
2375                 {
2376                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2377                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2378                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2379                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2380                 },
2381                 {
2382                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2383                 });
2384
2385         verify_markers_test (
2386                 "build/test/verify_markers_incorrect_lfoc",
2387                 {
2388                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2389                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2390                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2391                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2392                 },
2393                 {
2394                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2395                 });
2396 }
2397
2398
2399 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2400 {
2401         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2402         prepare_directory (dir);
2403         auto dcp = make_simple (dir);
2404         auto cpl = dcp->cpls()[0];
2405         cpl->unset_version_number();
2406         dcp->write_xml (
2407                 dcp::Standard::SMPTE,
2408                 dcp::String::compose("libdcp %1", dcp::version),
2409                 dcp::String::compose("libdcp %1", dcp::version),
2410                 dcp::LocalTime().as_string(),
2411                 "A Test DCP"
2412                 );
2413
2414         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2415 }
2416
2417
2418 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2419 {
2420         path dir = "build/test/verify_missing_extension_metadata1";
2421         auto dcp = make_simple (dir);
2422         dcp->write_xml (
2423                 dcp::Standard::SMPTE,
2424                 dcp::String::compose("libdcp %1", dcp::version),
2425                 dcp::String::compose("libdcp %1", dcp::version),
2426                 dcp::LocalTime().as_string(),
2427                 "A Test DCP"
2428                 );
2429
2430         auto cpl = dcp->cpls()[0];
2431
2432         {
2433                 Editor e (cpl->file().get());
2434                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2435         }
2436
2437         check_verify_result (
2438                 {dir},
2439                 {
2440                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2441                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2442                 });
2443 }
2444
2445
2446 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2447 {
2448         path dir = "build/test/verify_missing_extension_metadata2";
2449         auto dcp = make_simple (dir);
2450         dcp->write_xml (
2451                 dcp::Standard::SMPTE,
2452                 dcp::String::compose("libdcp %1", dcp::version),
2453                 dcp::String::compose("libdcp %1", dcp::version),
2454                 dcp::LocalTime().as_string(),
2455                 "A Test DCP"
2456                 );
2457
2458         auto cpl = dcp->cpls()[0];
2459
2460         {
2461                 Editor e (cpl->file().get());
2462                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2463         }
2464
2465         check_verify_result (
2466                 {dir},
2467                 {
2468                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2469                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2470                 });
2471 }
2472
2473
2474 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2475 {
2476         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2477         auto dcp = make_simple (dir);
2478         dcp->write_xml (
2479                 dcp::Standard::SMPTE,
2480                 dcp::String::compose("libdcp %1", dcp::version),
2481                 dcp::String::compose("libdcp %1", dcp::version),
2482                 dcp::LocalTime().as_string(),
2483                 "A Test DCP"
2484                 );
2485
2486         auto const cpl = dcp->cpls()[0];
2487
2488         {
2489                 Editor e (cpl->file().get());
2490                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2491                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2492         }
2493
2494         check_verify_result (
2495                 {dir},
2496                 {
2497                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 75 },
2498                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2499                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2500                 });
2501 }
2502
2503
2504 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2505 {
2506         path dir = "build/test/verify_invalid_extension_metadata1";
2507         auto dcp = make_simple (dir);
2508         dcp->write_xml (
2509                 dcp::Standard::SMPTE,
2510                 dcp::String::compose("libdcp %1", dcp::version),
2511                 dcp::String::compose("libdcp %1", dcp::version),
2512                 dcp::LocalTime().as_string(),
2513                 "A Test DCP"
2514                 );
2515
2516         auto cpl = dcp->cpls()[0];
2517
2518         {
2519                 Editor e (cpl->file().get());
2520                 e.replace ("Application", "Fred");
2521         }
2522
2523         check_verify_result (
2524                 {dir},
2525                 {
2526                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2527                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2528                 });
2529 }
2530
2531
2532 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2533 {
2534         path dir = "build/test/verify_invalid_extension_metadata2";
2535         auto dcp = make_simple (dir);
2536         dcp->write_xml (
2537                 dcp::Standard::SMPTE,
2538                 dcp::String::compose("libdcp %1", dcp::version),
2539                 dcp::String::compose("libdcp %1", dcp::version),
2540                 dcp::LocalTime().as_string(),
2541                 "A Test DCP"
2542                 );
2543
2544         auto cpl = dcp->cpls()[0];
2545
2546         {
2547                 Editor e (cpl->file().get());
2548                 e.replace ("DCP Constraints Profile", "Fred");
2549         }
2550
2551         check_verify_result (
2552                 {dir},
2553                 {
2554                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2555                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2556                 });
2557 }
2558
2559
2560 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2561 {
2562         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2563         auto dcp = make_simple (dir);
2564         dcp->write_xml (
2565                 dcp::Standard::SMPTE,
2566                 dcp::String::compose("libdcp %1", dcp::version),
2567                 dcp::String::compose("libdcp %1", dcp::version),
2568                 dcp::LocalTime().as_string(),
2569                 "A Test DCP"
2570                 );
2571
2572         auto const cpl = dcp->cpls()[0];
2573
2574         {
2575                 Editor e (cpl->file().get());
2576                 e.replace ("<meta:Value>", "<meta:ValueX>");
2577                 e.replace ("</meta:Value>", "</meta:ValueX>");
2578         }
2579
2580         check_verify_result (
2581                 {dir},
2582                 {
2583                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 79 },
2584                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 80 },
2585                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2586                 });
2587 }
2588
2589
2590 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2591 {
2592         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2593         auto dcp = make_simple (dir);
2594         dcp->write_xml (
2595                 dcp::Standard::SMPTE,
2596                 dcp::String::compose("libdcp %1", dcp::version),
2597                 dcp::String::compose("libdcp %1", dcp::version),
2598                 dcp::LocalTime().as_string(),
2599                 "A Test DCP"
2600                 );
2601
2602         auto const cpl = dcp->cpls()[0];
2603
2604         {
2605                 Editor e (cpl->file().get());
2606                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2607         }
2608
2609         check_verify_result (
2610                 {dir},
2611                 {
2612                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2613                         { 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() },
2614                 });
2615 }
2616
2617
2618 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2619 {
2620         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2621         auto dcp = make_simple (dir);
2622         dcp->write_xml (
2623                 dcp::Standard::SMPTE,
2624                 dcp::String::compose("libdcp %1", dcp::version),
2625                 dcp::String::compose("libdcp %1", dcp::version),
2626                 dcp::LocalTime().as_string(),
2627                 "A Test DCP"
2628                 );
2629
2630         auto const cpl = dcp->cpls()[0];
2631
2632         {
2633                 Editor e (cpl->file().get());
2634                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2635                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2636         }
2637
2638         check_verify_result (
2639                 {dir},
2640                 {
2641                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 77 },
2642                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 81 },
2643                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2644                 });
2645 }
2646
2647
2648 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2649 {
2650         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2651         auto dcp = make_simple (dir);
2652         dcp->write_xml (
2653                 dcp::Standard::SMPTE,
2654                 dcp::String::compose("libdcp %1", dcp::version),
2655                 dcp::String::compose("libdcp %1", dcp::version),
2656                 dcp::LocalTime().as_string(),
2657                 "A Test DCP"
2658                 );
2659
2660         auto const cpl = dcp->cpls()[0];
2661
2662         {
2663                 Editor e (cpl->file().get());
2664                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2665                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2666         }
2667
2668         check_verify_result (
2669                 {dir},
2670                 {
2671                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 76 },
2672                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 82 },
2673                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2674                 });
2675 }
2676
2677
2678
2679 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2680 {
2681         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2682         prepare_directory (dir);
2683         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2684                 copy_file (i.path(), dir / i.path().filename());
2685         }
2686
2687         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2688         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2689
2690         {
2691                 Editor e (cpl);
2692                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2693         }
2694
2695         check_verify_result (
2696                 {dir},
2697                 {
2698                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2699                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2700                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2701                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2702                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2703                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2704                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2705                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2706                 });
2707 }
2708
2709
2710 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2711 {
2712         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2713         prepare_directory (dir);
2714         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2715                 copy_file (i.path(), dir / i.path().filename());
2716         }
2717
2718         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2719         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2720         {
2721                 Editor e (pkl);
2722                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2723         }
2724
2725         check_verify_result (
2726                 {dir},
2727                 {
2728                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2729                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2730                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2731                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2732                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2733                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2734                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2735                 });
2736 }
2737
2738
2739 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2740 {
2741         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2742         prepare_directory (dir);
2743         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2744                 copy_file (i.path(), dir / i.path().filename());
2745         }
2746
2747         {
2748                 Editor e (dir / dcp_test1_pkl);
2749                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2750         }
2751
2752         check_verify_result ({dir}, {});
2753 }
2754
2755
2756 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2757 {
2758         path dir ("build/test/verify_must_not_be_partially_encrypted");
2759         prepare_directory (dir);
2760
2761         dcp::DCP d (dir);
2762
2763         auto signer = make_shared<dcp::CertificateChain>();
2764         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2765         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2766         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2767         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2768
2769         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER);
2770
2771         dcp::Key key;
2772
2773         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2774         mp->set_key (key);
2775
2776         auto writer = mp->start_write (dir / "video.mxf", false);
2777         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2778         for (int i = 0; i < 24; ++i) {
2779                 writer->write (j2c.data(), j2c.size());
2780         }
2781         writer->finalize ();
2782
2783         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2784
2785         auto reel = make_shared<dcp::Reel>(
2786                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2787                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2788                 );
2789
2790         reel->add (simple_markers());
2791
2792         cpl->add (reel);
2793
2794         cpl->set_content_version (
2795                 {"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"}
2796                 );
2797         cpl->set_annotation_text ("A Test DCP");
2798         cpl->set_issuer ("OpenDCP 0.0.25");
2799         cpl->set_creator ("OpenDCP 0.0.25");
2800         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2801         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
2802         cpl->set_main_sound_sample_rate (48000);
2803         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
2804         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
2805         cpl->set_version_number (1);
2806
2807         d.add (cpl);
2808
2809         d.write_xml (dcp::Standard::SMPTE, "OpenDCP 0.0.25", "OpenDCP 0.0.25", "2012-07-17T04:45:18+00:00", "A Test DCP", signer);
2810
2811         check_verify_result (
2812                 {dir},
2813                 {
2814                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
2815                 });
2816 }
2817
2818
2819 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
2820 {
2821         vector<dcp::VerificationNote> notes;
2822         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"));
2823         auto reader = picture.start_read ();
2824         auto frame = reader->get_frame (0);
2825         verify_j2k (frame, notes);
2826         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2827 }
2828
2829
2830 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
2831 {
2832         vector<dcp::VerificationNote> notes;
2833         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
2834         auto reader = picture.start_read ();
2835         auto frame = reader->get_frame (0);
2836         verify_j2k (frame, notes);
2837         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2838 }
2839
2840
2841 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
2842 {
2843         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
2844         prepare_directory (dir);
2845         auto dcp = make_simple (dir);
2846         dcp->write_xml (dcp::Standard::SMPTE);
2847         vector<dcp::VerificationNote> notes;
2848         dcp::MonoPictureAsset picture (find_file(dir, "video"));
2849         auto reader = picture.start_read ();
2850         auto frame = reader->get_frame (0);
2851         verify_j2k (frame, notes);
2852         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2853 }
2854
2855
2856 /** Check that ResourceID and the XML ID being different is spotted */
2857 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
2858 {
2859         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
2860         prepare_directory (dir);
2861
2862         ASDCP::WriterInfo writer_info;
2863         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2864
2865         unsigned int c;
2866         auto mxf_id = dcp::make_uuid ();
2867         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2868         BOOST_REQUIRE (c == Kumu::UUID_Length);
2869
2870         auto resource_id = dcp::make_uuid ();
2871         ASDCP::TimedText::TimedTextDescriptor descriptor;
2872         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2873         DCP_ASSERT (c == Kumu::UUID_Length);
2874
2875         auto xml_id = dcp::make_uuid ();
2876         ASDCP::TimedText::MXFWriter writer;
2877         auto subs_mxf = dir / "subs.mxf";
2878         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2879         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2880         writer.WriteTimedTextResource (dcp::String::compose(
2881                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2882                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2883                 "<Id>urn:uuid:%1</Id>"
2884                 "<ContentTitleText>Content</ContentTitleText>"
2885                 "<AnnotationText>Annotation</AnnotationText>"
2886                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2887                 "<ReelNumber>1</ReelNumber>"
2888                 "<Language>en-US</Language>"
2889                 "<EditRate>25 1</EditRate>"
2890                 "<TimeCodeRate>25</TimeCodeRate>"
2891                 "<StartTime>00:00:00:00</StartTime>"
2892                 "<SubtitleList>"
2893                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2894                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2895                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2896                 "</Subtitle>"
2897                 "</Font>"
2898                 "</SubtitleList>"
2899                 "</SubtitleReel>",
2900                 xml_id).c_str());
2901
2902         writer.Finalize();
2903
2904         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2905         auto subs_reel = make_shared<dcp::ReelSubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2906
2907         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2908
2909         check_verify_result (
2910                 { dir },
2911                 {
2912                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2913                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
2914                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2915                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2916                 });
2917 }
2918
2919
2920 /** Check that ResourceID and the MXF ID being the same is spotted */
2921 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
2922 {
2923         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
2924         prepare_directory (dir);
2925
2926         ASDCP::WriterInfo writer_info;
2927         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2928
2929         unsigned int c;
2930         auto mxf_id = dcp::make_uuid ();
2931         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2932         BOOST_REQUIRE (c == Kumu::UUID_Length);
2933
2934         auto resource_id = mxf_id;
2935         ASDCP::TimedText::TimedTextDescriptor descriptor;
2936         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2937         DCP_ASSERT (c == Kumu::UUID_Length);
2938
2939         auto xml_id = resource_id;
2940         ASDCP::TimedText::MXFWriter writer;
2941         auto subs_mxf = dir / "subs.mxf";
2942         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2943         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2944         writer.WriteTimedTextResource (dcp::String::compose(
2945                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2946                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2947                 "<Id>urn:uuid:%1</Id>"
2948                 "<ContentTitleText>Content</ContentTitleText>"
2949                 "<AnnotationText>Annotation</AnnotationText>"
2950                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2951                 "<ReelNumber>1</ReelNumber>"
2952                 "<Language>en-US</Language>"
2953                 "<EditRate>25 1</EditRate>"
2954                 "<TimeCodeRate>25</TimeCodeRate>"
2955                 "<StartTime>00:00:00:00</StartTime>"
2956                 "<SubtitleList>"
2957                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2958                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2959                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2960                 "</Subtitle>"
2961                 "</Font>"
2962                 "</SubtitleList>"
2963                 "</SubtitleReel>",
2964                 xml_id).c_str());
2965
2966         writer.Finalize();
2967
2968         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2969         auto subs_reel = make_shared<dcp::ReelSubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2970
2971         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2972
2973         check_verify_result (
2974                 { dir },
2975                 {
2976                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2977                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
2978                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2979                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2980                 });
2981 }