2 Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
35 /** @file src/verify.cc
36 * @brief dcp::verify() method and associated code
40 #include "compose.hpp"
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"
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"
60 #include "verify_j2k.h"
61 #include <xercesc/dom/DOMAttr.hpp>
62 #include <xercesc/dom/DOMDocument.hpp>
63 #include <xercesc/dom/DOMError.hpp>
64 #include <xercesc/dom/DOMErrorHandler.hpp>
65 #include <xercesc/dom/DOMException.hpp>
66 #include <xercesc/dom/DOMImplementation.hpp>
67 #include <xercesc/dom/DOMImplementationLS.hpp>
68 #include <xercesc/dom/DOMImplementationRegistry.hpp>
69 #include <xercesc/dom/DOMLSParser.hpp>
70 #include <xercesc/dom/DOMLocator.hpp>
71 #include <xercesc/dom/DOMNamedNodeMap.hpp>
72 #include <xercesc/dom/DOMNodeList.hpp>
73 #include <xercesc/framework/LocalFileInputSource.hpp>
74 #include <xercesc/framework/MemBufInputSource.hpp>
75 #include <xercesc/parsers/AbstractDOMParser.hpp>
76 #include <xercesc/parsers/XercesDOMParser.hpp>
77 #include <xercesc/sax/HandlerBase.hpp>
78 #include <xercesc/util/PlatformUtils.hpp>
79 #include <boost/algorithm/string.hpp>
87 using std::dynamic_pointer_cast;
89 using std::make_shared;
93 using std::shared_ptr;
96 using boost::optional;
97 using boost::function;
101 using namespace xercesc;
106 xml_ch_to_string (XMLCh const * a)
108 char* x = XMLString::transcode(a);
110 XMLString::release(&x);
115 class XMLValidationError
118 XMLValidationError (SAXParseException const & e)
119 : _message (xml_ch_to_string(e.getMessage()))
120 , _line (e.getLineNumber())
121 , _column (e.getColumnNumber())
122 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
123 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
128 string message () const {
132 uint64_t line () const {
136 uint64_t column () const {
140 string public_id () const {
144 string system_id () const {
157 class DCPErrorHandler : public ErrorHandler
160 void warning(const SAXParseException& e) override
162 maybe_add (XMLValidationError(e));
165 void error(const SAXParseException& e) override
167 maybe_add (XMLValidationError(e));
170 void fatalError(const SAXParseException& e) override
172 maybe_add (XMLValidationError(e));
175 void resetErrors() override {
179 list<XMLValidationError> errors () const {
184 void maybe_add (XMLValidationError e)
186 /* XXX: nasty hack */
188 e.message().find("schema document") != string::npos &&
189 e.message().find("has different target namespace from the one specified in instance document") != string::npos
194 _errors.push_back (e);
197 list<XMLValidationError> _errors;
204 StringToXMLCh (string a)
206 _buffer = XMLString::transcode(a.c_str());
209 StringToXMLCh (StringToXMLCh const&) = delete;
210 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
214 XMLString::release (&_buffer);
217 XMLCh const * get () const {
226 class LocalFileResolver : public EntityResolver
229 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
230 : _xsd_dtd_directory (xsd_dtd_directory)
232 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
233 * found without being here.
235 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
236 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
237 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
238 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
239 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
240 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
243 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
244 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
245 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
246 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
247 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
250 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
255 auto system_id_str = xml_ch_to_string (system_id);
256 auto p = _xsd_dtd_directory;
257 if (_files.find(system_id_str) == _files.end()) {
260 p /= _files[system_id_str];
262 StringToXMLCh ch (p.string());
263 return new LocalFileInputSource(ch.get());
267 void add (string uri, string file)
272 std::map<string, string> _files;
273 boost::filesystem::path _xsd_dtd_directory;
278 parse (XercesDOMParser& parser, boost::filesystem::path xml)
280 parser.parse(xml.string().c_str());
285 parse (XercesDOMParser& parser, string xml)
287 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
294 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
297 XMLPlatformUtils::Initialize ();
298 } catch (XMLException& e) {
299 throw MiscError ("Failed to initialise xerces library");
302 DCPErrorHandler error_handler;
304 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
306 XercesDOMParser parser;
307 parser.setValidationScheme(XercesDOMParser::Val_Always);
308 parser.setDoNamespaces(true);
309 parser.setDoSchema(true);
311 vector<string> schema;
312 schema.push_back("xml.xsd");
313 schema.push_back("xmldsig-core-schema.xsd");
314 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
315 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
316 schema.push_back("SMPTE-429-9-2007-AM.xsd");
317 schema.push_back("Main-Stereo-Picture-CPL.xsd");
318 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
319 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
320 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
321 schema.push_back("DCSubtitle.v1.mattsson.xsd");
322 schema.push_back("DCDMSubtitle-2010.xsd");
323 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
324 schema.push_back("SMPTE-429-16.xsd");
325 schema.push_back("Dolby-2012-AD.xsd");
326 schema.push_back("SMPTE-429-10-2008.xsd");
327 schema.push_back("xlink.xsd");
328 schema.push_back("SMPTE-335-2012.xsd");
329 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
330 schema.push_back("isdcf-mca.xsd");
331 schema.push_back("SMPTE-429-12-2008.xsd");
333 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
334 * Schemas that are not mentioned in this list are not read, and the things
335 * they describe are not checked.
338 for (auto i: schema) {
339 locations += String::compose("%1 %1 ", i, i);
342 parser.setExternalSchemaLocation(locations.c_str());
343 parser.setValidationSchemaFullChecking(true);
344 parser.setErrorHandler(&error_handler);
346 LocalFileResolver resolver (xsd_dtd_directory);
347 parser.setEntityResolver(&resolver);
350 parser.resetDocumentPool();
352 } catch (XMLException& e) {
353 throw MiscError(xml_ch_to_string(e.getMessage()));
354 } catch (DOMException& e) {
355 throw MiscError(xml_ch_to_string(e.getMessage()));
357 throw MiscError("Unknown exception from xerces");
361 XMLPlatformUtils::Terminate ();
363 for (auto i: error_handler.errors()) {
365 VerificationNote::Type::ERROR,
366 VerificationNote::Code::INVALID_XML,
368 boost::trim_copy(i.public_id() + " " + i.system_id()),
375 enum class VerifyAssetResult {
382 static VerifyAssetResult
383 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
385 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
387 auto pkls = dcp->pkls();
388 /* We've read this DCP in so it must have at least one PKL */
389 DCP_ASSERT (!pkls.empty());
391 auto asset = reel_file_asset->asset_ref().asset();
393 optional<string> pkl_hash;
395 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
401 DCP_ASSERT (pkl_hash);
403 auto cpl_hash = reel_file_asset->hash();
404 if (cpl_hash && *cpl_hash != *pkl_hash) {
405 return VerifyAssetResult::CPL_PKL_DIFFER;
408 if (actual_hash != *pkl_hash) {
409 return VerifyAssetResult::BAD;
412 return VerifyAssetResult::GOOD;
417 verify_language_tag (string tag, vector<VerificationNote>& notes)
420 LanguageTag test (tag);
421 } catch (LanguageTagError &) {
422 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
428 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
430 int biggest_frame = 0;
431 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
432 auto const duration = asset->intrinsic_duration ();
434 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
435 for (auto i: j2k_notes) {
436 if (find(notes.begin(), notes.end(), i) == notes.end()) {
442 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
443 auto reader = mono_asset->start_read ();
444 for (int64_t i = 0; i < duration; ++i) {
445 auto frame = reader->get_frame (i);
446 biggest_frame = max(biggest_frame, frame->size());
447 if (!mono_asset->encrypted() || mono_asset->key()) {
448 vector<VerificationNote> j2k_notes;
449 verify_j2k (frame, j2k_notes);
450 check_and_add (j2k_notes);
452 progress (float(i) / duration);
454 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
455 auto reader = stereo_asset->start_read ();
456 for (int64_t i = 0; i < duration; ++i) {
457 auto frame = reader->get_frame (i);
458 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
459 if (!stereo_asset->encrypted() || stereo_asset->key()) {
460 vector<VerificationNote> j2k_notes;
461 verify_j2k (frame->left(), j2k_notes);
462 verify_j2k (frame->right(), j2k_notes);
463 check_and_add (j2k_notes);
465 progress (float(i) / duration);
470 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
471 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
472 if (biggest_frame > max_frame) {
474 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
476 } else if (biggest_frame > risky_frame) {
478 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
485 verify_main_picture_asset (
486 shared_ptr<const DCP> dcp,
487 shared_ptr<const ReelPictureAsset> reel_asset,
488 function<void (string, optional<boost::filesystem::path>)> stage,
489 function<void (float)> progress,
490 vector<VerificationNote>& notes
493 auto asset = reel_asset->asset();
494 auto const file = *asset->file();
495 stage ("Checking picture asset hash", file);
496 auto const r = verify_asset (dcp, reel_asset, progress);
498 case VerifyAssetResult::BAD:
500 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
503 case VerifyAssetResult::CPL_PKL_DIFFER:
505 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
511 stage ("Checking picture frame sizes", asset->file());
512 verify_picture_asset (reel_asset, file, notes, progress);
514 /* Only flat/scope allowed by Bv2.1 */
516 asset->size() != Size(2048, 858) &&
517 asset->size() != Size(1998, 1080) &&
518 asset->size() != Size(4096, 1716) &&
519 asset->size() != Size(3996, 2160)) {
521 VerificationNote::Type::BV21_ERROR,
522 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
523 String::compose("%1x%2", asset->size().width, asset->size().height),
528 /* Only 24, 25, 48fps allowed for 2K */
530 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
531 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
534 VerificationNote::Type::BV21_ERROR,
535 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
536 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
541 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
542 /* Only 24fps allowed for 4K */
543 if (asset->edit_rate() != Fraction(24, 1)) {
545 VerificationNote::Type::BV21_ERROR,
546 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
547 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
552 /* Only 2D allowed for 4K */
553 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
555 VerificationNote::Type::BV21_ERROR,
556 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
557 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
568 verify_main_sound_asset (
569 shared_ptr<const DCP> dcp,
570 shared_ptr<const ReelSoundAsset> reel_asset,
571 function<void (string, optional<boost::filesystem::path>)> stage,
572 function<void (float)> progress,
573 vector<VerificationNote>& notes
576 auto asset = reel_asset->asset();
577 stage ("Checking sound asset hash", asset->file());
578 auto const r = verify_asset (dcp, reel_asset, progress);
580 case VerifyAssetResult::BAD:
581 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
583 case VerifyAssetResult::CPL_PKL_DIFFER:
584 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
590 stage ("Checking sound asset metadata", asset->file());
592 if (auto lang = asset->language()) {
593 verify_language_tag (*lang, notes);
595 if (asset->sampling_rate() != 48000) {
596 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
602 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
604 /* XXX: is Language compulsory? */
605 if (reel_asset->language()) {
606 verify_language_tag (*reel_asset->language(), notes);
609 if (!reel_asset->entry_point()) {
610 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
611 } else if (reel_asset->entry_point().get()) {
612 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
618 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
620 /* XXX: is Language compulsory? */
621 if (reel_asset->language()) {
622 verify_language_tag (*reel_asset->language(), notes);
625 if (!reel_asset->entry_point()) {
626 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
627 } else if (reel_asset->entry_point().get()) {
628 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
635 boost::optional<string> subtitle_language;
639 /** Verify stuff that is common to both subtitles and closed captions */
641 verify_smpte_timed_text_asset (
642 shared_ptr<const SMPTESubtitleAsset> asset,
643 optional<int64_t> reel_asset_duration,
644 vector<VerificationNote>& notes
647 if (asset->language()) {
648 verify_language_tag (*asset->language(), notes);
650 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
653 auto const size = boost::filesystem::file_size(asset->file().get());
654 if (size > 115 * 1024 * 1024) {
656 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
660 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
661 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
663 auto fonts = asset->font_data ();
665 for (auto i: fonts) {
666 total_size += i.second.size();
668 if (total_size > 10 * 1024 * 1024) {
669 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
672 if (!asset->start_time()) {
673 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
674 } else if (asset->start_time() != Time()) {
675 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
678 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
681 VerificationNote::Type::BV21_ERROR,
682 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
683 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
690 /** Verify Interop subtitle-only stuff */
692 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
694 if (asset->subtitles().empty()) {
695 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
700 /** Verify SMPTE subtitle-only stuff */
702 verify_smpte_subtitle_asset (
703 shared_ptr<const SMPTESubtitleAsset> asset,
704 vector<VerificationNote>& notes,
708 if (asset->language()) {
709 if (!state.subtitle_language) {
710 state.subtitle_language = *asset->language();
711 } else if (state.subtitle_language != *asset->language()) {
712 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
716 DCP_ASSERT (asset->resource_id());
717 auto xml_id = asset->xml_id();
719 if (asset->resource_id().get() != xml_id) {
720 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
723 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
724 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
727 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
732 /** Verify all subtitle stuff */
734 verify_subtitle_asset (
735 shared_ptr<const SubtitleAsset> asset,
736 optional<int64_t> reel_asset_duration,
737 function<void (string, optional<boost::filesystem::path>)> stage,
738 boost::filesystem::path xsd_dtd_directory,
739 vector<VerificationNote>& notes,
743 stage ("Checking subtitle XML", asset->file());
744 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
745 * gets passed through libdcp which may clean up and therefore hide errors.
747 if (asset->raw_xml()) {
748 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
750 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
753 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
755 verify_interop_subtitle_asset(interop, notes);
758 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
760 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
761 verify_smpte_subtitle_asset (smpte, notes, state);
766 /** Verify all closed caption stuff */
768 verify_closed_caption_asset (
769 shared_ptr<const SubtitleAsset> asset,
770 optional<int64_t> reel_asset_duration,
771 function<void (string, optional<boost::filesystem::path>)> stage,
772 boost::filesystem::path xsd_dtd_directory,
773 vector<VerificationNote>& notes
776 stage ("Checking closed caption XML", asset->file());
777 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
778 * gets passed through libdcp which may clean up and therefore hide errors.
780 auto raw_xml = asset->raw_xml();
782 validate_xml (*raw_xml, xsd_dtd_directory, notes);
783 if (raw_xml->size() > 256 * 1024) {
784 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
787 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
790 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
792 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
797 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
800 verify_text_details (
801 vector<shared_ptr<Reel>> reels,
803 vector<VerificationNote>& notes,
804 std::function<bool (shared_ptr<Reel>)> check,
805 std::function<optional<string> (shared_ptr<Reel>)> xml,
806 std::function<int64_t (shared_ptr<Reel>)> duration
809 /* end of last subtitle (in editable units) */
810 optional<int64_t> last_out;
811 auto too_short = false;
812 auto too_close = false;
813 auto too_early = false;
814 auto reel_overlap = false;
815 auto empty_text = false;
816 /* current reel start time (in editable units) */
817 int64_t reel_offset = 0;
819 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
820 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
821 if (node->name() == "Subtitle") {
822 Time in (node->string_attribute("TimeIn"), tcr);
826 Time out (node->string_attribute("TimeOut"), tcr);
830 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
833 auto length = out - in;
834 if (length.as_editable_units_ceil(er) < 15) {
838 /* XXX: this feels dubious - is it really what Bv2.1 means? */
839 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
840 if (distance >= 0 && distance < 2) {
844 last_out = reel_offset + out.as_editable_units_floor(er);
845 } else if (node->name() == "Text") {
846 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
847 if (!node->content().empty()) {
850 for (auto i: node->node_children()) {
851 if (node_has_content(i)) {
857 if (!node_has_content(node)) {
862 for (auto i: node->node_children()) {
863 parse(i, tcr, start_time, er, first_reel);
867 for (auto i = 0U; i < reels.size(); ++i) {
868 if (!check(reels[i])) {
872 auto reel_xml = xml(reels[i]);
874 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
878 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
879 * read in by libdcp's parser.
882 shared_ptr<cxml::Document> doc;
884 optional<Time> start_time;
886 doc = make_shared<cxml::Document>("SubtitleReel");
887 doc->read_string (*reel_xml);
888 tcr = doc->number_child<int>("TimeCodeRate");
889 auto start_time_string = doc->optional_string_child("StartTime");
890 if (start_time_string) {
891 start_time = Time(*start_time_string, tcr);
894 doc = make_shared<cxml::Document>("DCSubtitle");
895 doc->read_string (*reel_xml);
897 parse (doc, tcr, start_time, edit_rate, i == 0);
898 auto end = reel_offset + duration(reels[i]);
899 if (last_out && *last_out > end) {
905 if (last_out && *last_out > reel_offset) {
911 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
917 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
923 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
929 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
935 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
943 verify_closed_caption_details (
944 vector<shared_ptr<Reel>> reels,
945 vector<VerificationNote>& notes
948 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
949 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
950 for (auto i: node->node_children()) {
951 if (i->name() == "Text") {
952 text_or_image.push_back (i);
954 find_text_or_image (i, text_or_image);
959 auto mismatched_valign = false;
960 auto incorrect_order = false;
962 std::function<void (cxml::ConstNodePtr)> parse;
963 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
964 if (node->name() == "Subtitle") {
965 vector<cxml::ConstNodePtr> text_or_image;
966 find_text_or_image (node, text_or_image);
967 optional<string> last_valign;
968 optional<float> last_vpos;
969 for (auto i: text_or_image) {
970 auto valign = i->optional_string_attribute("VAlign");
972 valign = i->optional_string_attribute("Valign").get_value_or("center");
974 auto vpos = i->optional_number_attribute<float>("VPosition");
976 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
980 if (*last_valign != valign) {
981 mismatched_valign = true;
984 last_valign = valign;
986 if (!mismatched_valign) {
988 if (*last_valign == "top" || *last_valign == "center") {
989 if (*vpos < *last_vpos) {
990 incorrect_order = true;
993 if (*vpos > *last_vpos) {
994 incorrect_order = true;
1003 for (auto i: node->node_children()) {
1008 for (auto reel: reels) {
1009 for (auto ccap: reel->closed_captions()) {
1010 auto reel_xml = ccap->asset()->raw_xml();
1012 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1016 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1017 * read in by libdcp's parser.
1020 shared_ptr<cxml::Document> doc;
1022 optional<Time> start_time;
1024 doc = make_shared<cxml::Document>("SubtitleReel");
1025 doc->read_string (*reel_xml);
1027 doc = make_shared<cxml::Document>("DCSubtitle");
1028 doc->read_string (*reel_xml);
1034 if (mismatched_valign) {
1036 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1040 if (incorrect_order) {
1042 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1048 struct LinesCharactersResult
1050 bool warning_length_exceeded = false;
1051 bool error_length_exceeded = false;
1052 bool line_count_exceeded = false;
1058 verify_text_lines_and_characters (
1059 shared_ptr<SubtitleAsset> asset,
1062 LinesCharactersResult* result
1068 Event (Time time_, float position_, int characters_)
1070 , position (position_)
1071 , characters (characters_)
1074 Event (Time time_, shared_ptr<Event> start_)
1080 int position; //< position from 0 at top of screen to 100 at bottom
1082 shared_ptr<Event> start;
1085 vector<shared_ptr<Event>> events;
1087 auto position = [](shared_ptr<const SubtitleString> sub) {
1088 switch (sub->v_align()) {
1090 return lrintf(sub->v_position() * 100);
1091 case VAlign::CENTER:
1092 return lrintf((0.5f + sub->v_position()) * 100);
1093 case VAlign::BOTTOM:
1094 return lrintf((1.0f - sub->v_position()) * 100);
1100 for (auto j: asset->subtitles()) {
1101 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1103 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1104 events.push_back(in);
1105 events.push_back(make_shared<Event>(text->out(), in));
1109 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1110 return a->time < b->time;
1113 map<int, int> current;
1114 for (auto i: events) {
1115 if (current.size() > 3) {
1116 result->line_count_exceeded = true;
1118 for (auto j: current) {
1119 if (j.second > warning_length) {
1120 result->warning_length_exceeded = true;
1122 if (j.second > error_length) {
1123 result->error_length_exceeded = true;
1128 /* end of a subtitle */
1129 DCP_ASSERT (current.find(i->start->position) != current.end());
1130 if (current[i->start->position] == i->start->characters) {
1131 current.erase(i->start->position);
1133 current[i->start->position] -= i->start->characters;
1136 /* start of a subtitle */
1137 if (current.find(i->position) == current.end()) {
1138 current[i->position] = i->characters;
1140 current[i->position] += i->characters;
1149 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1151 if (reels.empty()) {
1155 if (reels[0]->main_subtitle()) {
1156 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1157 [](shared_ptr<Reel> reel) {
1158 return static_cast<bool>(reel->main_subtitle());
1160 [](shared_ptr<Reel> reel) {
1161 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1163 return interop->asset()->raw_xml();
1165 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1167 return smpte->asset()->raw_xml();
1169 [](shared_ptr<Reel> reel) {
1170 return reel->main_subtitle()->actual_duration();
1175 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1176 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1177 [i](shared_ptr<Reel> reel) {
1178 return i < reel->closed_captions().size();
1180 [i](shared_ptr<Reel> reel) {
1181 return reel->closed_captions()[i]->asset()->raw_xml();
1183 [i](shared_ptr<Reel> reel) {
1184 return reel->closed_captions()[i]->actual_duration();
1189 verify_closed_caption_details (reels, notes);
1194 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1196 DCP_ASSERT (cpl->file());
1197 cxml::Document doc ("CompositionPlaylist");
1198 doc.read_file (cpl->file().get());
1200 auto missing = false;
1203 if (auto reel_list = doc.node_child("ReelList")) {
1204 auto reels = reel_list->node_children("Reel");
1205 if (!reels.empty()) {
1206 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1207 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1208 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1210 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1211 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1215 if (auto name = extension->optional_node_child("Name")) {
1216 if (name->content() != "Application") {
1217 malformed = "<Name> should be 'Application'";
1220 if (auto property_list = extension->optional_node_child("PropertyList")) {
1221 if (auto property = property_list->optional_node_child("Property")) {
1222 if (auto name = property->optional_node_child("Name")) {
1223 if (name->content() != "DCP Constraints Profile") {
1224 malformed = "<Name> property should be 'DCP Constraints Profile'";
1227 if (auto value = property->optional_node_child("Value")) {
1228 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1229 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1244 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1245 } else if (!malformed.empty()) {
1246 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1252 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1254 vector<string> encrypted;
1255 for (auto i: dcp->cpls()) {
1256 for (auto j: i->reel_file_assets()) {
1257 if (j->asset_ref().resolved()) {
1258 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1259 if (mxf && mxf->encrypted()) {
1260 encrypted.push_back(j->asset_ref().id());
1266 for (auto i: pkl->assets()) {
1267 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1279 shared_ptr<const DCP> dcp,
1280 shared_ptr<const CPL> cpl,
1281 shared_ptr<const Reel> reel,
1282 optional<dcp::Size> main_picture_active_area,
1283 function<void (string, optional<boost::filesystem::path>)> stage,
1284 boost::filesystem::path xsd_dtd_directory,
1285 function<void (float)> progress,
1286 vector<VerificationNote>& notes,
1288 bool* have_main_subtitle,
1289 bool* have_no_main_subtitle,
1290 size_t* most_closed_captions,
1291 size_t* fewest_closed_captions,
1292 map<Marker, Time>* markers_seen
1295 for (auto i: reel->assets()) {
1296 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1297 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1299 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1300 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1302 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1303 if (i->encryptable() && !file_asset->hash()) {
1304 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1308 if (dcp->standard() == Standard::SMPTE) {
1309 boost::optional<int64_t> duration;
1310 for (auto i: reel->assets()) {
1312 duration = i->actual_duration();
1313 } else if (*duration != i->actual_duration()) {
1314 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1320 if (reel->main_picture()) {
1321 /* Check reel stuff */
1322 auto const frame_rate = reel->main_picture()->frame_rate();
1323 if (frame_rate.denominator != 1 ||
1324 (frame_rate.numerator != 24 &&
1325 frame_rate.numerator != 25 &&
1326 frame_rate.numerator != 30 &&
1327 frame_rate.numerator != 48 &&
1328 frame_rate.numerator != 50 &&
1329 frame_rate.numerator != 60 &&
1330 frame_rate.numerator != 96)) {
1332 VerificationNote::Type::ERROR,
1333 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1334 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1338 if (reel->main_picture()->asset_ref().resolved()) {
1339 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, notes);
1340 auto const asset_size = reel->main_picture()->asset()->size();
1341 if (main_picture_active_area) {
1342 if (main_picture_active_area->width > asset_size.width) {
1344 VerificationNote::Type::ERROR,
1345 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1346 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1350 if (main_picture_active_area->height > asset_size.height) {
1352 VerificationNote::Type::ERROR,
1353 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1354 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1362 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1363 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, notes);
1366 if (reel->main_subtitle()) {
1367 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1368 if (reel->main_subtitle()->asset_ref().resolved()) {
1369 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1371 *have_main_subtitle = true;
1373 *have_no_main_subtitle = true;
1376 for (auto i: reel->closed_captions()) {
1377 verify_closed_caption_reel(i, notes);
1378 if (i->asset_ref().resolved()) {
1379 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1383 if (reel->main_markers()) {
1384 for (auto const& i: reel->main_markers()->get()) {
1385 markers_seen->insert(i);
1387 if (reel->main_markers()->entry_point()) {
1388 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1390 if (reel->main_markers()->duration()) {
1391 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1395 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1396 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1404 shared_ptr<const DCP> dcp,
1405 shared_ptr<const CPL> cpl,
1406 function<void (string, optional<boost::filesystem::path>)> stage,
1407 boost::filesystem::path xsd_dtd_directory,
1408 function<void (float)> progress,
1409 vector<VerificationNote>& notes,
1413 stage("Checking CPL", cpl->file());
1414 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1416 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1417 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1420 for (auto const& i: cpl->additional_subtitle_languages()) {
1421 verify_language_tag(i, notes);
1424 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1425 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1426 * of the approved ones.
1428 auto all = ContentKind::all();
1429 auto name = cpl->content_kind().name();
1430 transform(name.begin(), name.end(), name.begin(), ::tolower);
1431 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1432 if (iter == all.end()) {
1433 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1437 if (cpl->release_territory()) {
1438 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") {
1439 auto terr = cpl->release_territory().get();
1440 /* Must be a valid region tag, or "001" */
1442 LanguageTag::RegionSubtag test(terr);
1444 if (terr != "001") {
1445 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1451 if (dcp->standard() == Standard::SMPTE) {
1452 if (!cpl->annotation_text()) {
1453 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1454 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1455 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1459 for (auto i: dcp->pkls()) {
1460 /* Check that the CPL's hash corresponds to the PKL */
1461 optional<string> h = i->hash(cpl->id());
1462 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1463 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1466 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1467 optional<string> required_annotation_text;
1468 for (auto j: i->assets()) {
1469 /* See if this is a CPL */
1470 for (auto k: dcp->cpls()) {
1471 if (j->id() == k->id()) {
1472 if (!required_annotation_text) {
1473 /* First CPL we have found; this is the required AnnotationText unless we find another */
1474 required_annotation_text = cpl->content_title_text();
1476 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1477 required_annotation_text = boost::none;
1483 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1484 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1488 /* set to true if any reel has a MainSubtitle */
1489 auto have_main_subtitle = false;
1490 /* set to true if any reel has no MainSubtitle */
1491 auto have_no_main_subtitle = false;
1492 /* fewest number of closed caption assets seen in a reel */
1493 size_t fewest_closed_captions = SIZE_MAX;
1494 /* most number of closed caption assets seen in a reel */
1495 size_t most_closed_captions = 0;
1496 map<Marker, Time> markers_seen;
1498 auto const main_picture_active_area = cpl->main_picture_active_area();
1499 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1501 VerificationNote::Type::ERROR,
1502 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1503 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1507 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1509 VerificationNote::Type::ERROR,
1510 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1511 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1516 for (auto reel: cpl->reels()) {
1517 stage("Checking reel", optional<boost::filesystem::path>());
1522 main_picture_active_area,
1528 &have_main_subtitle,
1529 &have_no_main_subtitle,
1530 &most_closed_captions,
1531 &fewest_closed_captions,
1536 verify_text_details(cpl->reels(), notes);
1538 if (dcp->standard() == Standard::SMPTE) {
1540 if (have_main_subtitle && have_no_main_subtitle) {
1541 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1544 if (fewest_closed_captions != most_closed_captions) {
1545 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1548 if (cpl->content_kind() == ContentKind::FEATURE) {
1549 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1550 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1552 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1553 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1557 auto ffoc = markers_seen.find(Marker::FFOC);
1558 if (ffoc == markers_seen.end()) {
1559 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1560 } else if (ffoc->second.e != 1) {
1561 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1564 auto lfoc = markers_seen.find(Marker::LFOC);
1565 if (lfoc == markers_seen.end()) {
1566 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1568 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1569 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1570 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1574 LinesCharactersResult result;
1575 for (auto reel: cpl->reels()) {
1576 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1577 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1581 if (result.line_count_exceeded) {
1582 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1584 if (result.error_length_exceeded) {
1585 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1586 } else if (result.warning_length_exceeded) {
1587 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1590 result = LinesCharactersResult();
1591 for (auto reel: cpl->reels()) {
1592 for (auto i: reel->closed_captions()) {
1594 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1599 if (result.line_count_exceeded) {
1600 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1602 if (result.error_length_exceeded) {
1603 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1606 if (!cpl->read_composition_metadata()) {
1607 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1608 } else if (!cpl->version_number()) {
1609 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1612 verify_extension_metadata(cpl, notes);
1614 if (cpl->any_encrypted()) {
1615 cxml::Document doc("CompositionPlaylist");
1616 DCP_ASSERT(cpl->file());
1617 doc.read_file(cpl->file().get());
1618 if (!doc.optional_node_child("Signature")) {
1619 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1629 shared_ptr<const DCP> dcp,
1630 shared_ptr<const PKL> pkl,
1631 boost::filesystem::path xsd_dtd_directory,
1632 vector<VerificationNote>& notes
1635 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1637 if (pkl_has_encrypted_assets(dcp, pkl)) {
1638 cxml::Document doc("PackingList");
1639 doc.read_file(pkl->file().get());
1640 if (!doc.optional_node_child("Signature")) {
1641 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1645 set<string> uuid_set;
1646 for (auto asset: pkl->assets()) {
1647 if (!uuid_set.insert(asset->id()).second) {
1648 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1659 shared_ptr<const DCP> dcp,
1660 boost::filesystem::path xsd_dtd_directory,
1661 vector<VerificationNote>& notes
1664 auto asset_map = dcp->asset_map();
1665 DCP_ASSERT(asset_map);
1667 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1669 set<string> uuid_set;
1670 for (auto const& asset: asset_map->assets()) {
1671 if (!uuid_set.insert(asset.id()).second) {
1672 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1679 vector<VerificationNote>
1681 vector<boost::filesystem::path> directories,
1682 function<void (string, optional<boost::filesystem::path>)> stage,
1683 function<void (float)> progress,
1684 optional<boost::filesystem::path> xsd_dtd_directory
1687 if (!xsd_dtd_directory) {
1688 xsd_dtd_directory = resources_directory() / "xsd";
1690 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1692 vector<VerificationNote> notes;
1695 vector<shared_ptr<DCP>> dcps;
1696 for (auto i: directories) {
1697 dcps.push_back (make_shared<DCP>(i));
1700 for (auto dcp: dcps) {
1701 stage ("Checking DCP", dcp->directory());
1702 bool carry_on = true;
1704 dcp->read (¬es, true);
1705 } catch (MissingAssetmapError& e) {
1706 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1708 } catch (ReadError& e) {
1709 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1710 } catch (XMLError& e) {
1711 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1712 } catch (MXFFileError& e) {
1713 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1714 } catch (cxml::Error& e) {
1715 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1722 if (dcp->standard() != Standard::SMPTE) {
1723 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1726 for (auto cpl: dcp->cpls()) {
1738 for (auto pkl: dcp->pkls()) {
1739 stage("Checking PKL", pkl->file());
1740 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1743 if (dcp->asset_map_file()) {
1744 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1745 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1747 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1756 dcp::note_to_string (VerificationNote note)
1758 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1760 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1761 * not "ClosedCaption assets must have an <EntryPoint> tag."
1763 * It's OK to use XML tag names where they are clear.
1764 * If both ID and filename are available, use only the ID.
1765 * End messages with a full stop.
1766 * Messages should not mention whether or not their errors are a part of Bv2.1.
1768 switch (note.code()) {
1769 case VerificationNote::Code::FAILED_READ:
1770 return *note.note();
1771 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1772 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1773 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1774 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1775 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1776 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1777 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1778 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1779 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1780 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1781 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1782 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1783 case VerificationNote::Code::EMPTY_ASSET_PATH:
1784 return "The asset map contains an empty asset path.";
1785 case VerificationNote::Code::MISSING_ASSET:
1786 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1787 case VerificationNote::Code::MISMATCHED_STANDARD:
1788 return "The DCP contains both SMPTE and Interop parts.";
1789 case VerificationNote::Code::INVALID_XML:
1790 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1791 case VerificationNote::Code::MISSING_ASSETMAP:
1792 return "No ASSETMAP or ASSETMAP.xml was found.";
1793 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1794 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1795 case VerificationNote::Code::INVALID_DURATION:
1796 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1797 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1798 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());
1799 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1800 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());
1801 case VerificationNote::Code::EXTERNAL_ASSET:
1802 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());
1803 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1804 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1805 case VerificationNote::Code::INVALID_STANDARD:
1806 return "This DCP does not use the SMPTE standard.";
1807 case VerificationNote::Code::INVALID_LANGUAGE:
1808 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1809 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1810 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1811 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1812 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1813 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1814 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1815 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1816 return "3D 4K DCPs are not allowed.";
1817 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1818 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1819 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1820 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1821 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1822 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());
1823 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1824 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1825 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1826 return "Some subtitle assets have different <Language> tags than others";
1827 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1828 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1829 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1830 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1831 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1832 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1833 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1834 return "At least one subtitle lasts less than 15 frames.";
1835 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1836 return "At least one pair of subtitles is separated by less than 2 frames.";
1837 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1838 return "At least one subtitle extends outside of its reel.";
1839 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1840 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1841 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1842 return "There are more than 52 characters in at least one subtitle line.";
1843 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1844 return "There are more than 79 characters in at least one subtitle line.";
1845 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1846 return "There are more than 3 closed caption lines in at least one place.";
1847 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1848 return "There are more than 32 characters in at least one closed caption line.";
1849 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1850 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1851 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1852 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1853 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1854 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1855 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1856 return "All assets in a reel do not have the same duration.";
1857 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1858 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1859 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1860 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1861 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1862 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1863 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1864 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1865 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1866 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1867 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1868 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1869 case VerificationNote::Code::MISSING_HASH:
1870 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1871 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1872 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1873 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1874 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1875 case VerificationNote::Code::MISSING_FFOC:
1876 return "There should be a FFOC (first frame of content) marker.";
1877 case VerificationNote::Code::MISSING_LFOC:
1878 return "There should be a LFOC (last frame of content) marker.";
1879 case VerificationNote::Code::INCORRECT_FFOC:
1880 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1881 case VerificationNote::Code::INCORRECT_LFOC:
1882 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1883 case VerificationNote::Code::MISSING_CPL_METADATA:
1884 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1885 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1886 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1887 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1888 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1889 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1890 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1891 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1892 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1893 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1894 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1895 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1896 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1897 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1898 return "Some assets are encrypted but some are not.";
1899 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1900 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1901 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1902 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1903 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1904 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1905 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1906 return "The JPEG2000 tile size is not the same as the image size.";
1907 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1908 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1909 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1910 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1911 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1912 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1913 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1914 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1915 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1916 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1917 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1918 return "POC marker found outside main header.";
1919 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1920 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1921 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1922 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1923 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1924 return "No TLM marker was found in a JPEG2000 codestream.";
1925 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1926 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1927 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1928 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1929 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1931 vector<string> parts;
1932 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1933 DCP_ASSERT (parts.size() == 2);
1934 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]);
1936 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1937 return "Some aspect of this DCP could not be checked because it is encrypted.";
1938 case VerificationNote::Code::EMPTY_TEXT:
1939 return "There is an empty <Text> node in a subtitle or closed caption.";
1940 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1941 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1942 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1943 return "Some closed captions are not listed in the order of their vertical position.";
1944 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1945 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1946 case VerificationNote::Code::UNEXPECTED_DURATION:
1947 return "There is an <Duration> node inside a <MainMarkers>.";
1948 case VerificationNote::Code::INVALID_CONTENT_KIND:
1949 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
1950 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
1951 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
1952 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
1953 return String::compose("The PKL %1 has more than one asset with the same ID", note.note().get());
1954 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
1955 return String::compose("The ASSETMAP %1 has more than one asset with the same ID", note.note().get());
1956 case VerificationNote::Code::MISSING_SUBTITLE:
1957 return String::compose("The subtitle asset %1 has no subtitles", note.note().get());
1965 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1967 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1972 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1974 if (a.type() != b.type()) {
1975 return a.type() < b.type();
1978 if (a.code() != b.code()) {
1979 return a.code() < b.code();
1982 if (a.note() != b.note()) {
1983 return a.note().get_value_or("") < b.note().get_value_or("");
1986 if (a.file() != b.file()) {
1987 return a.file().get_value_or("") < b.file().get_value_or("");
1990 return a.line().get_value_or(0) < b.line().get_value_or(0);
1995 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1997 s << note_to_string (note);
1999 s << " [" << note.note().get() << "]";
2002 s << " [" << note.file().get() << "]";
2005 s << " [" << note.line().get() << "]";