Add OK note when content version label text is valid.
[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                                 context.ok(VerificationNote::Code::CORRECT_PICTURE_HASH, file);
623                                 break;
624                 }
625         }
626
627         context.stage("Checking picture frame sizes", asset->file());
628         verify_picture_asset(context, reel_asset, file, start_frame);
629
630         /* Only flat/scope allowed by Bv2.1 */
631         if (
632                 asset->size() != Size(2048, 858) &&
633                 asset->size() != Size(1998, 1080) &&
634                 asset->size() != Size(4096, 1716) &&
635                 asset->size() != Size(3996, 2160)) {
636                 context.bv21_error(VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS, String::compose("%1x%2", asset->size().width, asset->size().height), file);
637         }
638
639         /* Only 24, 25, 48fps allowed for 2K */
640         if (
641                 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
642                 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
643            ) {
644                 context.bv21_error(
645                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
646                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
647                         file
648                 );
649         }
650
651         if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
652                 /* Only 24fps allowed for 4K */
653                 if (asset->edit_rate() != Fraction(24, 1)) {
654                         context.bv21_error(
655                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
656                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
657                                 file
658                         );
659                 }
660
661                 /* Only 2D allowed for 4K */
662                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
663                         context.bv21_error(
664                                 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
665                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
666                                 file
667                         );
668
669                 }
670         }
671 }
672
673
674 static void
675 verify_main_sound_asset(Context& context, shared_ptr<const ReelSoundAsset> reel_asset)
676 {
677         auto asset = reel_asset->asset();
678         auto const file = *asset->file();
679
680         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)) {
681                 context.stage("Checking sound asset hash", file);
682                 string reference_hash;
683                 string calculated_hash;
684                 auto const r = verify_asset(context, reel_asset, &reference_hash, &calculated_hash);
685                 switch (r) {
686                         case VerifyAssetResult::BAD:
687                                 context.add_note(
688                                         dcp::VerificationNote(
689                                                 VerificationNote::Type::ERROR,
690                                                 VerificationNote::Code::INCORRECT_SOUND_HASH,
691                                                 file
692                                                 ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
693                                         );
694                                 break;
695                         case VerifyAssetResult::CPL_PKL_DIFFER:
696                                 context.error(VerificationNote::Code::MISMATCHED_SOUND_HASHES, file);
697                                 break;
698                         default:
699                                 break;
700                 }
701         }
702
703         if (!context.audio_channels) {
704                 context.audio_channels = asset->channels();
705         } else if (*context.audio_channels != asset->channels()) {
706                 context.error(VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file);
707         }
708
709         context.stage("Checking sound asset metadata", file);
710
711         if (auto lang = asset->language()) {
712                 verify_language_tag(context, *lang);
713         }
714         if (asset->sampling_rate() != 48000) {
715                 context.bv21_error(VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file);
716         }
717 }
718
719
720 static void
721 verify_main_subtitle_reel(Context& context, shared_ptr<const ReelSubtitleAsset> reel_asset)
722 {
723         /* XXX: is Language compulsory? */
724         if (reel_asset->language()) {
725                 verify_language_tag(context, *reel_asset->language());
726         }
727
728         if (!reel_asset->entry_point()) {
729                 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id());
730         } else if (reel_asset->entry_point().get()) {
731                 context.bv21_error(VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id());
732         }
733 }
734
735
736 static void
737 verify_closed_caption_reel(Context& context, shared_ptr<const ReelClosedCaptionAsset> reel_asset)
738 {
739         /* XXX: is Language compulsory? */
740         if (reel_asset->language()) {
741                 verify_language_tag(context, *reel_asset->language());
742         }
743
744         if (!reel_asset->entry_point()) {
745                 context.bv21_error(VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
746         } else if (reel_asset->entry_point().get()) {
747                 context.bv21_error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id());
748         }
749 }
750
751
752 /** Verify stuff that is common to both subtitles and closed captions */
753 void
754 verify_smpte_timed_text_asset (
755         Context& context,
756         shared_ptr<const SMPTESubtitleAsset> asset,
757         optional<int64_t> reel_asset_duration
758         )
759 {
760         if (asset->language()) {
761                 verify_language_tag(context, *asset->language());
762         } else {
763                 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file());
764         }
765
766         auto const size = filesystem::file_size(asset->file().get());
767         if (size > 115 * 1024 * 1024) {
768                 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file());
769         }
770
771         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
772          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
773          */
774         auto fonts = asset->font_data ();
775         int total_size = 0;
776         for (auto i: fonts) {
777                 total_size += i.second.size();
778         }
779         if (total_size > 10 * 1024 * 1024) {
780                 context.bv21_error(VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get());
781         }
782
783         if (!asset->start_time()) {
784                 context.bv21_error(VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get());
785         } else if (asset->start_time() != Time()) {
786                 context.bv21_error(VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get());
787         }
788
789         if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
790                 context.bv21_error(
791                         VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
792                         String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
793                         asset->file().get()
794                         );
795         }
796 }
797
798
799 /** Verify Interop subtitle / CCAP stuff */
800 void
801 verify_interop_text_asset(Context& context, shared_ptr<const InteropSubtitleAsset> asset)
802 {
803         if (asset->subtitles().empty()) {
804                 context.error(VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get());
805         }
806         auto const unresolved = asset->unresolved_fonts();
807         if (!unresolved.empty()) {
808                 context.error(VerificationNote::Code::MISSING_FONT, unresolved.front());
809         }
810 }
811
812
813 /** Verify SMPTE subtitle-only stuff */
814 void
815 verify_smpte_subtitle_asset(Context& context, shared_ptr<const SMPTESubtitleAsset> asset)
816 {
817         if (asset->language()) {
818                 if (!context.subtitle_language) {
819                         context.subtitle_language = *asset->language();
820                 } else if (context.subtitle_language != *asset->language()) {
821                         context.bv21_error(VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES);
822                 }
823         }
824
825         DCP_ASSERT (asset->resource_id());
826         auto xml_id = asset->xml_id();
827         if (xml_id) {
828                 if (asset->resource_id().get() != xml_id) {
829                         context.bv21_error(VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID);
830                 }
831
832                 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
833                         context.bv21_error(VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID);
834                 }
835         } else {
836                 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
837         }
838
839         if (asset->raw_xml()) {
840                 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
841                 cxml::Document doc("SubtitleReel");
842                 doc.read_string(*asset->raw_xml());
843                 auto issue_date = doc.string_child("IssueDate");
844                 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
845                 if (!std::regex_match(issue_date, reg)) {
846                         context.warning(VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date);
847                 }
848         }
849 }
850
851
852 /** Verify all subtitle stuff */
853 static void
854 verify_subtitle_asset(Context& context, shared_ptr<const SubtitleAsset> asset, optional<int64_t> reel_asset_duration)
855 {
856         context.stage("Checking subtitle XML", asset->file());
857         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
858          * gets passed through libdcp which may clean up and therefore hide errors.
859          */
860         if (asset->raw_xml()) {
861                 validate_xml(context, asset->raw_xml().get());
862         } else {
863                 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
864         }
865
866         auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
867                 cxml::Document doc(root_node);
868                 doc.read_string(asset->raw_xml().get());
869                 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
870                 int count = 0;
871                 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
872                         ++count;
873                 }
874                 return count;
875         };
876
877         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
878         if (interop) {
879                 verify_interop_text_asset(context, interop);
880                 if (namespace_count(asset, "DCSubtitle") > 1) {
881                         context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
882                 }
883         }
884
885         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
886         if (smpte) {
887                 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
888                 verify_smpte_subtitle_asset(context, smpte);
889                 /* This asset may be encrypted and in that case we'll have no raw_xml() */
890                 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
891                         context.warning(VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id());
892                 }
893         }
894 }
895
896
897 /** Verify all closed caption stuff */
898 static void
899 verify_closed_caption_asset (
900         Context& context,
901         shared_ptr<const SubtitleAsset> asset,
902         optional<int64_t> reel_asset_duration
903         )
904 {
905         context.stage("Checking closed caption XML", asset->file());
906         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
907          * gets passed through libdcp which may clean up and therefore hide errors.
908          */
909         auto raw_xml = asset->raw_xml();
910         if (raw_xml) {
911                 validate_xml(context, *raw_xml);
912                 if (raw_xml->size() > 256 * 1024) {
913                         context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file());
914                 }
915         } else {
916                 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
917         }
918
919         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
920         if (interop) {
921                 verify_interop_text_asset(context, interop);
922         }
923
924         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
925         if (smpte) {
926                 verify_smpte_timed_text_asset(context, smpte, reel_asset_duration);
927         }
928 }
929
930
931 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
932 static
933 void
934 verify_text_details (
935         Context& context,
936         vector<shared_ptr<Reel>> reels,
937         int edit_rate,
938         std::function<bool (shared_ptr<Reel>)> check,
939         std::function<optional<string> (shared_ptr<Reel>)> xml,
940         std::function<int64_t (shared_ptr<Reel>)> duration,
941         std::function<std::string (shared_ptr<Reel>)> id
942         )
943 {
944         /* end of last subtitle (in editable units) */
945         optional<int64_t> last_out;
946         auto too_short = false;
947         auto too_close = false;
948         auto too_early = false;
949         auto reel_overlap = false;
950         auto empty_text = false;
951         /* current reel start time (in editable units) */
952         int64_t reel_offset = 0;
953         optional<string> missing_load_font_id;
954
955         std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
956
957         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
958                 cxml::ConstNodePtr node,
959                 optional<int> tcr,
960                 optional<Time> start_time,
961                 int er,
962                 bool first_reel,
963                 bool& has_text,
964                 vector<string>& font_ids
965                 ) {
966                 if (node->name() == "Subtitle") {
967                         Time in (node->string_attribute("TimeIn"), tcr);
968                         if (start_time) {
969                                 in -= *start_time;
970                         }
971                         Time out (node->string_attribute("TimeOut"), tcr);
972                         if (start_time) {
973                                 out -= *start_time;
974                         }
975                         if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
976                                 too_early = true;
977                         }
978                         auto length = out - in;
979                         if (length.as_editable_units_ceil(er) < 15) {
980                                 too_short = true;
981                         }
982                         if (last_out) {
983                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
984                                 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
985                                 if (distance >= 0 && distance < 2) {
986                                         too_close = true;
987                                 }
988                         }
989                         last_out = reel_offset + out.as_editable_units_floor(er);
990                 } else if (node->name() == "Text") {
991                         std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
992                                 if (!node->content().empty()) {
993                                         return true;
994                                 }
995                                 for (auto i: node->node_children()) {
996                                         if (node_has_content(i)) {
997                                                 return true;
998                                         }
999                                 }
1000                                 return false;
1001                         };
1002                         if (!node_has_content(node)) {
1003                                 empty_text = true;
1004                         }
1005                         has_text = true;
1006                 } else if (node->name() == "LoadFont") {
1007                         if (auto const id = node->optional_string_attribute("Id")) {
1008                                 font_ids.push_back(*id);
1009                         } else if (auto const id = node->optional_string_attribute("ID")) {
1010                                 font_ids.push_back(*id);
1011                         }
1012                 } else if (node->name() == "Font") {
1013                         if (auto const font_id = node->optional_string_attribute("Id")) {
1014                                 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
1015                                         missing_load_font_id = font_id;
1016                                 }
1017                         }
1018                 }
1019                 for (auto i: node->node_children()) {
1020                         parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
1021                 }
1022         };
1023
1024         for (auto i = 0U; i < reels.size(); ++i) {
1025                 if (!check(reels[i])) {
1026                         continue;
1027                 }
1028
1029                 auto reel_xml = xml(reels[i]);
1030                 if (!reel_xml) {
1031                         context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1032                         continue;
1033                 }
1034
1035                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1036                  * read in by libdcp's parser.
1037                  */
1038
1039                 shared_ptr<cxml::Document> doc;
1040                 optional<int> tcr;
1041                 optional<Time> start_time;
1042                 switch (context.dcp->standard().get_value_or(dcp::Standard::SMPTE)) {
1043                 case dcp::Standard::INTEROP:
1044                         doc = make_shared<cxml::Document>("DCSubtitle");
1045                         doc->read_string (*reel_xml);
1046                         break;
1047                 case dcp::Standard::SMPTE:
1048                         doc = make_shared<cxml::Document>("SubtitleReel");
1049                         doc->read_string (*reel_xml);
1050                         tcr = doc->number_child<int>("TimeCodeRate");
1051                         if (auto start_time_string = doc->optional_string_child("StartTime")) {
1052                                 start_time = Time(*start_time_string, tcr);
1053                         }
1054                         break;
1055                 }
1056                 bool has_text = false;
1057                 vector<string> font_ids;
1058                 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
1059                 auto end = reel_offset + duration(reels[i]);
1060                 if (last_out && *last_out > end) {
1061                         reel_overlap = true;
1062                 }
1063                 reel_offset = end;
1064
1065                 if (context.dcp->standard() && *context.dcp->standard() == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1066                         context.add_note(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1067                 }
1068         }
1069
1070         if (last_out && *last_out > reel_offset) {
1071                 reel_overlap = true;
1072         }
1073
1074         if (too_early) {
1075                 context.warning(VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME);
1076         }
1077
1078         if (too_short) {
1079                 context.warning(VerificationNote::Code::INVALID_SUBTITLE_DURATION);
1080         }
1081
1082         if (too_close) {
1083                 context.warning(VerificationNote::Code::INVALID_SUBTITLE_SPACING);
1084         }
1085
1086         if (reel_overlap) {
1087                 context.error(VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY);
1088         }
1089
1090         if (empty_text) {
1091                 context.warning(VerificationNote::Code::EMPTY_TEXT);
1092         }
1093
1094         if (missing_load_font_id) {
1095                 context.add_note(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1096         }
1097 }
1098
1099
1100 static
1101 void
1102 verify_closed_caption_details(Context& context, vector<shared_ptr<Reel>> reels)
1103 {
1104         std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1105         find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1106                 for (auto i: node->node_children()) {
1107                         if (i->name() == "Text") {
1108                                 text_or_image.push_back (i);
1109                         } else {
1110                                 find_text_or_image (i, text_or_image);
1111                         }
1112                 }
1113         };
1114
1115         auto mismatched_valign = false;
1116         auto incorrect_order = false;
1117
1118         std::function<void (cxml::ConstNodePtr)> parse;
1119         parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1120                 if (node->name() == "Subtitle") {
1121                         vector<cxml::ConstNodePtr> text_or_image;
1122                         find_text_or_image (node, text_or_image);
1123                         optional<string> last_valign;
1124                         optional<float> last_vpos;
1125                         for (auto i: text_or_image) {
1126                                 auto valign = i->optional_string_attribute("VAlign");
1127                                 if (!valign) {
1128                                         valign = i->optional_string_attribute("Valign").get_value_or("center");
1129                                 }
1130                                 auto vpos = i->optional_number_attribute<float>("VPosition");
1131                                 if (!vpos) {
1132                                         vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1133                                 }
1134
1135                                 if (last_valign) {
1136                                         if (*last_valign != valign) {
1137                                                 mismatched_valign = true;
1138                                         }
1139                                 }
1140                                 last_valign = valign;
1141
1142                                 if (!mismatched_valign) {
1143                                         if (last_vpos) {
1144                                                 if (*last_valign == "top" || *last_valign == "center") {
1145                                                         if (*vpos < *last_vpos) {
1146                                                                 incorrect_order = true;
1147                                                         }
1148                                                 } else {
1149                                                         if (*vpos > *last_vpos) {
1150                                                                 incorrect_order = true;
1151                                                         }
1152                                                 }
1153                                         }
1154                                         last_vpos = vpos;
1155                                 }
1156                         }
1157                 }
1158
1159                 for (auto i: node->node_children()) {
1160                         parse(i);
1161                 }
1162         };
1163
1164         for (auto reel: reels) {
1165                 for (auto ccap: reel->closed_captions()) {
1166                         auto reel_xml = ccap->asset()->raw_xml();
1167                         if (!reel_xml) {
1168                                 context.warning(VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED);
1169                                 continue;
1170                         }
1171
1172                         /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1173                          * read in by libdcp's parser.
1174                          */
1175
1176                         shared_ptr<cxml::Document> doc;
1177                         optional<int> tcr;
1178                         optional<Time> start_time;
1179                         try {
1180                                 doc = make_shared<cxml::Document>("SubtitleReel");
1181                                 doc->read_string (*reel_xml);
1182                         } catch (...) {
1183                                 doc = make_shared<cxml::Document>("DCSubtitle");
1184                                 doc->read_string (*reel_xml);
1185                         }
1186                         parse (doc);
1187                 }
1188         }
1189
1190         if (mismatched_valign) {
1191                 context.error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN);
1192         }
1193
1194         if (incorrect_order) {
1195                 context.error(VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING);
1196         }
1197 }
1198
1199
1200 struct LinesCharactersResult
1201 {
1202         bool warning_length_exceeded = false;
1203         bool error_length_exceeded = false;
1204         bool line_count_exceeded = false;
1205 };
1206
1207
1208 static
1209 void
1210 verify_text_lines_and_characters (
1211         shared_ptr<SubtitleAsset> asset,
1212         int warning_length,
1213         int error_length,
1214         LinesCharactersResult* result
1215         )
1216 {
1217         class Event
1218         {
1219         public:
1220                 Event (Time time_, float position_, int characters_)
1221                         : time (time_)
1222                         , position (position_)
1223                         , characters (characters_)
1224                 {}
1225
1226                 Event (Time time_, shared_ptr<Event> start_)
1227                         : time (time_)
1228                         , start (start_)
1229                 {}
1230
1231                 Time time;
1232                 int position; //< position from 0 at top of screen to 100 at bottom
1233                 int characters;
1234                 shared_ptr<Event> start;
1235         };
1236
1237         vector<shared_ptr<Event>> events;
1238
1239         auto position = [](shared_ptr<const SubtitleString> sub) {
1240                 switch (sub->v_align()) {
1241                 case VAlign::TOP:
1242                         return lrintf(sub->v_position() * 100);
1243                 case VAlign::CENTER:
1244                         return lrintf((0.5f + sub->v_position()) * 100);
1245                 case VAlign::BOTTOM:
1246                         return lrintf((1.0f - sub->v_position()) * 100);
1247                 }
1248
1249                 return 0L;
1250         };
1251
1252         for (auto j: asset->subtitles()) {
1253                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1254                 if (text) {
1255                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1256                         events.push_back(in);
1257                         events.push_back(make_shared<Event>(text->out(), in));
1258                 }
1259         }
1260
1261         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1262                 return a->time < b->time;
1263         });
1264
1265         map<int, int> current;
1266         for (auto i: events) {
1267                 if (current.size() > 3) {
1268                         result->line_count_exceeded = true;
1269                 }
1270                 for (auto j: current) {
1271                         if (j.second > warning_length) {
1272                                 result->warning_length_exceeded = true;
1273                         }
1274                         if (j.second > error_length) {
1275                                 result->error_length_exceeded = true;
1276                         }
1277                 }
1278
1279                 if (i->start) {
1280                         /* end of a subtitle */
1281                         DCP_ASSERT (current.find(i->start->position) != current.end());
1282                         if (current[i->start->position] == i->start->characters) {
1283                                 current.erase(i->start->position);
1284                         } else {
1285                                 current[i->start->position] -= i->start->characters;
1286                         }
1287                 } else {
1288                         /* start of a subtitle */
1289                         if (current.find(i->position) == current.end()) {
1290                                 current[i->position] = i->characters;
1291                         } else {
1292                                 current[i->position] += i->characters;
1293                         }
1294                 }
1295         }
1296 }
1297
1298
1299 static
1300 void
1301 verify_text_details(Context& context, vector<shared_ptr<Reel>> reels)
1302 {
1303         if (reels.empty()) {
1304                 return;
1305         }
1306
1307         if (reels[0]->main_subtitle() && reels[0]->main_subtitle()->asset_ref().resolved()) {
1308                 verify_text_details(context, reels, reels[0]->main_subtitle()->edit_rate().numerator,
1309                         [](shared_ptr<Reel> reel) {
1310                                 return static_cast<bool>(reel->main_subtitle());
1311                         },
1312                         [](shared_ptr<Reel> reel) {
1313                                 return reel->main_subtitle()->asset()->raw_xml();
1314                         },
1315                         [](shared_ptr<Reel> reel) {
1316                                 return reel->main_subtitle()->actual_duration();
1317                         },
1318                         [](shared_ptr<Reel> reel) {
1319                                 return reel->main_subtitle()->id();
1320                         }
1321                 );
1322         }
1323
1324         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1325                 verify_text_details(context, reels, reels[0]->closed_captions()[i]->edit_rate().numerator,
1326                         [i](shared_ptr<Reel> reel) {
1327                                 return i < reel->closed_captions().size();
1328                         },
1329                         [i](shared_ptr<Reel> reel) {
1330                                 return reel->closed_captions()[i]->asset()->raw_xml();
1331                         },
1332                         [i](shared_ptr<Reel> reel) {
1333                                 return reel->closed_captions()[i]->actual_duration();
1334                         },
1335                         [i](shared_ptr<Reel> reel) {
1336                                 return reel->closed_captions()[i]->id();
1337                         }
1338                 );
1339         }
1340
1341         verify_closed_caption_details(context, reels);
1342 }
1343
1344
1345 void
1346 verify_extension_metadata(Context& context, shared_ptr<const CPL> cpl)
1347 {
1348         DCP_ASSERT (cpl->file());
1349         cxml::Document doc ("CompositionPlaylist");
1350         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1351
1352         auto missing = false;
1353         string malformed;
1354
1355         if (auto reel_list = doc.node_child("ReelList")) {
1356                 auto reels = reel_list->node_children("Reel");
1357                 if (!reels.empty()) {
1358                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1359                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1360                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1361                                                 missing = true;
1362                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1363                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1364                                                                 continue;
1365                                                         }
1366                                                         missing = false;
1367                                                         if (auto name = extension->optional_node_child("Name")) {
1368                                                                 if (name->content() != "Application") {
1369                                                                         malformed = "<Name> should be 'Application'";
1370                                                                 }
1371                                                         }
1372                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
1373                                                                 if (auto property = property_list->optional_node_child("Property")) {
1374                                                                         if (auto name = property->optional_node_child("Name")) {
1375                                                                                 if (name->content() != "DCP Constraints Profile") {
1376                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
1377                                                                                 }
1378                                                                         }
1379                                                                         if (auto value = property->optional_node_child("Value")) {
1380                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1381                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1382                                                                                 }
1383                                                                         }
1384                                                                 }
1385                                                         }
1386                                                 }
1387                                         } else {
1388                                                 missing = true;
1389                                         }
1390                                 }
1391                         }
1392                 }
1393         }
1394
1395         if (missing) {
1396                 context.bv21_error(VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->file().get());
1397         } else if (!malformed.empty()) {
1398                 context.bv21_error(VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get());
1399         }
1400 }
1401
1402
1403 bool
1404 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1405 {
1406         vector<string> encrypted;
1407         for (auto i: dcp->cpls()) {
1408                 for (auto j: i->reel_file_assets()) {
1409                         if (j->asset_ref().resolved()) {
1410                                 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1411                                 if (mxf && mxf->encrypted()) {
1412                                         encrypted.push_back(j->asset_ref().id());
1413                                 }
1414                         }
1415                 }
1416         }
1417
1418         for (auto i: pkl->assets()) {
1419                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1420                         return true;
1421                 }
1422         }
1423
1424         return false;
1425 }
1426
1427
1428 static
1429 void
1430 verify_reel(
1431         Context& context,
1432         shared_ptr<const Reel> reel,
1433         int64_t start_frame,
1434         optional<dcp::Size> main_picture_active_area,
1435         bool* have_main_subtitle,
1436         bool* have_no_main_subtitle,
1437         size_t* most_closed_captions,
1438         size_t* fewest_closed_captions,
1439         map<Marker, Time>* markers_seen
1440         )
1441 {
1442         for (auto i: reel->assets()) {
1443                 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1444                         context.error(VerificationNote::Code::INVALID_DURATION, i->id());
1445                 }
1446                 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1447                         context.error(VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id());
1448                 }
1449                 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1450                 if (i->encryptable() && !file_asset->hash()) {
1451                         context.bv21_error(VerificationNote::Code::MISSING_HASH, i->id());
1452                 }
1453         }
1454
1455         if (context.dcp->standard() == Standard::SMPTE) {
1456                 boost::optional<int64_t> duration;
1457                 for (auto i: reel->assets()) {
1458                         if (!duration) {
1459                                 duration = i->actual_duration();
1460                         } else if (*duration != i->actual_duration()) {
1461                                 context.bv21_error(VerificationNote::Code::MISMATCHED_ASSET_DURATION);
1462                                 break;
1463                         }
1464                 }
1465         }
1466
1467         if (reel->main_picture()) {
1468                 /* Check reel stuff */
1469                 auto const frame_rate = reel->main_picture()->frame_rate();
1470                 if (frame_rate.denominator != 1 ||
1471                     (frame_rate.numerator != 24 &&
1472                      frame_rate.numerator != 25 &&
1473                      frame_rate.numerator != 30 &&
1474                      frame_rate.numerator != 48 &&
1475                      frame_rate.numerator != 50 &&
1476                      frame_rate.numerator != 60 &&
1477                      frame_rate.numerator != 96)) {
1478                         context.error(VerificationNote::Code::INVALID_PICTURE_FRAME_RATE, String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator));
1479                 }
1480                 /* Check asset */
1481                 if (reel->main_picture()->asset_ref().resolved()) {
1482                         verify_main_picture_asset(context, reel->main_picture(), start_frame);
1483                         auto const asset_size = reel->main_picture()->asset()->size();
1484                         if (main_picture_active_area) {
1485                                 if (main_picture_active_area->width > asset_size.width) {
1486                                         context.error(
1487                                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1488                                                 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1489                                                 context.cpl->file().get()
1490                                         );
1491                                 }
1492                                 if (main_picture_active_area->height > asset_size.height) {
1493                                         context.error(
1494                                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1495                                                 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1496                                                 context.cpl->file().get()
1497                                         );
1498                                 }
1499                         }
1500                 }
1501
1502         }
1503
1504         if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1505                 verify_main_sound_asset(context, reel->main_sound());
1506         }
1507
1508         if (reel->main_subtitle()) {
1509                 verify_main_subtitle_reel(context, reel->main_subtitle());
1510                 if (reel->main_subtitle()->asset_ref().resolved()) {
1511                         verify_subtitle_asset(context, reel->main_subtitle()->asset(), reel->main_subtitle()->duration());
1512                 }
1513                 *have_main_subtitle = true;
1514         } else {
1515                 *have_no_main_subtitle = true;
1516         }
1517
1518         for (auto i: reel->closed_captions()) {
1519                 verify_closed_caption_reel(context, i);
1520                 if (i->asset_ref().resolved()) {
1521                         verify_closed_caption_asset(context, i->asset(), i->duration());
1522                 }
1523         }
1524
1525         if (reel->main_markers()) {
1526                 for (auto const& i: reel->main_markers()->get()) {
1527                         markers_seen->insert(i);
1528                 }
1529                 if (reel->main_markers()->entry_point()) {
1530                         context.error(VerificationNote::Code::UNEXPECTED_ENTRY_POINT);
1531                 }
1532                 if (reel->main_markers()->duration()) {
1533                         context.error(VerificationNote::Code::UNEXPECTED_DURATION);
1534                 }
1535         }
1536
1537         *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1538         *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1539
1540 }
1541
1542
1543 static
1544 void
1545 verify_cpl(Context& context, shared_ptr<const CPL> cpl)
1546 {
1547         context.stage("Checking CPL", cpl->file());
1548         validate_xml(context, cpl->file().get());
1549
1550         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1551                 context.bv21_error(VerificationNote::Code::PARTIALLY_ENCRYPTED);
1552         } else if (cpl->all_encrypted()) {
1553                 context.ok(VerificationNote::Code::ALL_ENCRYPTED);
1554         } else if (!cpl->all_encrypted()) {
1555                 context.ok(VerificationNote::Code::NONE_ENCRYPTED);
1556         }
1557
1558         for (auto const& i: cpl->additional_subtitle_languages()) {
1559                 verify_language_tag(context, i);
1560         }
1561
1562         if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1563                 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1564                  * of the approved ones.
1565                  */
1566                 auto all = ContentKind::all();
1567                 auto name = cpl->content_kind().name();
1568                 transform(name.begin(), name.end(), name.begin(), ::tolower);
1569                 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1570                 if (iter == all.end()) {
1571                         context.error(VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name());
1572                 } else {
1573                         context.ok(VerificationNote::Code::VALID_CONTENT_KIND, cpl->content_kind().name());
1574                 }
1575         }
1576
1577         if (cpl->release_territory()) {
1578                 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") {
1579                         auto terr = cpl->release_territory().get();
1580                         bool valid = true;
1581                         /* Must be a valid region tag, or "001" */
1582                         try {
1583                                 LanguageTag::RegionSubtag test(terr);
1584                         } catch (...) {
1585                                 if (terr != "001") {
1586                                         context.bv21_error(VerificationNote::Code::INVALID_LANGUAGE, terr);
1587                                         valid = false;
1588                                 }
1589                         }
1590                         if (valid) {
1591                                 context.ok(VerificationNote::Code::VALID_RELEASE_TERRITORY, terr);
1592                         }
1593                 }
1594         }
1595
1596         for (auto version: cpl->content_versions()) {
1597                 if (version.label_text.empty()) {
1598                         context.warning(VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get());
1599                         break;
1600                 } else {
1601                         context.ok(VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT, version.label_text);
1602                 }
1603         }
1604
1605         if (context.dcp->standard() == Standard::SMPTE) {
1606                 if (!cpl->annotation_text()) {
1607                         context.bv21_error(VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->file().get());
1608                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1609                         context.warning(VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->file().get());
1610                 } else {
1611                         context.ok(VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT, cpl->annotation_text().get());
1612                 }
1613         }
1614
1615         for (auto i: context.dcp->pkls()) {
1616                 /* Check that the CPL's hash corresponds to the PKL */
1617                 optional<string> h = i->hash(cpl->id());
1618                 auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
1619                 if (h && calculated_cpl_hash != *h) {
1620                         context.add_note(
1621                                 dcp::VerificationNote(
1622                                         VerificationNote::Type::ERROR,
1623                                         VerificationNote::Code::MISMATCHED_CPL_HASHES,
1624                                         cpl->file().get()
1625                                         ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
1626                                 );
1627                 } else {
1628                         context.ok(VerificationNote::Code::MATCHING_CPL_HASHES);
1629                 }
1630
1631                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1632                 optional<string> required_annotation_text;
1633                 for (auto j: i->assets()) {
1634                         /* See if this is a CPL */
1635                         for (auto k: context.dcp->cpls()) {
1636                                 if (j->id() == k->id()) {
1637                                         if (!required_annotation_text) {
1638                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1639                                                 required_annotation_text = cpl->content_title_text();
1640                                         } else {
1641                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1642                                                 required_annotation_text = boost::none;
1643                                         }
1644                                 }
1645                         }
1646                 }
1647
1648                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1649                         context.bv21_error(VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get());
1650                 } else {
1651                         context.ok(VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL);
1652                 }
1653         }
1654
1655         /* set to true if any reel has a MainSubtitle */
1656         auto have_main_subtitle = false;
1657         /* set to true if any reel has no MainSubtitle */
1658         auto have_no_main_subtitle = false;
1659         /* fewest number of closed caption assets seen in a reel */
1660         size_t fewest_closed_captions = SIZE_MAX;
1661         /* most number of closed caption assets seen in a reel */
1662         size_t most_closed_captions = 0;
1663         map<Marker, Time> markers_seen;
1664
1665         auto const main_picture_active_area = cpl->main_picture_active_area();
1666         bool active_area_ok = true;
1667         if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1668                 context.error(
1669                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1670                         String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1671                         cpl->file().get()
1672                      );
1673                 active_area_ok = false;
1674         }
1675         if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1676                 context.error(
1677                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1678                         String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1679                         cpl->file().get()
1680                      );
1681                 active_area_ok = false;
1682         }
1683
1684         if (main_picture_active_area && active_area_ok) {
1685                 context.ok(
1686                         VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA, String::compose("%1x%2", main_picture_active_area->width, main_picture_active_area->height),
1687                         cpl->file().get()
1688                         );
1689         }
1690
1691         int64_t frame = 0;
1692         for (auto reel: cpl->reels()) {
1693                 context.stage("Checking reel", optional<boost::filesystem::path>());
1694                 verify_reel(
1695                         context,
1696                         reel,
1697                         frame,
1698                         main_picture_active_area,
1699                         &have_main_subtitle,
1700                         &have_no_main_subtitle,
1701                         &most_closed_captions,
1702                         &fewest_closed_captions,
1703                         &markers_seen
1704                         );
1705                 frame += reel->duration();
1706         }
1707
1708         verify_text_details(context, cpl->reels());
1709
1710         if (context.dcp->standard() == Standard::SMPTE) {
1711                 if (auto msc = cpl->main_sound_configuration()) {
1712                         if (context.audio_channels && msc->channels() != *context.audio_channels) {
1713                                 context.error(
1714                                         VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1715                                         String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *context.audio_channels),
1716                                         cpl->file().get()
1717                                 );
1718                         }
1719                 }
1720
1721                 if (have_main_subtitle && have_no_main_subtitle) {
1722                         context.bv21_error(VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS);
1723                 }
1724
1725                 if (fewest_closed_captions != most_closed_captions) {
1726                         context.bv21_error(VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS);
1727                 }
1728
1729                 if (cpl->content_kind() == ContentKind::FEATURE) {
1730                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1731                                 context.bv21_error(VerificationNote::Code::MISSING_FFEC_IN_FEATURE);
1732                         }
1733                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1734                                 context.bv21_error(VerificationNote::Code::MISSING_FFMC_IN_FEATURE);
1735                         }
1736                 }
1737
1738                 auto ffoc = markers_seen.find(Marker::FFOC);
1739                 if (ffoc == markers_seen.end()) {
1740                         context.warning(VerificationNote::Code::MISSING_FFOC);
1741                 } else if (ffoc->second.e != 1) {
1742                         context.warning(VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e));
1743                 }
1744
1745                 auto lfoc = markers_seen.find(Marker::LFOC);
1746                 if (lfoc == markers_seen.end()) {
1747                         context.warning(VerificationNote::Code::MISSING_LFOC);
1748                 } else {
1749                         auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1750                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1751                                 context.warning(VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time));
1752                         }
1753                 }
1754
1755                 LinesCharactersResult result;
1756                 for (auto reel: cpl->reels()) {
1757                         if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
1758                                 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1759                         }
1760                 }
1761
1762                 if (result.line_count_exceeded) {
1763                         context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT);
1764                 }
1765                 if (result.error_length_exceeded) {
1766                         context.warning(VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH);
1767                 } else if (result.warning_length_exceeded) {
1768                         context.warning(VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH);
1769                 }
1770
1771                 result = LinesCharactersResult();
1772                 for (auto reel: cpl->reels()) {
1773                         for (auto i: reel->closed_captions()) {
1774                                 if (i->asset()) {
1775                                         verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1776                                 }
1777                         }
1778                 }
1779
1780                 if (result.line_count_exceeded) {
1781                         context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT);
1782                 }
1783                 if (result.error_length_exceeded) {
1784                         context.bv21_error(VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH);
1785                 }
1786
1787                 if (!cpl->read_composition_metadata()) {
1788                         context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA, cpl->file().get());
1789                 } else if (!cpl->version_number()) {
1790                         context.bv21_error(VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->file().get());
1791                 }
1792
1793                 verify_extension_metadata(context, cpl);
1794
1795                 if (cpl->any_encrypted()) {
1796                         cxml::Document doc("CompositionPlaylist");
1797                         DCP_ASSERT(cpl->file());
1798                         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1799                         if (!doc.optional_node_child("Signature")) {
1800                                 context.bv21_error(VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->file().get());
1801                         }
1802                 }
1803         }
1804 }
1805
1806
1807 static
1808 void
1809 verify_pkl(Context& context, shared_ptr<const PKL> pkl)
1810 {
1811         validate_xml(context, pkl->file().get());
1812
1813         if (pkl_has_encrypted_assets(context.dcp, pkl)) {
1814                 cxml::Document doc("PackingList");
1815                 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1816                 if (!doc.optional_node_child("Signature")) {
1817                         context.bv21_error(VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get());
1818                 }
1819         }
1820
1821         set<string> uuid_set;
1822         for (auto asset: pkl->assets()) {
1823                 if (!uuid_set.insert(asset->id()).second) {
1824                         context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get());
1825                         break;
1826                 }
1827         }
1828 }
1829
1830
1831
1832 static
1833 void
1834 verify_assetmap(Context& context, shared_ptr<const DCP> dcp)
1835 {
1836         auto asset_map = dcp->asset_map();
1837         DCP_ASSERT(asset_map);
1838
1839         validate_xml(context, asset_map->file().get());
1840
1841         set<string> uuid_set;
1842         for (auto const& asset: asset_map->assets()) {
1843                 if (!uuid_set.insert(asset.id()).second) {
1844                         context.error(VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get());
1845                         break;
1846                 }
1847         }
1848 }
1849
1850
1851 dcp::VerifyResult
1852 dcp::verify (
1853         vector<boost::filesystem::path> directories,
1854         vector<dcp::DecryptedKDM> kdms,
1855         function<void (string, optional<boost::filesystem::path>)> stage,
1856         function<void (float)> progress,
1857         VerificationOptions options,
1858         optional<boost::filesystem::path> xsd_dtd_directory
1859         )
1860 {
1861         if (!xsd_dtd_directory) {
1862                 xsd_dtd_directory = resources_directory() / "xsd";
1863         }
1864         *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1865
1866         vector<VerificationNote> notes;
1867         Context context(notes, *xsd_dtd_directory, stage, progress, options);
1868
1869         vector<shared_ptr<DCP>> dcps;
1870         for (auto i: directories) {
1871                 dcps.push_back (make_shared<DCP>(i));
1872         }
1873
1874         for (auto dcp: dcps) {
1875                 stage ("Checking DCP", dcp->directory());
1876
1877                 context.dcp = dcp;
1878
1879                 bool carry_on = true;
1880                 try {
1881                         dcp->read (&notes, true);
1882                 } catch (MissingAssetmapError& e) {
1883                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1884                         carry_on = false;
1885                 } catch (ReadError& e) {
1886                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1887                 } catch (XMLError& e) {
1888                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1889                 } catch (MXFFileError& e) {
1890                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1891                 } catch (BadURNUUIDError& e) {
1892                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1893                 } catch (cxml::Error& e) {
1894                         context.error(VerificationNote::Code::FAILED_READ, string(e.what()));
1895                 }
1896
1897                 if (!carry_on) {
1898                         continue;
1899                 }
1900
1901                 if (dcp->standard() != Standard::SMPTE) {
1902                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1903                 }
1904
1905                 for (auto kdm: kdms) {
1906                         dcp->add(kdm);
1907                 }
1908
1909                 for (auto cpl: dcp->cpls()) {
1910                         try {
1911                                 context.cpl = cpl;
1912                                 verify_cpl(context, cpl);
1913                                 context.cpl.reset();
1914                         } catch (ReadError& e) {
1915                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1916                         }
1917                 }
1918
1919                 for (auto pkl: dcp->pkls()) {
1920                         stage("Checking PKL", pkl->file());
1921                         verify_pkl(context, pkl);
1922                 }
1923
1924                 if (dcp->asset_map_file()) {
1925                         stage("Checking ASSETMAP", dcp->asset_map_file().get());
1926                         verify_assetmap(context, dcp);
1927                 } else {
1928                         context.error(VerificationNote::Code::MISSING_ASSETMAP);
1929                 }
1930         }
1931
1932         return { notes, dcps };
1933 }
1934
1935
1936 string
1937 dcp::note_to_string (VerificationNote note)
1938 {
1939         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1940          *
1941          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1942          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1943          *
1944          *  It's OK to use XML tag names where they are clear.
1945          *  If both ID and filename are available, use only the ID.
1946          *  End messages with a full stop.
1947          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1948          */
1949         switch (note.code()) {
1950         case VerificationNote::Code::FAILED_READ:
1951                 return *note.note();
1952         case VerificationNote::Code::MATCHING_CPL_HASHES:
1953                 return "The hash of the CPL in the PKL matches the CPL file.";
1954         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1955                 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());
1956         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1957                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1958         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1959                 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());
1960         case VerificationNote::Code::CORRECT_PICTURE_HASH:
1961                 return String::compose("The picture asset %1 has the expected hashes in the CPL and PKL.", note.file()->filename());
1962         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1963                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1964         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1965                 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());
1966         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1967                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1968         case VerificationNote::Code::EMPTY_ASSET_PATH:
1969                 return "The asset map contains an empty asset path.";
1970         case VerificationNote::Code::MISSING_ASSET:
1971                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1972         case VerificationNote::Code::MISMATCHED_STANDARD:
1973                 return "The DCP contains both SMPTE and Interop parts.";
1974         case VerificationNote::Code::INVALID_XML:
1975                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1976         case VerificationNote::Code::MISSING_ASSETMAP:
1977                 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1978         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1979                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1980         case VerificationNote::Code::INVALID_DURATION:
1981                 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1982         case VerificationNote::Code::VALID_PICTURE_FRAME_SIZES_IN_BYTES:
1983                 return String::compose("Each frame of the picture asset %1 has a bit rate safely under the limit of 250Mbit/s.", note.file()->filename());
1984         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1985                 return String::compose(
1986                         "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is larger than the limit of 250Mbit/s.",
1987                         note.frame().get(),
1988                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1989                         note.file()->filename()
1990                         );
1991         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1992                 return String::compose(
1993                         "Frame %1 (timecode %2) in asset %3 has an instantaneous bit rate that is close to the limit of 250Mbit/s.",
1994                         note.frame().get(),
1995                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
1996                         note.file()->filename()
1997                         );
1998         case VerificationNote::Code::EXTERNAL_ASSET:
1999                 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());
2000         case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
2001                 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
2002         case VerificationNote::Code::INVALID_STANDARD:
2003                 return "This DCP does not use the SMPTE standard.";
2004         case VerificationNote::Code::INVALID_LANGUAGE:
2005                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
2006         case VerificationNote::Code::VALID_RELEASE_TERRITORY:
2007                 return String::compose("Valid release territory %1.", note.note().get());
2008         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
2009                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
2010         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
2011                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
2012         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
2013                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
2014         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
2015                 return "3D 4K DCPs are not allowed.";
2016         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
2017                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
2018         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
2019                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
2020         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
2021                 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());
2022         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
2023                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
2024         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
2025                 return "Some subtitle assets have different <Language> tags than others";
2026         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
2027                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
2028         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
2029                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
2030         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
2031                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
2032         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
2033                 return "At least one subtitle lasts less than 15 frames.";
2034         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
2035                 return "At least one pair of subtitles is separated by less than 2 frames.";
2036         case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
2037                 return "At least one subtitle extends outside of its reel.";
2038         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
2039                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
2040         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
2041                 return "There are more than 52 characters in at least one subtitle line.";
2042         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
2043                 return "There are more than 79 characters in at least one subtitle line.";
2044         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
2045                 return "There are more than 3 closed caption lines in at least one place.";
2046         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
2047                 return "There are more than 32 characters in at least one closed caption line.";
2048         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
2049                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
2050         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
2051                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.cpl_id().get());
2052         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
2053                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.cpl_id().get());
2054         case VerificationNote::Code::VALID_CPL_ANNOTATION_TEXT:
2055                 return String::compose("Valid CPL annotation text %1", note.note().get());
2056         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
2057                 return "All assets in a reel do not have the same duration.";
2058         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
2059                 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
2060         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
2061                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
2062         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
2063                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2064         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2065                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2066         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2067                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2068         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2069                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2070         case VerificationNote::Code::MISSING_HASH:
2071                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2072         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2073                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2074         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2075                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2076         case VerificationNote::Code::MISSING_FFOC:
2077                 return "There should be a FFOC (first frame of content) marker.";
2078         case VerificationNote::Code::MISSING_LFOC:
2079                 return "There should be a LFOC (last frame of content) marker.";
2080         case VerificationNote::Code::INCORRECT_FFOC:
2081                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2082         case VerificationNote::Code::INCORRECT_LFOC:
2083                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2084         case VerificationNote::Code::MISSING_CPL_METADATA:
2085                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.cpl_id().get());
2086         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2087                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2088         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2089                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.cpl_id().get());
2090         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2091                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2092         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2093                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.cpl_id().get());
2094         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2095                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2096         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2097                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2098         case VerificationNote::Code::MATCHING_PKL_ANNOTATION_TEXT_WITH_CPL:
2099                 return "The PKL and CPL annotation texts match.";
2100         case VerificationNote::Code::ALL_ENCRYPTED:
2101                 return "All the assets are encrypted.";
2102         case VerificationNote::Code::NONE_ENCRYPTED:
2103                 return "All the assets are unencrypted.";
2104         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2105                 return "Some assets are encrypted but some are not.";
2106         case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2107                 return String::compose(
2108                         "Frame %1 (timecode %2) has an invalid JPEG2000 codestream (%3).",
2109                         note.frame().get(),
2110                         dcp::Time(note.frame().get(), note.frame_rate().get(), note.frame_rate().get()).as_string(dcp::Standard::SMPTE),
2111                         note.note().get()
2112                         );
2113         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2114                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2115         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2116                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2117         case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2118                 return "The JPEG2000 tile size is not the same as the image size.";
2119         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2120                 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2121         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2122                 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2123         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2124                 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2125         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2126                 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2127         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2128                 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2129         case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2130                 return "POC marker found outside main header.";
2131         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2132                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2133         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2134                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2135         case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2136                 return "No TLM marker was found in a JPEG2000 codestream.";
2137         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2138                 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2139         case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2140                 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2141         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2142         {
2143                 vector<string> parts;
2144                 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2145                 DCP_ASSERT (parts.size() == 2);
2146                 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]);
2147         }
2148         case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2149                 return "Some aspect of this DCP could not be checked because it is encrypted.";
2150         case VerificationNote::Code::EMPTY_TEXT:
2151                 return "There is an empty <Text> node in a subtitle or closed caption.";
2152         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2153                 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2154         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2155                 return "Some closed captions are not listed in the order of their vertical position.";
2156         case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2157                 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2158         case VerificationNote::Code::UNEXPECTED_DURATION:
2159                 return "There is an <Duration> node inside a <MainMarkers>.";
2160         case VerificationNote::Code::INVALID_CONTENT_KIND:
2161                 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2162         case VerificationNote::Code::VALID_CONTENT_KIND:
2163                 return String::compose("Valid <ContentKind> %1.", note.note().get());
2164         case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2165                 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2166         case VerificationNote::Code::VALID_MAIN_PICTURE_ACTIVE_AREA:
2167                 return String::compose("<MainPictureActiveaArea> %1 is valid", note.note().get());
2168         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2169                 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2170         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2171                 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2172         case VerificationNote::Code::MISSING_SUBTITLE:
2173                 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2174         case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2175                 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2176         case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2177                 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2178         case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2179                 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2180         case VerificationNote::Code::MISSING_FONT:
2181                 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2182         case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2183                 return String::compose(
2184                         "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2185                         note.frame().get(), note.component().get(), note.size().get()
2186                         );
2187         case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2188                 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2189         case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2190                 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());
2191         case VerificationNote::Code::MISSING_LOAD_FONT:
2192                 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2193         case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2194                 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());
2195         case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2196                 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.cpl_id().get());
2197         case VerificationNote::Code::VALID_CONTENT_VERSION_LABEL_TEXT:
2198                 return String::compose("CPL has valid <ContentVersion> %1", note.note().get());
2199         }
2200
2201         return "";
2202 }
2203
2204
2205 bool
2206 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2207 {
2208         return a.type() == b.type() &&
2209                 a.code() == b.code() &&
2210                 a.note() == b.note() &&
2211                 a.file() == b.file() &&
2212                 a.line() == b.line() &&
2213                 a.frame() == b.frame() &&
2214                 a.component() == b.component() &&
2215                 a.size() == b.size() &&
2216                 a.id() == b.id() &&
2217                 a.other_id() == b.other_id() &&
2218                 a.frame_rate() == b.frame_rate() &&
2219                 a.cpl_id() == b.cpl_id() &&
2220                 a.reference_hash() == b.reference_hash() &&
2221                 a.calculated_hash() == b.calculated_hash();
2222 }
2223
2224
2225 bool
2226 dcp::operator!=(dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2227 {
2228         return !(a == b);
2229 }
2230
2231
2232 bool
2233 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2234 {
2235         if (a.type() != b.type()) {
2236                 return a.type() < b.type();
2237         }
2238
2239         if (a.code() != b.code()) {
2240                 return a.code() < b.code();
2241         }
2242
2243         if (a.note() != b.note()) {
2244                 return a.note().get_value_or("") < b.note().get_value_or("");
2245         }
2246
2247         if (a.file() != b.file()) {
2248                 return a.file().get_value_or("") < b.file().get_value_or("");
2249         }
2250
2251         if (a.line() != b.line()) {
2252                 return a.line().get_value_or(0) < b.line().get_value_or(0);
2253         }
2254
2255         if (a.frame() != b.frame()) {
2256                 return a.frame().get_value_or(0) < b.frame().get_value_or(0);
2257         }
2258
2259         if (a.component() != b.component()) {
2260                 return a.component().get_value_or(0) < b.component().get_value_or(0);
2261         }
2262
2263         if (a.size() != b.size()) {
2264                 return a.size().get_value_or(0) < b.size().get_value_or(0);
2265         }
2266
2267         if (a.id() != b.id()) {
2268                 return a.id().get_value_or("") < b.id().get_value_or("");
2269         }
2270
2271         if (a.other_id() != b.other_id()) {
2272                 return a.other_id().get_value_or("") < b.other_id().get_value_or("");
2273         }
2274
2275         return a.frame_rate().get_value_or(0) != b.frame_rate().get_value_or(0);
2276 }
2277
2278
2279 std::ostream&
2280 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2281 {
2282         s << note_to_string (note);
2283         if (note.note()) {
2284                 s << " [" << note.note().get() << "]";
2285         }
2286         if (note.file()) {
2287                 s << " [" << note.file().get() << "]";
2288         }
2289         if (note.line()) {
2290                 s << " [" << note.line().get() << "]";
2291         }
2292         return s;
2293 }
2294