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>
92 using std::shared_ptr;
93 using std::make_shared;
94 using boost::optional;
95 using boost::function;
96 using std::dynamic_pointer_cast;
100 using namespace xercesc;
105 xml_ch_to_string (XMLCh const * a)
107 char* x = XMLString::transcode(a);
109 XMLString::release(&x);
114 class XMLValidationError
117 XMLValidationError (SAXParseException const & e)
118 : _message (xml_ch_to_string(e.getMessage()))
119 , _line (e.getLineNumber())
120 , _column (e.getColumnNumber())
121 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
122 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
127 string message () const {
131 uint64_t line () const {
135 uint64_t column () const {
139 string public_id () const {
143 string system_id () const {
156 class DCPErrorHandler : public ErrorHandler
159 void warning(const SAXParseException& e) override
161 maybe_add (XMLValidationError(e));
164 void error(const SAXParseException& e) override
166 maybe_add (XMLValidationError(e));
169 void fatalError(const SAXParseException& e) override
171 maybe_add (XMLValidationError(e));
174 void resetErrors() override {
178 list<XMLValidationError> errors () const {
183 void maybe_add (XMLValidationError e)
185 /* XXX: nasty hack */
187 e.message().find("schema document") != string::npos &&
188 e.message().find("has different target namespace from the one specified in instance document") != string::npos
193 _errors.push_back (e);
196 list<XMLValidationError> _errors;
203 StringToXMLCh (string a)
205 _buffer = XMLString::transcode(a.c_str());
208 StringToXMLCh (StringToXMLCh const&) = delete;
209 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
213 XMLString::release (&_buffer);
216 XMLCh const * get () const {
225 class LocalFileResolver : public EntityResolver
228 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
229 : _xsd_dtd_directory (xsd_dtd_directory)
231 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
232 * found without being here.
234 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
235 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
236 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
237 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
238 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
239 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
240 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
242 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
243 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
244 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
245 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
246 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
249 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
254 auto system_id_str = xml_ch_to_string (system_id);
255 auto p = _xsd_dtd_directory;
256 if (_files.find(system_id_str) == _files.end()) {
259 p /= _files[system_id_str];
261 StringToXMLCh ch (p.string());
262 return new LocalFileInputSource(ch.get());
266 void add (string uri, string file)
271 std::map<string, string> _files;
272 boost::filesystem::path _xsd_dtd_directory;
277 parse (XercesDOMParser& parser, boost::filesystem::path xml)
279 parser.parse(xml.string().c_str());
284 parse (XercesDOMParser& parser, string xml)
286 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
293 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
296 XMLPlatformUtils::Initialize ();
297 } catch (XMLException& e) {
298 throw MiscError ("Failed to initialise xerces library");
301 DCPErrorHandler error_handler;
303 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
305 XercesDOMParser parser;
306 parser.setValidationScheme(XercesDOMParser::Val_Always);
307 parser.setDoNamespaces(true);
308 parser.setDoSchema(true);
310 vector<string> schema;
311 schema.push_back("xml.xsd");
312 schema.push_back("xmldsig-core-schema.xsd");
313 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
314 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
315 schema.push_back("SMPTE-429-9-2007-AM.xsd");
316 schema.push_back("Main-Stereo-Picture-CPL.xsd");
317 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
318 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
319 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
320 schema.push_back("DCSubtitle.v1.mattsson.xsd");
321 schema.push_back("DCDMSubtitle-2010.xsd");
322 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
323 schema.push_back("SMPTE-429-16.xsd");
324 schema.push_back("Dolby-2012-AD.xsd");
325 schema.push_back("SMPTE-429-10-2008.xsd");
326 schema.push_back("xlink.xsd");
327 schema.push_back("SMPTE-335-2012.xsd");
328 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
329 schema.push_back("isdcf-mca.xsd");
330 schema.push_back("SMPTE-429-12-2008.xsd");
332 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
333 * Schemas that are not mentioned in this list are not read, and the things
334 * they describe are not checked.
337 for (auto i: schema) {
338 locations += String::compose("%1 %1 ", i, i);
341 parser.setExternalSchemaLocation(locations.c_str());
342 parser.setValidationSchemaFullChecking(true);
343 parser.setErrorHandler(&error_handler);
345 LocalFileResolver resolver (xsd_dtd_directory);
346 parser.setEntityResolver(&resolver);
349 parser.resetDocumentPool();
351 } catch (XMLException& e) {
352 throw MiscError(xml_ch_to_string(e.getMessage()));
353 } catch (DOMException& e) {
354 throw MiscError(xml_ch_to_string(e.getMessage()));
356 throw MiscError("Unknown exception from xerces");
360 XMLPlatformUtils::Terminate ();
362 for (auto i: error_handler.errors()) {
364 VerificationNote::Type::ERROR,
365 VerificationNote::Code::INVALID_XML,
367 boost::trim_copy(i.public_id() + " " + i.system_id()),
374 enum class VerifyAssetResult {
381 static VerifyAssetResult
382 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
384 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
386 auto pkls = dcp->pkls();
387 /* We've read this DCP in so it must have at least one PKL */
388 DCP_ASSERT (!pkls.empty());
390 auto asset = reel_file_asset->asset_ref().asset();
392 optional<string> pkl_hash;
394 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
400 DCP_ASSERT (pkl_hash);
402 auto cpl_hash = reel_file_asset->hash();
403 if (cpl_hash && *cpl_hash != *pkl_hash) {
404 return VerifyAssetResult::CPL_PKL_DIFFER;
407 if (actual_hash != *pkl_hash) {
408 return VerifyAssetResult::BAD;
411 return VerifyAssetResult::GOOD;
416 verify_language_tag (string tag, vector<VerificationNote>& notes)
419 LanguageTag test (tag);
420 } catch (LanguageTagError &) {
421 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
427 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
429 int biggest_frame = 0;
430 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
431 auto const duration = asset->intrinsic_duration ();
433 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
434 for (auto i: j2k_notes) {
435 if (find(notes.begin(), notes.end(), i) == notes.end()) {
441 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
442 auto reader = mono_asset->start_read ();
443 for (int64_t i = 0; i < duration; ++i) {
444 auto frame = reader->get_frame (i);
445 biggest_frame = max(biggest_frame, frame->size());
446 if (!mono_asset->encrypted() || mono_asset->key()) {
447 vector<VerificationNote> j2k_notes;
448 verify_j2k (frame, j2k_notes);
449 check_and_add (j2k_notes);
451 progress (float(i) / duration);
453 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
454 auto reader = stereo_asset->start_read ();
455 for (int64_t i = 0; i < duration; ++i) {
456 auto frame = reader->get_frame (i);
457 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
458 if (!stereo_asset->encrypted() || mono_asset->key()) {
459 vector<VerificationNote> j2k_notes;
460 verify_j2k (frame->left(), j2k_notes);
461 verify_j2k (frame->right(), j2k_notes);
462 check_and_add (j2k_notes);
464 progress (float(i) / duration);
469 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
470 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
471 if (biggest_frame > max_frame) {
473 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
475 } else if (biggest_frame > risky_frame) {
477 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
484 verify_main_picture_asset (
485 shared_ptr<const DCP> dcp,
486 shared_ptr<const ReelPictureAsset> reel_asset,
487 function<void (string, optional<boost::filesystem::path>)> stage,
488 function<void (float)> progress,
489 vector<VerificationNote>& notes
492 auto asset = reel_asset->asset();
493 auto const file = *asset->file();
494 stage ("Checking picture asset hash", file);
495 auto const r = verify_asset (dcp, reel_asset, progress);
497 case VerifyAssetResult::BAD:
499 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
502 case VerifyAssetResult::CPL_PKL_DIFFER:
504 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
510 stage ("Checking picture frame sizes", asset->file());
511 verify_picture_asset (reel_asset, file, notes, progress);
513 /* Only flat/scope allowed by Bv2.1 */
515 asset->size() != Size(2048, 858) &&
516 asset->size() != Size(1998, 1080) &&
517 asset->size() != Size(4096, 1716) &&
518 asset->size() != Size(3996, 2160)) {
520 VerificationNote::Type::BV21_ERROR,
521 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
522 String::compose("%1x%2", asset->size().width, asset->size().height),
527 /* Only 24, 25, 48fps allowed for 2K */
529 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
530 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
533 VerificationNote::Type::BV21_ERROR,
534 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
535 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
540 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
541 /* Only 24fps allowed for 4K */
542 if (asset->edit_rate() != Fraction(24, 1)) {
544 VerificationNote::Type::BV21_ERROR,
545 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
546 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
551 /* Only 2D allowed for 4K */
552 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
554 VerificationNote::Type::BV21_ERROR,
555 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
556 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
567 verify_main_sound_asset (
568 shared_ptr<const DCP> dcp,
569 shared_ptr<const ReelSoundAsset> reel_asset,
570 function<void (string, optional<boost::filesystem::path>)> stage,
571 function<void (float)> progress,
572 vector<VerificationNote>& notes
575 auto asset = reel_asset->asset();
576 stage ("Checking sound asset hash", asset->file());
577 auto const r = verify_asset (dcp, reel_asset, progress);
579 case VerifyAssetResult::BAD:
580 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
582 case VerifyAssetResult::CPL_PKL_DIFFER:
583 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
589 stage ("Checking sound asset metadata", asset->file());
591 if (auto lang = asset->language()) {
592 verify_language_tag (*lang, notes);
594 if (asset->sampling_rate() != 48000) {
595 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
601 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
603 /* XXX: is Language compulsory? */
604 if (reel_asset->language()) {
605 verify_language_tag (*reel_asset->language(), notes);
608 if (!reel_asset->entry_point()) {
609 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
610 } else if (reel_asset->entry_point().get()) {
611 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
617 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
619 /* XXX: is Language compulsory? */
620 if (reel_asset->language()) {
621 verify_language_tag (*reel_asset->language(), notes);
624 if (!reel_asset->entry_point()) {
625 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
626 } else if (reel_asset->entry_point().get()) {
627 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
634 boost::optional<string> subtitle_language;
638 /** Verify stuff that is common to both subtitles and closed captions */
640 verify_smpte_timed_text_asset (
641 shared_ptr<const SMPTESubtitleAsset> asset,
642 optional<int64_t> reel_asset_duration,
643 vector<VerificationNote>& notes
646 if (asset->language()) {
647 verify_language_tag (*asset->language(), notes);
649 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
652 auto const size = boost::filesystem::file_size(asset->file().get());
653 if (size > 115 * 1024 * 1024) {
655 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
659 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
660 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
662 auto fonts = asset->font_data ();
664 for (auto i: fonts) {
665 total_size += i.second.size();
667 if (total_size > 10 * 1024 * 1024) {
668 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
671 if (!asset->start_time()) {
672 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
673 } else if (asset->start_time() != Time()) {
674 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
677 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
680 VerificationNote::Type::BV21_ERROR,
681 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
682 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
689 /** Verify SMPTE subtitle-only stuff */
691 verify_smpte_subtitle_asset (
692 shared_ptr<const SMPTESubtitleAsset> asset,
693 vector<VerificationNote>& notes,
697 if (asset->language()) {
698 if (!state.subtitle_language) {
699 state.subtitle_language = *asset->language();
700 } else if (state.subtitle_language != *asset->language()) {
701 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
705 DCP_ASSERT (asset->resource_id());
706 auto xml_id = asset->xml_id();
708 if (asset->resource_id().get() != xml_id) {
709 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
712 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
713 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
716 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
721 /** Verify all subtitle stuff */
723 verify_subtitle_asset (
724 shared_ptr<const SubtitleAsset> asset,
725 optional<int64_t> reel_asset_duration,
726 function<void (string, optional<boost::filesystem::path>)> stage,
727 boost::filesystem::path xsd_dtd_directory,
728 vector<VerificationNote>& notes,
732 stage ("Checking subtitle XML", asset->file());
733 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
734 * gets passed through libdcp which may clean up and therefore hide errors.
736 if (asset->raw_xml()) {
737 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
739 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
742 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
744 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
745 verify_smpte_subtitle_asset (smpte, notes, state);
750 /** Verify all closed caption stuff */
752 verify_closed_caption_asset (
753 shared_ptr<const SubtitleAsset> asset,
754 optional<int64_t> reel_asset_duration,
755 function<void (string, optional<boost::filesystem::path>)> stage,
756 boost::filesystem::path xsd_dtd_directory,
757 vector<VerificationNote>& notes
760 stage ("Checking closed caption XML", asset->file());
761 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
762 * gets passed through libdcp which may clean up and therefore hide errors.
764 auto raw_xml = asset->raw_xml();
766 validate_xml (*raw_xml, xsd_dtd_directory, notes);
767 if (raw_xml->size() > 256 * 1024) {
768 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
771 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
774 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
776 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
784 vector<shared_ptr<Reel>> reels,
786 vector<VerificationNote>& notes,
787 std::function<bool (shared_ptr<Reel>)> check,
788 std::function<optional<string> (shared_ptr<Reel>)> xml,
789 std::function<int64_t (shared_ptr<Reel>)> duration
792 /* end of last subtitle (in editable units) */
793 optional<int64_t> last_out;
794 auto too_short = false;
795 auto too_close = false;
796 auto too_early = false;
797 auto reel_overlap = false;
798 /* current reel start time (in editable units) */
799 int64_t reel_offset = 0;
801 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
802 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
803 if (node->name() == "Subtitle") {
804 Time in (node->string_attribute("TimeIn"), tcr);
808 Time out (node->string_attribute("TimeOut"), tcr);
812 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
815 auto length = out - in;
816 if (length.as_editable_units_ceil(er) < 15) {
820 /* XXX: this feels dubious - is it really what Bv2.1 means? */
821 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
822 if (distance >= 0 && distance < 2) {
826 last_out = reel_offset + out.as_editable_units_floor(er);
828 for (auto i: node->node_children()) {
829 parse(i, tcr, start_time, er, first_reel);
834 for (auto i = 0U; i < reels.size(); ++i) {
835 if (!check(reels[i])) {
839 auto reel_xml = xml(reels[i]);
841 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
845 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
846 * read in by libdcp's parser.
849 shared_ptr<cxml::Document> doc;
851 optional<Time> start_time;
853 doc = make_shared<cxml::Document>("SubtitleReel");
854 doc->read_string (*reel_xml);
855 tcr = doc->number_child<int>("TimeCodeRate");
856 auto start_time_string = doc->optional_string_child("StartTime");
857 if (start_time_string) {
858 start_time = Time(*start_time_string, tcr);
861 doc = make_shared<cxml::Document>("DCSubtitle");
862 doc->read_string (*reel_xml);
864 parse (doc, tcr, start_time, edit_rate, i == 0);
865 auto end = reel_offset + duration(reels[i]);
866 if (last_out && *last_out > end) {
872 if (last_out && *last_out > reel_offset) {
878 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
884 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
890 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
896 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
902 struct LinesCharactersResult
904 bool warning_length_exceeded = false;
905 bool error_length_exceeded = false;
906 bool line_count_exceeded = false;
912 verify_text_lines_and_characters (
913 shared_ptr<SubtitleAsset> asset,
916 LinesCharactersResult* result
922 Event (Time time_, float position_, int characters_)
924 , position (position_)
925 , characters (characters_)
928 Event (Time time_, shared_ptr<Event> start_)
934 int position; //< position from 0 at top of screen to 100 at bottom
936 shared_ptr<Event> start;
939 vector<shared_ptr<Event>> events;
941 auto position = [](shared_ptr<const SubtitleString> sub) {
942 switch (sub->v_align()) {
944 return lrintf(sub->v_position() * 100);
946 return lrintf((0.5f + sub->v_position()) * 100);
948 return lrintf((1.0f - sub->v_position()) * 100);
954 for (auto j: asset->subtitles()) {
955 auto text = dynamic_pointer_cast<const SubtitleString>(j);
957 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
958 events.push_back(in);
959 events.push_back(make_shared<Event>(text->out(), in));
963 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
964 return a->time < b->time;
967 map<int, int> current;
968 for (auto i: events) {
969 if (current.size() > 3) {
970 result->line_count_exceeded = true;
972 for (auto j: current) {
973 if (j.second >= warning_length) {
974 result->warning_length_exceeded = true;
976 if (j.second >= error_length) {
977 result->error_length_exceeded = true;
982 /* end of a subtitle */
983 DCP_ASSERT (current.find(i->start->position) != current.end());
984 if (current[i->start->position] == i->start->characters) {
985 current.erase(i->start->position);
987 current[i->start->position] -= i->start->characters;
990 /* start of a subtitle */
991 if (current.find(i->position) == current.end()) {
992 current[i->position] = i->characters;
994 current[i->position] += i->characters;
1003 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1005 if (reels.empty()) {
1009 if (reels[0]->main_subtitle()) {
1010 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1011 [](shared_ptr<Reel> reel) {
1012 return static_cast<bool>(reel->main_subtitle());
1014 [](shared_ptr<Reel> reel) {
1015 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1017 return interop->asset()->raw_xml();
1019 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1021 return smpte->asset()->raw_xml();
1023 [](shared_ptr<Reel> reel) {
1024 return reel->main_subtitle()->actual_duration();
1029 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1030 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1031 [i](shared_ptr<Reel> reel) {
1032 return i < reel->closed_captions().size();
1034 [i](shared_ptr<Reel> reel) {
1035 return reel->closed_captions()[i]->asset()->raw_xml();
1037 [i](shared_ptr<Reel> reel) {
1038 return reel->closed_captions()[i]->actual_duration();
1046 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1048 DCP_ASSERT (cpl->file());
1049 cxml::Document doc ("CompositionPlaylist");
1050 doc.read_file (cpl->file().get());
1052 auto missing = false;
1055 if (auto reel_list = doc.node_child("ReelList")) {
1056 auto reels = reel_list->node_children("Reel");
1057 if (!reels.empty()) {
1058 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1059 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1060 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1062 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1063 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1067 if (auto name = extension->optional_node_child("Name")) {
1068 if (name->content() != "Application") {
1069 malformed = "<Name> should be 'Application'";
1072 if (auto property_list = extension->optional_node_child("PropertyList")) {
1073 if (auto property = property_list->optional_node_child("Property")) {
1074 if (auto name = property->optional_node_child("Name")) {
1075 if (name->content() != "DCP Constraints Profile") {
1076 malformed = "<Name> property should be 'DCP Constraints Profile'";
1079 if (auto value = property->optional_node_child("Value")) {
1080 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1081 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1096 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1097 } else if (!malformed.empty()) {
1098 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1104 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1106 vector<string> encrypted;
1107 for (auto i: dcp->cpls()) {
1108 for (auto j: i->reel_file_assets()) {
1109 if (j->asset_ref().resolved()) {
1110 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1111 if (mxf && mxf->encrypted()) {
1112 encrypted.push_back(j->asset_ref().id());
1118 for (auto i: pkl->asset_list()) {
1119 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1128 vector<VerificationNote>
1130 vector<boost::filesystem::path> directories,
1131 function<void (string, optional<boost::filesystem::path>)> stage,
1132 function<void (float)> progress,
1133 optional<boost::filesystem::path> xsd_dtd_directory
1136 if (!xsd_dtd_directory) {
1137 xsd_dtd_directory = resources_directory() / "xsd";
1139 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1141 vector<VerificationNote> notes;
1144 vector<shared_ptr<DCP>> dcps;
1145 for (auto i: directories) {
1146 dcps.push_back (make_shared<DCP>(i));
1149 for (auto dcp: dcps) {
1150 stage ("Checking DCP", dcp->directory());
1151 bool carry_on = true;
1153 dcp->read (¬es, true);
1154 } catch (MissingAssetmapError& e) {
1155 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1157 } catch (ReadError& e) {
1158 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1159 } catch (XMLError& e) {
1160 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1161 } catch (MXFFileError& e) {
1162 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1163 } catch (cxml::Error& e) {
1164 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1171 if (dcp->standard() != Standard::SMPTE) {
1172 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1175 for (auto cpl: dcp->cpls()) {
1176 stage ("Checking CPL", cpl->file());
1177 validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1179 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1180 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1183 for (auto const& i: cpl->additional_subtitle_languages()) {
1184 verify_language_tag (i, notes);
1187 if (cpl->release_territory()) {
1188 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") {
1189 auto terr = cpl->release_territory().get();
1190 /* Must be a valid region tag, or "001" */
1192 LanguageTag::RegionSubtag test (terr);
1194 if (terr != "001") {
1195 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1201 if (dcp->standard() == Standard::SMPTE) {
1202 if (!cpl->annotation_text()) {
1203 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1204 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1205 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1209 for (auto i: dcp->pkls()) {
1210 /* Check that the CPL's hash corresponds to the PKL */
1211 optional<string> h = i->hash(cpl->id());
1212 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1213 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1216 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1217 optional<string> required_annotation_text;
1218 for (auto j: i->asset_list()) {
1219 /* See if this is a CPL */
1220 for (auto k: dcp->cpls()) {
1221 if (j->id() == k->id()) {
1222 if (!required_annotation_text) {
1223 /* First CPL we have found; this is the required AnnotationText unless we find another */
1224 required_annotation_text = cpl->content_title_text();
1226 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1227 required_annotation_text = boost::none;
1233 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1234 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1238 /* set to true if any reel has a MainSubtitle */
1239 auto have_main_subtitle = false;
1240 /* set to true if any reel has no MainSubtitle */
1241 auto have_no_main_subtitle = false;
1242 /* fewest number of closed caption assets seen in a reel */
1243 size_t fewest_closed_captions = SIZE_MAX;
1244 /* most number of closed caption assets seen in a reel */
1245 size_t most_closed_captions = 0;
1246 map<Marker, Time> markers_seen;
1248 for (auto reel: cpl->reels()) {
1249 stage ("Checking reel", optional<boost::filesystem::path>());
1251 for (auto i: reel->assets()) {
1252 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1253 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1255 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1256 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1258 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1259 if (i->encryptable() && !file_asset->hash()) {
1260 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1264 if (dcp->standard() == Standard::SMPTE) {
1265 boost::optional<int64_t> duration;
1266 for (auto i: reel->assets()) {
1268 duration = i->actual_duration();
1269 } else if (*duration != i->actual_duration()) {
1270 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1276 if (reel->main_picture()) {
1277 /* Check reel stuff */
1278 auto const frame_rate = reel->main_picture()->frame_rate();
1279 if (frame_rate.denominator != 1 ||
1280 (frame_rate.numerator != 24 &&
1281 frame_rate.numerator != 25 &&
1282 frame_rate.numerator != 30 &&
1283 frame_rate.numerator != 48 &&
1284 frame_rate.numerator != 50 &&
1285 frame_rate.numerator != 60 &&
1286 frame_rate.numerator != 96)) {
1288 VerificationNote::Type::ERROR,
1289 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1290 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1294 if (reel->main_picture()->asset_ref().resolved()) {
1295 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1299 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1300 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1303 if (reel->main_subtitle()) {
1304 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1305 if (reel->main_subtitle()->asset_ref().resolved()) {
1306 verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1308 have_main_subtitle = true;
1310 have_no_main_subtitle = true;
1313 for (auto i: reel->closed_captions()) {
1314 verify_closed_caption_reel (i, notes);
1315 if (i->asset_ref().resolved()) {
1316 verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1320 if (reel->main_markers()) {
1321 for (auto const& i: reel->main_markers()->get()) {
1322 markers_seen.insert (i);
1326 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1327 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1330 verify_text_timing (cpl->reels(), notes);
1332 if (dcp->standard() == Standard::SMPTE) {
1334 if (have_main_subtitle && have_no_main_subtitle) {
1335 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1338 if (fewest_closed_captions != most_closed_captions) {
1339 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1342 if (cpl->content_kind() == ContentKind::FEATURE) {
1343 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1344 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1346 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1347 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1351 auto ffoc = markers_seen.find(Marker::FFOC);
1352 if (ffoc == markers_seen.end()) {
1353 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1354 } else if (ffoc->second.e != 1) {
1355 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1358 auto lfoc = markers_seen.find(Marker::LFOC);
1359 if (lfoc == markers_seen.end()) {
1360 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1362 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1363 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1364 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1368 LinesCharactersResult result;
1369 for (auto reel: cpl->reels()) {
1370 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1371 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1375 if (result.line_count_exceeded) {
1376 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1378 if (result.error_length_exceeded) {
1379 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1380 } else if (result.warning_length_exceeded) {
1381 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1384 result = LinesCharactersResult();
1385 for (auto reel: cpl->reels()) {
1386 for (auto i: reel->closed_captions()) {
1388 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1393 if (result.line_count_exceeded) {
1394 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1396 if (result.error_length_exceeded) {
1397 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1400 if (!cpl->full_content_title_text()) {
1401 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1402 * can use it as a proxy for CompositionMetadataAsset's existence.
1404 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1405 } else if (!cpl->version_number()) {
1406 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1409 verify_extension_metadata (cpl, notes);
1411 if (cpl->any_encrypted()) {
1412 cxml::Document doc ("CompositionPlaylist");
1413 DCP_ASSERT (cpl->file());
1414 doc.read_file (cpl->file().get());
1415 if (!doc.optional_node_child("Signature")) {
1416 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1422 for (auto pkl: dcp->pkls()) {
1423 stage ("Checking PKL", pkl->file());
1424 validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1425 if (pkl_has_encrypted_assets(dcp, pkl)) {
1426 cxml::Document doc ("PackingList");
1427 doc.read_file (pkl->file().get());
1428 if (!doc.optional_node_child("Signature")) {
1429 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1434 if (dcp->asset_map_path()) {
1435 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1436 validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1438 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1447 dcp::note_to_string (VerificationNote note)
1449 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1451 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1452 * not "ClosedCaption assets must have an <EntryPoint> tag."
1454 * It's OK to use XML tag names where they are clear.
1455 * If both ID and filename are available, use only the ID.
1456 * End messages with a full stop.
1457 * Messages should not mention whether or not their errors are a part of Bv2.1.
1459 switch (note.code()) {
1460 case VerificationNote::Code::FAILED_READ:
1461 return *note.note();
1462 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1463 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1464 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1465 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1466 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1467 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1468 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1469 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1470 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1471 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1472 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1473 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1474 case VerificationNote::Code::EMPTY_ASSET_PATH:
1475 return "The asset map contains an empty asset path.";
1476 case VerificationNote::Code::MISSING_ASSET:
1477 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1478 case VerificationNote::Code::MISMATCHED_STANDARD:
1479 return "The DCP contains both SMPTE and Interop parts.";
1480 case VerificationNote::Code::INVALID_XML:
1481 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1482 case VerificationNote::Code::MISSING_ASSETMAP:
1483 return "No ASSETMAP or ASSETMAP.xml was found.";
1484 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1485 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1486 case VerificationNote::Code::INVALID_DURATION:
1487 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1488 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1489 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());
1490 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1491 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());
1492 case VerificationNote::Code::EXTERNAL_ASSET:
1493 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());
1494 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1495 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1496 case VerificationNote::Code::INVALID_STANDARD:
1497 return "This DCP does not use the SMPTE standard.";
1498 case VerificationNote::Code::INVALID_LANGUAGE:
1499 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1500 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1501 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1502 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1503 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1504 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1505 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1506 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1507 return "3D 4K DCPs are not allowed.";
1508 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1509 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1510 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1511 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1512 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1513 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());
1514 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1515 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1516 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1517 return "Some subtitle assets have different <Language> tags than others";
1518 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1519 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1520 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1521 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1522 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1523 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1524 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1525 return "At least one subtitle lasts less than 15 frames.";
1526 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1527 return "At least one pair of subtitles is separated by less than 2 frames.";
1528 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1529 return "At least one subtitle extends outside of its reel.";
1530 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1531 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1532 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1533 return "There are more than 52 characters in at least one subtitle line.";
1534 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1535 return "There are more than 79 characters in at least one subtitle line.";
1536 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1537 return "There are more than 3 closed caption lines in at least one place.";
1538 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1539 return "There are more than 32 characters in at least one closed caption line.";
1540 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1541 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1542 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1543 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1544 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1545 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1546 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1547 return "All assets in a reel do not have the same duration.";
1548 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1549 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1550 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1551 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1552 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1553 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1554 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1555 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1556 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1557 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1558 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1559 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1560 case VerificationNote::Code::MISSING_HASH:
1561 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1562 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1563 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1564 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1565 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1566 case VerificationNote::Code::MISSING_FFOC:
1567 return "There should be a FFOC (first frame of content) marker";
1568 case VerificationNote::Code::MISSING_LFOC:
1569 return "There should be a LFOC (last frame of content) marker";
1570 case VerificationNote::Code::INCORRECT_FFOC:
1571 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1572 case VerificationNote::Code::INCORRECT_LFOC:
1573 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1574 case VerificationNote::Code::MISSING_CPL_METADATA:
1575 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1576 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1577 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1578 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1579 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1580 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1581 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1582 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1583 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1584 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1585 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1586 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1587 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1588 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1589 return "Some assets are encrypted but some are not.";
1590 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1591 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1592 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1593 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1594 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1595 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1596 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1597 return "The JPEG2000 tile size is not the same as the image size.";
1598 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1599 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1600 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1601 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1602 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1603 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1604 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1605 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1606 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1607 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1608 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1609 return "POC marker found outside main header";
1610 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1611 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1612 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1613 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1614 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1615 return "No TLM marker was found in a JPEG2000 codestream.";
1616 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1617 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1618 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1619 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1620 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1622 vector<string> parts;
1623 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1624 DCP_ASSERT (parts.size() == 2);
1625 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]);
1627 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1628 return "Some aspect of this DCP could not be checked because it is encrypted.";
1636 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1638 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1643 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1645 if (a.type() != b.type()) {
1646 return a.type() < b.type();
1649 if (a.code() != b.code()) {
1650 return a.code() < b.code();
1653 if (a.note() != b.note()) {
1654 return a.note().get_value_or("") < b.note().get_value_or("");
1657 if (a.file() != b.file()) {
1658 return a.file().get_value_or("") < b.file().get_value_or("");
1661 return a.line().get_value_or(0) < b.line().get_value_or(0);
1666 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1668 s << note_to_string (note);
1670 s << " [" << note.note().get() << "]";
1673 s << " [" << note.file().get() << "]";
1676 s << " [" << note.line().get() << "]";