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