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