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