Report every frame (with index) that has a JPEG2000 codestream error (DoM #2698).
[libdcp.git] / src / verify.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34
35 /** @file  src/verify.cc
36  *  @brief dcp::verify() method and associated code
37  */
38
39
40 #include "compose.hpp"
41 #include "cpl.h"
42 #include "dcp.h"
43 #include "exceptions.h"
44 #include "filesystem.h"
45 #include "interop_subtitle_asset.h"
46 #include "mono_picture_asset.h"
47 #include "mono_picture_frame.h"
48 #include "raw_convert.h"
49 #include "reel.h"
50 #include "reel_closed_caption_asset.h"
51 #include "reel_interop_subtitle_asset.h"
52 #include "reel_markers_asset.h"
53 #include "reel_picture_asset.h"
54 #include "reel_sound_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_subtitle_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "stereo_picture_asset.h"
59 #include "stereo_picture_frame.h"
60 #include "verify.h"
61 #include "verify_j2k.h"
62 #include <libxml/parserInternals.h>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMDocument.hpp>
65 #include <xercesc/dom/DOMError.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/dom/DOMException.hpp>
68 #include <xercesc/dom/DOMImplementation.hpp>
69 #include <xercesc/dom/DOMImplementationLS.hpp>
70 #include <xercesc/dom/DOMImplementationRegistry.hpp>
71 #include <xercesc/dom/DOMLSParser.hpp>
72 #include <xercesc/dom/DOMLocator.hpp>
73 #include <xercesc/dom/DOMNamedNodeMap.hpp>
74 #include <xercesc/dom/DOMNodeList.hpp>
75 #include <xercesc/framework/LocalFileInputSource.hpp>
76 #include <xercesc/framework/MemBufInputSource.hpp>
77 #include <xercesc/parsers/AbstractDOMParser.hpp>
78 #include <xercesc/parsers/XercesDOMParser.hpp>
79 #include <xercesc/sax/HandlerBase.hpp>
80 #include <xercesc/util/PlatformUtils.hpp>
81 #include <boost/algorithm/string.hpp>
82 #include <iostream>
83 #include <map>
84 #include <regex>
85 #include <set>
86 #include <vector>
87
88
89 using std::cout;
90 using std::dynamic_pointer_cast;
91 using std::list;
92 using std::make_shared;
93 using std::map;
94 using std::max;
95 using std::set;
96 using std::shared_ptr;
97 using std::string;
98 using std::vector;
99 using boost::optional;
100 using boost::function;
101
102
103 using namespace dcp;
104 using namespace xercesc;
105
106
107 static
108 string
109 xml_ch_to_string (XMLCh const * a)
110 {
111         char* x = XMLString::transcode(a);
112         string const o(x);
113         XMLString::release(&x);
114         return o;
115 }
116
117
118 class XMLValidationError
119 {
120 public:
121         XMLValidationError (SAXParseException const & e)
122                 : _message (xml_ch_to_string(e.getMessage()))
123                 , _line (e.getLineNumber())
124                 , _column (e.getColumnNumber())
125                 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
126                 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
127         {
128
129         }
130
131         string message () const {
132                 return _message;
133         }
134
135         uint64_t line () const {
136                 return _line;
137         }
138
139         uint64_t column () const {
140                 return _column;
141         }
142
143         string public_id () const {
144                 return _public_id;
145         }
146
147         string system_id () const {
148                 return _system_id;
149         }
150
151 private:
152         string _message;
153         uint64_t _line;
154         uint64_t _column;
155         string _public_id;
156         string _system_id;
157 };
158
159
160 class DCPErrorHandler : public ErrorHandler
161 {
162 public:
163         void warning(const SAXParseException& e) override
164         {
165                 maybe_add (XMLValidationError(e));
166         }
167
168         void error(const SAXParseException& e) override
169         {
170                 maybe_add (XMLValidationError(e));
171         }
172
173         void fatalError(const SAXParseException& e) override
174         {
175                 maybe_add (XMLValidationError(e));
176         }
177
178         void resetErrors() override {
179                 _errors.clear ();
180         }
181
182         list<XMLValidationError> errors () const {
183                 return _errors;
184         }
185
186 private:
187         void maybe_add (XMLValidationError e)
188         {
189                 /* XXX: nasty hack */
190                 if (
191                         e.message().find("schema document") != string::npos &&
192                         e.message().find("has different target namespace from the one specified in instance document") != string::npos
193                         ) {
194                         return;
195                 }
196
197                 _errors.push_back (e);
198         }
199
200         list<XMLValidationError> _errors;
201 };
202
203
204 class StringToXMLCh
205 {
206 public:
207         StringToXMLCh (string a)
208         {
209                 _buffer = XMLString::transcode(a.c_str());
210         }
211
212         StringToXMLCh (StringToXMLCh const&) = delete;
213         StringToXMLCh& operator= (StringToXMLCh const&) = delete;
214
215         ~StringToXMLCh ()
216         {
217                 XMLString::release (&_buffer);
218         }
219
220         XMLCh const * get () const {
221                 return _buffer;
222         }
223
224 private:
225         XMLCh* _buffer;
226 };
227
228
229 class LocalFileResolver : public EntityResolver
230 {
231 public:
232         LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
233                 : _xsd_dtd_directory (xsd_dtd_directory)
234         {
235                 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
236                  * found without being here.
237                  */
238                 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
239                 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
240                 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
241                 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
242                 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
243                 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
244                 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
245                 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
246                 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
247                 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
248                 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
249                 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
250                 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
251                 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
252         }
253
254         InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
255         {
256                 if (!system_id) {
257                         return 0;
258                 }
259                 auto system_id_str = xml_ch_to_string (system_id);
260                 auto p = _xsd_dtd_directory;
261                 if (_files.find(system_id_str) == _files.end()) {
262                         p /= system_id_str;
263                 } else {
264                         p /= _files[system_id_str];
265                 }
266                 StringToXMLCh ch (p.string());
267                 return new LocalFileInputSource(ch.get());
268         }
269
270 private:
271         void add (string uri, string file)
272         {
273                 _files[uri] = file;
274         }
275
276         std::map<string, string> _files;
277         boost::filesystem::path _xsd_dtd_directory;
278 };
279
280
281 static void
282 parse (XercesDOMParser& parser, boost::filesystem::path xml)
283 {
284         parser.parse(xml.c_str());
285 }
286
287
288 static void
289 parse (XercesDOMParser& parser, string xml)
290 {
291         xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
292         parser.parse(buf);
293 }
294
295
296 template <class T>
297 void
298 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
299 {
300         try {
301                 XMLPlatformUtils::Initialize ();
302         } catch (XMLException& e) {
303                 throw MiscError ("Failed to initialise xerces library");
304         }
305
306         DCPErrorHandler error_handler;
307
308         /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
309         {
310                 XercesDOMParser parser;
311                 parser.setValidationScheme(XercesDOMParser::Val_Always);
312                 parser.setDoNamespaces(true);
313                 parser.setDoSchema(true);
314
315                 vector<string> schema;
316                 schema.push_back("xml.xsd");
317                 schema.push_back("xmldsig-core-schema.xsd");
318                 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
319                 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
320                 schema.push_back("SMPTE-429-9-2007-AM.xsd");
321                 schema.push_back("Main-Stereo-Picture-CPL.xsd");
322                 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
323                 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
324                 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
325                 schema.push_back("DCSubtitle.v1.mattsson.xsd");
326                 schema.push_back("DCDMSubtitle-2010.xsd");
327                 schema.push_back("DCDMSubtitle-2014.xsd");
328                 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
329                 schema.push_back("SMPTE-429-16.xsd");
330                 schema.push_back("Dolby-2012-AD.xsd");
331                 schema.push_back("SMPTE-429-10-2008.xsd");
332                 schema.push_back("xlink.xsd");
333                 schema.push_back("SMPTE-335-2012.xsd");
334                 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
335                 schema.push_back("isdcf-mca.xsd");
336                 schema.push_back("SMPTE-429-12-2008.xsd");
337
338                 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
339                  * Schemas that are not mentioned in this list are not read, and the things
340                  * they describe are not checked.
341                  */
342                 string locations;
343                 for (auto i: schema) {
344                         locations += String::compose("%1 %1 ", i, i);
345                 }
346
347                 parser.setExternalSchemaLocation(locations.c_str());
348                 parser.setValidationSchemaFullChecking(true);
349                 parser.setErrorHandler(&error_handler);
350
351                 LocalFileResolver resolver (xsd_dtd_directory);
352                 parser.setEntityResolver(&resolver);
353
354                 try {
355                         parser.resetDocumentPool();
356                         parse(parser, xml);
357                 } catch (XMLException& e) {
358                         throw MiscError(xml_ch_to_string(e.getMessage()));
359                 } catch (DOMException& e) {
360                         throw MiscError(xml_ch_to_string(e.getMessage()));
361                 } catch (...) {
362                         throw MiscError("Unknown exception from xerces");
363                 }
364         }
365
366         XMLPlatformUtils::Terminate ();
367
368         for (auto i: error_handler.errors()) {
369                 notes.push_back ({
370                         VerificationNote::Type::ERROR,
371                         VerificationNote::Code::INVALID_XML,
372                         i.message(),
373                         boost::trim_copy(i.public_id() + " " + i.system_id()),
374                         i.line()
375                 });
376         }
377 }
378
379
380 enum class VerifyAssetResult {
381         GOOD,
382         CPL_PKL_DIFFER,
383         BAD
384 };
385
386
387 static VerifyAssetResult
388 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
389 {
390         /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
391          * We want to calculate the hash of the actual file contents here, so that we
392          * can check it.  unset_hash() means that this calculation will happen on the
393          * call to hash().
394          */
395         reel_file_asset->asset_ref()->unset_hash();
396         auto const actual_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
397                 progress(float(done) / total);
398         });
399
400         auto pkls = dcp->pkls();
401         /* We've read this DCP in so it must have at least one PKL */
402         DCP_ASSERT (!pkls.empty());
403
404         auto asset = reel_file_asset->asset_ref().asset();
405
406         optional<string> pkl_hash;
407         for (auto i: pkls) {
408                 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
409                 if (pkl_hash) {
410                         break;
411                 }
412         }
413
414         DCP_ASSERT (pkl_hash);
415
416         auto cpl_hash = reel_file_asset->hash();
417         if (cpl_hash && *cpl_hash != *pkl_hash) {
418                 return VerifyAssetResult::CPL_PKL_DIFFER;
419         }
420
421         if (actual_hash != *pkl_hash) {
422                 return VerifyAssetResult::BAD;
423         }
424
425         return VerifyAssetResult::GOOD;
426 }
427
428
429 void
430 verify_language_tag (string tag, vector<VerificationNote>& notes)
431 {
432         try {
433                 LanguageTag test (tag);
434         } catch (LanguageTagError &) {
435                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
436         }
437 }
438
439
440 static void
441 verify_picture_asset(shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, int64_t start_frame, vector<VerificationNote>& notes, function<void (float)> progress)
442 {
443         int biggest_frame = 0;
444         auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
445         auto const duration = asset->intrinsic_duration ();
446
447         auto check_and_add = [&notes](vector<VerificationNote> const& j2k_notes) {
448                 for (auto i: j2k_notes) {
449                         if (find(notes.begin(), notes.end(), i) == notes.end()) {
450                                 notes.push_back (i);
451                         }
452                 }
453         };
454
455         if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
456                 auto reader = mono_asset->start_read ();
457                 for (int64_t i = 0; i < duration; ++i) {
458                         auto frame = reader->get_frame (i);
459                         biggest_frame = max(biggest_frame, frame->size());
460                         if (!mono_asset->encrypted() || mono_asset->key()) {
461                                 vector<VerificationNote> j2k_notes;
462                                 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
463                                 check_and_add (j2k_notes);
464                         }
465                         progress (float(i) / duration);
466                 }
467         } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
468                 auto reader = stereo_asset->start_read ();
469                 for (int64_t i = 0; i < duration; ++i) {
470                         auto frame = reader->get_frame (i);
471                         biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
472                         if (!stereo_asset->encrypted() || stereo_asset->key()) {
473                                 vector<VerificationNote> j2k_notes;
474                                 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
475                                 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
476                                 check_and_add (j2k_notes);
477                         }
478                         progress (float(i) / duration);
479                 }
480
481         }
482
483         static const int max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
484         static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
485         if (biggest_frame > max_frame) {
486                 notes.push_back ({
487                         VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
488                 });
489         } else if (biggest_frame > risky_frame) {
490                 notes.push_back ({
491                         VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
492                 });
493         }
494 }
495
496
497 static void
498 verify_main_picture_asset (
499         shared_ptr<const DCP> dcp,
500         shared_ptr<const ReelPictureAsset> reel_asset,
501         int64_t start_frame,
502         function<void (string, optional<boost::filesystem::path>)> stage,
503         function<void (float)> progress,
504         VerificationOptions options,
505         vector<VerificationNote>& notes
506         )
507 {
508         auto asset = reel_asset->asset();
509         auto const file = *asset->file();
510
511         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
512                 stage ("Checking picture asset hash", file);
513                 auto const r = verify_asset (dcp, reel_asset, progress);
514                 switch (r) {
515                         case VerifyAssetResult::BAD:
516                                 notes.push_back ({
517                                         VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
518                                 });
519                                 break;
520                         case VerifyAssetResult::CPL_PKL_DIFFER:
521                                 notes.push_back ({
522                                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
523                                 });
524                                 break;
525                         default:
526                                 break;
527                 }
528         }
529
530         stage ("Checking picture frame sizes", asset->file());
531         verify_picture_asset(reel_asset, file, start_frame, notes, progress);
532
533         /* Only flat/scope allowed by Bv2.1 */
534         if (
535                 asset->size() != Size(2048, 858) &&
536                 asset->size() != Size(1998, 1080) &&
537                 asset->size() != Size(4096, 1716) &&
538                 asset->size() != Size(3996, 2160)) {
539                 notes.push_back({
540                         VerificationNote::Type::BV21_ERROR,
541                         VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
542                         String::compose("%1x%2", asset->size().width, asset->size().height),
543                         file
544                 });
545         }
546
547         /* Only 24, 25, 48fps allowed for 2K */
548         if (
549                 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
550                 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
551            ) {
552                 notes.push_back({
553                         VerificationNote::Type::BV21_ERROR,
554                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
555                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
556                         file
557                 });
558         }
559
560         if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
561                 /* Only 24fps allowed for 4K */
562                 if (asset->edit_rate() != Fraction(24, 1)) {
563                         notes.push_back({
564                                 VerificationNote::Type::BV21_ERROR,
565                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
566                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
567                                 file
568                         });
569                 }
570
571                 /* Only 2D allowed for 4K */
572                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
573                         notes.push_back({
574                                 VerificationNote::Type::BV21_ERROR,
575                                 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
576                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
577                                 file
578                         });
579
580                 }
581         }
582
583 }
584
585
586 struct State
587 {
588         boost::optional<string> subtitle_language;
589         boost::optional<int> audio_channels;
590 };
591
592
593 static void
594 verify_main_sound_asset (
595         shared_ptr<const DCP> dcp,
596         shared_ptr<const ReelSoundAsset> reel_asset,
597         function<void (string, optional<boost::filesystem::path>)> stage,
598         function<void (float)> progress,
599         VerificationOptions options,
600         vector<VerificationNote>& notes,
601         State& state
602         )
603 {
604         auto asset = reel_asset->asset();
605         auto const file = *asset->file();
606
607         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
608                 stage("Checking sound asset hash", file);
609                 auto const r = verify_asset (dcp, reel_asset, progress);
610                 switch (r) {
611                         case VerifyAssetResult::BAD:
612                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
613                                 break;
614                         case VerifyAssetResult::CPL_PKL_DIFFER:
615                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
616                                 break;
617                         default:
618                                 break;
619                 }
620         }
621
622         if (!state.audio_channels) {
623                 state.audio_channels = asset->channels();
624         } else if (*state.audio_channels != asset->channels()) {
625                 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
626         }
627
628         stage ("Checking sound asset metadata", file);
629
630         if (auto lang = asset->language()) {
631                 verify_language_tag (*lang, notes);
632         }
633         if (asset->sampling_rate() != 48000) {
634                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
635         }
636 }
637
638
639 static void
640 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
641 {
642         /* XXX: is Language compulsory? */
643         if (reel_asset->language()) {
644                 verify_language_tag (*reel_asset->language(), notes);
645         }
646
647         if (!reel_asset->entry_point()) {
648                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
649         } else if (reel_asset->entry_point().get()) {
650                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
651         }
652 }
653
654
655 static void
656 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
657 {
658         /* XXX: is Language compulsory? */
659         if (reel_asset->language()) {
660                 verify_language_tag (*reel_asset->language(), notes);
661         }
662
663         if (!reel_asset->entry_point()) {
664                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
665         } else if (reel_asset->entry_point().get()) {
666                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
667         }
668 }
669
670
671 /** Verify stuff that is common to both subtitles and closed captions */
672 void
673 verify_smpte_timed_text_asset (
674         shared_ptr<const SMPTESubtitleAsset> asset,
675         optional<int64_t> reel_asset_duration,
676         vector<VerificationNote>& notes
677         )
678 {
679         if (asset->language()) {
680                 verify_language_tag (*asset->language(), notes);
681         } else {
682                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
683         }
684
685         auto const size = filesystem::file_size(asset->file().get());
686         if (size > 115 * 1024 * 1024) {
687                 notes.push_back (
688                         { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
689                         );
690         }
691
692         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
693          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
694          */
695         auto fonts = asset->font_data ();
696         int total_size = 0;
697         for (auto i: fonts) {
698                 total_size += i.second.size();
699         }
700         if (total_size > 10 * 1024 * 1024) {
701                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
702         }
703
704         if (!asset->start_time()) {
705                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
706         } else if (asset->start_time() != Time()) {
707                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
708         }
709
710         if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
711                 notes.push_back (
712                         {
713                                 VerificationNote::Type::BV21_ERROR,
714                                 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
715                                 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
716                                 asset->file().get()
717                         });
718         }
719 }
720
721
722 /** Verify Interop subtitle / CCAP stuff */
723 void
724 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
725 {
726         if (asset->subtitles().empty()) {
727                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
728         }
729         auto const unresolved = asset->unresolved_fonts();
730         if (!unresolved.empty()) {
731                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
732         }
733 }
734
735
736 /** Verify SMPTE subtitle-only stuff */
737 void
738 verify_smpte_subtitle_asset (
739         shared_ptr<const SMPTESubtitleAsset> asset,
740         vector<VerificationNote>& notes,
741         State& state
742         )
743 {
744         if (asset->language()) {
745                 if (!state.subtitle_language) {
746                         state.subtitle_language = *asset->language();
747                 } else if (state.subtitle_language != *asset->language()) {
748                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
749                 }
750         }
751
752         DCP_ASSERT (asset->resource_id());
753         auto xml_id = asset->xml_id();
754         if (xml_id) {
755                 if (asset->resource_id().get() != xml_id) {
756                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
757                 }
758
759                 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
760                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
761                 }
762         } else {
763                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
764         }
765
766         if (asset->raw_xml()) {
767                 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
768                 cxml::Document doc("SubtitleReel");
769                 doc.read_string(*asset->raw_xml());
770                 auto issue_date = doc.string_child("IssueDate");
771                 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
772                 if (!std::regex_match(issue_date, reg)) {
773                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
774                 }
775         }
776 }
777
778
779 /** Verify all subtitle stuff */
780 static void
781 verify_subtitle_asset (
782         shared_ptr<const SubtitleAsset> asset,
783         optional<int64_t> reel_asset_duration,
784         function<void (string, optional<boost::filesystem::path>)> stage,
785         boost::filesystem::path xsd_dtd_directory,
786         vector<VerificationNote>& notes,
787         State& state
788         )
789 {
790         stage ("Checking subtitle XML", asset->file());
791         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
792          * gets passed through libdcp which may clean up and therefore hide errors.
793          */
794         if (asset->raw_xml()) {
795                 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
796         } else {
797                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
798         }
799
800         auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
801                 cxml::Document doc(root_node);
802                 doc.read_string(asset->raw_xml().get());
803                 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
804                 int count = 0;
805                 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
806                         ++count;
807                 }
808                 return count;
809         };
810
811         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
812         if (interop) {
813                 verify_interop_text_asset(interop, notes);
814                 if (namespace_count(asset, "DCSubtitle") > 1) {
815                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
816                 }
817         }
818
819         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
820         if (smpte) {
821                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
822                 verify_smpte_subtitle_asset (smpte, notes, state);
823                 /* This asset may be encrypted and in that case we'll have no raw_xml() */
824                 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
825                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
826                 }
827         }
828 }
829
830
831 /** Verify all closed caption stuff */
832 static void
833 verify_closed_caption_asset (
834         shared_ptr<const SubtitleAsset> asset,
835         optional<int64_t> reel_asset_duration,
836         function<void (string, optional<boost::filesystem::path>)> stage,
837         boost::filesystem::path xsd_dtd_directory,
838         vector<VerificationNote>& notes
839         )
840 {
841         stage ("Checking closed caption XML", asset->file());
842         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
843          * gets passed through libdcp which may clean up and therefore hide errors.
844          */
845         auto raw_xml = asset->raw_xml();
846         if (raw_xml) {
847                 validate_xml (*raw_xml, xsd_dtd_directory, notes);
848                 if (raw_xml->size() > 256 * 1024) {
849                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
850                 }
851         } else {
852                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
853         }
854
855         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
856         if (interop) {
857                 verify_interop_text_asset(interop, notes);
858         }
859
860         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
861         if (smpte) {
862                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
863         }
864 }
865
866
867 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
868 static
869 void
870 verify_text_details (
871         dcp::Standard standard,
872         vector<shared_ptr<Reel>> reels,
873         int edit_rate,
874         vector<VerificationNote>& notes,
875         std::function<bool (shared_ptr<Reel>)> check,
876         std::function<optional<string> (shared_ptr<Reel>)> xml,
877         std::function<int64_t (shared_ptr<Reel>)> duration,
878         std::function<std::string (shared_ptr<Reel>)> id
879         )
880 {
881         /* end of last subtitle (in editable units) */
882         optional<int64_t> last_out;
883         auto too_short = false;
884         auto too_close = false;
885         auto too_early = false;
886         auto reel_overlap = false;
887         auto empty_text = false;
888         /* current reel start time (in editable units) */
889         int64_t reel_offset = 0;
890         optional<string> missing_load_font_id;
891
892         std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
893
894         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
895                 cxml::ConstNodePtr node,
896                 optional<int> tcr,
897                 optional<Time> start_time,
898                 int er,
899                 bool first_reel,
900                 bool& has_text,
901                 vector<string>& font_ids
902                 ) {
903                 if (node->name() == "Subtitle") {
904                         Time in (node->string_attribute("TimeIn"), tcr);
905                         if (start_time) {
906                                 in -= *start_time;
907                         }
908                         Time out (node->string_attribute("TimeOut"), tcr);
909                         if (start_time) {
910                                 out -= *start_time;
911                         }
912                         if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
913                                 too_early = true;
914                         }
915                         auto length = out - in;
916                         if (length.as_editable_units_ceil(er) < 15) {
917                                 too_short = true;
918                         }
919                         if (last_out) {
920                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
921                                 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
922                                 if (distance >= 0 && distance < 2) {
923                                         too_close = true;
924                                 }
925                         }
926                         last_out = reel_offset + out.as_editable_units_floor(er);
927                 } else if (node->name() == "Text") {
928                         std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
929                                 if (!node->content().empty()) {
930                                         return true;
931                                 }
932                                 for (auto i: node->node_children()) {
933                                         if (node_has_content(i)) {
934                                                 return true;
935                                         }
936                                 }
937                                 return false;
938                         };
939                         if (!node_has_content(node)) {
940                                 empty_text = true;
941                         }
942                         has_text = true;
943                 } else if (node->name() == "LoadFont") {
944                         if (auto const id = node->optional_string_attribute("Id")) {
945                                 font_ids.push_back(*id);
946                         } else if (auto const id = node->optional_string_attribute("ID")) {
947                                 font_ids.push_back(*id);
948                         }
949                 } else if (node->name() == "Font") {
950                         if (auto const font_id = node->optional_string_attribute("Id")) {
951                                 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
952                                         missing_load_font_id = font_id;
953                                 }
954                         }
955                 }
956                 for (auto i: node->node_children()) {
957                         parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
958                 }
959         };
960
961         for (auto i = 0U; i < reels.size(); ++i) {
962                 if (!check(reels[i])) {
963                         continue;
964                 }
965
966                 auto reel_xml = xml(reels[i]);
967                 if (!reel_xml) {
968                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
969                         continue;
970                 }
971
972                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
973                  * read in by libdcp's parser.
974                  */
975
976                 shared_ptr<cxml::Document> doc;
977                 optional<int> tcr;
978                 optional<Time> start_time;
979                 switch (standard) {
980                 case dcp::Standard::INTEROP:
981                         doc = make_shared<cxml::Document>("DCSubtitle");
982                         doc->read_string (*reel_xml);
983                         break;
984                 case dcp::Standard::SMPTE:
985                         doc = make_shared<cxml::Document>("SubtitleReel");
986                         doc->read_string (*reel_xml);
987                         tcr = doc->number_child<int>("TimeCodeRate");
988                         if (auto start_time_string = doc->optional_string_child("StartTime")) {
989                                 start_time = Time(*start_time_string, tcr);
990                         }
991                         break;
992                 }
993                 bool has_text = false;
994                 vector<string> font_ids;
995                 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
996                 auto end = reel_offset + duration(reels[i]);
997                 if (last_out && *last_out > end) {
998                         reel_overlap = true;
999                 }
1000                 reel_offset = end;
1001
1002                 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1003                         notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1004                 }
1005         }
1006
1007         if (last_out && *last_out > reel_offset) {
1008                 reel_overlap = true;
1009         }
1010
1011         if (too_early) {
1012                 notes.push_back({
1013                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1014                 });
1015         }
1016
1017         if (too_short) {
1018                 notes.push_back ({
1019                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1020                 });
1021         }
1022
1023         if (too_close) {
1024                 notes.push_back ({
1025                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1026                 });
1027         }
1028
1029         if (reel_overlap) {
1030                 notes.push_back ({
1031                         VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1032                 });
1033         }
1034
1035         if (empty_text) {
1036                 notes.push_back ({
1037                         VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1038                 });
1039         }
1040
1041         if (missing_load_font_id) {
1042                 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1043         }
1044 }
1045
1046
1047 static
1048 void
1049 verify_closed_caption_details (
1050         vector<shared_ptr<Reel>> reels,
1051         vector<VerificationNote>& notes
1052         )
1053 {
1054         std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1055         find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1056                 for (auto i: node->node_children()) {
1057                         if (i->name() == "Text") {
1058                                 text_or_image.push_back (i);
1059                         } else {
1060                                 find_text_or_image (i, text_or_image);
1061                         }
1062                 }
1063         };
1064
1065         auto mismatched_valign = false;
1066         auto incorrect_order = false;
1067
1068         std::function<void (cxml::ConstNodePtr)> parse;
1069         parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1070                 if (node->name() == "Subtitle") {
1071                         vector<cxml::ConstNodePtr> text_or_image;
1072                         find_text_or_image (node, text_or_image);
1073                         optional<string> last_valign;
1074                         optional<float> last_vpos;
1075                         for (auto i: text_or_image) {
1076                                 auto valign = i->optional_string_attribute("VAlign");
1077                                 if (!valign) {
1078                                         valign = i->optional_string_attribute("Valign").get_value_or("center");
1079                                 }
1080                                 auto vpos = i->optional_number_attribute<float>("VPosition");
1081                                 if (!vpos) {
1082                                         vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1083                                 }
1084
1085                                 if (last_valign) {
1086                                         if (*last_valign != valign) {
1087                                                 mismatched_valign = true;
1088                                         }
1089                                 }
1090                                 last_valign = valign;
1091
1092                                 if (!mismatched_valign) {
1093                                         if (last_vpos) {
1094                                                 if (*last_valign == "top" || *last_valign == "center") {
1095                                                         if (*vpos < *last_vpos) {
1096                                                                 incorrect_order = true;
1097                                                         }
1098                                                 } else {
1099                                                         if (*vpos > *last_vpos) {
1100                                                                 incorrect_order = true;
1101                                                         }
1102                                                 }
1103                                         }
1104                                         last_vpos = vpos;
1105                                 }
1106                         }
1107                 }
1108
1109                 for (auto i: node->node_children()) {
1110                         parse(i);
1111                 }
1112         };
1113
1114         for (auto reel: reels) {
1115                 for (auto ccap: reel->closed_captions()) {
1116                         auto reel_xml = ccap->asset()->raw_xml();
1117                         if (!reel_xml) {
1118                                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1119                                 continue;
1120                         }
1121
1122                         /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1123                          * read in by libdcp's parser.
1124                          */
1125
1126                         shared_ptr<cxml::Document> doc;
1127                         optional<int> tcr;
1128                         optional<Time> start_time;
1129                         try {
1130                                 doc = make_shared<cxml::Document>("SubtitleReel");
1131                                 doc->read_string (*reel_xml);
1132                         } catch (...) {
1133                                 doc = make_shared<cxml::Document>("DCSubtitle");
1134                                 doc->read_string (*reel_xml);
1135                         }
1136                         parse (doc);
1137                 }
1138         }
1139
1140         if (mismatched_valign) {
1141                 notes.push_back ({
1142                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1143                 });
1144         }
1145
1146         if (incorrect_order) {
1147                 notes.push_back ({
1148                         VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1149                 });
1150         }
1151 }
1152
1153
1154 struct LinesCharactersResult
1155 {
1156         bool warning_length_exceeded = false;
1157         bool error_length_exceeded = false;
1158         bool line_count_exceeded = false;
1159 };
1160
1161
1162 static
1163 void
1164 verify_text_lines_and_characters (
1165         shared_ptr<SubtitleAsset> asset,
1166         int warning_length,
1167         int error_length,
1168         LinesCharactersResult* result
1169         )
1170 {
1171         class Event
1172         {
1173         public:
1174                 Event (Time time_, float position_, int characters_)
1175                         : time (time_)
1176                         , position (position_)
1177                         , characters (characters_)
1178                 {}
1179
1180                 Event (Time time_, shared_ptr<Event> start_)
1181                         : time (time_)
1182                         , start (start_)
1183                 {}
1184
1185                 Time time;
1186                 int position; //< position from 0 at top of screen to 100 at bottom
1187                 int characters;
1188                 shared_ptr<Event> start;
1189         };
1190
1191         vector<shared_ptr<Event>> events;
1192
1193         auto position = [](shared_ptr<const SubtitleString> sub) {
1194                 switch (sub->v_align()) {
1195                 case VAlign::TOP:
1196                         return lrintf(sub->v_position() * 100);
1197                 case VAlign::CENTER:
1198                         return lrintf((0.5f + sub->v_position()) * 100);
1199                 case VAlign::BOTTOM:
1200                         return lrintf((1.0f - sub->v_position()) * 100);
1201                 }
1202
1203                 return 0L;
1204         };
1205
1206         for (auto j: asset->subtitles()) {
1207                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1208                 if (text) {
1209                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1210                         events.push_back(in);
1211                         events.push_back(make_shared<Event>(text->out(), in));
1212                 }
1213         }
1214
1215         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1216                 return a->time < b->time;
1217         });
1218
1219         map<int, int> current;
1220         for (auto i: events) {
1221                 if (current.size() > 3) {
1222                         result->line_count_exceeded = true;
1223                 }
1224                 for (auto j: current) {
1225                         if (j.second > warning_length) {
1226                                 result->warning_length_exceeded = true;
1227                         }
1228                         if (j.second > error_length) {
1229                                 result->error_length_exceeded = true;
1230                         }
1231                 }
1232
1233                 if (i->start) {
1234                         /* end of a subtitle */
1235                         DCP_ASSERT (current.find(i->start->position) != current.end());
1236                         if (current[i->start->position] == i->start->characters) {
1237                                 current.erase(i->start->position);
1238                         } else {
1239                                 current[i->start->position] -= i->start->characters;
1240                         }
1241                 } else {
1242                         /* start of a subtitle */
1243                         if (current.find(i->position) == current.end()) {
1244                                 current[i->position] = i->characters;
1245                         } else {
1246                                 current[i->position] += i->characters;
1247                         }
1248                 }
1249         }
1250 }
1251
1252
1253 static
1254 void
1255 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1256 {
1257         if (reels.empty()) {
1258                 return;
1259         }
1260
1261         if (reels[0]->main_subtitle()) {
1262                 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1263                         [](shared_ptr<Reel> reel) {
1264                                 return static_cast<bool>(reel->main_subtitle());
1265                         },
1266                         [](shared_ptr<Reel> reel) {
1267                                 return reel->main_subtitle()->asset()->raw_xml();
1268                         },
1269                         [](shared_ptr<Reel> reel) {
1270                                 return reel->main_subtitle()->actual_duration();
1271                         },
1272                         [](shared_ptr<Reel> reel) {
1273                                 return reel->main_subtitle()->id();
1274                         }
1275                 );
1276         }
1277
1278         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1279                 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1280                         [i](shared_ptr<Reel> reel) {
1281                                 return i < reel->closed_captions().size();
1282                         },
1283                         [i](shared_ptr<Reel> reel) {
1284                                 return reel->closed_captions()[i]->asset()->raw_xml();
1285                         },
1286                         [i](shared_ptr<Reel> reel) {
1287                                 return reel->closed_captions()[i]->actual_duration();
1288                         },
1289                         [i](shared_ptr<Reel> reel) {
1290                                 return reel->closed_captions()[i]->id();
1291                         }
1292                 );
1293         }
1294
1295         verify_closed_caption_details (reels, notes);
1296 }
1297
1298
1299 void
1300 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1301 {
1302         DCP_ASSERT (cpl->file());
1303         cxml::Document doc ("CompositionPlaylist");
1304         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1305
1306         auto missing = false;
1307         string malformed;
1308
1309         if (auto reel_list = doc.node_child("ReelList")) {
1310                 auto reels = reel_list->node_children("Reel");
1311                 if (!reels.empty()) {
1312                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1313                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1314                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1315                                                 missing = true;
1316                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1317                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1318                                                                 continue;
1319                                                         }
1320                                                         missing = false;
1321                                                         if (auto name = extension->optional_node_child("Name")) {
1322                                                                 if (name->content() != "Application") {
1323                                                                         malformed = "<Name> should be 'Application'";
1324                                                                 }
1325                                                         }
1326                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
1327                                                                 if (auto property = property_list->optional_node_child("Property")) {
1328                                                                         if (auto name = property->optional_node_child("Name")) {
1329                                                                                 if (name->content() != "DCP Constraints Profile") {
1330                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
1331                                                                                 }
1332                                                                         }
1333                                                                         if (auto value = property->optional_node_child("Value")) {
1334                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1335                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1336                                                                                 }
1337                                                                         }
1338                                                                 }
1339                                                         }
1340                                                 }
1341                                         } else {
1342                                                 missing = true;
1343                                         }
1344                                 }
1345                         }
1346                 }
1347         }
1348
1349         if (missing) {
1350                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1351         } else if (!malformed.empty()) {
1352                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1353         }
1354 }
1355
1356
1357 bool
1358 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1359 {
1360         vector<string> encrypted;
1361         for (auto i: dcp->cpls()) {
1362                 for (auto j: i->reel_file_assets()) {
1363                         if (j->asset_ref().resolved()) {
1364                                 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1365                                 if (mxf && mxf->encrypted()) {
1366                                         encrypted.push_back(j->asset_ref().id());
1367                                 }
1368                         }
1369                 }
1370         }
1371
1372         for (auto i: pkl->assets()) {
1373                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1374                         return true;
1375                 }
1376         }
1377
1378         return false;
1379 }
1380
1381
1382 static
1383 void
1384 verify_reel(
1385         shared_ptr<const DCP> dcp,
1386         shared_ptr<const CPL> cpl,
1387         shared_ptr<const Reel> reel,
1388         int64_t start_frame,
1389         optional<dcp::Size> main_picture_active_area,
1390         function<void (string, optional<boost::filesystem::path>)> stage,
1391         boost::filesystem::path xsd_dtd_directory,
1392         function<void (float)> progress,
1393         VerificationOptions options,
1394         vector<VerificationNote>& notes,
1395         State& state,
1396         bool* have_main_subtitle,
1397         bool* have_no_main_subtitle,
1398         size_t* most_closed_captions,
1399         size_t* fewest_closed_captions,
1400         map<Marker, Time>* markers_seen
1401         )
1402 {
1403         for (auto i: reel->assets()) {
1404                 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1405                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1406                 }
1407                 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1408                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1409                 }
1410                 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1411                 if (i->encryptable() && !file_asset->hash()) {
1412                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1413                 }
1414         }
1415
1416         if (dcp->standard() == Standard::SMPTE) {
1417                 boost::optional<int64_t> duration;
1418                 for (auto i: reel->assets()) {
1419                         if (!duration) {
1420                                 duration = i->actual_duration();
1421                         } else if (*duration != i->actual_duration()) {
1422                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1423                                 break;
1424                         }
1425                 }
1426         }
1427
1428         if (reel->main_picture()) {
1429                 /* Check reel stuff */
1430                 auto const frame_rate = reel->main_picture()->frame_rate();
1431                 if (frame_rate.denominator != 1 ||
1432                     (frame_rate.numerator != 24 &&
1433                      frame_rate.numerator != 25 &&
1434                      frame_rate.numerator != 30 &&
1435                      frame_rate.numerator != 48 &&
1436                      frame_rate.numerator != 50 &&
1437                      frame_rate.numerator != 60 &&
1438                      frame_rate.numerator != 96)) {
1439                         notes.push_back({
1440                                 VerificationNote::Type::ERROR,
1441                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1442                                 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1443                         });
1444                 }
1445                 /* Check asset */
1446                 if (reel->main_picture()->asset_ref().resolved()) {
1447                         verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1448                         auto const asset_size = reel->main_picture()->asset()->size();
1449                         if (main_picture_active_area) {
1450                                 if (main_picture_active_area->width > asset_size.width) {
1451                                         notes.push_back({
1452                                                         VerificationNote::Type::ERROR,
1453                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1454                                                         String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1455                                                         cpl->file().get()
1456                                                         });
1457                                 }
1458                                 if (main_picture_active_area->height > asset_size.height) {
1459                                         notes.push_back({
1460                                                         VerificationNote::Type::ERROR,
1461                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1462                                                         String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1463                                                         cpl->file().get()
1464                                                         });
1465                                 }
1466                         }
1467                 }
1468
1469         }
1470
1471         if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1472                 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1473         }
1474
1475         if (reel->main_subtitle()) {
1476                 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1477                 if (reel->main_subtitle()->asset_ref().resolved()) {
1478                         verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1479                 }
1480                 *have_main_subtitle = true;
1481         } else {
1482                 *have_no_main_subtitle = true;
1483         }
1484
1485         for (auto i: reel->closed_captions()) {
1486                 verify_closed_caption_reel(i, notes);
1487                 if (i->asset_ref().resolved()) {
1488                         verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1489                 }
1490         }
1491
1492         if (reel->main_markers()) {
1493                 for (auto const& i: reel->main_markers()->get()) {
1494                         markers_seen->insert(i);
1495                 }
1496                 if (reel->main_markers()->entry_point()) {
1497                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1498                 }
1499                 if (reel->main_markers()->duration()) {
1500                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1501                 }
1502         }
1503
1504         *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1505         *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1506
1507 }
1508
1509
1510 static
1511 void
1512 verify_cpl(
1513         shared_ptr<const DCP> dcp,
1514         shared_ptr<const CPL> cpl,
1515         function<void (string, optional<boost::filesystem::path>)> stage,
1516         boost::filesystem::path xsd_dtd_directory,
1517         function<void (float)> progress,
1518         VerificationOptions options,
1519         vector<VerificationNote>& notes,
1520         State& state
1521         )
1522 {
1523         stage("Checking CPL", cpl->file());
1524         validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1525
1526         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1527                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1528         }
1529
1530         for (auto const& i: cpl->additional_subtitle_languages()) {
1531                 verify_language_tag(i, notes);
1532         }
1533
1534         if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1535                 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1536                  * of the approved ones.
1537                  */
1538                 auto all = ContentKind::all();
1539                 auto name = cpl->content_kind().name();
1540                 transform(name.begin(), name.end(), name.begin(), ::tolower);
1541                 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1542                 if (iter == all.end()) {
1543                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1544                 }
1545         }
1546
1547         if (cpl->release_territory()) {
1548                 if (!cpl->release_territory_scope() || cpl->release_territory_scope().get() != "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata#scope/release-territory/UNM49") {
1549                         auto terr = cpl->release_territory().get();
1550                         /* Must be a valid region tag, or "001" */
1551                         try {
1552                                 LanguageTag::RegionSubtag test(terr);
1553                         } catch (...) {
1554                                 if (terr != "001") {
1555                                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1556                                 }
1557                         }
1558                 }
1559         }
1560
1561         for (auto version: cpl->content_versions()) {
1562                 if (version.label_text.empty()) {
1563                         notes.push_back(
1564                                 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1565                                 );
1566                         break;
1567                 }
1568         }
1569
1570         if (dcp->standard() == Standard::SMPTE) {
1571                 if (!cpl->annotation_text()) {
1572                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1573                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1574                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1575                 }
1576         }
1577
1578         for (auto i: dcp->pkls()) {
1579                 /* Check that the CPL's hash corresponds to the PKL */
1580                 optional<string> h = i->hash(cpl->id());
1581                 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1582                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1583                 }
1584
1585                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1586                 optional<string> required_annotation_text;
1587                 for (auto j: i->assets()) {
1588                         /* See if this is a CPL */
1589                         for (auto k: dcp->cpls()) {
1590                                 if (j->id() == k->id()) {
1591                                         if (!required_annotation_text) {
1592                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1593                                                 required_annotation_text = cpl->content_title_text();
1594                                         } else {
1595                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1596                                                 required_annotation_text = boost::none;
1597                                         }
1598                                 }
1599                         }
1600                 }
1601
1602                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1603                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1604                 }
1605         }
1606
1607         /* set to true if any reel has a MainSubtitle */
1608         auto have_main_subtitle = false;
1609         /* set to true if any reel has no MainSubtitle */
1610         auto have_no_main_subtitle = false;
1611         /* fewest number of closed caption assets seen in a reel */
1612         size_t fewest_closed_captions = SIZE_MAX;
1613         /* most number of closed caption assets seen in a reel */
1614         size_t most_closed_captions = 0;
1615         map<Marker, Time> markers_seen;
1616
1617         auto const main_picture_active_area = cpl->main_picture_active_area();
1618         if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1619                 notes.push_back({
1620                                 VerificationNote::Type::ERROR,
1621                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1622                                 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1623                                 cpl->file().get()
1624                         });
1625         }
1626         if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1627                 notes.push_back({
1628                                 VerificationNote::Type::ERROR,
1629                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1630                                 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1631                                 cpl->file().get()
1632                         });
1633         }
1634
1635         int64_t frame = 0;
1636         for (auto reel: cpl->reels()) {
1637                 stage("Checking reel", optional<boost::filesystem::path>());
1638                 verify_reel(
1639                         dcp,
1640                         cpl,
1641                         reel,
1642                         frame,
1643                         main_picture_active_area,
1644                         stage,
1645                         xsd_dtd_directory,
1646                         progress,
1647                         options,
1648                         notes,
1649                         state,
1650                         &have_main_subtitle,
1651                         &have_no_main_subtitle,
1652                         &most_closed_captions,
1653                         &fewest_closed_captions,
1654                         &markers_seen
1655                         );
1656                 frame += reel->duration();
1657         }
1658
1659         verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1660
1661         if (dcp->standard() == Standard::SMPTE) {
1662                 if (auto msc = cpl->main_sound_configuration()) {
1663                         if (state.audio_channels && msc->channels() != *state.audio_channels) {
1664                                 notes.push_back({
1665                                                 VerificationNote::Type::ERROR,
1666                                                 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1667                                                 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1668                                                 cpl->file().get()
1669                                         });
1670                         }
1671                 }
1672
1673                 if (have_main_subtitle && have_no_main_subtitle) {
1674                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1675                 }
1676
1677                 if (fewest_closed_captions != most_closed_captions) {
1678                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1679                 }
1680
1681                 if (cpl->content_kind() == ContentKind::FEATURE) {
1682                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1683                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1684                         }
1685                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1686                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1687                         }
1688                 }
1689
1690                 auto ffoc = markers_seen.find(Marker::FFOC);
1691                 if (ffoc == markers_seen.end()) {
1692                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1693                 } else if (ffoc->second.e != 1) {
1694                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1695                 }
1696
1697                 auto lfoc = markers_seen.find(Marker::LFOC);
1698                 if (lfoc == markers_seen.end()) {
1699                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1700                 } else {
1701                         auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1702                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1703                                 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1704                         }
1705                 }
1706
1707                 LinesCharactersResult result;
1708                 for (auto reel: cpl->reels()) {
1709                         if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1710                                 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1711                         }
1712                 }
1713
1714                 if (result.line_count_exceeded) {
1715                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1716                 }
1717                 if (result.error_length_exceeded) {
1718                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1719                 } else if (result.warning_length_exceeded) {
1720                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1721                 }
1722
1723                 result = LinesCharactersResult();
1724                 for (auto reel: cpl->reels()) {
1725                         for (auto i: reel->closed_captions()) {
1726                                 if (i->asset()) {
1727                                         verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1728                                 }
1729                         }
1730                 }
1731
1732                 if (result.line_count_exceeded) {
1733                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1734                 }
1735                 if (result.error_length_exceeded) {
1736                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1737                 }
1738
1739                 if (!cpl->read_composition_metadata()) {
1740                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1741                 } else if (!cpl->version_number()) {
1742                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1743                 }
1744
1745                 verify_extension_metadata(cpl, notes);
1746
1747                 if (cpl->any_encrypted()) {
1748                         cxml::Document doc("CompositionPlaylist");
1749                         DCP_ASSERT(cpl->file());
1750                         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1751                         if (!doc.optional_node_child("Signature")) {
1752                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1753                         }
1754                 }
1755         }
1756 }
1757
1758
1759 static
1760 void
1761 verify_pkl(
1762         shared_ptr<const DCP> dcp,
1763         shared_ptr<const PKL> pkl,
1764         boost::filesystem::path xsd_dtd_directory,
1765         vector<VerificationNote>& notes
1766         )
1767 {
1768         validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1769
1770         if (pkl_has_encrypted_assets(dcp, pkl)) {
1771                 cxml::Document doc("PackingList");
1772                 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1773                 if (!doc.optional_node_child("Signature")) {
1774                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1775                 }
1776         }
1777
1778         set<string> uuid_set;
1779         for (auto asset: pkl->assets()) {
1780                 if (!uuid_set.insert(asset->id()).second) {
1781                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1782                         break;
1783                 }
1784         }
1785 }
1786
1787
1788
1789 static
1790 void
1791 verify_assetmap(
1792         shared_ptr<const DCP> dcp,
1793         boost::filesystem::path xsd_dtd_directory,
1794         vector<VerificationNote>& notes
1795         )
1796 {
1797         auto asset_map = dcp->asset_map();
1798         DCP_ASSERT(asset_map);
1799
1800         validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1801
1802         set<string> uuid_set;
1803         for (auto const& asset: asset_map->assets()) {
1804                 if (!uuid_set.insert(asset.id()).second) {
1805                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1806                         break;
1807                 }
1808         }
1809 }
1810
1811
1812 vector<VerificationNote>
1813 dcp::verify (
1814         vector<boost::filesystem::path> directories,
1815         vector<dcp::DecryptedKDM> kdms,
1816         function<void (string, optional<boost::filesystem::path>)> stage,
1817         function<void (float)> progress,
1818         VerificationOptions options,
1819         optional<boost::filesystem::path> xsd_dtd_directory
1820         )
1821 {
1822         if (!xsd_dtd_directory) {
1823                 xsd_dtd_directory = resources_directory() / "xsd";
1824         }
1825         *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1826
1827         vector<VerificationNote> notes;
1828         State state{};
1829
1830         vector<shared_ptr<DCP>> dcps;
1831         for (auto i: directories) {
1832                 dcps.push_back (make_shared<DCP>(i));
1833         }
1834
1835         for (auto dcp: dcps) {
1836                 stage ("Checking DCP", dcp->directory());
1837                 bool carry_on = true;
1838                 try {
1839                         dcp->read (&notes, true);
1840                 } catch (MissingAssetmapError& e) {
1841                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1842                         carry_on = false;
1843                 } catch (ReadError& e) {
1844                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1845                 } catch (XMLError& e) {
1846                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1847                 } catch (MXFFileError& e) {
1848                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1849                 } catch (BadURNUUIDError& e) {
1850                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1851                 } catch (cxml::Error& e) {
1852                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1853                 }
1854
1855                 if (!carry_on) {
1856                         continue;
1857                 }
1858
1859                 if (dcp->standard() != Standard::SMPTE) {
1860                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1861                 }
1862
1863                 for (auto kdm: kdms) {
1864                         dcp->add(kdm);
1865                 }
1866
1867                 for (auto cpl: dcp->cpls()) {
1868                         verify_cpl(
1869                                 dcp,
1870                                 cpl,
1871                                 stage,
1872                                 *xsd_dtd_directory,
1873                                 progress,
1874                                 options,
1875                                 notes,
1876                                 state
1877                                 );
1878                 }
1879
1880                 for (auto pkl: dcp->pkls()) {
1881                         stage("Checking PKL", pkl->file());
1882                         verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1883                 }
1884
1885                 if (dcp->asset_map_file()) {
1886                         stage("Checking ASSETMAP", dcp->asset_map_file().get());
1887                         verify_assetmap(dcp, *xsd_dtd_directory, notes);
1888                 } else {
1889                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1890                 }
1891         }
1892
1893         return notes;
1894 }
1895
1896
1897 string
1898 dcp::note_to_string (VerificationNote note)
1899 {
1900         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1901          *
1902          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1903          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1904          *
1905          *  It's OK to use XML tag names where they are clear.
1906          *  If both ID and filename are available, use only the ID.
1907          *  End messages with a full stop.
1908          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1909          */
1910         switch (note.code()) {
1911         case VerificationNote::Code::FAILED_READ:
1912                 return *note.note();
1913         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1914                 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1915         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1916                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1917         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1918                 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1919         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1920                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1921         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1922                 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1923         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1924                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1925         case VerificationNote::Code::EMPTY_ASSET_PATH:
1926                 return "The asset map contains an empty asset path.";
1927         case VerificationNote::Code::MISSING_ASSET:
1928                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1929         case VerificationNote::Code::MISMATCHED_STANDARD:
1930                 return "The DCP contains both SMPTE and Interop parts.";
1931         case VerificationNote::Code::INVALID_XML:
1932                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1933         case VerificationNote::Code::MISSING_ASSETMAP:
1934                 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1935         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1936                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1937         case VerificationNote::Code::INVALID_DURATION:
1938                 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1939         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1940                 return String::compose("The instantaneous bit rate of the picture asset %1 is larger than the limit of 250Mbit/s in at least one place.", note.file()->filename());
1941         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1942                 return String::compose("The instantaneous bit rate of the picture asset %1 is close to the limit of 250Mbit/s in at least one place.", note.file()->filename());
1943         case VerificationNote::Code::EXTERNAL_ASSET:
1944                 return String::compose("The asset %1 that this DCP refers to is not included in the DCP.  It may be a VF.", note.note().get());
1945         case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1946                 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1947         case VerificationNote::Code::INVALID_STANDARD:
1948                 return "This DCP does not use the SMPTE standard.";
1949         case VerificationNote::Code::INVALID_LANGUAGE:
1950                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1951         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1952                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1953         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1954                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1955         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1956                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1957         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1958                 return "3D 4K DCPs are not allowed.";
1959         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1960                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1961         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1962                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1963         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1964                 return String::compose("The size %1 of the fonts in timed text asset %2 is larger than the 10MB maximum.", note.note().get(), note.file()->filename());
1965         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1966                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1967         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1968                 return "Some subtitle assets have different <Language> tags than others";
1969         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1970                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1971         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1972                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1973         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1974                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1975         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1976                 return "At least one subtitle lasts less than 15 frames.";
1977         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1978                 return "At least one pair of subtitles is separated by less than 2 frames.";
1979         case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1980                 return "At least one subtitle extends outside of its reel.";
1981         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1982                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1983         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1984                 return "There are more than 52 characters in at least one subtitle line.";
1985         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1986                 return "There are more than 79 characters in at least one subtitle line.";
1987         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1988                 return "There are more than 3 closed caption lines in at least one place.";
1989         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1990                 return "There are more than 32 characters in at least one closed caption line.";
1991         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1992                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1993         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1994                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1995         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1996                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1997         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1998                 return "All assets in a reel do not have the same duration.";
1999         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2000                 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2001         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2002                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2003         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2004                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2005         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2006                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2007         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2008                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2009         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2010                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2011         case VerificationNote::Code::MISSING_HASH:
2012                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2013         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2014                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2015         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2016                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2017         case VerificationNote::Code::MISSING_FFOC:
2018                 return "There should be a FFOC (first frame of content) marker.";
2019         case VerificationNote::Code::MISSING_LFOC:
2020                 return "There should be a LFOC (last frame of content) marker.";
2021         case VerificationNote::Code::INCORRECT_FFOC:
2022                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2023         case VerificationNote::Code::INCORRECT_LFOC:
2024                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2025         case VerificationNote::Code::MISSING_CPL_METADATA:
2026                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2027         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2028                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2029         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2030                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2031         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2032                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2033         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2034                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2035         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2036                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2037         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2038                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2039         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2040                 return "Some assets are encrypted but some are not.";
2041         case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2042                 return String::compose(
2043                         "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%2).",
2044                         note.frame().get(),
2045                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2046                         note.note().get()
2047                         );
2048         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2049                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2050         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2051                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2052         case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2053                 return "The JPEG2000 tile size is not the same as the image size.";
2054         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2055                 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2056         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2057                 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2058         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2059                 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2060         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2061                 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2062         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2063                 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2064         case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2065                 return "POC marker found outside main header.";
2066         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2067                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2068         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2069                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2070         case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2071                 return "No TLM marker was found in a JPEG2000 codestream.";
2072         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2073                 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2074         case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2075                 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2076         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2077         {
2078                 vector<string> parts;
2079                 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2080                 DCP_ASSERT (parts.size() == 2);
2081                 return String::compose("The reel duration of some timed text (%1) is not the same as the ContainerDuration of its MXF (%2).", parts[0], parts[1]);
2082         }
2083         case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2084                 return "Some aspect of this DCP could not be checked because it is encrypted.";
2085         case VerificationNote::Code::EMPTY_TEXT:
2086                 return "There is an empty <Text> node in a subtitle or closed caption.";
2087         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2088                 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2089         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2090                 return "Some closed captions are not listed in the order of their vertical position.";
2091         case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2092                 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2093         case VerificationNote::Code::UNEXPECTED_DURATION:
2094                 return "There is an <Duration> node inside a <MainMarkers>.";
2095         case VerificationNote::Code::INVALID_CONTENT_KIND:
2096                 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2097         case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2098                 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2099         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2100                 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2101         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2102                 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2103         case VerificationNote::Code::MISSING_SUBTITLE:
2104                 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2105         case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2106                 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2107         case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2108                 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2109         case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2110                 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2111         case VerificationNote::Code::MISSING_FONT:
2112                 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2113         case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2114                 return String::compose(
2115                         "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2116                         note.frame().get(), note.component().get(), note.size().get()
2117                         );
2118         case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2119                 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2120         case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2121                 return String::compose("A subtitle or closed caption refers to a font with ID %1 that does not have a corresponding <LoadFont> node", note.id().get());
2122         case VerificationNote::Code::MISSING_LOAD_FONT:
2123                 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2124         case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2125                 return String::compose("The asset with ID %1 in the asset map actually has an id of %2", note.id().get(), note.other_id().get());
2126         case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2127                 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2128         }
2129
2130         return "";
2131 }
2132
2133
2134 bool
2135 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2136 {
2137         return a.type() == b.type() &&
2138                 a.code() == b.code() &&
2139                 a.note() == b.note() &&
2140                 a.file() == b.file() &&
2141                 a.line() == b.line() &&
2142                 a.frame() == b.frame() &&
2143                 a.component() == b.component() &&
2144                 a.size() == b.size() &&
2145                 a.id() == b.id() &&
2146                 a.other_id() == b.other_id() &&
2147                 a.frame_rate() == b.frame_rate();
2148 }
2149
2150
2151 bool
2152 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2153 {
2154         if (a.type() != b.type()) {
2155                 return a.type() < b.type();
2156         }
2157
2158         if (a.code() != b.code()) {
2159                 return a.code() < b.code();
2160         }
2161
2162         if (a.note() != b.note()) {
2163                 return a.note().get_value_or("") < b.note().get_value_or("");
2164         }
2165
2166         if (a.file() != b.file()) {
2167                 return a.file().get_value_or("") < b.file().get_value_or("");
2168         }
2169
2170         if (a.line() != b.line()) {
2171                 return a.line().get_value_or(0) < b.line().get_value_or(0);
2172         }
2173
2174         if (a.frame() != b.frame()) {
2175                 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2176         }
2177
2178         if (a.component() != b.component()) {
2179                 return a.component().get_value_or(0) < b.component().get_value_or(0);
2180         }
2181
2182         if (a.size() != b.size()) {
2183                 return a.size().get_value_or(0) < b.size().get_value_or(0);
2184         }
2185
2186         if (a.id() != b.id()) {
2187                 return a.id().get_value_or("") < b.id().get_value_or("");
2188         }
2189
2190         return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2191 }
2192
2193
2194 std::ostream&
2195 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2196 {
2197         s << note_to_string (note);
2198         if (note.note()) {
2199                 s << " [" << note.note().get() << "]";
2200         }
2201         if (note.file()) {
2202                 s << " [" << note.file().get() << "]";
2203         }
2204         if (note.line()) {
2205                 s << " [" << note.line().get() << "]";
2206         }
2207         return s;
2208 }
2209