Bv2.1 8.3.2: text tracks must have <EntryPoint> and it must be zero.
[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 #include "verify.h"
35 #include "dcp.h"
36 #include "cpl.h"
37 #include "reel.h"
38 #include "reel_closed_caption_asset.h"
39 #include "reel_picture_asset.h"
40 #include "reel_sound_asset.h"
41 #include "reel_subtitle_asset.h"
42 #include "interop_subtitle_asset.h"
43 #include "mono_picture_asset.h"
44 #include "mono_picture_frame.h"
45 #include "stereo_picture_asset.h"
46 #include "stereo_picture_frame.h"
47 #include "exceptions.h"
48 #include "compose.hpp"
49 #include "raw_convert.h"
50 #include "smpte_subtitle_asset.h"
51 #include <xercesc/util/PlatformUtils.hpp>
52 #include <xercesc/parsers/XercesDOMParser.hpp>
53 #include <xercesc/parsers/AbstractDOMParser.hpp>
54 #include <xercesc/sax/HandlerBase.hpp>
55 #include <xercesc/dom/DOMImplementation.hpp>
56 #include <xercesc/dom/DOMImplementationLS.hpp>
57 #include <xercesc/dom/DOMImplementationRegistry.hpp>
58 #include <xercesc/dom/DOMLSParser.hpp>
59 #include <xercesc/dom/DOMException.hpp>
60 #include <xercesc/dom/DOMDocument.hpp>
61 #include <xercesc/dom/DOMNodeList.hpp>
62 #include <xercesc/dom/DOMError.hpp>
63 #include <xercesc/dom/DOMLocator.hpp>
64 #include <xercesc/dom/DOMNamedNodeMap.hpp>
65 #include <xercesc/dom/DOMAttr.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/framework/LocalFileInputSource.hpp>
68 #include <xercesc/framework/MemBufInputSource.hpp>
69 #include <boost/noncopyable.hpp>
70 #include <boost/algorithm/string.hpp>
71 #include <map>
72 #include <vector>
73 #include <iostream>
74
75 using std::list;
76 using std::vector;
77 using std::string;
78 using std::cout;
79 using std::map;
80 using std::max;
81 using std::shared_ptr;
82 using std::make_shared;
83 using boost::optional;
84 using boost::function;
85 using std::dynamic_pointer_cast;
86
87 using namespace dcp;
88 using namespace xercesc;
89
90 static
91 string
92 xml_ch_to_string (XMLCh const * a)
93 {
94         char* x = XMLString::transcode(a);
95         string const o(x);
96         XMLString::release(&x);
97         return o;
98 }
99
100 class XMLValidationError
101 {
102 public:
103         XMLValidationError (SAXParseException const & e)
104                 : _message (xml_ch_to_string(e.getMessage()))
105                 , _line (e.getLineNumber())
106                 , _column (e.getColumnNumber())
107                 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
108                 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
109         {
110
111         }
112
113         string message () const {
114                 return _message;
115         }
116
117         uint64_t line () const {
118                 return _line;
119         }
120
121         uint64_t column () const {
122                 return _column;
123         }
124
125         string public_id () const {
126                 return _public_id;
127         }
128
129         string system_id () const {
130                 return _system_id;
131         }
132
133 private:
134         string _message;
135         uint64_t _line;
136         uint64_t _column;
137         string _public_id;
138         string _system_id;
139 };
140
141
142 class DCPErrorHandler : public ErrorHandler
143 {
144 public:
145         void warning(const SAXParseException& e)
146         {
147                 maybe_add (XMLValidationError(e));
148         }
149
150         void error(const SAXParseException& e)
151         {
152                 maybe_add (XMLValidationError(e));
153         }
154
155         void fatalError(const SAXParseException& e)
156         {
157                 maybe_add (XMLValidationError(e));
158         }
159
160         void resetErrors() {
161                 _errors.clear ();
162         }
163
164         list<XMLValidationError> errors () const {
165                 return _errors;
166         }
167
168 private:
169         void maybe_add (XMLValidationError e)
170         {
171                 /* XXX: nasty hack */
172                 if (
173                         e.message().find("schema document") != string::npos &&
174                         e.message().find("has different target namespace from the one specified in instance document") != string::npos
175                         ) {
176                         return;
177                 }
178
179                 _errors.push_back (e);
180         }
181
182         list<XMLValidationError> _errors;
183 };
184
185 class StringToXMLCh : public boost::noncopyable
186 {
187 public:
188         StringToXMLCh (string a)
189         {
190                 _buffer = XMLString::transcode(a.c_str());
191         }
192
193         ~StringToXMLCh ()
194         {
195                 XMLString::release (&_buffer);
196         }
197
198         XMLCh const * get () const {
199                 return _buffer;
200         }
201
202 private:
203         XMLCh* _buffer;
204 };
205
206 class LocalFileResolver : public EntityResolver
207 {
208 public:
209         LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
210                 : _xsd_dtd_directory (xsd_dtd_directory)
211         {
212                 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
213                  * found without being here.
214                  */
215                 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
216                 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
217                 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
218                 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
219                 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
220                 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
221                 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
222                 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
223                 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
224                 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
225                 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
226                 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
227                 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
228         }
229
230         InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
231         {
232                 if (!system_id) {
233                         return 0;
234                 }
235                 auto system_id_str = xml_ch_to_string (system_id);
236                 auto p = _xsd_dtd_directory;
237                 if (_files.find(system_id_str) == _files.end()) {
238                         p /= system_id_str;
239                 } else {
240                         p /= _files[system_id_str];
241                 }
242                 StringToXMLCh ch (p.string());
243                 return new LocalFileInputSource(ch.get());
244         }
245
246 private:
247         void add (string uri, string file)
248         {
249                 _files[uri] = file;
250         }
251
252         std::map<string, string> _files;
253         boost::filesystem::path _xsd_dtd_directory;
254 };
255
256
257 static void
258 parse (XercesDOMParser& parser, boost::filesystem::path xml)
259 {
260         parser.parse(xml.string().c_str());
261 }
262
263
264 static void
265 parse (XercesDOMParser& parser, string xml)
266 {
267         xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
268         parser.parse(buf);
269 }
270
271
272 template <class T>
273 void
274 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
275 {
276         try {
277                 XMLPlatformUtils::Initialize ();
278         } catch (XMLException& e) {
279                 throw MiscError ("Failed to initialise xerces library");
280         }
281
282         DCPErrorHandler error_handler;
283
284         /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
285         {
286                 XercesDOMParser parser;
287                 parser.setValidationScheme(XercesDOMParser::Val_Always);
288                 parser.setDoNamespaces(true);
289                 parser.setDoSchema(true);
290
291                 vector<string> schema;
292                 schema.push_back("xml.xsd");
293                 schema.push_back("xmldsig-core-schema.xsd");
294                 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
295                 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
296                 schema.push_back("SMPTE-429-9-2007-AM.xsd");
297                 schema.push_back("Main-Stereo-Picture-CPL.xsd");
298                 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
299                 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
300                 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
301                 schema.push_back("DCSubtitle.v1.mattsson.xsd");
302                 schema.push_back("DCDMSubtitle-2010.xsd");
303                 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
304                 schema.push_back("SMPTE-429-16.xsd");
305                 schema.push_back("Dolby-2012-AD.xsd");
306                 schema.push_back("SMPTE-429-10-2008.xsd");
307                 schema.push_back("xlink.xsd");
308                 schema.push_back("SMPTE-335-2012.xsd");
309                 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
310                 schema.push_back("isdcf-mca.xsd");
311                 schema.push_back("SMPTE-429-12-2008.xsd");
312
313                 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
314                  * Schemas that are not mentioned in this list are not read, and the things
315                  * they describe are not checked.
316                  */
317                 string locations;
318                 for (auto i: schema) {
319                         locations += String::compose("%1 %1 ", i, i);
320                 }
321
322                 parser.setExternalSchemaLocation(locations.c_str());
323                 parser.setValidationSchemaFullChecking(true);
324                 parser.setErrorHandler(&error_handler);
325
326                 LocalFileResolver resolver (xsd_dtd_directory);
327                 parser.setEntityResolver(&resolver);
328
329                 try {
330                         parser.resetDocumentPool();
331                         parse(parser, xml);
332                 } catch (XMLException& e) {
333                         throw MiscError(xml_ch_to_string(e.getMessage()));
334                 } catch (DOMException& e) {
335                         throw MiscError(xml_ch_to_string(e.getMessage()));
336                 } catch (...) {
337                         throw MiscError("Unknown exception from xerces");
338                 }
339         }
340
341         XMLPlatformUtils::Terminate ();
342
343         for (auto i: error_handler.errors()) {
344                 notes.push_back (
345                         VerificationNote(
346                                 VerificationNote::VERIFY_ERROR,
347                                 VerificationNote::XML_VALIDATION_ERROR,
348                                 i.message(),
349                                 boost::trim_copy(i.public_id() + " " + i.system_id()),
350                                 i.line()
351                                 )
352                         );
353         }
354 }
355
356
357 enum VerifyAssetResult {
358         VERIFY_ASSET_RESULT_GOOD,
359         VERIFY_ASSET_RESULT_CPL_PKL_DIFFER,
360         VERIFY_ASSET_RESULT_BAD
361 };
362
363
364 static VerifyAssetResult
365 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
366 {
367         auto const actual_hash = reel_mxf->asset_ref()->hash(progress);
368
369         auto pkls = dcp->pkls();
370         /* We've read this DCP in so it must have at least one PKL */
371         DCP_ASSERT (!pkls.empty());
372
373         auto asset = reel_mxf->asset_ref().asset();
374
375         optional<string> pkl_hash;
376         for (auto i: pkls) {
377                 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
378                 if (pkl_hash) {
379                         break;
380                 }
381         }
382
383         DCP_ASSERT (pkl_hash);
384
385         auto cpl_hash = reel_mxf->hash();
386         if (cpl_hash && *cpl_hash != *pkl_hash) {
387                 return VERIFY_ASSET_RESULT_CPL_PKL_DIFFER;
388         }
389
390         if (actual_hash != *pkl_hash) {
391                 return VERIFY_ASSET_RESULT_BAD;
392         }
393
394         return VERIFY_ASSET_RESULT_GOOD;
395 }
396
397
398 void
399 verify_language_tag (string tag, vector<VerificationNote>& notes)
400 {
401         try {
402                 dcp::LanguageTag test (tag);
403         } catch (dcp::LanguageTagError &) {
404                 notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, tag));
405         }
406 }
407
408
409 enum VerifyPictureAssetResult
410 {
411         VERIFY_PICTURE_ASSET_RESULT_GOOD,
412         VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE,
413         VERIFY_PICTURE_ASSET_RESULT_BAD,
414 };
415
416
417 int
418 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
419 {
420         return frame->size ();
421 }
422
423 int
424 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
425 {
426         return max(frame->left()->size(), frame->right()->size());
427 }
428
429
430 template <class A, class R, class F>
431 optional<VerifyPictureAssetResult>
432 verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
433 {
434         auto asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
435         if (!asset) {
436                 return optional<VerifyPictureAssetResult>();
437         }
438
439         int biggest_frame = 0;
440         auto reader = asset->start_read ();
441         auto const duration = asset->intrinsic_duration ();
442         for (int64_t i = 0; i < duration; ++i) {
443                 shared_ptr<const F> frame = reader->get_frame (i);
444                 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
445                 progress (float(i) / duration);
446         }
447
448         static const int max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
449         static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
450         if (biggest_frame > max_frame) {
451                 return VERIFY_PICTURE_ASSET_RESULT_BAD;
452         } else if (biggest_frame > risky_frame) {
453                 return VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE;
454         }
455
456         return VERIFY_PICTURE_ASSET_RESULT_GOOD;
457 }
458
459
460 static VerifyPictureAssetResult
461 verify_picture_asset (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
462 {
463         auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
464         if (!r) {
465                 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
466         }
467
468         DCP_ASSERT (r);
469         return *r;
470 }
471
472
473 static void
474 verify_main_picture_asset (
475         shared_ptr<const DCP> dcp,
476         shared_ptr<const ReelPictureAsset> reel_asset,
477         function<void (string, optional<boost::filesystem::path>)> stage,
478         function<void (float)> progress,
479         vector<VerificationNote>& notes
480         )
481 {
482         auto asset = reel_asset->asset();
483         auto const file = *asset->file();
484         stage ("Checking picture asset hash", file);
485         auto const r = verify_asset (dcp, reel_asset, progress);
486         switch (r) {
487                 case VERIFY_ASSET_RESULT_BAD:
488                         notes.push_back (
489                                 VerificationNote(
490                                         VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, file
491                                         )
492                                 );
493                         break;
494                 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
495                         notes.push_back (
496                                 VerificationNote(
497                                         VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER, file
498                                         )
499                                 );
500                         break;
501                 default:
502                         break;
503         }
504         stage ("Checking picture frame sizes", asset->file());
505         auto const pr = verify_picture_asset (reel_asset, progress);
506         switch (pr) {
507                 case VERIFY_PICTURE_ASSET_RESULT_BAD:
508                         notes.push_back (
509                                 VerificationNote(
510                                         VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES, file
511                                         )
512                                 );
513                         break;
514                 case VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_LARGE:
515                         notes.push_back (
516                                 VerificationNote(
517                                         VerificationNote::VERIFY_WARNING, VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES, file
518                                         )
519                                 );
520                         break;
521                 default:
522                         break;
523         }
524
525         /* Only flat/scope allowed by Bv2.1 */
526         if (
527                 asset->size() != dcp::Size(2048, 858) &&
528                 asset->size() != dcp::Size(1998, 1080) &&
529                 asset->size() != dcp::Size(4096, 1716) &&
530                 asset->size() != dcp::Size(3996, 2160)) {
531                 notes.push_back(
532                         VerificationNote(
533                                 VerificationNote::VERIFY_BV21_ERROR,
534                                 VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS,
535                                 String::compose("%1x%2", asset->size().width, asset->size().height),
536                                 file
537                                 )
538                         );
539         }
540
541         /* Only 24, 25, 48fps allowed for 2K */
542         if (
543                 (asset->size() == dcp::Size(2048, 858) || asset->size() == dcp::Size(1998, 1080)) &&
544                 (asset->edit_rate() != dcp::Fraction(24, 1) && asset->edit_rate() != dcp::Fraction(25, 1) && asset->edit_rate() != dcp::Fraction(48, 1))
545            ) {
546                 notes.push_back(
547                         VerificationNote(
548                                 VerificationNote::VERIFY_BV21_ERROR,
549                                 VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K,
550                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
551                                 file
552                                 )
553                         );
554         }
555
556         if (asset->size() == dcp::Size(4096, 1716) || asset->size() == dcp::Size(3996, 2160)) {
557                 /* Only 24fps allowed for 4K */
558                 if (asset->edit_rate() != dcp::Fraction(24, 1)) {
559                         notes.push_back(
560                                 VerificationNote(
561                                         VerificationNote::VERIFY_BV21_ERROR,
562                                         VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K,
563                                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
564                                         file
565                                         )
566                                 );
567                 }
568
569                 /* Only 2D allowed for 4K */
570                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
571                         notes.push_back(
572                                 VerificationNote(
573                                         VerificationNote::VERIFY_BV21_ERROR,
574                                         VerificationNote::PICTURE_ASSET_4K_3D,
575                                         file
576                                         )
577                                 );
578
579                 }
580         }
581
582 }
583
584
585 static void
586 verify_main_sound_asset (
587         shared_ptr<const DCP> dcp,
588         shared_ptr<const ReelSoundAsset> reel_asset,
589         function<void (string, optional<boost::filesystem::path>)> stage,
590         function<void (float)> progress,
591         vector<VerificationNote>& notes
592         )
593 {
594         auto asset = reel_asset->asset();
595         stage ("Checking sound asset hash", asset->file());
596         auto const r = verify_asset (dcp, reel_asset, progress);
597         switch (r) {
598                 case VERIFY_ASSET_RESULT_BAD:
599                         notes.push_back (
600                                 VerificationNote(
601                                         VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *asset->file()
602                                         )
603                                 );
604                         break;
605                 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
606                         notes.push_back (
607                                 VerificationNote(
608                                         VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER, *asset->file()
609                                         )
610                                 );
611                         break;
612                 default:
613                         break;
614         }
615
616         stage ("Checking sound asset metadata", asset->file());
617
618         verify_language_tag (asset->language(), notes);
619         if (asset->sampling_rate() != 48000) {
620                 notes.push_back (
621                         VerificationNote(
622                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_SOUND_FRAME_RATE, *asset->file()
623                                 )
624                         );
625         }
626 }
627
628
629 static void
630 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
631 {
632         /* XXX: is Language compulsory? */
633         if (reel_asset->language()) {
634                 verify_language_tag (*reel_asset->language(), notes);
635         }
636
637         if (!reel_asset->entry_point()) {
638                 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_ENTRY_POINT });
639         } else if (reel_asset->entry_point().get()) {
640                 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO });
641         }
642 }
643
644
645 static void
646 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
647 {
648         /* XXX: is Language compulsory? */
649         if (reel_asset->language()) {
650                 verify_language_tag (*reel_asset->language(), notes);
651         }
652
653         if (!reel_asset->entry_point()) {
654                 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT });
655         } else if (reel_asset->entry_point().get()) {
656                 notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO });
657         }
658 }
659
660
661 struct State
662 {
663         boost::optional<string> subtitle_language;
664 };
665
666
667
668 void
669 verify_smpte_subtitle_asset (
670         shared_ptr<const dcp::SMPTESubtitleAsset> asset,
671         vector<VerificationNote>& notes,
672         State& state
673         )
674 {
675         if (asset->language()) {
676                 auto const language = *asset->language();
677                 verify_language_tag (language, notes);
678                 if (!state.subtitle_language) {
679                         state.subtitle_language = language;
680                 } else if (state.subtitle_language != language) {
681                         notes.push_back (
682                                 VerificationNote(
683                                         VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_LANGUAGES_DIFFER, *asset->file()
684                                         )
685                                 );
686                 }
687         } else {
688                 notes.push_back (
689                         VerificationNote(
690                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_LANGUAGE, *asset->file()
691                                 )
692                         );
693         }
694         if (boost::filesystem::file_size(*asset->file()) > 115 * 1024 * 1024) {
695                 notes.push_back (
696                         VerificationNote(
697                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES, *asset->file()
698                                 )
699                         );
700         }
701         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
702          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
703          */
704         auto fonts = asset->font_data ();
705         int total_size = 0;
706         for (auto i: fonts) {
707                 total_size += i.second.size();
708         }
709         if (total_size > 10 * 1024 * 1024) {
710                 notes.push_back (
711                         VerificationNote(
712                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES, *asset->file()
713                                 )
714                         );
715         }
716
717         if (!asset->start_time()) {
718                 notes.push_back (
719                         VerificationNote(
720                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_START_TIME, *asset->file())
721                         );
722         } else if (asset->start_time() != dcp::Time()) {
723                 notes.push_back (
724                         VerificationNote(
725                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_START_TIME_NON_ZERO, *asset->file())
726                         );
727         }
728 }
729
730
731 static void
732 verify_subtitle_asset (
733         shared_ptr<const SubtitleAsset> asset,
734         function<void (string, optional<boost::filesystem::path>)> stage,
735         boost::filesystem::path xsd_dtd_directory,
736         vector<VerificationNote>& notes,
737         State& state
738         )
739 {
740         stage ("Checking subtitle XML", asset->file());
741         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
742          * gets passed through libdcp which may clean up and therefore hide errors.
743          */
744         validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
745
746         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
747         if (smpte) {
748                 verify_smpte_subtitle_asset (smpte, notes, state);
749         }
750 }
751
752
753 static void
754 verify_closed_caption_asset (
755         shared_ptr<const SubtitleAsset> asset,
756         function<void (string, optional<boost::filesystem::path>)> stage,
757         boost::filesystem::path xsd_dtd_directory,
758         vector<VerificationNote>& notes,
759         State& state
760         )
761 {
762         verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
763
764         if (asset->raw_xml().size() > 256 * 1024) {
765                 notes.push_back (
766                         VerificationNote(
767                                 VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES, *asset->file()
768                                 )
769                         );
770         }
771 }
772
773
774 static
775 void
776 check_text_timing (
777         vector<shared_ptr<dcp::Reel>> reels,
778         optional<int> picture_frame_rate,
779         vector<VerificationNote>& notes,
780         std::function<bool (shared_ptr<dcp::Reel>)> check,
781         std::function<string (shared_ptr<dcp::Reel>)> xml,
782         std::function<int64_t (shared_ptr<dcp::Reel>)> duration
783         )
784 {
785         /* end of last subtitle (in editable units) */
786         optional<int64_t> last_out;
787         auto too_short = false;
788         auto too_close = false;
789         auto too_early = false;
790         /* current reel start time (in editable units) */
791         int64_t reel_offset = 0;
792
793         std::function<void (cxml::ConstNodePtr, int, int, bool)> parse;
794         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) {
795                 if (node->name() == "Subtitle") {
796                         dcp::Time in (node->string_attribute("TimeIn"), tcr);
797                         dcp::Time out (node->string_attribute("TimeOut"), tcr);
798                         if (first_reel && in < dcp::Time(0, 0, 4, 0, tcr)) {
799                                 too_early = true;
800                         }
801                         auto length = out - in;
802                         if (length.as_editable_units(pfr) < 15) {
803                                 too_short = true;
804                         }
805                         if (last_out) {
806                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
807                                 auto distance = reel_offset + in.as_editable_units(pfr) - *last_out;
808                                 if (distance >= 0 && distance < 2) {
809                                         too_close = true;
810                                 }
811                         }
812                         last_out = reel_offset + out.as_editable_units(pfr);
813                 } else {
814                         for (auto i: node->node_children()) {
815                                 parse(i, tcr, pfr, first_reel);
816                         }
817                 }
818         };
819
820         for (auto i = 0U; i < reels.size(); ++i) {
821                 if (!check(reels[i])) {
822                         continue;
823                 }
824
825                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
826                  * read in by libdcp's parser.
827                  */
828
829                 auto doc = make_shared<cxml::Document>("SubtitleReel");
830                 doc->read_string (xml(reels[i]));
831                 auto const tcr = doc->number_child<int>("TimeCodeRate");
832                 parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
833                 reel_offset += duration(reels[i]);
834         }
835
836         if (too_early) {
837                 notes.push_back(
838                         VerificationNote(
839                                 VerificationNote::VERIFY_WARNING, VerificationNote::FIRST_TEXT_TOO_EARLY
840                                 )
841                         );
842         }
843
844         if (too_short) {
845                 notes.push_back (
846                         VerificationNote(
847                                 VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_SHORT
848                                 )
849                         );
850         }
851
852         if (too_close) {
853                 notes.push_back (
854                         VerificationNote(
855                                 VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_TOO_CLOSE
856                                 )
857                         );
858         }
859 }
860
861
862 struct LinesCharactersResult
863 {
864         bool warning_length_exceeded = false;
865         bool error_length_exceeded = false;
866         bool line_count_exceeded = false;
867 };
868
869
870 static
871 void
872 check_text_lines_and_characters (
873         shared_ptr<SubtitleAsset> asset,
874         int warning_length,
875         int error_length,
876         LinesCharactersResult* result
877         )
878 {
879         class Event
880         {
881         public:
882                 Event (dcp::Time time_, float position_, int characters_)
883                         : time (time_)
884                         , position (position_)
885                         , characters (characters_)
886                 {}
887
888                 Event (dcp::Time time_, shared_ptr<Event> start_)
889                         : time (time_)
890                         , start (start_)
891                 {}
892
893                 dcp::Time time;
894                 int position; //< position from 0 at top of screen to 100 at bottom
895                 int characters;
896                 shared_ptr<Event> start;
897         };
898
899         vector<shared_ptr<Event>> events;
900
901         auto position = [](shared_ptr<const SubtitleString> sub) {
902                 switch (sub->v_align()) {
903                 case VALIGN_TOP:
904                         return lrintf(sub->v_position() * 100);
905                 case VALIGN_CENTER:
906                         return lrintf((0.5f + sub->v_position()) * 100);
907                 case VALIGN_BOTTOM:
908                         return lrintf((1.0f - sub->v_position()) * 100);
909                 }
910
911                 return 0L;
912         };
913
914         for (auto j: asset->subtitles()) {
915                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
916                 if (text) {
917                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
918                         events.push_back(in);
919                         events.push_back(make_shared<Event>(text->out(), in));
920                 }
921         }
922
923         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
924                 return a->time < b->time;
925         });
926
927         map<int, int> current;
928         for (auto i: events) {
929                 if (current.size() > 3) {
930                         result->line_count_exceeded = true;
931                 }
932                 for (auto j: current) {
933                         if (j.second >= warning_length) {
934                                 result->warning_length_exceeded = true;
935                         }
936                         if (j.second >= error_length) {
937                                 result->error_length_exceeded = true;
938                         }
939                 }
940
941                 if (i->start) {
942                         /* end of a subtitle */
943                         DCP_ASSERT (current.find(i->start->position) != current.end());
944                         if (current[i->start->position] == i->start->characters) {
945                                 current.erase(i->start->position);
946                         } else {
947                                 current[i->start->position] -= i->start->characters;
948                         }
949                 } else {
950                         /* start of a subtitle */
951                         if (current.find(i->position) == current.end()) {
952                                 current[i->position] = i->characters;
953                         } else {
954                                 current[i->position] += i->characters;
955                         }
956                 }
957         }
958 }
959
960
961 static
962 void
963 check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
964 {
965         if (reels.empty()) {
966                 return;
967         }
968
969         optional<int> picture_frame_rate;
970         if (reels[0]->main_picture()) {
971                 picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
972         }
973
974         if (reels[0]->main_subtitle()) {
975                 check_text_timing (reels, picture_frame_rate, notes,
976                         [](shared_ptr<dcp::Reel> reel) {
977                                 return static_cast<bool>(reel->main_subtitle());
978                         },
979                         [](shared_ptr<dcp::Reel> reel) {
980                                 return reel->main_subtitle()->asset()->raw_xml();
981                         },
982                         [](shared_ptr<dcp::Reel> reel) {
983                                 return reel->main_subtitle()->actual_duration();
984                         }
985                 );
986         }
987
988         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
989                 check_text_timing (reels, picture_frame_rate, notes,
990                         [i](shared_ptr<dcp::Reel> reel) {
991                                 return i < reel->closed_captions().size();
992                         },
993                         [i](shared_ptr<dcp::Reel> reel) {
994                                 return reel->closed_captions()[i]->asset()->raw_xml();
995                         },
996                         [i](shared_ptr<dcp::Reel> reel) {
997                                 return reel->closed_captions()[i]->actual_duration();
998                         }
999                 );
1000         }
1001 }
1002
1003
1004 vector<VerificationNote>
1005 dcp::verify (
1006         vector<boost::filesystem::path> directories,
1007         function<void (string, optional<boost::filesystem::path>)> stage,
1008         function<void (float)> progress,
1009         boost::filesystem::path xsd_dtd_directory
1010         )
1011 {
1012         xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
1013
1014         vector<VerificationNote> notes;
1015         State state;
1016
1017         vector<shared_ptr<DCP>> dcps;
1018         for (auto i: directories) {
1019                 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
1020         }
1021
1022         for (auto dcp: dcps) {
1023                 stage ("Checking DCP", dcp->directory());
1024                 try {
1025                         dcp->read (&notes);
1026                 } catch (ReadError& e) {
1027                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1028                 } catch (XMLError& e) {
1029                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1030                 } catch (MXFFileError& e) {
1031                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1032                 } catch (cxml::Error& e) {
1033                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
1034                 }
1035
1036                 if (dcp->standard() != dcp::SMPTE) {
1037                         notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::NOT_SMPTE));
1038                 }
1039
1040                 for (auto cpl: dcp->cpls()) {
1041                         stage ("Checking CPL", cpl->file());
1042                         validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
1043
1044                         for (auto const& i: cpl->additional_subtitle_languages()) {
1045                                 verify_language_tag (i, notes);
1046                         }
1047
1048                         if (cpl->release_territory()) {
1049                                 verify_language_tag (cpl->release_territory().get(), notes);
1050                         }
1051
1052                         if (dcp->standard() == dcp::SMPTE) {
1053                                 if (!cpl->annotation_text()) {
1054                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL));
1055                                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1056                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT));
1057                                 }
1058                         }
1059
1060                         /* Check that the CPL's hash corresponds to the PKL */
1061                         for (auto i: dcp->pkls()) {
1062                                 optional<string> h = i->hash(cpl->id());
1063                                 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1064                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
1065                                 }
1066                         }
1067
1068                         /* set to true if any reel has a MainSubtitle */
1069                         auto have_main_subtitle = false;
1070                         /* set to true if any reel has no MainSubtitle */
1071                         auto have_no_main_subtitle = false;
1072                         /* fewest number of closed caption assets seen in a reel */
1073                         size_t fewest_closed_captions = SIZE_MAX;
1074                         /* most number of closed caption assets seen in a reel */
1075                         size_t most_closed_captions = 0;
1076
1077                         for (auto reel: cpl->reels()) {
1078                                 stage ("Checking reel", optional<boost::filesystem::path>());
1079
1080                                 for (auto i: reel->assets()) {
1081                                         if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1082                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::DURATION_TOO_SMALL, i->id()));
1083                                         }
1084                                         if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1085                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id()));
1086                                         }
1087                                 }
1088
1089                                 if (dcp->standard() == dcp::SMPTE) {
1090                                         boost::optional<int64_t> duration;
1091                                         for (auto i: reel->assets()) {
1092                                                 if (!duration) {
1093                                                         duration = i->actual_duration();
1094                                                 } else if (*duration != i->actual_duration()) {
1095                                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISMATCHED_ASSET_DURATION, i->id()));
1096                                                         break;
1097                                                 }
1098                                         }
1099                                 }
1100
1101                                 if (reel->main_picture()) {
1102                                         /* Check reel stuff */
1103                                         auto const frame_rate = reel->main_picture()->frame_rate();
1104                                         if (frame_rate.denominator != 1 ||
1105                                             (frame_rate.numerator != 24 &&
1106                                              frame_rate.numerator != 25 &&
1107                                              frame_rate.numerator != 30 &&
1108                                              frame_rate.numerator != 48 &&
1109                                              frame_rate.numerator != 50 &&
1110                                              frame_rate.numerator != 60 &&
1111                                              frame_rate.numerator != 96)) {
1112                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
1113                                         }
1114                                         /* Check asset */
1115                                         if (reel->main_picture()->asset_ref().resolved()) {
1116                                                 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1117                                         }
1118                                 }
1119
1120                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1121                                         verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1122                                 }
1123
1124                                 if (reel->main_subtitle()) {
1125                                         verify_main_subtitle_reel (reel->main_subtitle(), notes);
1126                                         if (reel->main_subtitle()->asset_ref().resolved()) {
1127                                                 verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
1128                                         }
1129                                         have_main_subtitle = true;
1130                                 } else {
1131                                         have_no_main_subtitle = true;
1132                                 }
1133
1134                                 for (auto i: reel->closed_captions()) {
1135                                         verify_closed_caption_reel (i, notes);
1136                                         if (i->asset_ref().resolved()) {
1137                                                 verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
1138                                         }
1139                                 }
1140
1141                                 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1142                                 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1143                         }
1144
1145                         if (dcp->standard() == dcp::SMPTE) {
1146
1147                                 if (have_main_subtitle && have_no_main_subtitle) {
1148                                         notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS});
1149                                 }
1150
1151                                 if (fewest_closed_captions != most_closed_captions) {
1152                                         notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER});
1153                                 }
1154
1155                                 check_text_timing (cpl->reels(), notes);
1156
1157                                 LinesCharactersResult result;
1158                                 for (auto reel: cpl->reels()) {
1159                                         if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1160                                                 check_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1161                                         }
1162                                 }
1163
1164                                 if (result.line_count_exceeded) {
1165                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES));
1166                                 }
1167                                 if (result.error_length_exceeded) {
1168                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG));
1169                                 } else if (result.warning_length_exceeded) {
1170                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED));
1171                                 }
1172
1173                                 result = LinesCharactersResult();
1174                                 for (auto reel: cpl->reels()) {
1175                                         for (auto i: reel->closed_captions()) {
1176                                                 if (i->asset()) {
1177                                                         check_text_lines_and_characters (i->asset(), 32, 32, &result);
1178                                                 }
1179                                         }
1180                                 }
1181
1182                                 if (result.line_count_exceeded) {
1183                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES));
1184                                 }
1185                                 if (result.error_length_exceeded) {
1186                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG));
1187                                 }
1188                         }
1189                 }
1190
1191                 for (auto pkl: dcp->pkls()) {
1192                         stage ("Checking PKL", pkl->file());
1193                         validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
1194                 }
1195
1196                 if (dcp->asset_map_path()) {
1197                         stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1198                         validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
1199                 } else {
1200                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSETMAP));
1201                 }
1202         }
1203
1204         return notes;
1205 }
1206
1207 string
1208 dcp::note_to_string (dcp::VerificationNote note)
1209 {
1210         switch (note.code()) {
1211         case dcp::VerificationNote::GENERAL_READ:
1212                 return *note.note();
1213         case dcp::VerificationNote::CPL_HASH_INCORRECT:
1214                 return "The hash of the CPL in the PKL does not agree with the CPL file.";
1215         case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
1216                 return "The picture in a reel has an invalid frame rate.";
1217         case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
1218                 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1219         case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DIFFER:
1220                 return dcp::String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1221         case dcp::VerificationNote::SOUND_HASH_INCORRECT:
1222                 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1223         case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DIFFER:
1224                 return dcp::String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1225         case dcp::VerificationNote::EMPTY_ASSET_PATH:
1226                 return "The asset map contains an empty asset path.";
1227         case dcp::VerificationNote::MISSING_ASSET:
1228                 return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename());
1229         case dcp::VerificationNote::MISMATCHED_STANDARD:
1230                 return "The DCP contains both SMPTE and Interop parts.";
1231         case dcp::VerificationNote::XML_VALIDATION_ERROR:
1232                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1233         case dcp::VerificationNote::MISSING_ASSETMAP:
1234                 return "No ASSETMAP or ASSETMAP.xml was found.";
1235         case dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL:
1236                 return String::compose("The intrinsic duration of an asset is less than 1 second long: %1", note.note().get());
1237         case dcp::VerificationNote::DURATION_TOO_SMALL:
1238                 return String::compose("The duration of an asset is less than 1 second long: %1", note.note().get());
1239         case dcp::VerificationNote::PICTURE_FRAME_TOO_LARGE_IN_BYTES:
1240                 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());
1241         case dcp::VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE_IN_BYTES:
1242                 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());
1243         case dcp::VerificationNote::EXTERNAL_ASSET:
1244                 return String::compose("An asset that this DCP refers to is not included in the DCP.  It may be a VF.  Missing asset is %1.", note.note().get());
1245         case dcp::VerificationNote::NOT_SMPTE:
1246                 return "This DCP does not use the SMPTE standard, which is required for Bv2.1 compliance.";
1247         case dcp::VerificationNote::BAD_LANGUAGE:
1248                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1249         case dcp::VerificationNote::PICTURE_ASSET_INVALID_SIZE_IN_PIXELS:
1250                 return String::compose("A picture asset's size (%1) is not one of those allowed by Bv2.1 (2048x858, 1998x1080, 4096x1716 or 3996x2160)", note.note().get());
1251         case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_2K:
1252                 return String::compose("A picture asset's frame rate (%1) is not one of those allowed for 2K DCPs by Bv2.1 (24, 25 or 48fps)", note.note().get());
1253         case dcp::VerificationNote::PICTURE_ASSET_INVALID_FRAME_RATE_FOR_4K:
1254                 return String::compose("A picture asset's frame rate (%1) is not 24fps as required for 4K DCPs by Bv2.1", note.note().get());
1255         case dcp::VerificationNote::PICTURE_ASSET_4K_3D:
1256                 return "3D 4K DCPs are not allowed by Bv2.1";
1257         case dcp::VerificationNote::CLOSED_CAPTION_XML_TOO_LARGE_IN_BYTES:
1258                 return String::compose("The XML for the closed caption asset %1 is longer than the 256KB maximum required by Bv2.1", note.file()->filename());
1259         case dcp::VerificationNote::TIMED_TEXT_ASSET_TOO_LARGE_IN_BYTES:
1260                 return String::compose("The total size of the timed text asset %1 is larger than the 115MB maximum required by Bv2.1", note.file()->filename());
1261         case dcp::VerificationNote::TIMED_TEXT_FONTS_TOO_LARGE_IN_BYTES:
1262                 return String::compose("The total size of the fonts in timed text asset %1 is larger than the 10MB maximum required by Bv2.1", note.file()->filename());
1263         case dcp::VerificationNote::MISSING_SUBTITLE_LANGUAGE:
1264                 return String::compose("The XML for a SMPTE subtitle asset has no <Language> tag, which is required by Bv2.1", note.file()->filename());
1265         case dcp::VerificationNote::SUBTITLE_LANGUAGES_DIFFER:
1266                 return String::compose("Some subtitle assets have different <Language> tags than others", note.file()->filename());
1267         case dcp::VerificationNote::MISSING_SUBTITLE_START_TIME:
1268                 return String::compose("The XML for a SMPTE subtitle asset has no <StartTime> tag, which is required by Bv2.1", note.file()->filename());
1269         case dcp::VerificationNote::SUBTITLE_START_TIME_NON_ZERO:
1270                 return String::compose("The XML for a SMPTE subtitle asset has a non-zero <StartTime> tag, which is disallowed by Bv2.1", note.file()->filename());
1271         case dcp::VerificationNote::FIRST_TEXT_TOO_EARLY:
1272                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1273         case dcp::VerificationNote::SUBTITLE_TOO_SHORT:
1274                 return "At least one subtitle is less than the minimum of 15 frames suggested by Bv2.1";
1275         case dcp::VerificationNote::SUBTITLE_TOO_CLOSE:
1276                 return "At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by Bv2.1";
1277         case dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES:
1278                 return "There are more than 3 subtitle lines in at least one place in the DCP, which Bv2.1 advises against.";
1279         case dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED:
1280                 return "There are more than 52 characters in at least one subtitle line, which Bv2.1 advises against.";
1281         case dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG:
1282                 return "There are more than 79 characters in at least one subtitle line, which Bv2.1 strongly advises against.";
1283         case dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES:
1284                 return "There are more than 3 closed caption lines in at least one place, which is disallowed by Bv2.1";
1285         case dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG:
1286                 return "There are more than 32 characters in at least one closed caption line, which is disallowed by Bv2.1";
1287         case dcp::VerificationNote::INVALID_SOUND_FRAME_RATE:
1288                 return "A sound asset has a sampling rate other than 48kHz, which is disallowed by Bv2.1";
1289         case dcp::VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL:
1290                 return "The CPL has no <AnnotationText> tag, which is required by Bv2.1";
1291         case dcp::VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT:
1292                 return "The CPL's <AnnotationText> differs from its <ContentTitleText>, which Bv2.1 advises against.";
1293         case dcp::VerificationNote::MISMATCHED_ASSET_DURATION:
1294                 return "All assets in a reel do not have the same duration, which is required by Bv2.1";
1295         case dcp::VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS:
1296                 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1297         case dcp::VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER:
1298                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1299         case dcp::VerificationNote::MISSING_SUBTITLE_ENTRY_POINT:
1300                 return "Subtitle assets must have an <EntryPoint> tag.";
1301         case dcp::VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO:
1302                 return "Subtitle assets must have an <EntryPoint> of 0.";
1303         case dcp::VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1304                 return "Closed caption assets must have an <EntryPoint> tag.";
1305         case dcp::VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO:
1306                 return "Closed caption assets must have an <EntryPoint> of 0.";
1307         }
1308
1309         return "";
1310 }