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