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 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
733 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
735 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
736 verify_smpte_subtitle_asset (smpte, notes, state);
741 /** Verify all closed caption stuff */
743 verify_closed_caption_asset (
744 shared_ptr<const SubtitleAsset> asset,
745 optional<int64_t> reel_asset_duration,
746 function<void (string, optional<boost::filesystem::path>)> stage,
747 boost::filesystem::path xsd_dtd_directory,
748 vector<VerificationNote>& notes
751 stage ("Checking closed caption XML", asset->file());
752 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
753 * gets passed through libdcp which may clean up and therefore hide errors.
755 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
757 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
759 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
762 if (asset->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>(asset->raw_xml().size()), *asset->file()});
771 vector<shared_ptr<Reel>> reels,
773 vector<VerificationNote>& notes,
774 std::function<bool (shared_ptr<Reel>)> check,
775 std::function<string (shared_ptr<Reel>)> xml,
776 std::function<int64_t (shared_ptr<Reel>)> duration
779 /* end of last subtitle (in editable units) */
780 optional<int64_t> last_out;
781 auto too_short = false;
782 auto too_close = false;
783 auto too_early = false;
784 auto reel_overlap = false;
785 /* current reel start time (in editable units) */
786 int64_t reel_offset = 0;
788 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
789 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) {
790 if (node->name() == "Subtitle") {
791 Time in (node->string_attribute("TimeIn"), tcr);
795 Time out (node->string_attribute("TimeOut"), tcr);
799 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
802 auto length = out - in;
803 if (length.as_editable_units_ceil(er) < 15) {
807 /* XXX: this feels dubious - is it really what Bv2.1 means? */
808 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
809 if (distance >= 0 && distance < 2) {
813 last_out = reel_offset + out.as_editable_units_floor(er);
815 for (auto i: node->node_children()) {
816 parse(i, tcr, start_time, er, first_reel);
821 for (auto i = 0U; i < reels.size(); ++i) {
822 if (!check(reels[i])) {
826 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
827 * read in by libdcp's parser.
830 shared_ptr<cxml::Document> doc;
832 optional<Time> start_time;
834 doc = make_shared<cxml::Document>("SubtitleReel");
835 doc->read_string (xml(reels[i]));
836 tcr = doc->number_child<int>("TimeCodeRate");
837 auto start_time_string = doc->optional_string_child("StartTime");
838 if (start_time_string) {
839 start_time = Time(*start_time_string, tcr);
842 doc = make_shared<cxml::Document>("DCSubtitle");
843 doc->read_string (xml(reels[i]));
845 parse (doc, tcr, start_time, edit_rate, i == 0);
846 auto end = reel_offset + duration(reels[i]);
847 if (last_out && *last_out > end) {
853 if (last_out && *last_out > reel_offset) {
859 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
865 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
871 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
877 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
883 struct LinesCharactersResult
885 bool warning_length_exceeded = false;
886 bool error_length_exceeded = false;
887 bool line_count_exceeded = false;
893 verify_text_lines_and_characters (
894 shared_ptr<SubtitleAsset> asset,
897 LinesCharactersResult* result
903 Event (Time time_, float position_, int characters_)
905 , position (position_)
906 , characters (characters_)
909 Event (Time time_, shared_ptr<Event> start_)
915 int position; //< position from 0 at top of screen to 100 at bottom
917 shared_ptr<Event> start;
920 vector<shared_ptr<Event>> events;
922 auto position = [](shared_ptr<const SubtitleString> sub) {
923 switch (sub->v_align()) {
925 return lrintf(sub->v_position() * 100);
927 return lrintf((0.5f + sub->v_position()) * 100);
929 return lrintf((1.0f - sub->v_position()) * 100);
935 for (auto j: asset->subtitles()) {
936 auto text = dynamic_pointer_cast<const SubtitleString>(j);
938 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
939 events.push_back(in);
940 events.push_back(make_shared<Event>(text->out(), in));
944 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
945 return a->time < b->time;
948 map<int, int> current;
949 for (auto i: events) {
950 if (current.size() > 3) {
951 result->line_count_exceeded = true;
953 for (auto j: current) {
954 if (j.second >= warning_length) {
955 result->warning_length_exceeded = true;
957 if (j.second >= error_length) {
958 result->error_length_exceeded = true;
963 /* end of a subtitle */
964 DCP_ASSERT (current.find(i->start->position) != current.end());
965 if (current[i->start->position] == i->start->characters) {
966 current.erase(i->start->position);
968 current[i->start->position] -= i->start->characters;
971 /* start of a subtitle */
972 if (current.find(i->position) == current.end()) {
973 current[i->position] = i->characters;
975 current[i->position] += i->characters;
984 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
990 if (reels[0]->main_subtitle()) {
991 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
992 [](shared_ptr<Reel> reel) {
993 return static_cast<bool>(reel->main_subtitle());
995 [](shared_ptr<Reel> reel) {
996 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
998 return interop->asset()->raw_xml();
1000 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1002 return smpte->asset()->raw_xml();
1004 [](shared_ptr<Reel> reel) {
1005 return reel->main_subtitle()->actual_duration();
1010 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1011 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1012 [i](shared_ptr<Reel> reel) {
1013 return i < reel->closed_captions().size();
1015 [i](shared_ptr<Reel> reel) {
1016 return reel->closed_captions()[i]->asset()->raw_xml();
1018 [i](shared_ptr<Reel> reel) {
1019 return reel->closed_captions()[i]->actual_duration();
1027 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1029 DCP_ASSERT (cpl->file());
1030 cxml::Document doc ("CompositionPlaylist");
1031 doc.read_file (cpl->file().get());
1033 auto missing = false;
1036 if (auto reel_list = doc.node_child("ReelList")) {
1037 auto reels = reel_list->node_children("Reel");
1038 if (!reels.empty()) {
1039 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1040 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1041 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1043 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1044 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1048 if (auto name = extension->optional_node_child("Name")) {
1049 if (name->content() != "Application") {
1050 malformed = "<Name> should be 'Application'";
1053 if (auto property_list = extension->optional_node_child("PropertyList")) {
1054 if (auto property = property_list->optional_node_child("Property")) {
1055 if (auto name = property->optional_node_child("Name")) {
1056 if (name->content() != "DCP Constraints Profile") {
1057 malformed = "<Name> property should be 'DCP Constraints Profile'";
1060 if (auto value = property->optional_node_child("Value")) {
1061 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1062 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1077 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1078 } else if (!malformed.empty()) {
1079 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1085 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1087 vector<string> encrypted;
1088 for (auto i: dcp->cpls()) {
1089 for (auto j: i->reel_file_assets()) {
1090 if (j->asset_ref().resolved()) {
1091 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1092 if (mxf && mxf->encrypted()) {
1093 encrypted.push_back(j->asset_ref().id());
1099 for (auto i: pkl->asset_list()) {
1100 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1109 vector<VerificationNote>
1111 vector<boost::filesystem::path> directories,
1112 function<void (string, optional<boost::filesystem::path>)> stage,
1113 function<void (float)> progress,
1114 optional<boost::filesystem::path> xsd_dtd_directory
1117 if (!xsd_dtd_directory) {
1118 xsd_dtd_directory = resources_directory() / "xsd";
1120 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1122 vector<VerificationNote> notes;
1125 vector<shared_ptr<DCP>> dcps;
1126 for (auto i: directories) {
1127 dcps.push_back (make_shared<DCP>(i));
1130 for (auto dcp: dcps) {
1131 stage ("Checking DCP", dcp->directory());
1132 bool carry_on = true;
1135 } catch (MissingAssetmapError& e) {
1136 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1138 } catch (ReadError& e) {
1139 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1140 } catch (XMLError& e) {
1141 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1142 } catch (MXFFileError& e) {
1143 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1144 } catch (cxml::Error& e) {
1145 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1152 if (dcp->standard() != Standard::SMPTE) {
1153 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1156 for (auto cpl: dcp->cpls()) {
1157 stage ("Checking CPL", cpl->file());
1158 validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1160 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1161 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1164 for (auto const& i: cpl->additional_subtitle_languages()) {
1165 verify_language_tag (i, notes);
1168 if (cpl->release_territory()) {
1169 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") {
1170 auto terr = cpl->release_territory().get();
1171 /* Must be a valid region tag, or "001" */
1173 LanguageTag::RegionSubtag test (terr);
1175 if (terr != "001") {
1176 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1182 if (dcp->standard() == Standard::SMPTE) {
1183 if (!cpl->annotation_text()) {
1184 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1185 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1186 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1190 for (auto i: dcp->pkls()) {
1191 /* Check that the CPL's hash corresponds to the PKL */
1192 optional<string> h = i->hash(cpl->id());
1193 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1194 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1197 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1198 optional<string> required_annotation_text;
1199 for (auto j: i->asset_list()) {
1200 /* See if this is a CPL */
1201 for (auto k: dcp->cpls()) {
1202 if (j->id() == k->id()) {
1203 if (!required_annotation_text) {
1204 /* First CPL we have found; this is the required AnnotationText unless we find another */
1205 required_annotation_text = cpl->content_title_text();
1207 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1208 required_annotation_text = boost::none;
1214 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1215 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1219 /* set to true if any reel has a MainSubtitle */
1220 auto have_main_subtitle = false;
1221 /* set to true if any reel has no MainSubtitle */
1222 auto have_no_main_subtitle = false;
1223 /* fewest number of closed caption assets seen in a reel */
1224 size_t fewest_closed_captions = SIZE_MAX;
1225 /* most number of closed caption assets seen in a reel */
1226 size_t most_closed_captions = 0;
1227 map<Marker, Time> markers_seen;
1229 for (auto reel: cpl->reels()) {
1230 stage ("Checking reel", optional<boost::filesystem::path>());
1232 for (auto i: reel->assets()) {
1233 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1234 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1236 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1237 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1239 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1240 if (i->encryptable() && !file_asset->hash()) {
1241 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1245 if (dcp->standard() == Standard::SMPTE) {
1246 boost::optional<int64_t> duration;
1247 for (auto i: reel->assets()) {
1249 duration = i->actual_duration();
1250 } else if (*duration != i->actual_duration()) {
1251 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1257 if (reel->main_picture()) {
1258 /* Check reel stuff */
1259 auto const frame_rate = reel->main_picture()->frame_rate();
1260 if (frame_rate.denominator != 1 ||
1261 (frame_rate.numerator != 24 &&
1262 frame_rate.numerator != 25 &&
1263 frame_rate.numerator != 30 &&
1264 frame_rate.numerator != 48 &&
1265 frame_rate.numerator != 50 &&
1266 frame_rate.numerator != 60 &&
1267 frame_rate.numerator != 96)) {
1269 VerificationNote::Type::ERROR,
1270 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1271 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1275 if (reel->main_picture()->asset_ref().resolved()) {
1276 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1280 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1281 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1284 if (reel->main_subtitle()) {
1285 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1286 if (reel->main_subtitle()->asset_ref().resolved()) {
1287 verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1289 have_main_subtitle = true;
1291 have_no_main_subtitle = true;
1294 for (auto i: reel->closed_captions()) {
1295 verify_closed_caption_reel (i, notes);
1296 if (i->asset_ref().resolved()) {
1297 verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1301 if (reel->main_markers()) {
1302 for (auto const& i: reel->main_markers()->get()) {
1303 markers_seen.insert (i);
1307 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1308 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1311 verify_text_timing (cpl->reels(), notes);
1313 if (dcp->standard() == Standard::SMPTE) {
1315 if (have_main_subtitle && have_no_main_subtitle) {
1316 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1319 if (fewest_closed_captions != most_closed_captions) {
1320 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1323 if (cpl->content_kind() == ContentKind::FEATURE) {
1324 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1325 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1327 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1328 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1332 auto ffoc = markers_seen.find(Marker::FFOC);
1333 if (ffoc == markers_seen.end()) {
1334 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1335 } else if (ffoc->second.e != 1) {
1336 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1339 auto lfoc = markers_seen.find(Marker::LFOC);
1340 if (lfoc == markers_seen.end()) {
1341 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1343 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1344 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1345 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1349 LinesCharactersResult result;
1350 for (auto reel: cpl->reels()) {
1351 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1352 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1356 if (result.line_count_exceeded) {
1357 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1359 if (result.error_length_exceeded) {
1360 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1361 } else if (result.warning_length_exceeded) {
1362 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1365 result = LinesCharactersResult();
1366 for (auto reel: cpl->reels()) {
1367 for (auto i: reel->closed_captions()) {
1369 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1374 if (result.line_count_exceeded) {
1375 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1377 if (result.error_length_exceeded) {
1378 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1381 if (!cpl->full_content_title_text()) {
1382 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1383 * can use it as a proxy for CompositionMetadataAsset's existence.
1385 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1386 } else if (!cpl->version_number()) {
1387 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1390 verify_extension_metadata (cpl, notes);
1392 if (cpl->any_encrypted()) {
1393 cxml::Document doc ("CompositionPlaylist");
1394 DCP_ASSERT (cpl->file());
1395 doc.read_file (cpl->file().get());
1396 if (!doc.optional_node_child("Signature")) {
1397 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1403 for (auto pkl: dcp->pkls()) {
1404 stage ("Checking PKL", pkl->file());
1405 validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1406 if (pkl_has_encrypted_assets(dcp, pkl)) {
1407 cxml::Document doc ("PackingList");
1408 doc.read_file (pkl->file().get());
1409 if (!doc.optional_node_child("Signature")) {
1410 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1415 if (dcp->asset_map_path()) {
1416 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1417 validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1419 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1428 dcp::note_to_string (VerificationNote note)
1430 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1432 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1433 * not "ClosedCaption assets must have an <EntryPoint> tag."
1435 * It's OK to use XML tag names where they are clear.
1436 * If both ID and filename are available, use only the ID.
1437 * End messages with a full stop.
1438 * Messages should not mention whether or not their errors are a part of Bv2.1.
1440 switch (note.code()) {
1441 case VerificationNote::Code::FAILED_READ:
1442 return *note.note();
1443 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1444 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1445 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1446 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1447 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1448 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1449 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1450 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1451 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1452 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1453 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1454 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1455 case VerificationNote::Code::EMPTY_ASSET_PATH:
1456 return "The asset map contains an empty asset path.";
1457 case VerificationNote::Code::MISSING_ASSET:
1458 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1459 case VerificationNote::Code::MISMATCHED_STANDARD:
1460 return "The DCP contains both SMPTE and Interop parts.";
1461 case VerificationNote::Code::INVALID_XML:
1462 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1463 case VerificationNote::Code::MISSING_ASSETMAP:
1464 return "No ASSETMAP or ASSETMAP.xml was found.";
1465 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1466 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1467 case VerificationNote::Code::INVALID_DURATION:
1468 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1469 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1470 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());
1471 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1472 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());
1473 case VerificationNote::Code::EXTERNAL_ASSET:
1474 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());
1475 case VerificationNote::Code::INVALID_STANDARD:
1476 return "This DCP does not use the SMPTE standard.";
1477 case VerificationNote::Code::INVALID_LANGUAGE:
1478 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1479 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1480 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1481 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1482 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1483 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1484 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1485 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1486 return "3D 4K DCPs are not allowed.";
1487 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1488 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1489 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1490 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1491 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1492 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());
1493 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1494 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1495 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1496 return "Some subtitle assets have different <Language> tags than others";
1497 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1498 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1499 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1500 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1501 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1502 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1503 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1504 return "At least one subtitle lasts less than 15 frames.";
1505 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1506 return "At least one pair of subtitles is separated by less than 2 frames.";
1507 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1508 return "At least one subtitle extends outside of its reel.";
1509 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1510 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1511 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1512 return "There are more than 52 characters in at least one subtitle line.";
1513 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1514 return "There are more than 79 characters in at least one subtitle line.";
1515 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1516 return "There are more than 3 closed caption lines in at least one place.";
1517 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1518 return "There are more than 32 characters in at least one closed caption line.";
1519 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1520 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1521 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1522 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1523 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1524 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1525 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1526 return "All assets in a reel do not have the same duration.";
1527 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1528 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1529 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1530 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1531 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1532 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1533 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1534 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1535 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1536 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1537 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1538 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1539 case VerificationNote::Code::MISSING_HASH:
1540 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1541 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1542 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1543 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1544 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1545 case VerificationNote::Code::MISSING_FFOC:
1546 return "There should be a FFOC (first frame of content) marker";
1547 case VerificationNote::Code::MISSING_LFOC:
1548 return "There should be a LFOC (last frame of content) marker";
1549 case VerificationNote::Code::INCORRECT_FFOC:
1550 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1551 case VerificationNote::Code::INCORRECT_LFOC:
1552 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1553 case VerificationNote::Code::MISSING_CPL_METADATA:
1554 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1555 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1556 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1557 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1558 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1559 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1560 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1561 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1562 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1563 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1564 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1565 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1566 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1567 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1568 return "Some assets are encrypted but some are not.";
1569 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1570 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1571 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1572 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1573 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1574 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1575 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1576 return "The JPEG2000 tile size is not the same as the image size.";
1577 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1578 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1579 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1580 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1581 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1582 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1583 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1584 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1585 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1586 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1587 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1588 return "POC marker found outside main header";
1589 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1590 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1591 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1592 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1593 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1594 return "No TLM marker was found in a JPEG2000 codestream.";
1595 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1596 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1597 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1598 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1599 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1601 vector<string> parts;
1602 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1603 DCP_ASSERT (parts.size() == 2);
1604 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]);
1613 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1615 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1620 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1622 s << note_to_string (note);
1624 s << " [" << note.note().get() << "]";
1627 s << " [" << note.file().get() << "]";
1630 s << " [" << note.line().get() << "]";