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