In-line run of subs_in_out so that it gets the environment more easily.
[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(
389         shared_ptr<const DCP> dcp,
390         shared_ptr<const ReelFileAsset> reel_file_asset,
391         function<void (float)> progress,
392         string* reference_hash,
393         string* calculated_hash
394         )
395 {
396         DCP_ASSERT(reference_hash);
397         DCP_ASSERT(calculated_hash);
398
399         /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
400          * We want to calculate the hash of the actual file contents here, so that we
401          * can check it.  unset_hash() means that this calculation will happen on the
402          * call to hash().
403          */
404         reel_file_asset->asset_ref()->unset_hash();
405         *calculated_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
406                 progress(float(done) / total);
407         });
408
409         auto pkls = dcp->pkls();
410         /* We've read this DCP in so it must have at least one PKL */
411         DCP_ASSERT (!pkls.empty());
412
413         auto asset = reel_file_asset->asset_ref().asset();
414
415         optional<string> maybe_pkl_hash;
416         for (auto i: pkls) {
417                 maybe_pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
418                 if (maybe_pkl_hash) {
419                         break;
420                 }
421         }
422
423         DCP_ASSERT(maybe_pkl_hash);
424         *reference_hash = *maybe_pkl_hash;
425
426         auto cpl_hash = reel_file_asset->hash();
427         if (cpl_hash && *cpl_hash != *reference_hash) {
428                 return VerifyAssetResult::CPL_PKL_DIFFER;
429         }
430
431         if (*calculated_hash != *reference_hash) {
432                 return VerifyAssetResult::BAD;
433         }
434
435         return VerifyAssetResult::GOOD;
436 }
437
438
439 void
440 verify_language_tag (string tag, vector<VerificationNote>& notes)
441 {
442         try {
443                 LanguageTag test (tag);
444         } catch (LanguageTagError &) {
445                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
446         }
447 }
448
449
450 static void
451 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)
452 {
453         auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
454         auto const duration = asset->intrinsic_duration ();
455
456         auto check_and_add = [&notes](vector<VerificationNote> const& j2k_notes) {
457                 for (auto i: j2k_notes) {
458                         if (find(notes.begin(), notes.end(), i) == notes.end()) {
459                                 notes.push_back (i);
460                         }
461                 }
462         };
463
464         int const max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
465         int const risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
466
467         auto check_frame_size = [max_frame, risky_frame, file, start_frame](int index, int size, int frame_rate, vector<VerificationNote>& notes) {
468                 if (size > max_frame) {
469                         notes.push_back(
470                                 VerificationNote(
471                                         VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
472                                         ).set_frame(start_frame + index).set_frame_rate(frame_rate)
473                         );
474                 } else if (size > risky_frame) {
475                         notes.push_back(
476                                 VerificationNote(
477                                         VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
478                                         ).set_frame(start_frame + index).set_frame_rate(frame_rate)
479                         );
480                 }
481         };
482
483         if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
484                 auto reader = mono_asset->start_read ();
485                 for (int64_t i = 0; i < duration; ++i) {
486                         auto frame = reader->get_frame (i);
487                         check_frame_size(i, frame->size(), mono_asset->frame_rate().numerator, notes);
488                         if (!mono_asset->encrypted() || mono_asset->key()) {
489                                 vector<VerificationNote> j2k_notes;
490                                 verify_j2k(frame, start_frame, i, mono_asset->frame_rate().numerator, j2k_notes);
491                                 check_and_add (j2k_notes);
492                         }
493                         progress (float(i) / duration);
494                 }
495         } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
496                 auto reader = stereo_asset->start_read ();
497                 for (int64_t i = 0; i < duration; ++i) {
498                         auto frame = reader->get_frame (i);
499                         check_frame_size(i, frame->left()->size(), stereo_asset->frame_rate().numerator, notes);
500                         check_frame_size(i, frame->right()->size(), stereo_asset->frame_rate().numerator, notes);
501                         if (!stereo_asset->encrypted() || stereo_asset->key()) {
502                                 vector<VerificationNote> j2k_notes;
503                                 verify_j2k(frame->left(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
504                                 verify_j2k(frame->right(), start_frame, i, stereo_asset->frame_rate().numerator, j2k_notes);
505                                 check_and_add (j2k_notes);
506                         }
507                         progress (float(i) / duration);
508                 }
509
510         }
511 }
512
513
514 static void
515 verify_main_picture_asset (
516         shared_ptr<const DCP> dcp,
517         shared_ptr<const ReelPictureAsset> reel_asset,
518         int64_t start_frame,
519         function<void (string, optional<boost::filesystem::path>)> stage,
520         function<void (float)> progress,
521         VerificationOptions options,
522         vector<VerificationNote>& notes
523         )
524 {
525         auto asset = reel_asset->asset();
526         auto const file = *asset->file();
527
528         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
529                 stage ("Checking picture asset hash", file);
530                 string reference_hash;
531                 string calculated_hash;
532                 auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
533                 switch (r) {
534                         case VerifyAssetResult::BAD:
535                                 notes.push_back(
536                                         dcp::VerificationNote(
537                                                 VerificationNote::Type::ERROR,
538                                                 VerificationNote::Code::INCORRECT_PICTURE_HASH,
539                                                 file
540                                                 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
541                                         );
542                                 break;
543                         case VerifyAssetResult::CPL_PKL_DIFFER:
544                                 notes.push_back ({
545                                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
546                                 });
547                                 break;
548                         default:
549                                 break;
550                 }
551         }
552
553         stage ("Checking picture frame sizes", asset->file());
554         verify_picture_asset(reel_asset, file, start_frame, notes, progress);
555
556         /* Only flat/scope allowed by Bv2.1 */
557         if (
558                 asset->size() != Size(2048, 858) &&
559                 asset->size() != Size(1998, 1080) &&
560                 asset->size() != Size(4096, 1716) &&
561                 asset->size() != Size(3996, 2160)) {
562                 notes.push_back({
563                         VerificationNote::Type::BV21_ERROR,
564                         VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
565                         String::compose("%1x%2", asset->size().width, asset->size().height),
566                         file
567                 });
568         }
569
570         /* Only 24, 25, 48fps allowed for 2K */
571         if (
572                 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
573                 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
574            ) {
575                 notes.push_back({
576                         VerificationNote::Type::BV21_ERROR,
577                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
578                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
579                         file
580                 });
581         }
582
583         if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
584                 /* Only 24fps allowed for 4K */
585                 if (asset->edit_rate() != Fraction(24, 1)) {
586                         notes.push_back({
587                                 VerificationNote::Type::BV21_ERROR,
588                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
589                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
590                                 file
591                         });
592                 }
593
594                 /* Only 2D allowed for 4K */
595                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
596                         notes.push_back({
597                                 VerificationNote::Type::BV21_ERROR,
598                                 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
599                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
600                                 file
601                         });
602
603                 }
604         }
605
606 }
607
608
609 struct State
610 {
611         boost::optional<string> subtitle_language;
612         boost::optional<int> audio_channels;
613 };
614
615
616 static void
617 verify_main_sound_asset (
618         shared_ptr<const DCP> dcp,
619         shared_ptr<const ReelSoundAsset> reel_asset,
620         function<void (string, optional<boost::filesystem::path>)> stage,
621         function<void (float)> progress,
622         VerificationOptions options,
623         vector<VerificationNote>& notes,
624         State& state
625         )
626 {
627         auto asset = reel_asset->asset();
628         auto const file = *asset->file();
629
630         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
631                 stage("Checking sound asset hash", file);
632                 string reference_hash;
633                 string calculated_hash;
634                 auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
635                 switch (r) {
636                         case VerifyAssetResult::BAD:
637                                 notes.push_back(
638                                         dcp::VerificationNote(
639                                                 VerificationNote::Type::ERROR,
640                                                 VerificationNote::Code::INCORRECT_SOUND_HASH,
641                                                 file
642                                                 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
643                                         );
644                                 break;
645                         case VerifyAssetResult::CPL_PKL_DIFFER:
646                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
647                                 break;
648                         default:
649                                 break;
650                 }
651         }
652
653         if (!state.audio_channels) {
654                 state.audio_channels = asset->channels();
655         } else if (*state.audio_channels != asset->channels()) {
656                 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
657         }
658
659         stage ("Checking sound asset metadata", file);
660
661         if (auto lang = asset->language()) {
662                 verify_language_tag (*lang, notes);
663         }
664         if (asset->sampling_rate() != 48000) {
665                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
666         }
667 }
668
669
670 static void
671 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
672 {
673         /* XXX: is Language compulsory? */
674         if (reel_asset->language()) {
675                 verify_language_tag (*reel_asset->language(), notes);
676         }
677
678         if (!reel_asset->entry_point()) {
679                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
680         } else if (reel_asset->entry_point().get()) {
681                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
682         }
683 }
684
685
686 static void
687 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
688 {
689         /* XXX: is Language compulsory? */
690         if (reel_asset->language()) {
691                 verify_language_tag (*reel_asset->language(), notes);
692         }
693
694         if (!reel_asset->entry_point()) {
695                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
696         } else if (reel_asset->entry_point().get()) {
697                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
698         }
699 }
700
701
702 /** Verify stuff that is common to both subtitles and closed captions */
703 void
704 verify_smpte_timed_text_asset (
705         shared_ptr<const SMPTESubtitleAsset> asset,
706         optional<int64_t> reel_asset_duration,
707         vector<VerificationNote>& notes
708         )
709 {
710         if (asset->language()) {
711                 verify_language_tag (*asset->language(), notes);
712         } else {
713                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
714         }
715
716         auto const size = filesystem::file_size(asset->file().get());
717         if (size > 115 * 1024 * 1024) {
718                 notes.push_back (
719                         { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
720                         );
721         }
722
723         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
724          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
725          */
726         auto fonts = asset->font_data ();
727         int total_size = 0;
728         for (auto i: fonts) {
729                 total_size += i.second.size();
730         }
731         if (total_size > 10 * 1024 * 1024) {
732                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
733         }
734
735         if (!asset->start_time()) {
736                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
737         } else if (asset->start_time() != Time()) {
738                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
739         }
740
741         if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
742                 notes.push_back (
743                         {
744                                 VerificationNote::Type::BV21_ERROR,
745                                 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
746                                 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
747                                 asset->file().get()
748                         });
749         }
750 }
751
752
753 /** Verify Interop subtitle / CCAP stuff */
754 void
755 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
756 {
757         if (asset->subtitles().empty()) {
758                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
759         }
760         auto const unresolved = asset->unresolved_fonts();
761         if (!unresolved.empty()) {
762                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
763         }
764 }
765
766
767 /** Verify SMPTE subtitle-only stuff */
768 void
769 verify_smpte_subtitle_asset (
770         shared_ptr<const SMPTESubtitleAsset> asset,
771         vector<VerificationNote>& notes,
772         State& state
773         )
774 {
775         if (asset->language()) {
776                 if (!state.subtitle_language) {
777                         state.subtitle_language = *asset->language();
778                 } else if (state.subtitle_language != *asset->language()) {
779                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
780                 }
781         }
782
783         DCP_ASSERT (asset->resource_id());
784         auto xml_id = asset->xml_id();
785         if (xml_id) {
786                 if (asset->resource_id().get() != xml_id) {
787                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
788                 }
789
790                 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
791                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
792                 }
793         } else {
794                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
795         }
796
797         if (asset->raw_xml()) {
798                 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
799                 cxml::Document doc("SubtitleReel");
800                 doc.read_string(*asset->raw_xml());
801                 auto issue_date = doc.string_child("IssueDate");
802                 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
803                 if (!std::regex_match(issue_date, reg)) {
804                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
805                 }
806         }
807 }
808
809
810 /** Verify all subtitle stuff */
811 static void
812 verify_subtitle_asset (
813         shared_ptr<const SubtitleAsset> asset,
814         optional<int64_t> reel_asset_duration,
815         function<void (string, optional<boost::filesystem::path>)> stage,
816         boost::filesystem::path xsd_dtd_directory,
817         vector<VerificationNote>& notes,
818         State& state
819         )
820 {
821         stage ("Checking subtitle XML", asset->file());
822         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
823          * gets passed through libdcp which may clean up and therefore hide errors.
824          */
825         if (asset->raw_xml()) {
826                 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
827         } else {
828                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
829         }
830
831         auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
832                 cxml::Document doc(root_node);
833                 doc.read_string(asset->raw_xml().get());
834                 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
835                 int count = 0;
836                 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
837                         ++count;
838                 }
839                 return count;
840         };
841
842         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
843         if (interop) {
844                 verify_interop_text_asset(interop, notes);
845                 if (namespace_count(asset, "DCSubtitle") > 1) {
846                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
847                 }
848         }
849
850         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
851         if (smpte) {
852                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
853                 verify_smpte_subtitle_asset (smpte, notes, state);
854                 /* This asset may be encrypted and in that case we'll have no raw_xml() */
855                 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
856                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
857                 }
858         }
859 }
860
861
862 /** Verify all closed caption stuff */
863 static void
864 verify_closed_caption_asset (
865         shared_ptr<const SubtitleAsset> asset,
866         optional<int64_t> reel_asset_duration,
867         function<void (string, optional<boost::filesystem::path>)> stage,
868         boost::filesystem::path xsd_dtd_directory,
869         vector<VerificationNote>& notes
870         )
871 {
872         stage ("Checking closed caption XML", asset->file());
873         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
874          * gets passed through libdcp which may clean up and therefore hide errors.
875          */
876         auto raw_xml = asset->raw_xml();
877         if (raw_xml) {
878                 validate_xml (*raw_xml, xsd_dtd_directory, notes);
879                 if (raw_xml->size() > 256 * 1024) {
880                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
881                 }
882         } else {
883                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
884         }
885
886         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
887         if (interop) {
888                 verify_interop_text_asset(interop, notes);
889         }
890
891         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
892         if (smpte) {
893                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
894         }
895 }
896
897
898 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
899 static
900 void
901 verify_text_details (
902         dcp::Standard standard,
903         vector<shared_ptr<Reel>> reels,
904         int edit_rate,
905         vector<VerificationNote>& notes,
906         std::function<bool (shared_ptr<Reel>)> check,
907         std::function<optional<string> (shared_ptr<Reel>)> xml,
908         std::function<int64_t (shared_ptr<Reel>)> duration,
909         std::function<std::string (shared_ptr<Reel>)> id
910         )
911 {
912         /* end of last subtitle (in editable units) */
913         optional<int64_t> last_out;
914         auto too_short = false;
915         auto too_close = false;
916         auto too_early = false;
917         auto reel_overlap = false;
918         auto empty_text = false;
919         /* current reel start time (in editable units) */
920         int64_t reel_offset = 0;
921         optional<string> missing_load_font_id;
922
923         std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
924
925         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
926                 cxml::ConstNodePtr node,
927                 optional<int> tcr,
928                 optional<Time> start_time,
929                 int er,
930                 bool first_reel,
931                 bool& has_text,
932                 vector<string>& font_ids
933                 ) {
934                 if (node->name() == "Subtitle") {
935                         Time in (node->string_attribute("TimeIn"), tcr);
936                         if (start_time) {
937                                 in -= *start_time;
938                         }
939                         Time out (node->string_attribute("TimeOut"), tcr);
940                         if (start_time) {
941                                 out -= *start_time;
942                         }
943                         if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
944                                 too_early = true;
945                         }
946                         auto length = out - in;
947                         if (length.as_editable_units_ceil(er) < 15) {
948                                 too_short = true;
949                         }
950                         if (last_out) {
951                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
952                                 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
953                                 if (distance >= 0 && distance < 2) {
954                                         too_close = true;
955                                 }
956                         }
957                         last_out = reel_offset + out.as_editable_units_floor(er);
958                 } else if (node->name() == "Text") {
959                         std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
960                                 if (!node->content().empty()) {
961                                         return true;
962                                 }
963                                 for (auto i: node->node_children()) {
964                                         if (node_has_content(i)) {
965                                                 return true;
966                                         }
967                                 }
968                                 return false;
969                         };
970                         if (!node_has_content(node)) {
971                                 empty_text = true;
972                         }
973                         has_text = true;
974                 } else if (node->name() == "LoadFont") {
975                         if (auto const id = node->optional_string_attribute("Id")) {
976                                 font_ids.push_back(*id);
977                         } else if (auto const id = node->optional_string_attribute("ID")) {
978                                 font_ids.push_back(*id);
979                         }
980                 } else if (node->name() == "Font") {
981                         if (auto const font_id = node->optional_string_attribute("Id")) {
982                                 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
983                                         missing_load_font_id = font_id;
984                                 }
985                         }
986                 }
987                 for (auto i: node->node_children()) {
988                         parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
989                 }
990         };
991
992         for (auto i = 0U; i < reels.size(); ++i) {
993                 if (!check(reels[i])) {
994                         continue;
995                 }
996
997                 auto reel_xml = xml(reels[i]);
998                 if (!reel_xml) {
999                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1000                         continue;
1001                 }
1002
1003                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1004                  * read in by libdcp's parser.
1005                  */
1006
1007                 shared_ptr<cxml::Document> doc;
1008                 optional<int> tcr;
1009                 optional<Time> start_time;
1010                 switch (standard) {
1011                 case dcp::Standard::INTEROP:
1012                         doc = make_shared<cxml::Document>("DCSubtitle");
1013                         doc->read_string (*reel_xml);
1014                         break;
1015                 case dcp::Standard::SMPTE:
1016                         doc = make_shared<cxml::Document>("SubtitleReel");
1017                         doc->read_string (*reel_xml);
1018                         tcr = doc->number_child<int>("TimeCodeRate");
1019                         if (auto start_time_string = doc->optional_string_child("StartTime")) {
1020                                 start_time = Time(*start_time_string, tcr);
1021                         }
1022                         break;
1023                 }
1024                 bool has_text = false;
1025                 vector<string> font_ids;
1026                 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1027                 auto end = reel_offset + duration(reels[i]);
1028                 if (last_out && *last_out > end) {
1029                         reel_overlap = true;
1030                 }
1031                 reel_offset = end;
1032
1033                 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1034                         notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1035                 }
1036         }
1037
1038         if (last_out && *last_out > reel_offset) {
1039                 reel_overlap = true;
1040         }
1041
1042         if (too_early) {
1043                 notes.push_back({
1044                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1045                 });
1046         }
1047
1048         if (too_short) {
1049                 notes.push_back ({
1050                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1051                 });
1052         }
1053
1054         if (too_close) {
1055                 notes.push_back ({
1056                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1057                 });
1058         }
1059
1060         if (reel_overlap) {
1061                 notes.push_back ({
1062                         VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1063                 });
1064         }
1065
1066         if (empty_text) {
1067                 notes.push_back ({
1068                         VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1069                 });
1070         }
1071
1072         if (missing_load_font_id) {
1073                 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1074         }
1075 }
1076
1077
1078 static
1079 void
1080 verify_closed_caption_details (
1081         vector<shared_ptr<Reel>> reels,
1082         vector<VerificationNote>& notes
1083         )
1084 {
1085         std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1086         find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1087                 for (auto i: node->node_children()) {
1088                         if (i->name() == "Text") {
1089                                 text_or_image.push_back (i);
1090                         } else {
1091                                 find_text_or_image (i, text_or_image);
1092                         }
1093                 }
1094         };
1095
1096         auto mismatched_valign = false;
1097         auto incorrect_order = false;
1098
1099         std::function<void (cxml::ConstNodePtr)> parse;
1100         parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1101                 if (node->name() == "Subtitle") {
1102                         vector<cxml::ConstNodePtr> text_or_image;
1103                         find_text_or_image (node, text_or_image);
1104                         optional<string> last_valign;
1105                         optional<float> last_vpos;
1106                         for (auto i: text_or_image) {
1107                                 auto valign = i->optional_string_attribute("VAlign");
1108                                 if (!valign) {
1109                                         valign = i->optional_string_attribute("Valign").get_value_or("center");
1110                                 }
1111                                 auto vpos = i->optional_number_attribute<float>("VPosition");
1112                                 if (!vpos) {
1113                                         vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1114                                 }
1115
1116                                 if (last_valign) {
1117                                         if (*last_valign != valign) {
1118                                                 mismatched_valign = true;
1119                                         }
1120                                 }
1121                                 last_valign = valign;
1122
1123                                 if (!mismatched_valign) {
1124                                         if (last_vpos) {
1125                                                 if (*last_valign == "top" || *last_valign == "center") {
1126                                                         if (*vpos < *last_vpos) {
1127                                                                 incorrect_order = true;
1128                                                         }
1129                                                 } else {
1130                                                         if (*vpos > *last_vpos) {
1131                                                                 incorrect_order = true;
1132                                                         }
1133                                                 }
1134                                         }
1135                                         last_vpos = vpos;
1136                                 }
1137                         }
1138                 }
1139
1140                 for (auto i: node->node_children()) {
1141                         parse(i);
1142                 }
1143         };
1144
1145         for (auto reel: reels) {
1146                 for (auto ccap: reel->closed_captions()) {
1147                         auto reel_xml = ccap->asset()->raw_xml();
1148                         if (!reel_xml) {
1149                                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1150                                 continue;
1151                         }
1152
1153                         /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1154                          * read in by libdcp's parser.
1155                          */
1156
1157                         shared_ptr<cxml::Document> doc;
1158                         optional<int> tcr;
1159                         optional<Time> start_time;
1160                         try {
1161                                 doc = make_shared<cxml::Document>("SubtitleReel");
1162                                 doc->read_string (*reel_xml);
1163                         } catch (...) {
1164                                 doc = make_shared<cxml::Document>("DCSubtitle");
1165                                 doc->read_string (*reel_xml);
1166                         }
1167                         parse (doc);
1168                 }
1169         }
1170
1171         if (mismatched_valign) {
1172                 notes.push_back ({
1173                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1174                 });
1175         }
1176
1177         if (incorrect_order) {
1178                 notes.push_back ({
1179                         VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1180                 });
1181         }
1182 }
1183
1184
1185 struct LinesCharactersResult
1186 {
1187         bool warning_length_exceeded = false;
1188         bool error_length_exceeded = false;
1189         bool line_count_exceeded = false;
1190 };
1191
1192
1193 static
1194 void
1195 verify_text_lines_and_characters (
1196         shared_ptr<SubtitleAsset> asset,
1197         int warning_length,
1198         int error_length,
1199         LinesCharactersResult* result
1200         )
1201 {
1202         class Event
1203         {
1204         public:
1205                 Event (Time time_, float position_, int characters_)
1206                         : time (time_)
1207                         , position (position_)
1208                         , characters (characters_)
1209                 {}
1210
1211                 Event (Time time_, shared_ptr<Event> start_)
1212                         : time (time_)
1213                         , start (start_)
1214                 {}
1215
1216                 Time time;
1217                 int position; //< position from 0 at top of screen to 100 at bottom
1218                 int characters;
1219                 shared_ptr<Event> start;
1220         };
1221
1222         vector<shared_ptr<Event>> events;
1223
1224         auto position = [](shared_ptr<const SubtitleString> sub) {
1225                 switch (sub->v_align()) {
1226                 case VAlign::TOP:
1227                         return lrintf(sub->v_position() * 100);
1228                 case VAlign::CENTER:
1229                         return lrintf((0.5f + sub->v_position()) * 100);
1230                 case VAlign::BOTTOM:
1231                         return lrintf((1.0f - sub->v_position()) * 100);
1232                 }
1233
1234                 return 0L;
1235         };
1236
1237         for (auto j: asset->subtitles()) {
1238                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1239                 if (text) {
1240                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1241                         events.push_back(in);
1242                         events.push_back(make_shared<Event>(text->out(), in));
1243                 }
1244         }
1245
1246         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1247                 return a->time < b->time;
1248         });
1249
1250         map<int, int> current;
1251         for (auto i: events) {
1252                 if (current.size() > 3) {
1253                         result->line_count_exceeded = true;
1254                 }
1255                 for (auto j: current) {
1256                         if (j.second > warning_length) {
1257                                 result->warning_length_exceeded = true;
1258                         }
1259                         if (j.second > error_length) {
1260                                 result->error_length_exceeded = true;
1261                         }
1262                 }
1263
1264                 if (i->start) {
1265                         /* end of a subtitle */
1266                         DCP_ASSERT (current.find(i->start->position) != current.end());
1267                         if (current[i->start->position] == i->start->characters) {
1268                                 current.erase(i->start->position);
1269                         } else {
1270                                 current[i->start->position] -= i->start->characters;
1271                         }
1272                 } else {
1273                         /* start of a subtitle */
1274                         if (current.find(i->position) == current.end()) {
1275                                 current[i->position] = i->characters;
1276                         } else {
1277                                 current[i->position] += i->characters;
1278                         }
1279                 }
1280         }
1281 }
1282
1283
1284 static
1285 void
1286 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1287 {
1288         if (reels.empty()) {
1289                 return;
1290         }
1291
1292         if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1293                 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1294                         [](shared_ptr<Reel> reel) {
1295                                 return static_cast<bool>(reel->main_subtitle());
1296                         },
1297                         [](shared_ptr<Reel> reel) {
1298                                 return reel->main_subtitle()->asset()->raw_xml();
1299                         },
1300                         [](shared_ptr<Reel> reel) {
1301                                 return reel->main_subtitle()->actual_duration();
1302                         },
1303                         [](shared_ptr<Reel> reel) {
1304                                 return reel->main_subtitle()->id();
1305                         }
1306                 );
1307         }
1308
1309         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1310                 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1311                         [i](shared_ptr<Reel> reel) {
1312                                 return i < reel->closed_captions().size();
1313                         },
1314                         [i](shared_ptr<Reel> reel) {
1315                                 return reel->closed_captions()[i]->asset()->raw_xml();
1316                         },
1317                         [i](shared_ptr<Reel> reel) {
1318                                 return reel->closed_captions()[i]->actual_duration();
1319                         },
1320                         [i](shared_ptr<Reel> reel) {
1321                                 return reel->closed_captions()[i]->id();
1322                         }
1323                 );
1324         }
1325
1326         verify_closed_caption_details (reels, notes);
1327 }
1328
1329
1330 void
1331 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1332 {
1333         DCP_ASSERT (cpl->file());
1334         cxml::Document doc ("CompositionPlaylist");
1335         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1336
1337         auto missing = false;
1338         string malformed;
1339
1340         if (auto reel_list = doc.node_child("ReelList")) {
1341                 auto reels = reel_list->node_children("Reel");
1342                 if (!reels.empty()) {
1343                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1344                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1345                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1346                                                 missing = true;
1347                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1348                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1349                                                                 continue;
1350                                                         }
1351                                                         missing = false;
1352                                                         if (auto name = extension->optional_node_child("Name")) {
1353                                                                 if (name->content() != "Application") {
1354                                                                         malformed = "<Name> should be 'Application'";
1355                                                                 }
1356                                                         }
1357                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
1358                                                                 if (auto property = property_list->optional_node_child("Property")) {
1359                                                                         if (auto name = property->optional_node_child("Name")) {
1360                                                                                 if (name->content() != "DCP Constraints Profile") {
1361                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
1362                                                                                 }
1363                                                                         }
1364                                                                         if (auto value = property->optional_node_child("Value")) {
1365                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1366                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1367                                                                                 }
1368                                                                         }
1369                                                                 }
1370                                                         }
1371                                                 }
1372                                         } else {
1373                                                 missing = true;
1374                                         }
1375                                 }
1376                         }
1377                 }
1378         }
1379
1380         if (missing) {
1381                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1382         } else if (!malformed.empty()) {
1383                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1384         }
1385 }
1386
1387
1388 bool
1389 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1390 {
1391         vector<string> encrypted;
1392         for (auto i: dcp->cpls()) {
1393                 for (auto j: i->reel_file_assets()) {
1394                         if (j->asset_ref().resolved()) {
1395                                 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1396                                 if (mxf && mxf->encrypted()) {
1397                                         encrypted.push_back(j->asset_ref().id());
1398                                 }
1399                         }
1400                 }
1401         }
1402
1403         for (auto i: pkl->assets()) {
1404                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1405                         return true;
1406                 }
1407         }
1408
1409         return false;
1410 }
1411
1412
1413 static
1414 void
1415 verify_reel(
1416         shared_ptr<const DCP> dcp,
1417         shared_ptr<const CPL> cpl,
1418         shared_ptr<const Reel> reel,
1419         int64_t start_frame,
1420         optional<dcp::Size> main_picture_active_area,
1421         function<void (string, optional<boost::filesystem::path>)> stage,
1422         boost::filesystem::path xsd_dtd_directory,
1423         function<void (float)> progress,
1424         VerificationOptions options,
1425         vector<VerificationNote>& notes,
1426         State& state,
1427         bool* have_main_subtitle,
1428         bool* have_no_main_subtitle,
1429         size_t* most_closed_captions,
1430         size_t* fewest_closed_captions,
1431         map<Marker, Time>* markers_seen
1432         )
1433 {
1434         for (auto i: reel->assets()) {
1435                 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1436                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1437                 }
1438                 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1439                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1440                 }
1441                 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1442                 if (i->encryptable() && !file_asset->hash()) {
1443                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1444                 }
1445         }
1446
1447         if (dcp->standard() == Standard::SMPTE) {
1448                 boost::optional<int64_t> duration;
1449                 for (auto i: reel->assets()) {
1450                         if (!duration) {
1451                                 duration = i->actual_duration();
1452                         } else if (*duration != i->actual_duration()) {
1453                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1454                                 break;
1455                         }
1456                 }
1457         }
1458
1459         if (reel->main_picture()) {
1460                 /* Check reel stuff */
1461                 auto const frame_rate = reel->main_picture()->frame_rate();
1462                 if (frame_rate.denominator != 1 ||
1463                     (frame_rate.numerator != 24 &&
1464                      frame_rate.numerator != 25 &&
1465                      frame_rate.numerator != 30 &&
1466                      frame_rate.numerator != 48 &&
1467                      frame_rate.numerator != 50 &&
1468                      frame_rate.numerator != 60 &&
1469                      frame_rate.numerator != 96)) {
1470                         notes.push_back({
1471                                 VerificationNote::Type::ERROR,
1472                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1473                                 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1474                         });
1475                 }
1476                 /* Check asset */
1477                 if (reel->main_picture()->asset_ref().resolved()) {
1478                         verify_main_picture_asset(dcp, reel->main_picture(), start_frame, stage, progress, options, notes);
1479                         auto const asset_size = reel->main_picture()->asset()->size();
1480                         if (main_picture_active_area) {
1481                                 if (main_picture_active_area->width > asset_size.width) {
1482                                         notes.push_back({
1483                                                         VerificationNote::Type::ERROR,
1484                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1485                                                         String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1486                                                         cpl->file().get()
1487                                                         });
1488                                 }
1489                                 if (main_picture_active_area->height > asset_size.height) {
1490                                         notes.push_back({
1491                                                         VerificationNote::Type::ERROR,
1492                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1493                                                         String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1494                                                         cpl->file().get()
1495                                                         });
1496                                 }
1497                         }
1498                 }
1499
1500         }
1501
1502         if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1503                 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1504         }
1505
1506         if (reel->main_subtitle()) {
1507                 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1508                 if (reel->main_subtitle()->asset_ref().resolved()) {
1509                         verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1510                 }
1511                 *have_main_subtitle = true;
1512         } else {
1513                 *have_no_main_subtitle = true;
1514         }
1515
1516         for (auto i: reel->closed_captions()) {
1517                 verify_closed_caption_reel(i, notes);
1518                 if (i->asset_ref().resolved()) {
1519                         verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1520                 }
1521         }
1522
1523         if (reel->main_markers()) {
1524                 for (auto const& i: reel->main_markers()->get()) {
1525                         markers_seen->insert(i);
1526                 }
1527                 if (reel->main_markers()->entry_point()) {
1528                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1529                 }
1530                 if (reel->main_markers()->duration()) {
1531                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1532                 }
1533         }
1534
1535         *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1536         *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1537
1538 }
1539
1540
1541 static
1542 void
1543 verify_cpl(
1544         shared_ptr<const DCP> dcp,
1545         shared_ptr<const CPL> cpl,
1546         function<void (string, optional<boost::filesystem::path>)> stage,
1547         boost::filesystem::path xsd_dtd_directory,
1548         function<void (float)> progress,
1549         VerificationOptions options,
1550         vector<VerificationNote>& notes,
1551         State& state
1552         )
1553 {
1554         stage("Checking CPL", cpl->file());
1555         validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1556
1557         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1558                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1559         }
1560
1561         for (auto const& i: cpl->additional_subtitle_languages()) {
1562                 verify_language_tag(i, notes);
1563         }
1564
1565         if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1566                 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1567                  * of the approved ones.
1568                  */
1569                 auto all = ContentKind::all();
1570                 auto name = cpl->content_kind().name();
1571                 transform(name.begin(), name.end(), name.begin(), ::tolower);
1572                 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1573                 if (iter == all.end()) {
1574                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1575                 }
1576         }
1577
1578         if (cpl->release_territory()) {
1579                 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") {
1580                         auto terr = cpl->release_territory().get();
1581                         /* Must be a valid region tag, or "001" */
1582                         try {
1583                                 LanguageTag::RegionSubtag test(terr);
1584                         } catch (...) {
1585                                 if (terr != "001") {
1586                                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1587                                 }
1588                         }
1589                 }
1590         }
1591
1592         for (auto version: cpl->content_versions()) {
1593                 if (version.label_text.empty()) {
1594                         notes.push_back(
1595                                 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1596                                 );
1597                         break;
1598                 }
1599         }
1600
1601         if (dcp->standard() == Standard::SMPTE) {
1602                 if (!cpl->annotation_text()) {
1603                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1604                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1605                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1606                 }
1607         }
1608
1609         for (auto i: dcp->pkls()) {
1610                 /* Check that the CPL's hash corresponds to the PKL */
1611                 optional<string> h = i->hash(cpl->id());
1612                 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1613                 if (h && calculated_cpl_hash != *h) {
1614                         notes.push_back(
1615                                 dcp::VerificationNote(
1616                                         VerificationNote::Type::ERROR,
1617                                         VerificationNote::Code::MISMATCHED_CPL_HASHES,
1618                                         cpl->id(),
1619                                         cpl->file().get()
1620                                         ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1621                                 );
1622                 }
1623
1624                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1625                 optional<string> required_annotation_text;
1626                 for (auto j: i->assets()) {
1627                         /* See if this is a CPL */
1628                         for (auto k: dcp->cpls()) {
1629                                 if (j->id() == k->id()) {
1630                                         if (!required_annotation_text) {
1631                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1632                                                 required_annotation_text = cpl->content_title_text();
1633                                         } else {
1634                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1635                                                 required_annotation_text = boost::none;
1636                                         }
1637                                 }
1638                         }
1639                 }
1640
1641                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1642                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1643                 }
1644         }
1645
1646         /* set to true if any reel has a MainSubtitle */
1647         auto have_main_subtitle = false;
1648         /* set to true if any reel has no MainSubtitle */
1649         auto have_no_main_subtitle = false;
1650         /* fewest number of closed caption assets seen in a reel */
1651         size_t fewest_closed_captions = SIZE_MAX;
1652         /* most number of closed caption assets seen in a reel */
1653         size_t most_closed_captions = 0;
1654         map<Marker, Time> markers_seen;
1655
1656         auto const main_picture_active_area = cpl->main_picture_active_area();
1657         if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1658                 notes.push_back({
1659                                 VerificationNote::Type::ERROR,
1660                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1661                                 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1662                                 cpl->file().get()
1663                         });
1664         }
1665         if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1666                 notes.push_back({
1667                                 VerificationNote::Type::ERROR,
1668                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1669                                 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1670                                 cpl->file().get()
1671                         });
1672         }
1673
1674         int64_t frame = 0;
1675         for (auto reel: cpl->reels()) {
1676                 stage("Checking reel", optional<boost::filesystem::path>());
1677                 verify_reel(
1678                         dcp,
1679                         cpl,
1680                         reel,
1681                         frame,
1682                         main_picture_active_area,
1683                         stage,
1684                         xsd_dtd_directory,
1685                         progress,
1686                         options,
1687                         notes,
1688                         state,
1689                         &have_main_subtitle,
1690                         &have_no_main_subtitle,
1691                         &most_closed_captions,
1692                         &fewest_closed_captions,
1693                         &markers_seen
1694                         );
1695                 frame += reel->duration();
1696         }
1697
1698         verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1699
1700         if (dcp->standard() == Standard::SMPTE) {
1701                 if (auto msc = cpl->main_sound_configuration()) {
1702                         if (state.audio_channels && msc->channels() != *state.audio_channels) {
1703                                 notes.push_back({
1704                                                 VerificationNote::Type::ERROR,
1705                                                 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1706                                                 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1707                                                 cpl->file().get()
1708                                         });
1709                         }
1710                 }
1711
1712                 if (have_main_subtitle && have_no_main_subtitle) {
1713                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1714                 }
1715
1716                 if (fewest_closed_captions != most_closed_captions) {
1717                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1718                 }
1719
1720                 if (cpl->content_kind() == ContentKind::FEATURE) {
1721                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1722                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1723                         }
1724                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1725                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1726                         }
1727                 }
1728
1729                 auto ffoc = markers_seen.find(Marker::FFOC);
1730                 if (ffoc == markers_seen.end()) {
1731                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1732                 } else if (ffoc->second.e != 1) {
1733                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1734                 }
1735
1736                 auto lfoc = markers_seen.find(Marker::LFOC);
1737                 if (lfoc == markers_seen.end()) {
1738                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1739                 } else {
1740                         auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1741                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1742                                 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1743                         }
1744                 }
1745
1746                 LinesCharactersResult result;
1747                 for (auto reel: cpl->reels()) {
1748                         if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1749                                 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1750                         }
1751                 }
1752
1753                 if (result.line_count_exceeded) {
1754                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1755                 }
1756                 if (result.error_length_exceeded) {
1757                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1758                 } else if (result.warning_length_exceeded) {
1759                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1760                 }
1761
1762                 result = LinesCharactersResult();
1763                 for (auto reel: cpl->reels()) {
1764                         for (auto i: reel->closed_captions()) {
1765                                 if (i->asset()) {
1766                                         verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1767                                 }
1768                         }
1769                 }
1770
1771                 if (result.line_count_exceeded) {
1772                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1773                 }
1774                 if (result.error_length_exceeded) {
1775                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1776                 }
1777
1778                 if (!cpl->read_composition_metadata()) {
1779                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1780                 } else if (!cpl->version_number()) {
1781                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1782                 }
1783
1784                 verify_extension_metadata(cpl, notes);
1785
1786                 if (cpl->any_encrypted()) {
1787                         cxml::Document doc("CompositionPlaylist");
1788                         DCP_ASSERT(cpl->file());
1789                         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1790                         if (!doc.optional_node_child("Signature")) {
1791                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1792                         }
1793                 }
1794         }
1795 }
1796
1797
1798 static
1799 void
1800 verify_pkl(
1801         shared_ptr<const DCP> dcp,
1802         shared_ptr<const PKL> pkl,
1803         boost::filesystem::path xsd_dtd_directory,
1804         vector<VerificationNote>& notes
1805         )
1806 {
1807         validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1808
1809         if (pkl_has_encrypted_assets(dcp, pkl)) {
1810                 cxml::Document doc("PackingList");
1811                 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1812                 if (!doc.optional_node_child("Signature")) {
1813                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1814                 }
1815         }
1816
1817         set<string> uuid_set;
1818         for (auto asset: pkl->assets()) {
1819                 if (!uuid_set.insert(asset->id()).second) {
1820                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1821                         break;
1822                 }
1823         }
1824 }
1825
1826
1827
1828 static
1829 void
1830 verify_assetmap(
1831         shared_ptr<const DCP> dcp,
1832         boost::filesystem::path xsd_dtd_directory,
1833         vector<VerificationNote>& notes
1834         )
1835 {
1836         auto asset_map = dcp->asset_map();
1837         DCP_ASSERT(asset_map);
1838
1839         validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1840
1841         set<string> uuid_set;
1842         for (auto const& asset: asset_map->assets()) {
1843                 if (!uuid_set.insert(asset.id()).second) {
1844                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1845                         break;
1846                 }
1847         }
1848 }
1849
1850
1851 vector<VerificationNote>
1852 dcp::verify (
1853         vector<boost::filesystem::path> directories,
1854         vector<dcp::DecryptedKDM> kdms,
1855         function<void (string, optional<boost::filesystem::path>)> stage,
1856         function<void (float)> progress,
1857         VerificationOptions options,
1858         optional<boost::filesystem::path> xsd_dtd_directory
1859         )
1860 {
1861         if (!xsd_dtd_directory) {
1862                 xsd_dtd_directory = resources_directory() / "xsd";
1863         }
1864         *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1865
1866         vector<VerificationNote> notes;
1867         State state{};
1868
1869         vector<shared_ptr<DCP>> dcps;
1870         for (auto i: directories) {
1871                 dcps.push_back (make_shared<DCP>(i));
1872         }
1873
1874         for (auto dcp: dcps) {
1875                 stage ("Checking DCP", dcp->directory());
1876                 bool carry_on = true;
1877                 try {
1878                         dcp->read (&notes, true);
1879                 } catch (MissingAssetmapError& e) {
1880                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1881                         carry_on = false;
1882                 } catch (ReadError& e) {
1883                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1884                 } catch (XMLError& e) {
1885                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1886                 } catch (MXFFileError& e) {
1887                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1888                 } catch (BadURNUUIDError& e) {
1889                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1890                 } catch (cxml::Error& e) {
1891                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1892                 }
1893
1894                 if (!carry_on) {
1895                         continue;
1896                 }
1897
1898                 if (dcp->standard() != Standard::SMPTE) {
1899                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1900                 }
1901
1902                 for (auto kdm: kdms) {
1903                         dcp->add(kdm);
1904                 }
1905
1906                 for (auto cpl: dcp->cpls()) {
1907                         try {
1908                                 verify_cpl(
1909                                         dcp,
1910                                         cpl,
1911                                         stage,
1912                                         *xsd_dtd_directory,
1913                                         progress,
1914                                         options,
1915                                         notes,
1916                                         state
1917                                         );
1918                         } catch (ReadError& e) {
1919                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1920                         }
1921                 }
1922
1923                 for (auto pkl: dcp->pkls()) {
1924                         stage("Checking PKL", pkl->file());
1925                         verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1926                 }
1927
1928                 if (dcp->asset_map_file()) {
1929                         stage("Checking ASSETMAP", dcp->asset_map_file().get());
1930                         verify_assetmap(dcp, *xsd_dtd_directory, notes);
1931                 } else {
1932                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1933                 }
1934         }
1935
1936         return notes;
1937 }
1938
1939
1940 string
1941 dcp::note_to_string (VerificationNote note)
1942 {
1943         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1944          *
1945          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1946          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1947          *
1948          *  It's OK to use XML tag names where they are clear.
1949          *  If both ID and filename are available, use only the ID.
1950          *  End messages with a full stop.
1951          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1952          */
1953         switch (note.code()) {
1954         case VerificationNote::Code::FAILED_READ:
1955                 return *note.note();
1956         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1957                 return String::compose("The hash (%1) of the CPL (%2) in the PKL does not agree with the CPL file (%3).", note.reference_hash().get(), note.note().get(), note.calculated_hash().get());
1958         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1959                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1960         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1961                 return String::compose("The hash (%1) of the picture asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
1962         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1963                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1964         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1965                 return String::compose("The hash (%1) of the sound asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
1966         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1967                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1968         case VerificationNote::Code::EMPTY_ASSET_PATH:
1969                 return "The asset map contains an empty asset path.";
1970         case VerificationNote::Code::MISSING_ASSET:
1971                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1972         case VerificationNote::Code::MISMATCHED_STANDARD:
1973                 return "The DCP contains both SMPTE and Interop parts.";
1974         case VerificationNote::Code::INVALID_XML:
1975                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1976         case VerificationNote::Code::MISSING_ASSETMAP:
1977                 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1978         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1979                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1980         case VerificationNote::Code::INVALID_DURATION:
1981                 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1982         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1983                 return String::compose(
1984                         "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1985                         note.frame().get(),
1986                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1987                         note.file()->filename()
1988                         );
1989         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1990                 return String::compose(
1991                         "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1992                         note.frame().get(),
1993                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1994                         note.file()->filename()
1995                         );
1996         case VerificationNote::Code::EXTERNAL_ASSET:
1997                 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());
1998         case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1999                 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
2000         case VerificationNote::Code::INVALID_STANDARD:
2001                 return "This DCP does not use the SMPTE standard.";
2002         case VerificationNote::Code::INVALID_LANGUAGE:
2003                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
2004         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
2005                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
2006         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
2007                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
2008         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
2009                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
2010         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
2011                 return "3D 4K DCPs are not allowed.";
2012         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
2013                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
2014         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
2015                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
2016         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
2017                 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());
2018         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
2019                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
2020         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
2021                 return "Some subtitle assets have different <Language> tags than others";
2022         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
2023                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2024         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2025                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2026         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2027                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2028         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2029                 return "At least one subtitle lasts less than 15 frames.";
2030         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2031                 return "At least one pair of subtitles is separated by less than 2 frames.";
2032         case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2033                 return "At least one subtitle extends outside of its reel.";
2034         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2035                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2036         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2037                 return "There are more than 52 characters in at least one subtitle line.";
2038         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2039                 return "There are more than 79 characters in at least one subtitle line.";
2040         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2041                 return "There are more than 3 closed caption lines in at least one place.";
2042         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2043                 return "There are more than 32 characters in at least one closed caption line.";
2044         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2045                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2046         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2047                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
2048         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2049                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
2050         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2051                 return "All assets in a reel do not have the same duration.";
2052         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2053                 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2054         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2055                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2056         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2057                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2058         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2059                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2060         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2061                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2062         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2063                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2064         case VerificationNote::Code::MISSING_HASH:
2065                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2066         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2067                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2068         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2069                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2070         case VerificationNote::Code::MISSING_FFOC:
2071                 return "There should be a FFOC (first frame of content) marker.";
2072         case VerificationNote::Code::MISSING_LFOC:
2073                 return "There should be a LFOC (last frame of content) marker.";
2074         case VerificationNote::Code::INCORRECT_FFOC:
2075                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2076         case VerificationNote::Code::INCORRECT_LFOC:
2077                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2078         case VerificationNote::Code::MISSING_CPL_METADATA:
2079                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2080         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2081                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2082         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2083                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2084         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2085                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2086         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2087                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2088         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2089                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2090         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2091                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2092         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2093                 return "Some assets are encrypted but some are not.";
2094         case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2095                 return String::compose(
2096                         "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2097                         note.frame().get(),
2098                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2099                         note.note().get()
2100                         );
2101         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2102                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2103         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2104                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2105         case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2106                 return "The JPEG2000 tile size is not the same as the image size.";
2107         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2108                 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2109         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2110                 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2111         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2112                 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2113         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2114                 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2115         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2116                 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2117         case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2118                 return "POC marker found outside main header.";
2119         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2120                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2121         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2122                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2123         case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2124                 return "No TLM marker was found in a JPEG2000 codestream.";
2125         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2126                 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2127         case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2128                 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2129         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2130         {
2131                 vector<string> parts;
2132                 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2133                 DCP_ASSERT (parts.size() == 2);
2134                 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]);
2135         }
2136         case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2137                 return "Some aspect of this DCP could not be checked because it is encrypted.";
2138         case VerificationNote::Code::EMPTY_TEXT:
2139                 return "There is an empty <Text> node in a subtitle or closed caption.";
2140         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2141                 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2142         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2143                 return "Some closed captions are not listed in the order of their vertical position.";
2144         case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2145                 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2146         case VerificationNote::Code::UNEXPECTED_DURATION:
2147                 return "There is an <Duration> node inside a <MainMarkers>.";
2148         case VerificationNote::Code::INVALID_CONTENT_KIND:
2149                 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2150         case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2151                 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2152         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2153                 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2154         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2155                 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2156         case VerificationNote::Code::MISSING_SUBTITLE:
2157                 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2158         case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2159                 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2160         case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2161                 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2162         case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2163                 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2164         case VerificationNote::Code::MISSING_FONT:
2165                 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2166         case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2167                 return String::compose(
2168                         "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2169                         note.frame().get(), note.component().get(), note.size().get()
2170                         );
2171         case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2172                 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2173         case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2174                 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());
2175         case VerificationNote::Code::MISSING_LOAD_FONT:
2176                 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2177         case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2178                 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());
2179         case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2180                 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2181         case VerificationNote::Code::INVALID_CPL_NAMESPACE:
2182                 return String::compose("The namespace %1 in CPL %2 is invalid", note.note().get(), note.file()->filename());
2183         case VerificationNote::Code::MISSING_CPL_CONTENT_VERSION:
2184                 return String::compose("The CPL %1 has no <ContentVersion> tag", note.note().get());
2185         }
2186
2187         return "";
2188 }
2189
2190
2191 bool
2192 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2193 {
2194         return a.type() == b.type() &&
2195                 a.code() == b.code() &&
2196                 a.note() == b.note() &&
2197                 a.file() == b.file() &&
2198                 a.line() == b.line() &&
2199                 a.frame() == b.frame() &&
2200                 a.component() == b.component() &&
2201                 a.size() == b.size() &&
2202                 a.id() == b.id() &&
2203                 a.other_id() == b.other_id() &&
2204                 a.frame_rate() == b.frame_rate() &&
2205                 a.reference_hash() == b.reference_hash() &&
2206                 a.calculated_hash() == b.calculated_hash();
2207 }
2208
2209
2210 bool
2211 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2212 {
2213         if (a.type() != b.type()) {
2214                 return a.type() < b.type();
2215         }
2216
2217         if (a.code() != b.code()) {
2218                 return a.code() < b.code();
2219         }
2220
2221         if (a.note() != b.note()) {
2222                 return a.note().get_value_or("") < b.note().get_value_or("");
2223         }
2224
2225         if (a.file() != b.file()) {
2226                 return a.file().get_value_or("") < b.file().get_value_or("");
2227         }
2228
2229         if (a.line() != b.line()) {
2230                 return a.line().get_value_or(0) < b.line().get_value_or(0);
2231         }
2232
2233         if (a.frame() != b.frame()) {
2234                 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2235         }
2236
2237         if (a.component() != b.component()) {
2238                 return a.component().get_value_or(0) < b.component().get_value_or(0);
2239         }
2240
2241         if (a.size() != b.size()) {
2242                 return a.size().get_value_or(0) < b.size().get_value_or(0);
2243         }
2244
2245         if (a.id() != b.id()) {
2246                 return a.id().get_value_or("") < b.id().get_value_or("");
2247         }
2248
2249         if (a.other_id() != b.other_id()) {
2250                 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2251         }
2252
2253         return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2254 }
2255
2256
2257 std::ostream&
2258 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2259 {
2260         s << note_to_string (note);
2261         if (note.note()) {
2262                 s << " [" << note.note().get() << "]";
2263         }
2264         if (note.file()) {
2265                 s << " [" << note.file().get() << "]";
2266         }
2267         if (note.line()) {
2268                 s << " [" << note.line().get() << "]";
2269         }
2270         return s;
2271 }
2272