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 if (asset->resource_id().get() != asset->xml_id()) {
707 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
710 if (asset->id() == asset->resource_id().get() || asset->id() == asset->xml_id()) {
711 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
716 /** Verify all subtitle stuff */
718 verify_subtitle_asset (
719 shared_ptr<const SubtitleAsset> asset,
720 optional<int64_t> reel_asset_duration,
721 function<void (string, optional<boost::filesystem::path>)> stage,
722 boost::filesystem::path xsd_dtd_directory,
723 vector<VerificationNote>& notes,
727 stage ("Checking subtitle XML", asset->file());
728 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
729 * gets passed through libdcp which may clean up and therefore hide errors.
731 if (asset->raw_xml()) {
732 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
734 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
737 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
739 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
740 verify_smpte_subtitle_asset (smpte, notes, state);
745 /** Verify all closed caption stuff */
747 verify_closed_caption_asset (
748 shared_ptr<const SubtitleAsset> asset,
749 optional<int64_t> reel_asset_duration,
750 function<void (string, optional<boost::filesystem::path>)> stage,
751 boost::filesystem::path xsd_dtd_directory,
752 vector<VerificationNote>& notes
755 stage ("Checking closed caption XML", asset->file());
756 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
757 * gets passed through libdcp which may clean up and therefore hide errors.
759 auto raw_xml = asset->raw_xml();
761 validate_xml (*raw_xml, xsd_dtd_directory, notes);
762 if (raw_xml->size() > 256 * 1024) {
763 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
766 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
769 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
771 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
779 vector<shared_ptr<Reel>> reels,
781 vector<VerificationNote>& notes,
782 std::function<bool (shared_ptr<Reel>)> check,
783 std::function<optional<string> (shared_ptr<Reel>)> xml,
784 std::function<int64_t (shared_ptr<Reel>)> duration
787 /* end of last subtitle (in editable units) */
788 optional<int64_t> last_out;
789 auto too_short = false;
790 auto too_close = false;
791 auto too_early = false;
792 auto reel_overlap = false;
793 /* current reel start time (in editable units) */
794 int64_t reel_offset = 0;
796 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
797 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) {
798 if (node->name() == "Subtitle") {
799 Time in (node->string_attribute("TimeIn"), tcr);
803 Time out (node->string_attribute("TimeOut"), tcr);
807 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
810 auto length = out - in;
811 if (length.as_editable_units_ceil(er) < 15) {
815 /* XXX: this feels dubious - is it really what Bv2.1 means? */
816 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
817 if (distance >= 0 && distance < 2) {
821 last_out = reel_offset + out.as_editable_units_floor(er);
823 for (auto i: node->node_children()) {
824 parse(i, tcr, start_time, er, first_reel);
829 for (auto i = 0U; i < reels.size(); ++i) {
830 if (!check(reels[i])) {
834 auto reel_xml = xml(reels[i]);
836 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
840 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
841 * read in by libdcp's parser.
844 shared_ptr<cxml::Document> doc;
846 optional<Time> start_time;
848 doc = make_shared<cxml::Document>("SubtitleReel");
849 doc->read_string (*reel_xml);
850 tcr = doc->number_child<int>("TimeCodeRate");
851 auto start_time_string = doc->optional_string_child("StartTime");
852 if (start_time_string) {
853 start_time = Time(*start_time_string, tcr);
856 doc = make_shared<cxml::Document>("DCSubtitle");
857 doc->read_string (*reel_xml);
859 parse (doc, tcr, start_time, edit_rate, i == 0);
860 auto end = reel_offset + duration(reels[i]);
861 if (last_out && *last_out > end) {
867 if (last_out && *last_out > reel_offset) {
873 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
879 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
885 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
891 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
897 struct LinesCharactersResult
899 bool warning_length_exceeded = false;
900 bool error_length_exceeded = false;
901 bool line_count_exceeded = false;
907 verify_text_lines_and_characters (
908 shared_ptr<SubtitleAsset> asset,
911 LinesCharactersResult* result
917 Event (Time time_, float position_, int characters_)
919 , position (position_)
920 , characters (characters_)
923 Event (Time time_, shared_ptr<Event> start_)
929 int position; //< position from 0 at top of screen to 100 at bottom
931 shared_ptr<Event> start;
934 vector<shared_ptr<Event>> events;
936 auto position = [](shared_ptr<const SubtitleString> sub) {
937 switch (sub->v_align()) {
939 return lrintf(sub->v_position() * 100);
941 return lrintf((0.5f + sub->v_position()) * 100);
943 return lrintf((1.0f - sub->v_position()) * 100);
949 for (auto j: asset->subtitles()) {
950 auto text = dynamic_pointer_cast<const SubtitleString>(j);
952 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
953 events.push_back(in);
954 events.push_back(make_shared<Event>(text->out(), in));
958 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
959 return a->time < b->time;
962 map<int, int> current;
963 for (auto i: events) {
964 if (current.size() > 3) {
965 result->line_count_exceeded = true;
967 for (auto j: current) {
968 if (j.second >= warning_length) {
969 result->warning_length_exceeded = true;
971 if (j.second >= error_length) {
972 result->error_length_exceeded = true;
977 /* end of a subtitle */
978 DCP_ASSERT (current.find(i->start->position) != current.end());
979 if (current[i->start->position] == i->start->characters) {
980 current.erase(i->start->position);
982 current[i->start->position] -= i->start->characters;
985 /* start of a subtitle */
986 if (current.find(i->position) == current.end()) {
987 current[i->position] = i->characters;
989 current[i->position] += i->characters;
998 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1000 if (reels.empty()) {
1004 if (reels[0]->main_subtitle()) {
1005 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1006 [](shared_ptr<Reel> reel) {
1007 return static_cast<bool>(reel->main_subtitle());
1009 [](shared_ptr<Reel> reel) {
1010 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1012 return interop->asset()->raw_xml();
1014 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1016 return smpte->asset()->raw_xml();
1018 [](shared_ptr<Reel> reel) {
1019 return reel->main_subtitle()->actual_duration();
1024 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1025 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1026 [i](shared_ptr<Reel> reel) {
1027 return i < reel->closed_captions().size();
1029 [i](shared_ptr<Reel> reel) {
1030 return reel->closed_captions()[i]->asset()->raw_xml();
1032 [i](shared_ptr<Reel> reel) {
1033 return reel->closed_captions()[i]->actual_duration();
1041 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1043 DCP_ASSERT (cpl->file());
1044 cxml::Document doc ("CompositionPlaylist");
1045 doc.read_file (cpl->file().get());
1047 auto missing = false;
1050 if (auto reel_list = doc.node_child("ReelList")) {
1051 auto reels = reel_list->node_children("Reel");
1052 if (!reels.empty()) {
1053 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1054 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1055 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1057 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1058 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1062 if (auto name = extension->optional_node_child("Name")) {
1063 if (name->content() != "Application") {
1064 malformed = "<Name> should be 'Application'";
1067 if (auto property_list = extension->optional_node_child("PropertyList")) {
1068 if (auto property = property_list->optional_node_child("Property")) {
1069 if (auto name = property->optional_node_child("Name")) {
1070 if (name->content() != "DCP Constraints Profile") {
1071 malformed = "<Name> property should be 'DCP Constraints Profile'";
1074 if (auto value = property->optional_node_child("Value")) {
1075 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1076 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1091 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1092 } else if (!malformed.empty()) {
1093 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1099 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1101 vector<string> encrypted;
1102 for (auto i: dcp->cpls()) {
1103 for (auto j: i->reel_file_assets()) {
1104 if (j->asset_ref().resolved()) {
1105 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1106 if (mxf && mxf->encrypted()) {
1107 encrypted.push_back(j->asset_ref().id());
1113 for (auto i: pkl->asset_list()) {
1114 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1123 vector<VerificationNote>
1125 vector<boost::filesystem::path> directories,
1126 function<void (string, optional<boost::filesystem::path>)> stage,
1127 function<void (float)> progress,
1128 optional<boost::filesystem::path> xsd_dtd_directory
1131 if (!xsd_dtd_directory) {
1132 xsd_dtd_directory = resources_directory() / "xsd";
1134 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1136 vector<VerificationNote> notes;
1139 vector<shared_ptr<DCP>> dcps;
1140 for (auto i: directories) {
1141 dcps.push_back (make_shared<DCP>(i));
1144 for (auto dcp: dcps) {
1145 stage ("Checking DCP", dcp->directory());
1146 bool carry_on = true;
1148 dcp->read (¬es, true);
1149 } catch (MissingAssetmapError& e) {
1150 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1152 } catch (ReadError& e) {
1153 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1154 } catch (XMLError& e) {
1155 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1156 } catch (MXFFileError& e) {
1157 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1158 } catch (cxml::Error& e) {
1159 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1166 if (dcp->standard() != Standard::SMPTE) {
1167 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1170 for (auto cpl: dcp->cpls()) {
1171 stage ("Checking CPL", cpl->file());
1172 validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1174 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1175 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1178 for (auto const& i: cpl->additional_subtitle_languages()) {
1179 verify_language_tag (i, notes);
1182 if (cpl->release_territory()) {
1183 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") {
1184 auto terr = cpl->release_territory().get();
1185 /* Must be a valid region tag, or "001" */
1187 LanguageTag::RegionSubtag test (terr);
1189 if (terr != "001") {
1190 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1196 if (dcp->standard() == Standard::SMPTE) {
1197 if (!cpl->annotation_text()) {
1198 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1199 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1200 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1204 for (auto i: dcp->pkls()) {
1205 /* Check that the CPL's hash corresponds to the PKL */
1206 optional<string> h = i->hash(cpl->id());
1207 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1208 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1211 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1212 optional<string> required_annotation_text;
1213 for (auto j: i->asset_list()) {
1214 /* See if this is a CPL */
1215 for (auto k: dcp->cpls()) {
1216 if (j->id() == k->id()) {
1217 if (!required_annotation_text) {
1218 /* First CPL we have found; this is the required AnnotationText unless we find another */
1219 required_annotation_text = cpl->content_title_text();
1221 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1222 required_annotation_text = boost::none;
1228 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1229 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1233 /* set to true if any reel has a MainSubtitle */
1234 auto have_main_subtitle = false;
1235 /* set to true if any reel has no MainSubtitle */
1236 auto have_no_main_subtitle = false;
1237 /* fewest number of closed caption assets seen in a reel */
1238 size_t fewest_closed_captions = SIZE_MAX;
1239 /* most number of closed caption assets seen in a reel */
1240 size_t most_closed_captions = 0;
1241 map<Marker, Time> markers_seen;
1243 for (auto reel: cpl->reels()) {
1244 stage ("Checking reel", optional<boost::filesystem::path>());
1246 for (auto i: reel->assets()) {
1247 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1248 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1250 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1251 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1253 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1254 if (i->encryptable() && !file_asset->hash()) {
1255 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1259 if (dcp->standard() == Standard::SMPTE) {
1260 boost::optional<int64_t> duration;
1261 for (auto i: reel->assets()) {
1263 duration = i->actual_duration();
1264 } else if (*duration != i->actual_duration()) {
1265 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1271 if (reel->main_picture()) {
1272 /* Check reel stuff */
1273 auto const frame_rate = reel->main_picture()->frame_rate();
1274 if (frame_rate.denominator != 1 ||
1275 (frame_rate.numerator != 24 &&
1276 frame_rate.numerator != 25 &&
1277 frame_rate.numerator != 30 &&
1278 frame_rate.numerator != 48 &&
1279 frame_rate.numerator != 50 &&
1280 frame_rate.numerator != 60 &&
1281 frame_rate.numerator != 96)) {
1283 VerificationNote::Type::ERROR,
1284 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1285 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1289 if (reel->main_picture()->asset_ref().resolved()) {
1290 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1294 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1295 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1298 if (reel->main_subtitle()) {
1299 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1300 if (reel->main_subtitle()->asset_ref().resolved()) {
1301 verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1303 have_main_subtitle = true;
1305 have_no_main_subtitle = true;
1308 for (auto i: reel->closed_captions()) {
1309 verify_closed_caption_reel (i, notes);
1310 if (i->asset_ref().resolved()) {
1311 verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1315 if (reel->main_markers()) {
1316 for (auto const& i: reel->main_markers()->get()) {
1317 markers_seen.insert (i);
1321 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1322 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1325 verify_text_timing (cpl->reels(), notes);
1327 if (dcp->standard() == Standard::SMPTE) {
1329 if (have_main_subtitle && have_no_main_subtitle) {
1330 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1333 if (fewest_closed_captions != most_closed_captions) {
1334 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1337 if (cpl->content_kind() == ContentKind::FEATURE) {
1338 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1339 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1341 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1342 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1346 auto ffoc = markers_seen.find(Marker::FFOC);
1347 if (ffoc == markers_seen.end()) {
1348 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1349 } else if (ffoc->second.e != 1) {
1350 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1353 auto lfoc = markers_seen.find(Marker::LFOC);
1354 if (lfoc == markers_seen.end()) {
1355 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1357 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1358 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1359 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1363 LinesCharactersResult result;
1364 for (auto reel: cpl->reels()) {
1365 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1366 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1370 if (result.line_count_exceeded) {
1371 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1373 if (result.error_length_exceeded) {
1374 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1375 } else if (result.warning_length_exceeded) {
1376 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1379 result = LinesCharactersResult();
1380 for (auto reel: cpl->reels()) {
1381 for (auto i: reel->closed_captions()) {
1383 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1388 if (result.line_count_exceeded) {
1389 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1391 if (result.error_length_exceeded) {
1392 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1395 if (!cpl->full_content_title_text()) {
1396 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1397 * can use it as a proxy for CompositionMetadataAsset's existence.
1399 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1400 } else if (!cpl->version_number()) {
1401 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1404 verify_extension_metadata (cpl, notes);
1406 if (cpl->any_encrypted()) {
1407 cxml::Document doc ("CompositionPlaylist");
1408 DCP_ASSERT (cpl->file());
1409 doc.read_file (cpl->file().get());
1410 if (!doc.optional_node_child("Signature")) {
1411 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1417 for (auto pkl: dcp->pkls()) {
1418 stage ("Checking PKL", pkl->file());
1419 validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1420 if (pkl_has_encrypted_assets(dcp, pkl)) {
1421 cxml::Document doc ("PackingList");
1422 doc.read_file (pkl->file().get());
1423 if (!doc.optional_node_child("Signature")) {
1424 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1429 if (dcp->asset_map_path()) {
1430 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1431 validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1433 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1442 dcp::note_to_string (VerificationNote note)
1444 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1446 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1447 * not "ClosedCaption assets must have an <EntryPoint> tag."
1449 * It's OK to use XML tag names where they are clear.
1450 * If both ID and filename are available, use only the ID.
1451 * End messages with a full stop.
1452 * Messages should not mention whether or not their errors are a part of Bv2.1.
1454 switch (note.code()) {
1455 case VerificationNote::Code::FAILED_READ:
1456 return *note.note();
1457 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1458 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1459 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1460 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1461 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1462 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1463 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1464 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1465 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1466 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1467 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1468 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1469 case VerificationNote::Code::EMPTY_ASSET_PATH:
1470 return "The asset map contains an empty asset path.";
1471 case VerificationNote::Code::MISSING_ASSET:
1472 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1473 case VerificationNote::Code::MISMATCHED_STANDARD:
1474 return "The DCP contains both SMPTE and Interop parts.";
1475 case VerificationNote::Code::INVALID_XML:
1476 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1477 case VerificationNote::Code::MISSING_ASSETMAP:
1478 return "No ASSETMAP or ASSETMAP.xml was found.";
1479 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1480 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1481 case VerificationNote::Code::INVALID_DURATION:
1482 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1483 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1484 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());
1485 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1486 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());
1487 case VerificationNote::Code::EXTERNAL_ASSET:
1488 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());
1489 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1490 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1491 case VerificationNote::Code::INVALID_STANDARD:
1492 return "This DCP does not use the SMPTE standard.";
1493 case VerificationNote::Code::INVALID_LANGUAGE:
1494 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1495 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1496 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1497 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1498 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1499 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1500 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1501 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1502 return "3D 4K DCPs are not allowed.";
1503 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1504 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1505 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1506 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1507 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1508 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());
1509 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1510 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1511 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1512 return "Some subtitle assets have different <Language> tags than others";
1513 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1514 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1515 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1516 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1517 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1518 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1519 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1520 return "At least one subtitle lasts less than 15 frames.";
1521 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1522 return "At least one pair of subtitles is separated by less than 2 frames.";
1523 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1524 return "At least one subtitle extends outside of its reel.";
1525 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1526 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1527 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1528 return "There are more than 52 characters in at least one subtitle line.";
1529 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1530 return "There are more than 79 characters in at least one subtitle line.";
1531 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1532 return "There are more than 3 closed caption lines in at least one place.";
1533 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1534 return "There are more than 32 characters in at least one closed caption line.";
1535 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1536 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1537 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1538 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1539 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1540 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1541 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1542 return "All assets in a reel do not have the same duration.";
1543 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1544 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1545 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1546 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1547 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1548 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1549 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1550 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1551 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1552 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1553 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1554 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1555 case VerificationNote::Code::MISSING_HASH:
1556 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1557 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1558 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1559 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1560 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1561 case VerificationNote::Code::MISSING_FFOC:
1562 return "There should be a FFOC (first frame of content) marker";
1563 case VerificationNote::Code::MISSING_LFOC:
1564 return "There should be a LFOC (last frame of content) marker";
1565 case VerificationNote::Code::INCORRECT_FFOC:
1566 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1567 case VerificationNote::Code::INCORRECT_LFOC:
1568 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1569 case VerificationNote::Code::MISSING_CPL_METADATA:
1570 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1571 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1572 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1573 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1574 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1575 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1576 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1577 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1578 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1579 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1580 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1581 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1582 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1583 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1584 return "Some assets are encrypted but some are not.";
1585 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1586 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1587 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1588 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1589 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1590 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1591 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1592 return "The JPEG2000 tile size is not the same as the image size.";
1593 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1594 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1595 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1596 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1597 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1598 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1599 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1600 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1601 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1602 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1603 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1604 return "POC marker found outside main header";
1605 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1606 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1607 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1608 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1609 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1610 return "No TLM marker was found in a JPEG2000 codestream.";
1611 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1612 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1613 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1614 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1615 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1617 vector<string> parts;
1618 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1619 DCP_ASSERT (parts.size() == 2);
1620 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]);
1622 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1623 return "Some aspect of this DCP could not be checked because it is encrypted.";
1631 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1633 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1638 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1640 if (a.type() != b.type()) {
1641 return a.type() < b.type();
1644 if (a.code() != b.code()) {
1645 return a.code() < b.code();
1648 if (a.note() != b.note()) {
1649 return a.note().get_value_or("") < b.note().get_value_or("");
1652 if (a.file() != b.file()) {
1653 return a.file().get_value_or("") < b.file().get_value_or("");
1656 return a.line().get_value_or(0) < b.line().get_value_or(0);
1661 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1663 s << note_to_string (note);
1665 s << " [" << note.note().get() << "]";
1668 s << " [" << note.file().get() << "]";
1671 s << " [" << note.line().get() << "]";