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