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