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