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