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