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 SMPTE subtitle-only stuff */
692 verify_smpte_subtitle_asset (
693 shared_ptr<const SMPTESubtitleAsset> asset,
694 vector<VerificationNote>& notes,
698 if (asset->language()) {
699 if (!state.subtitle_language) {
700 state.subtitle_language = *asset->language();
701 } else if (state.subtitle_language != *asset->language()) {
702 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
706 DCP_ASSERT (asset->resource_id());
707 auto xml_id = asset->xml_id();
709 if (asset->resource_id().get() != xml_id) {
710 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
713 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
714 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
717 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
722 /** Verify all subtitle stuff */
724 verify_subtitle_asset (
725 shared_ptr<const SubtitleAsset> asset,
726 optional<int64_t> reel_asset_duration,
727 function<void (string, optional<boost::filesystem::path>)> stage,
728 boost::filesystem::path xsd_dtd_directory,
729 vector<VerificationNote>& notes,
733 stage ("Checking subtitle XML", asset->file());
734 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
735 * gets passed through libdcp which may clean up and therefore hide errors.
737 if (asset->raw_xml()) {
738 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
740 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
743 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
745 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
746 verify_smpte_subtitle_asset (smpte, notes, state);
751 /** Verify all closed caption stuff */
753 verify_closed_caption_asset (
754 shared_ptr<const SubtitleAsset> asset,
755 optional<int64_t> reel_asset_duration,
756 function<void (string, optional<boost::filesystem::path>)> stage,
757 boost::filesystem::path xsd_dtd_directory,
758 vector<VerificationNote>& notes
761 stage ("Checking closed caption XML", asset->file());
762 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
763 * gets passed through libdcp which may clean up and therefore hide errors.
765 auto raw_xml = asset->raw_xml();
767 validate_xml (*raw_xml, xsd_dtd_directory, notes);
768 if (raw_xml->size() > 256 * 1024) {
769 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
772 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
775 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
777 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
782 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
785 verify_text_details (
786 vector<shared_ptr<Reel>> reels,
788 vector<VerificationNote>& notes,
789 std::function<bool (shared_ptr<Reel>)> check,
790 std::function<optional<string> (shared_ptr<Reel>)> xml,
791 std::function<int64_t (shared_ptr<Reel>)> duration
794 /* end of last subtitle (in editable units) */
795 optional<int64_t> last_out;
796 auto too_short = false;
797 auto too_close = false;
798 auto too_early = false;
799 auto reel_overlap = false;
800 auto empty_text = false;
801 /* current reel start time (in editable units) */
802 int64_t reel_offset = 0;
804 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
805 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) {
806 if (node->name() == "Subtitle") {
807 Time in (node->string_attribute("TimeIn"), tcr);
811 Time out (node->string_attribute("TimeOut"), tcr);
815 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
818 auto length = out - in;
819 if (length.as_editable_units_ceil(er) < 15) {
823 /* XXX: this feels dubious - is it really what Bv2.1 means? */
824 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
825 if (distance >= 0 && distance < 2) {
829 last_out = reel_offset + out.as_editable_units_floor(er);
830 } else if (node->name() == "Text") {
831 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
832 if (!node->content().empty()) {
835 for (auto i: node->node_children()) {
836 if (node_has_content(i)) {
842 if (!node_has_content(node)) {
847 for (auto i: node->node_children()) {
848 parse(i, tcr, start_time, er, first_reel);
852 for (auto i = 0U; i < reels.size(); ++i) {
853 if (!check(reels[i])) {
857 auto reel_xml = xml(reels[i]);
859 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
863 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
864 * read in by libdcp's parser.
867 shared_ptr<cxml::Document> doc;
869 optional<Time> start_time;
871 doc = make_shared<cxml::Document>("SubtitleReel");
872 doc->read_string (*reel_xml);
873 tcr = doc->number_child<int>("TimeCodeRate");
874 auto start_time_string = doc->optional_string_child("StartTime");
875 if (start_time_string) {
876 start_time = Time(*start_time_string, tcr);
879 doc = make_shared<cxml::Document>("DCSubtitle");
880 doc->read_string (*reel_xml);
882 parse (doc, tcr, start_time, edit_rate, i == 0);
883 auto end = reel_offset + duration(reels[i]);
884 if (last_out && *last_out > end) {
890 if (last_out && *last_out > reel_offset) {
896 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
902 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
908 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
914 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
920 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
928 verify_closed_caption_details (
929 vector<shared_ptr<Reel>> reels,
930 vector<VerificationNote>& notes
933 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
934 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
935 for (auto i: node->node_children()) {
936 if (i->name() == "Text") {
937 text_or_image.push_back (i);
939 find_text_or_image (i, text_or_image);
944 auto mismatched_valign = false;
945 auto incorrect_order = false;
947 std::function<void (cxml::ConstNodePtr)> parse;
948 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
949 if (node->name() == "Subtitle") {
950 vector<cxml::ConstNodePtr> text_or_image;
951 find_text_or_image (node, text_or_image);
952 optional<string> last_valign;
953 optional<float> last_vpos;
954 for (auto i: text_or_image) {
955 auto valign = i->optional_string_attribute("VAlign");
957 valign = i->optional_string_attribute("Valign").get_value_or("center");
959 auto vpos = i->optional_number_attribute<float>("VPosition");
961 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
965 if (*last_valign != valign) {
966 mismatched_valign = true;
969 last_valign = valign;
971 if (!mismatched_valign) {
973 if (*last_valign == "top" || *last_valign == "center") {
974 if (*vpos < *last_vpos) {
975 incorrect_order = true;
978 if (*vpos > *last_vpos) {
979 incorrect_order = true;
988 for (auto i: node->node_children()) {
993 for (auto reel: reels) {
994 for (auto ccap: reel->closed_captions()) {
995 auto reel_xml = ccap->asset()->raw_xml();
997 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1001 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1002 * read in by libdcp's parser.
1005 shared_ptr<cxml::Document> doc;
1007 optional<Time> start_time;
1009 doc = make_shared<cxml::Document>("SubtitleReel");
1010 doc->read_string (*reel_xml);
1012 doc = make_shared<cxml::Document>("DCSubtitle");
1013 doc->read_string (*reel_xml);
1019 if (mismatched_valign) {
1021 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1025 if (incorrect_order) {
1027 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1033 struct LinesCharactersResult
1035 bool warning_length_exceeded = false;
1036 bool error_length_exceeded = false;
1037 bool line_count_exceeded = false;
1043 verify_text_lines_and_characters (
1044 shared_ptr<SubtitleAsset> asset,
1047 LinesCharactersResult* result
1053 Event (Time time_, float position_, int characters_)
1055 , position (position_)
1056 , characters (characters_)
1059 Event (Time time_, shared_ptr<Event> start_)
1065 int position; //< position from 0 at top of screen to 100 at bottom
1067 shared_ptr<Event> start;
1070 vector<shared_ptr<Event>> events;
1072 auto position = [](shared_ptr<const SubtitleString> sub) {
1073 switch (sub->v_align()) {
1075 return lrintf(sub->v_position() * 100);
1076 case VAlign::CENTER:
1077 return lrintf((0.5f + sub->v_position()) * 100);
1078 case VAlign::BOTTOM:
1079 return lrintf((1.0f - sub->v_position()) * 100);
1085 for (auto j: asset->subtitles()) {
1086 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1088 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1089 events.push_back(in);
1090 events.push_back(make_shared<Event>(text->out(), in));
1094 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1095 return a->time < b->time;
1098 map<int, int> current;
1099 for (auto i: events) {
1100 if (current.size() > 3) {
1101 result->line_count_exceeded = true;
1103 for (auto j: current) {
1104 if (j.second > warning_length) {
1105 result->warning_length_exceeded = true;
1107 if (j.second > error_length) {
1108 result->error_length_exceeded = true;
1113 /* end of a subtitle */
1114 DCP_ASSERT (current.find(i->start->position) != current.end());
1115 if (current[i->start->position] == i->start->characters) {
1116 current.erase(i->start->position);
1118 current[i->start->position] -= i->start->characters;
1121 /* start of a subtitle */
1122 if (current.find(i->position) == current.end()) {
1123 current[i->position] = i->characters;
1125 current[i->position] += i->characters;
1134 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1136 if (reels.empty()) {
1140 if (reels[0]->main_subtitle()) {
1141 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1142 [](shared_ptr<Reel> reel) {
1143 return static_cast<bool>(reel->main_subtitle());
1145 [](shared_ptr<Reel> reel) {
1146 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1148 return interop->asset()->raw_xml();
1150 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1152 return smpte->asset()->raw_xml();
1154 [](shared_ptr<Reel> reel) {
1155 return reel->main_subtitle()->actual_duration();
1160 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1161 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1162 [i](shared_ptr<Reel> reel) {
1163 return i < reel->closed_captions().size();
1165 [i](shared_ptr<Reel> reel) {
1166 return reel->closed_captions()[i]->asset()->raw_xml();
1168 [i](shared_ptr<Reel> reel) {
1169 return reel->closed_captions()[i]->actual_duration();
1174 verify_closed_caption_details (reels, notes);
1179 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1181 DCP_ASSERT (cpl->file());
1182 cxml::Document doc ("CompositionPlaylist");
1183 doc.read_file (cpl->file().get());
1185 auto missing = false;
1188 if (auto reel_list = doc.node_child("ReelList")) {
1189 auto reels = reel_list->node_children("Reel");
1190 if (!reels.empty()) {
1191 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1192 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1193 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1195 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1196 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1200 if (auto name = extension->optional_node_child("Name")) {
1201 if (name->content() != "Application") {
1202 malformed = "<Name> should be 'Application'";
1205 if (auto property_list = extension->optional_node_child("PropertyList")) {
1206 if (auto property = property_list->optional_node_child("Property")) {
1207 if (auto name = property->optional_node_child("Name")) {
1208 if (name->content() != "DCP Constraints Profile") {
1209 malformed = "<Name> property should be 'DCP Constraints Profile'";
1212 if (auto value = property->optional_node_child("Value")) {
1213 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1214 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1229 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1230 } else if (!malformed.empty()) {
1231 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1237 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1239 vector<string> encrypted;
1240 for (auto i: dcp->cpls()) {
1241 for (auto j: i->reel_file_assets()) {
1242 if (j->asset_ref().resolved()) {
1243 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1244 if (mxf && mxf->encrypted()) {
1245 encrypted.push_back(j->asset_ref().id());
1251 for (auto i: pkl->asset_list()) {
1252 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1264 shared_ptr<const DCP> dcp,
1265 shared_ptr<const CPL> cpl,
1266 shared_ptr<const Reel> reel,
1267 optional<dcp::Size> main_picture_active_area,
1268 function<void (string, optional<boost::filesystem::path>)> stage,
1269 boost::filesystem::path xsd_dtd_directory,
1270 function<void (float)> progress,
1271 vector<VerificationNote>& notes,
1273 bool* have_main_subtitle,
1274 bool* have_no_main_subtitle,
1275 size_t* most_closed_captions,
1276 size_t* fewest_closed_captions,
1277 map<Marker, Time>* markers_seen
1280 for (auto i: reel->assets()) {
1281 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1282 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1284 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1285 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1287 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1288 if (i->encryptable() && !file_asset->hash()) {
1289 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1293 if (dcp->standard() == Standard::SMPTE) {
1294 boost::optional<int64_t> duration;
1295 for (auto i: reel->assets()) {
1297 duration = i->actual_duration();
1298 } else if (*duration != i->actual_duration()) {
1299 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1305 if (reel->main_picture()) {
1306 /* Check reel stuff */
1307 auto const frame_rate = reel->main_picture()->frame_rate();
1308 if (frame_rate.denominator != 1 ||
1309 (frame_rate.numerator != 24 &&
1310 frame_rate.numerator != 25 &&
1311 frame_rate.numerator != 30 &&
1312 frame_rate.numerator != 48 &&
1313 frame_rate.numerator != 50 &&
1314 frame_rate.numerator != 60 &&
1315 frame_rate.numerator != 96)) {
1317 VerificationNote::Type::ERROR,
1318 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1319 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1323 if (reel->main_picture()->asset_ref().resolved()) {
1324 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, notes);
1325 auto const asset_size = reel->main_picture()->asset()->size();
1326 if (main_picture_active_area) {
1327 if (main_picture_active_area->width > asset_size.width) {
1329 VerificationNote::Type::ERROR,
1330 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1331 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1335 if (main_picture_active_area->height > asset_size.height) {
1337 VerificationNote::Type::ERROR,
1338 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1339 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1347 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1348 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, notes);
1351 if (reel->main_subtitle()) {
1352 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1353 if (reel->main_subtitle()->asset_ref().resolved()) {
1354 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1356 *have_main_subtitle = true;
1358 *have_no_main_subtitle = true;
1361 for (auto i: reel->closed_captions()) {
1362 verify_closed_caption_reel(i, notes);
1363 if (i->asset_ref().resolved()) {
1364 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1368 if (reel->main_markers()) {
1369 for (auto const& i: reel->main_markers()->get()) {
1370 markers_seen->insert(i);
1372 if (reel->main_markers()->entry_point()) {
1373 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1375 if (reel->main_markers()->duration()) {
1376 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1380 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1381 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1389 shared_ptr<const DCP> dcp,
1390 shared_ptr<const CPL> cpl,
1391 function<void (string, optional<boost::filesystem::path>)> stage,
1392 boost::filesystem::path xsd_dtd_directory,
1393 function<void (float)> progress,
1394 vector<VerificationNote>& notes,
1398 stage("Checking CPL", cpl->file());
1399 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1401 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1402 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1405 for (auto const& i: cpl->additional_subtitle_languages()) {
1406 verify_language_tag(i, notes);
1409 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1410 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1411 * of the approved ones.
1413 auto all = ContentKind::all();
1414 auto name = cpl->content_kind().name();
1415 transform(name.begin(), name.end(), name.begin(), ::tolower);
1416 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1417 if (iter == all.end()) {
1418 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1422 if (cpl->release_territory()) {
1423 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") {
1424 auto terr = cpl->release_territory().get();
1425 /* Must be a valid region tag, or "001" */
1427 LanguageTag::RegionSubtag test(terr);
1429 if (terr != "001") {
1430 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1436 if (dcp->standard() == Standard::SMPTE) {
1437 if (!cpl->annotation_text()) {
1438 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1439 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1440 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1444 for (auto i: dcp->pkls()) {
1445 /* Check that the CPL's hash corresponds to the PKL */
1446 optional<string> h = i->hash(cpl->id());
1447 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1448 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1451 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1452 optional<string> required_annotation_text;
1453 for (auto j: i->asset_list()) {
1454 /* See if this is a CPL */
1455 for (auto k: dcp->cpls()) {
1456 if (j->id() == k->id()) {
1457 if (!required_annotation_text) {
1458 /* First CPL we have found; this is the required AnnotationText unless we find another */
1459 required_annotation_text = cpl->content_title_text();
1461 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1462 required_annotation_text = boost::none;
1468 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1469 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1473 /* set to true if any reel has a MainSubtitle */
1474 auto have_main_subtitle = false;
1475 /* set to true if any reel has no MainSubtitle */
1476 auto have_no_main_subtitle = false;
1477 /* fewest number of closed caption assets seen in a reel */
1478 size_t fewest_closed_captions = SIZE_MAX;
1479 /* most number of closed caption assets seen in a reel */
1480 size_t most_closed_captions = 0;
1481 map<Marker, Time> markers_seen;
1483 auto const main_picture_active_area = cpl->main_picture_active_area();
1484 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1486 VerificationNote::Type::ERROR,
1487 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1488 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1492 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1494 VerificationNote::Type::ERROR,
1495 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1496 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1501 for (auto reel: cpl->reels()) {
1502 stage("Checking reel", optional<boost::filesystem::path>());
1507 main_picture_active_area,
1513 &have_main_subtitle,
1514 &have_no_main_subtitle,
1515 &most_closed_captions,
1516 &fewest_closed_captions,
1521 verify_text_details(cpl->reels(), notes);
1523 if (dcp->standard() == Standard::SMPTE) {
1525 if (have_main_subtitle && have_no_main_subtitle) {
1526 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1529 if (fewest_closed_captions != most_closed_captions) {
1530 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1533 if (cpl->content_kind() == ContentKind::FEATURE) {
1534 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1535 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1537 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1538 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1542 auto ffoc = markers_seen.find(Marker::FFOC);
1543 if (ffoc == markers_seen.end()) {
1544 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1545 } else if (ffoc->second.e != 1) {
1546 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1549 auto lfoc = markers_seen.find(Marker::LFOC);
1550 if (lfoc == markers_seen.end()) {
1551 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1553 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1554 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1555 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1559 LinesCharactersResult result;
1560 for (auto reel: cpl->reels()) {
1561 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1562 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1566 if (result.line_count_exceeded) {
1567 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1569 if (result.error_length_exceeded) {
1570 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1571 } else if (result.warning_length_exceeded) {
1572 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1575 result = LinesCharactersResult();
1576 for (auto reel: cpl->reels()) {
1577 for (auto i: reel->closed_captions()) {
1579 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1584 if (result.line_count_exceeded) {
1585 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1587 if (result.error_length_exceeded) {
1588 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1591 if (!cpl->read_composition_metadata()) {
1592 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1593 } else if (!cpl->version_number()) {
1594 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1597 verify_extension_metadata(cpl, notes);
1599 if (cpl->any_encrypted()) {
1600 cxml::Document doc("CompositionPlaylist");
1601 DCP_ASSERT(cpl->file());
1602 doc.read_file(cpl->file().get());
1603 if (!doc.optional_node_child("Signature")) {
1604 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1614 shared_ptr<const DCP> dcp,
1615 shared_ptr<const PKL> pkl,
1616 boost::filesystem::path xsd_dtd_directory,
1617 vector<VerificationNote>& notes
1620 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1622 if (pkl_has_encrypted_assets(dcp, pkl)) {
1623 cxml::Document doc("PackingList");
1624 doc.read_file(pkl->file().get());
1625 if (!doc.optional_node_child("Signature")) {
1626 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1630 set<string> uuid_set;
1631 for (auto asset: pkl->asset_list()) {
1632 if (!uuid_set.insert(asset->id()).second) {
1633 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1644 shared_ptr<const DCP> dcp,
1645 boost::filesystem::path xsd_dtd_directory,
1646 vector<VerificationNote>& notes
1649 auto asset_map = dcp->asset_map();
1650 DCP_ASSERT(asset_map);
1652 validate_xml(asset_map->path().get(), xsd_dtd_directory, notes);
1654 set<string> uuid_set;
1655 for (auto const& asset: asset_map->assets()) {
1656 if (!uuid_set.insert(asset.id()).second) {
1657 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->path().get()});
1664 vector<VerificationNote>
1666 vector<boost::filesystem::path> directories,
1667 function<void (string, optional<boost::filesystem::path>)> stage,
1668 function<void (float)> progress,
1669 optional<boost::filesystem::path> xsd_dtd_directory
1672 if (!xsd_dtd_directory) {
1673 xsd_dtd_directory = resources_directory() / "xsd";
1675 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1677 vector<VerificationNote> notes;
1680 vector<shared_ptr<DCP>> dcps;
1681 for (auto i: directories) {
1682 dcps.push_back (make_shared<DCP>(i));
1685 for (auto dcp: dcps) {
1686 stage ("Checking DCP", dcp->directory());
1687 bool carry_on = true;
1689 dcp->read (¬es, true);
1690 } catch (MissingAssetmapError& e) {
1691 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1693 } catch (ReadError& e) {
1694 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1695 } catch (XMLError& e) {
1696 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1697 } catch (MXFFileError& e) {
1698 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1699 } catch (cxml::Error& e) {
1700 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1707 if (dcp->standard() != Standard::SMPTE) {
1708 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1711 for (auto cpl: dcp->cpls()) {
1723 for (auto pkl: dcp->pkls()) {
1724 stage("Checking PKL", pkl->file());
1725 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1728 if (dcp->asset_map_path()) {
1729 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1730 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1732 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1741 dcp::note_to_string (VerificationNote note)
1743 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1745 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1746 * not "ClosedCaption assets must have an <EntryPoint> tag."
1748 * It's OK to use XML tag names where they are clear.
1749 * If both ID and filename are available, use only the ID.
1750 * End messages with a full stop.
1751 * Messages should not mention whether or not their errors are a part of Bv2.1.
1753 switch (note.code()) {
1754 case VerificationNote::Code::FAILED_READ:
1755 return *note.note();
1756 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1757 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1758 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1759 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1760 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1761 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1762 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1763 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1764 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1765 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1766 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1767 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1768 case VerificationNote::Code::EMPTY_ASSET_PATH:
1769 return "The asset map contains an empty asset path.";
1770 case VerificationNote::Code::MISSING_ASSET:
1771 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1772 case VerificationNote::Code::MISMATCHED_STANDARD:
1773 return "The DCP contains both SMPTE and Interop parts.";
1774 case VerificationNote::Code::INVALID_XML:
1775 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1776 case VerificationNote::Code::MISSING_ASSETMAP:
1777 return "No ASSETMAP or ASSETMAP.xml was found.";
1778 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1779 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1780 case VerificationNote::Code::INVALID_DURATION:
1781 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1782 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1783 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());
1784 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1785 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());
1786 case VerificationNote::Code::EXTERNAL_ASSET:
1787 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());
1788 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1789 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1790 case VerificationNote::Code::INVALID_STANDARD:
1791 return "This DCP does not use the SMPTE standard.";
1792 case VerificationNote::Code::INVALID_LANGUAGE:
1793 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1794 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1795 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1796 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1797 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1798 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1799 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1800 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1801 return "3D 4K DCPs are not allowed.";
1802 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1803 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1804 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1805 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1806 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1807 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());
1808 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1809 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1810 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1811 return "Some subtitle assets have different <Language> tags than others";
1812 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1813 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1814 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1815 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1816 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1817 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1818 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1819 return "At least one subtitle lasts less than 15 frames.";
1820 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1821 return "At least one pair of subtitles is separated by less than 2 frames.";
1822 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1823 return "At least one subtitle extends outside of its reel.";
1824 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1825 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1826 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1827 return "There are more than 52 characters in at least one subtitle line.";
1828 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1829 return "There are more than 79 characters in at least one subtitle line.";
1830 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1831 return "There are more than 3 closed caption lines in at least one place.";
1832 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1833 return "There are more than 32 characters in at least one closed caption line.";
1834 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1835 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1836 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1837 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1838 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1839 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1840 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1841 return "All assets in a reel do not have the same duration.";
1842 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1843 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1844 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1845 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1846 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1847 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1848 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1849 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1850 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1851 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1852 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1853 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1854 case VerificationNote::Code::MISSING_HASH:
1855 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1856 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1857 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1858 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1859 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1860 case VerificationNote::Code::MISSING_FFOC:
1861 return "There should be a FFOC (first frame of content) marker.";
1862 case VerificationNote::Code::MISSING_LFOC:
1863 return "There should be a LFOC (last frame of content) marker.";
1864 case VerificationNote::Code::INCORRECT_FFOC:
1865 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1866 case VerificationNote::Code::INCORRECT_LFOC:
1867 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1868 case VerificationNote::Code::MISSING_CPL_METADATA:
1869 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1870 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1871 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1872 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1873 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1874 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1875 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1876 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1877 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1878 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1879 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1880 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1881 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1882 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1883 return "Some assets are encrypted but some are not.";
1884 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1885 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1886 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1887 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1888 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1889 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1890 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1891 return "The JPEG2000 tile size is not the same as the image size.";
1892 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1893 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1894 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1895 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1896 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1897 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1898 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1899 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1900 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1901 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1902 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1903 return "POC marker found outside main header.";
1904 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1905 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1906 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1907 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1908 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1909 return "No TLM marker was found in a JPEG2000 codestream.";
1910 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1911 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1912 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1913 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1914 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1916 vector<string> parts;
1917 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1918 DCP_ASSERT (parts.size() == 2);
1919 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]);
1921 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1922 return "Some aspect of this DCP could not be checked because it is encrypted.";
1923 case VerificationNote::Code::EMPTY_TEXT:
1924 return "There is an empty <Text> node in a subtitle or closed caption.";
1925 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1926 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1927 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1928 return "Some closed captions are not listed in the order of their vertical position.";
1929 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1930 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1931 case VerificationNote::Code::UNEXPECTED_DURATION:
1932 return "There is an <Duration> node inside a <MainMarkers>.";
1933 case VerificationNote::Code::INVALID_CONTENT_KIND:
1934 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
1935 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
1936 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
1937 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
1938 return String::compose("The PKL %1 has more than one asset with the same ID", note.note().get());
1939 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
1940 return String::compose("The ASSETMAP %1 has more than one asset with the same ID", note.note().get());
1948 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1950 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1955 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1957 if (a.type() != b.type()) {
1958 return a.type() < b.type();
1961 if (a.code() != b.code()) {
1962 return a.code() < b.code();
1965 if (a.note() != b.note()) {
1966 return a.note().get_value_or("") < b.note().get_value_or("");
1969 if (a.file() != b.file()) {
1970 return a.file().get_value_or("") < b.file().get_value_or("");
1973 return a.line().get_value_or(0) < b.line().get_value_or(0);
1978 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1980 s << note_to_string (note);
1982 s << " [" << note.note().get() << "]";
1985 s << " [" << note.file().get() << "]";
1988 s << " [" << note.line().get() << "]";