40f7c0b691cf8290ab549cb4d1e885c39745c146
[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         if (asset->resource_id().get() != asset->xml_id()) {
707                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
708         }
709
710         if (asset->id() == asset->resource_id().get() || asset->id() == asset->xml_id()) {
711                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
712         }
713 }
714
715
716 /** Verify all subtitle stuff */
717 static void
718 verify_subtitle_asset (
719         shared_ptr<const SubtitleAsset> asset,
720         optional<int64_t> reel_asset_duration,
721         function<void (string, optional<boost::filesystem::path>)> stage,
722         boost::filesystem::path xsd_dtd_directory,
723         vector<VerificationNote>& notes,
724         State& state
725         )
726 {
727         stage ("Checking subtitle XML", asset->file());
728         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
729          * gets passed through libdcp which may clean up and therefore hide errors.
730          */
731         if (asset->raw_xml()) {
732                 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
733         } else {
734                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
735         }
736
737         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
738         if (smpte) {
739                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
740                 verify_smpte_subtitle_asset (smpte, notes, state);
741         }
742 }
743
744
745 /** Verify all closed caption stuff */
746 static void
747 verify_closed_caption_asset (
748         shared_ptr<const SubtitleAsset> asset,
749         optional<int64_t> reel_asset_duration,
750         function<void (string, optional<boost::filesystem::path>)> stage,
751         boost::filesystem::path xsd_dtd_directory,
752         vector<VerificationNote>& notes
753         )
754 {
755         stage ("Checking closed caption XML", asset->file());
756         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
757          * gets passed through libdcp which may clean up and therefore hide errors.
758          */
759         auto raw_xml = asset->raw_xml();
760         if (raw_xml) {
761                 validate_xml (*raw_xml, xsd_dtd_directory, notes);
762                 if (raw_xml->size() > 256 * 1024) {
763                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
764                 }
765         } else {
766                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
767         }
768
769         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
770         if (smpte) {
771                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
772         }
773 }
774
775
776 static
777 void
778 verify_text_timing (
779         vector<shared_ptr<Reel>> reels,
780         int edit_rate,
781         vector<VerificationNote>& notes,
782         std::function<bool (shared_ptr<Reel>)> check,
783         std::function<optional<string> (shared_ptr<Reel>)> xml,
784         std::function<int64_t (shared_ptr<Reel>)> duration
785         )
786 {
787         /* end of last subtitle (in editable units) */
788         optional<int64_t> last_out;
789         auto too_short = false;
790         auto too_close = false;
791         auto too_early = false;
792         auto reel_overlap = false;
793         /* current reel start time (in editable units) */
794         int64_t reel_offset = 0;
795
796         std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
797         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) {
798                 if (node->name() == "Subtitle") {
799                         Time in (node->string_attribute("TimeIn"), tcr);
800                         if (start_time) {
801                                 in -= *start_time;
802                         }
803                         Time out (node->string_attribute("TimeOut"), tcr);
804                         if (start_time) {
805                                 out -= *start_time;
806                         }
807                         if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
808                                 too_early = true;
809                         }
810                         auto length = out - in;
811                         if (length.as_editable_units_ceil(er) < 15) {
812                                 too_short = true;
813                         }
814                         if (last_out) {
815                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
816                                 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
817                                 if (distance >= 0 && distance < 2) {
818                                         too_close = true;
819                                 }
820                         }
821                         last_out = reel_offset + out.as_editable_units_floor(er);
822                 } else {
823                         for (auto i: node->node_children()) {
824                                 parse(i, tcr, start_time, er, first_reel);
825                         }
826                 }
827         };
828
829         for (auto i = 0U; i < reels.size(); ++i) {
830                 if (!check(reels[i])) {
831                         continue;
832                 }
833
834                 auto reel_xml = xml(reels[i]);
835                 if (!reel_xml) {
836                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
837                         continue;
838                 }
839
840                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
841                  * read in by libdcp's parser.
842                  */
843
844                 shared_ptr<cxml::Document> doc;
845                 optional<int> tcr;
846                 optional<Time> start_time;
847                 try {
848                         doc = make_shared<cxml::Document>("SubtitleReel");
849                         doc->read_string (*reel_xml);
850                         tcr = doc->number_child<int>("TimeCodeRate");
851                         auto start_time_string = doc->optional_string_child("StartTime");
852                         if (start_time_string) {
853                                 start_time = Time(*start_time_string, tcr);
854                         }
855                 } catch (...) {
856                         doc = make_shared<cxml::Document>("DCSubtitle");
857                         doc->read_string (*reel_xml);
858                 }
859                 parse (doc, tcr, start_time, edit_rate, i == 0);
860                 auto end = reel_offset + duration(reels[i]);
861                 if (last_out && *last_out > end) {
862                         reel_overlap = true;
863                 }
864                 reel_offset = end;
865         }
866
867         if (last_out && *last_out > reel_offset) {
868                 reel_overlap = true;
869         }
870
871         if (too_early) {
872                 notes.push_back({
873                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
874                 });
875         }
876
877         if (too_short) {
878                 notes.push_back ({
879                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
880                 });
881         }
882
883         if (too_close) {
884                 notes.push_back ({
885                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
886                 });
887         }
888
889         if (reel_overlap) {
890                 notes.push_back ({
891                         VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
892                 });
893         }
894 }
895
896
897 struct LinesCharactersResult
898 {
899         bool warning_length_exceeded = false;
900         bool error_length_exceeded = false;
901         bool line_count_exceeded = false;
902 };
903
904
905 static
906 void
907 verify_text_lines_and_characters (
908         shared_ptr<SubtitleAsset> asset,
909         int warning_length,
910         int error_length,
911         LinesCharactersResult* result
912         )
913 {
914         class Event
915         {
916         public:
917                 Event (Time time_, float position_, int characters_)
918                         : time (time_)
919                         , position (position_)
920                         , characters (characters_)
921                 {}
922
923                 Event (Time time_, shared_ptr<Event> start_)
924                         : time (time_)
925                         , start (start_)
926                 {}
927
928                 Time time;
929                 int position; //< position from 0 at top of screen to 100 at bottom
930                 int characters;
931                 shared_ptr<Event> start;
932         };
933
934         vector<shared_ptr<Event>> events;
935
936         auto position = [](shared_ptr<const SubtitleString> sub) {
937                 switch (sub->v_align()) {
938                 case VAlign::TOP:
939                         return lrintf(sub->v_position() * 100);
940                 case VAlign::CENTER:
941                         return lrintf((0.5f + sub->v_position()) * 100);
942                 case VAlign::BOTTOM:
943                         return lrintf((1.0f - sub->v_position()) * 100);
944                 }
945
946                 return 0L;
947         };
948
949         for (auto j: asset->subtitles()) {
950                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
951                 if (text) {
952                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
953                         events.push_back(in);
954                         events.push_back(make_shared<Event>(text->out(), in));
955                 }
956         }
957
958         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
959                 return a->time < b->time;
960         });
961
962         map<int, int> current;
963         for (auto i: events) {
964                 if (current.size() > 3) {
965                         result->line_count_exceeded = true;
966                 }
967                 for (auto j: current) {
968                         if (j.second >= warning_length) {
969                                 result->warning_length_exceeded = true;
970                         }
971                         if (j.second >= error_length) {
972                                 result->error_length_exceeded = true;
973                         }
974                 }
975
976                 if (i->start) {
977                         /* end of a subtitle */
978                         DCP_ASSERT (current.find(i->start->position) != current.end());
979                         if (current[i->start->position] == i->start->characters) {
980                                 current.erase(i->start->position);
981                         } else {
982                                 current[i->start->position] -= i->start->characters;
983                         }
984                 } else {
985                         /* start of a subtitle */
986                         if (current.find(i->position) == current.end()) {
987                                 current[i->position] = i->characters;
988                         } else {
989                                 current[i->position] += i->characters;
990                         }
991                 }
992         }
993 }
994
995
996 static
997 void
998 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
999 {
1000         if (reels.empty()) {
1001                 return;
1002         }
1003
1004         if (reels[0]->main_subtitle()) {
1005                 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1006                         [](shared_ptr<Reel> reel) {
1007                                 return static_cast<bool>(reel->main_subtitle());
1008                         },
1009                         [](shared_ptr<Reel> reel) {
1010                                 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1011                                 if (interop) {
1012                                         return interop->asset()->raw_xml();
1013                                 }
1014                                 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1015                                 DCP_ASSERT (smpte);
1016                                 return smpte->asset()->raw_xml();
1017                         },
1018                         [](shared_ptr<Reel> reel) {
1019                                 return reel->main_subtitle()->actual_duration();
1020                         }
1021                 );
1022         }
1023
1024         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1025                 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1026                         [i](shared_ptr<Reel> reel) {
1027                                 return i < reel->closed_captions().size();
1028                         },
1029                         [i](shared_ptr<Reel> reel) {
1030                                 return reel->closed_captions()[i]->asset()->raw_xml();
1031                         },
1032                         [i](shared_ptr<Reel> reel) {
1033                                 return reel->closed_captions()[i]->actual_duration();
1034                         }
1035                 );
1036         }
1037 }
1038
1039
1040 void
1041 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1042 {
1043         DCP_ASSERT (cpl->file());
1044         cxml::Document doc ("CompositionPlaylist");
1045         doc.read_file (cpl->file().get());
1046
1047         auto missing = false;
1048         string malformed;
1049
1050         if (auto reel_list = doc.node_child("ReelList")) {
1051                 auto reels = reel_list->node_children("Reel");
1052                 if (!reels.empty()) {
1053                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1054                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1055                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1056                                                 missing = true;
1057                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1058                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1059                                                                 continue;
1060                                                         }
1061                                                         missing = false;
1062                                                         if (auto name = extension->optional_node_child("Name")) {
1063                                                                 if (name->content() != "Application") {
1064                                                                         malformed = "<Name> should be 'Application'";
1065                                                                 }
1066                                                         }
1067                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
1068                                                                 if (auto property = property_list->optional_node_child("Property")) {
1069                                                                         if (auto name = property->optional_node_child("Name")) {
1070                                                                                 if (name->content() != "DCP Constraints Profile") {
1071                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
1072                                                                                 }
1073                                                                         }
1074                                                                         if (auto value = property->optional_node_child("Value")) {
1075                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1076                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1077                                                                                 }
1078                                                                         }
1079                                                                 }
1080                                                         }
1081                                                 }
1082                                         } else {
1083                                                 missing = true;
1084                                         }
1085                                 }
1086                         }
1087                 }
1088         }
1089
1090         if (missing) {
1091                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1092         } else if (!malformed.empty()) {
1093                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1094         }
1095 }
1096
1097
1098 bool
1099 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1100 {
1101         vector<string> encrypted;
1102         for (auto i: dcp->cpls()) {
1103                 for (auto j: i->reel_file_assets()) {
1104                         if (j->asset_ref().resolved()) {
1105                                 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1106                                 if (mxf && mxf->encrypted()) {
1107                                         encrypted.push_back(j->asset_ref().id());
1108                                 }
1109                         }
1110                 }
1111         }
1112
1113         for (auto i: pkl->asset_list()) {
1114                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1115                         return true;
1116                 }
1117         }
1118
1119         return false;
1120 }
1121
1122
1123 vector<VerificationNote>
1124 dcp::verify (
1125         vector<boost::filesystem::path> directories,
1126         function<void (string, optional<boost::filesystem::path>)> stage,
1127         function<void (float)> progress,
1128         optional<boost::filesystem::path> xsd_dtd_directory
1129         )
1130 {
1131         if (!xsd_dtd_directory) {
1132                 xsd_dtd_directory = resources_directory() / "xsd";
1133         }
1134         *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1135
1136         vector<VerificationNote> notes;
1137         State state{};
1138
1139         vector<shared_ptr<DCP>> dcps;
1140         for (auto i: directories) {
1141                 dcps.push_back (make_shared<DCP>(i));
1142         }
1143
1144         for (auto dcp: dcps) {
1145                 stage ("Checking DCP", dcp->directory());
1146                 bool carry_on = true;
1147                 try {
1148                         dcp->read (&notes, true);
1149                 } catch (MissingAssetmapError& e) {
1150                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1151                         carry_on = false;
1152                 } catch (ReadError& e) {
1153                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1154                 } catch (XMLError& e) {
1155                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1156                 } catch (MXFFileError& e) {
1157                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1158                 } catch (cxml::Error& e) {
1159                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1160                 }
1161
1162                 if (!carry_on) {
1163                         continue;
1164                 }
1165
1166                 if (dcp->standard() != Standard::SMPTE) {
1167                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1168                 }
1169
1170                 for (auto cpl: dcp->cpls()) {
1171                         stage ("Checking CPL", cpl->file());
1172                         validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1173
1174                         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1175                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1176                         }
1177
1178                         for (auto const& i: cpl->additional_subtitle_languages()) {
1179                                 verify_language_tag (i, notes);
1180                         }
1181
1182                         if (cpl->release_territory()) {
1183                                 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") {
1184                                         auto terr = cpl->release_territory().get();
1185                                         /* Must be a valid region tag, or "001" */
1186                                         try {
1187                                                 LanguageTag::RegionSubtag test (terr);
1188                                         } catch (...) {
1189                                                 if (terr != "001") {
1190                                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1191                                                 }
1192                                         }
1193                                 }
1194                         }
1195
1196                         if (dcp->standard() == Standard::SMPTE) {
1197                                 if (!cpl->annotation_text()) {
1198                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1199                                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1200                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1201                                 }
1202                         }
1203
1204                         for (auto i: dcp->pkls()) {
1205                                 /* Check that the CPL's hash corresponds to the PKL */
1206                                 optional<string> h = i->hash(cpl->id());
1207                                 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1208                                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1209                                 }
1210
1211                                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1212                                 optional<string> required_annotation_text;
1213                                 for (auto j: i->asset_list()) {
1214                                         /* See if this is a CPL */
1215                                         for (auto k: dcp->cpls()) {
1216                                                 if (j->id() == k->id()) {
1217                                                         if (!required_annotation_text) {
1218                                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1219                                                                 required_annotation_text = cpl->content_title_text();
1220                                                         } else {
1221                                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1222                                                                 required_annotation_text = boost::none;
1223                                                         }
1224                                                 }
1225                                         }
1226                                 }
1227
1228                                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1229                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1230                                 }
1231                         }
1232
1233                         /* set to true if any reel has a MainSubtitle */
1234                         auto have_main_subtitle = false;
1235                         /* set to true if any reel has no MainSubtitle */
1236                         auto have_no_main_subtitle = false;
1237                         /* fewest number of closed caption assets seen in a reel */
1238                         size_t fewest_closed_captions = SIZE_MAX;
1239                         /* most number of closed caption assets seen in a reel */
1240                         size_t most_closed_captions = 0;
1241                         map<Marker, Time> markers_seen;
1242
1243                         for (auto reel: cpl->reels()) {
1244                                 stage ("Checking reel", optional<boost::filesystem::path>());
1245
1246                                 for (auto i: reel->assets()) {
1247                                         if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1248                                                 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1249                                         }
1250                                         if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1251                                                 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1252                                         }
1253                                         auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1254                                         if (i->encryptable() && !file_asset->hash()) {
1255                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1256                                         }
1257                                 }
1258
1259                                 if (dcp->standard() == Standard::SMPTE) {
1260                                         boost::optional<int64_t> duration;
1261                                         for (auto i: reel->assets()) {
1262                                                 if (!duration) {
1263                                                         duration = i->actual_duration();
1264                                                 } else if (*duration != i->actual_duration()) {
1265                                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1266                                                         break;
1267                                                 }
1268                                         }
1269                                 }
1270
1271                                 if (reel->main_picture()) {
1272                                         /* Check reel stuff */
1273                                         auto const frame_rate = reel->main_picture()->frame_rate();
1274                                         if (frame_rate.denominator != 1 ||
1275                                             (frame_rate.numerator != 24 &&
1276                                              frame_rate.numerator != 25 &&
1277                                              frame_rate.numerator != 30 &&
1278                                              frame_rate.numerator != 48 &&
1279                                              frame_rate.numerator != 50 &&
1280                                              frame_rate.numerator != 60 &&
1281                                              frame_rate.numerator != 96)) {
1282                                                 notes.push_back ({
1283                                                         VerificationNote::Type::ERROR,
1284                                                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1285                                                         String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1286                                                 });
1287                                         }
1288                                         /* Check asset */
1289                                         if (reel->main_picture()->asset_ref().resolved()) {
1290                                                 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1291                                         }
1292                                 }
1293
1294                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1295                                         verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1296                                 }
1297
1298                                 if (reel->main_subtitle()) {
1299                                         verify_main_subtitle_reel (reel->main_subtitle(), notes);
1300                                         if (reel->main_subtitle()->asset_ref().resolved()) {
1301                                                 verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1302                                         }
1303                                         have_main_subtitle = true;
1304                                 } else {
1305                                         have_no_main_subtitle = true;
1306                                 }
1307
1308                                 for (auto i: reel->closed_captions()) {
1309                                         verify_closed_caption_reel (i, notes);
1310                                         if (i->asset_ref().resolved()) {
1311                                                 verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1312                                         }
1313                                 }
1314
1315                                 if (reel->main_markers()) {
1316                                         for (auto const& i: reel->main_markers()->get()) {
1317                                                 markers_seen.insert (i);
1318                                         }
1319                                 }
1320
1321                                 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1322                                 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1323                         }
1324
1325                         verify_text_timing (cpl->reels(), notes);
1326
1327                         if (dcp->standard() == Standard::SMPTE) {
1328
1329                                 if (have_main_subtitle && have_no_main_subtitle) {
1330                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1331                                 }
1332
1333                                 if (fewest_closed_captions != most_closed_captions) {
1334                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1335                                 }
1336
1337                                 if (cpl->content_kind() == ContentKind::FEATURE) {
1338                                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1339                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1340                                         }
1341                                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1342                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1343                                         }
1344                                 }
1345
1346                                 auto ffoc = markers_seen.find(Marker::FFOC);
1347                                 if (ffoc == markers_seen.end()) {
1348                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1349                                 } else if (ffoc->second.e != 1) {
1350                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1351                                 }
1352
1353                                 auto lfoc = markers_seen.find(Marker::LFOC);
1354                                 if (lfoc == markers_seen.end()) {
1355                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1356                                 } else {
1357                                         auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1358                                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1359                                                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1360                                         }
1361                                 }
1362
1363                                 LinesCharactersResult result;
1364                                 for (auto reel: cpl->reels()) {
1365                                         if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1366                                                 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1367                                         }
1368                                 }
1369
1370                                 if (result.line_count_exceeded) {
1371                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1372                                 }
1373                                 if (result.error_length_exceeded) {
1374                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1375                                 } else if (result.warning_length_exceeded) {
1376                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1377                                 }
1378
1379                                 result = LinesCharactersResult();
1380                                 for (auto reel: cpl->reels()) {
1381                                         for (auto i: reel->closed_captions()) {
1382                                                 if (i->asset()) {
1383                                                         verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1384                                                 }
1385                                         }
1386                                 }
1387
1388                                 if (result.line_count_exceeded) {
1389                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1390                                 }
1391                                 if (result.error_length_exceeded) {
1392                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1393                                 }
1394
1395                                 if (!cpl->full_content_title_text()) {
1396                                         /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1397                                          * can use it as a proxy for CompositionMetadataAsset's existence.
1398                                          */
1399                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1400                                 } else if (!cpl->version_number()) {
1401                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1402                                 }
1403
1404                                 verify_extension_metadata (cpl, notes);
1405
1406                                 if (cpl->any_encrypted()) {
1407                                         cxml::Document doc ("CompositionPlaylist");
1408                                         DCP_ASSERT (cpl->file());
1409                                         doc.read_file (cpl->file().get());
1410                                         if (!doc.optional_node_child("Signature")) {
1411                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1412                                         }
1413                                 }
1414                         }
1415                 }
1416
1417                 for (auto pkl: dcp->pkls()) {
1418                         stage ("Checking PKL", pkl->file());
1419                         validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1420                         if (pkl_has_encrypted_assets(dcp, pkl)) {
1421                                 cxml::Document doc ("PackingList");
1422                                 doc.read_file (pkl->file().get());
1423                                 if (!doc.optional_node_child("Signature")) {
1424                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1425                                 }
1426                         }
1427                 }
1428
1429                 if (dcp->asset_map_path()) {
1430                         stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1431                         validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1432                 } else {
1433                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1434                 }
1435         }
1436
1437         return notes;
1438 }
1439
1440
1441 string
1442 dcp::note_to_string (VerificationNote note)
1443 {
1444         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1445          *
1446          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1447          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1448          *
1449          *  It's OK to use XML tag names where they are clear.
1450          *  If both ID and filename are available, use only the ID.
1451          *  End messages with a full stop.
1452          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1453          */
1454         switch (note.code()) {
1455         case VerificationNote::Code::FAILED_READ:
1456                 return *note.note();
1457         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1458                 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1459         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1460                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1461         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1462                 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1463         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1464                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1465         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1466                 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1467         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1468                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1469         case VerificationNote::Code::EMPTY_ASSET_PATH:
1470                 return "The asset map contains an empty asset path.";
1471         case VerificationNote::Code::MISSING_ASSET:
1472                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1473         case VerificationNote::Code::MISMATCHED_STANDARD:
1474                 return "The DCP contains both SMPTE and Interop parts.";
1475         case VerificationNote::Code::INVALID_XML:
1476                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1477         case VerificationNote::Code::MISSING_ASSETMAP:
1478                 return "No ASSETMAP or ASSETMAP.xml was found.";
1479         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1480                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1481         case VerificationNote::Code::INVALID_DURATION:
1482                 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1483         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1484                 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());
1485         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1486                 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());
1487         case VerificationNote::Code::EXTERNAL_ASSET:
1488                 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());
1489         case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1490                 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1491         case VerificationNote::Code::INVALID_STANDARD:
1492                 return "This DCP does not use the SMPTE standard.";
1493         case VerificationNote::Code::INVALID_LANGUAGE:
1494                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1495         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1496                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1497         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1498                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1499         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1500                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1501         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1502                 return "3D 4K DCPs are not allowed.";
1503         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1504                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1505         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1506                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1507         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1508                 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());
1509         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1510                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1511         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1512                 return "Some subtitle assets have different <Language> tags than others";
1513         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1514                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1515         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1516                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1517         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1518                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1519         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1520                 return "At least one subtitle lasts less than 15 frames.";
1521         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1522                 return "At least one pair of subtitles is separated by less than 2 frames.";
1523         case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1524                 return "At least one subtitle extends outside of its reel.";
1525         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1526                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1527         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1528                 return "There are more than 52 characters in at least one subtitle line.";
1529         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1530                 return "There are more than 79 characters in at least one subtitle line.";
1531         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1532                 return "There are more than 3 closed caption lines in at least one place.";
1533         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1534                 return "There are more than 32 characters in at least one closed caption line.";
1535         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1536                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1537         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1538                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1539         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1540                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1541         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1542                 return "All assets in a reel do not have the same duration.";
1543         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1544                 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1545         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1546                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1547         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1548                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1549         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1550                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1551         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1552                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1553         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1554                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1555         case VerificationNote::Code::MISSING_HASH:
1556                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1557         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1558                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1559         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1560                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1561         case VerificationNote::Code::MISSING_FFOC:
1562                 return "There should be a FFOC (first frame of content) marker";
1563         case VerificationNote::Code::MISSING_LFOC:
1564                 return "There should be a LFOC (last frame of content) marker";
1565         case VerificationNote::Code::INCORRECT_FFOC:
1566                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1567         case VerificationNote::Code::INCORRECT_LFOC:
1568                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1569         case VerificationNote::Code::MISSING_CPL_METADATA:
1570                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1571         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1572                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1573         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1574                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1575         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1576                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1577         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1578                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1579         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1580                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1581         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1582                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1583         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1584                 return "Some assets are encrypted but some are not.";
1585         case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1586                 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1587         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1588                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1589         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1590                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1591         case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1592                 return "The JPEG2000 tile size is not the same as the image size.";
1593         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1594                 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1595         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1596                 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1597         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1598                 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1599         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1600                 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1601         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1602                 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1603         case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1604                 return "POC marker found outside main header";
1605         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1606                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1607         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1608                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1609         case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1610                 return "No TLM marker was found in a JPEG2000 codestream.";
1611         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1612                 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1613         case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1614                 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1615         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1616         {
1617                 vector<string> parts;
1618                 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1619                 DCP_ASSERT (parts.size() == 2);
1620                 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]);
1621         }
1622         case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1623                 return "Some aspect of this DCP could not be checked because it is encrypted.";
1624         }
1625
1626         return "";
1627 }
1628
1629
1630 bool
1631 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1632 {
1633         return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1634 }
1635
1636
1637 bool
1638 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1639 {
1640         if (a.type() != b.type()) {
1641                 return a.type() < b.type();
1642         }
1643
1644         if (a.code() != b.code()) {
1645                 return a.code() < b.code();
1646         }
1647
1648         if (a.note() != b.note()) {
1649                 return a.note().get_value_or("") < b.note().get_value_or("");
1650         }
1651
1652         if (a.file() != b.file()) {
1653                 return a.file().get_value_or("") < b.file().get_value_or("");
1654         }
1655
1656         return a.line().get_value_or(0) < b.line().get_value_or(0);
1657 }
1658
1659
1660 std::ostream&
1661 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1662 {
1663         s << note_to_string (note);
1664         if (note.note()) {
1665                 s << " [" << note.note().get() << "]";
1666         }
1667         if (note.file()) {
1668                 s << " [" << note.file().get() << "]";
1669         }
1670         if (note.line()) {
1671                 s << " [" << note.line().get() << "]";
1672         }
1673         return s;
1674 }
1675