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