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_markers_asset.h"
51 #include "reel_picture_asset.h"
52 #include "reel_sound_asset.h"
53 #include "reel_subtitle_asset.h"
54 #include "smpte_subtitle_asset.h"
55 #include "stereo_picture_asset.h"
56 #include "stereo_picture_frame.h"
58 #include "verify_j2k.h"
59 #include <xercesc/dom/DOMAttr.hpp>
60 #include <xercesc/dom/DOMDocument.hpp>
61 #include <xercesc/dom/DOMError.hpp>
62 #include <xercesc/dom/DOMErrorHandler.hpp>
63 #include <xercesc/dom/DOMException.hpp>
64 #include <xercesc/dom/DOMImplementation.hpp>
65 #include <xercesc/dom/DOMImplementationLS.hpp>
66 #include <xercesc/dom/DOMImplementationRegistry.hpp>
67 #include <xercesc/dom/DOMLSParser.hpp>
68 #include <xercesc/dom/DOMLocator.hpp>
69 #include <xercesc/dom/DOMNamedNodeMap.hpp>
70 #include <xercesc/dom/DOMNodeList.hpp>
71 #include <xercesc/framework/LocalFileInputSource.hpp>
72 #include <xercesc/framework/MemBufInputSource.hpp>
73 #include <xercesc/parsers/AbstractDOMParser.hpp>
74 #include <xercesc/parsers/XercesDOMParser.hpp>
75 #include <xercesc/sax/HandlerBase.hpp>
76 #include <xercesc/util/PlatformUtils.hpp>
77 #include <boost/algorithm/string.hpp>
90 using std::shared_ptr;
91 using std::make_shared;
92 using boost::optional;
93 using boost::function;
94 using std::dynamic_pointer_cast;
98 using namespace xercesc;
103 xml_ch_to_string (XMLCh const * a)
105 char* x = XMLString::transcode(a);
107 XMLString::release(&x);
112 class XMLValidationError
115 XMLValidationError (SAXParseException const & e)
116 : _message (xml_ch_to_string(e.getMessage()))
117 , _line (e.getLineNumber())
118 , _column (e.getColumnNumber())
119 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
120 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
125 string message () const {
129 uint64_t line () const {
133 uint64_t column () const {
137 string public_id () const {
141 string system_id () const {
154 class DCPErrorHandler : public ErrorHandler
157 void warning(const SAXParseException& e)
159 maybe_add (XMLValidationError(e));
162 void error(const SAXParseException& e)
164 maybe_add (XMLValidationError(e));
167 void fatalError(const SAXParseException& e)
169 maybe_add (XMLValidationError(e));
176 list<XMLValidationError> errors () const {
181 void maybe_add (XMLValidationError e)
183 /* XXX: nasty hack */
185 e.message().find("schema document") != string::npos &&
186 e.message().find("has different target namespace from the one specified in instance document") != string::npos
191 _errors.push_back (e);
194 list<XMLValidationError> _errors;
201 StringToXMLCh (string a)
203 _buffer = XMLString::transcode(a.c_str());
206 StringToXMLCh (StringToXMLCh const&) = delete;
207 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
211 XMLString::release (&_buffer);
214 XMLCh const * get () const {
223 class LocalFileResolver : public EntityResolver
226 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
227 : _xsd_dtd_directory (xsd_dtd_directory)
229 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
230 * found without being here.
232 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
233 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
234 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
235 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
236 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
237 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
238 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
239 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
240 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
241 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
242 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
243 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
244 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
247 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
252 auto system_id_str = xml_ch_to_string (system_id);
253 auto p = _xsd_dtd_directory;
254 if (_files.find(system_id_str) == _files.end()) {
257 p /= _files[system_id_str];
259 StringToXMLCh ch (p.string());
260 return new LocalFileInputSource(ch.get());
264 void add (string uri, string file)
269 std::map<string, string> _files;
270 boost::filesystem::path _xsd_dtd_directory;
275 parse (XercesDOMParser& parser, boost::filesystem::path xml)
277 parser.parse(xml.string().c_str());
282 parse (XercesDOMParser& parser, string xml)
284 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
291 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
294 XMLPlatformUtils::Initialize ();
295 } catch (XMLException& e) {
296 throw MiscError ("Failed to initialise xerces library");
299 DCPErrorHandler error_handler;
301 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
303 XercesDOMParser parser;
304 parser.setValidationScheme(XercesDOMParser::Val_Always);
305 parser.setDoNamespaces(true);
306 parser.setDoSchema(true);
308 vector<string> schema;
309 schema.push_back("xml.xsd");
310 schema.push_back("xmldsig-core-schema.xsd");
311 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
312 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
313 schema.push_back("SMPTE-429-9-2007-AM.xsd");
314 schema.push_back("Main-Stereo-Picture-CPL.xsd");
315 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
316 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
317 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
318 schema.push_back("DCSubtitle.v1.mattsson.xsd");
319 schema.push_back("DCDMSubtitle-2010.xsd");
320 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
321 schema.push_back("SMPTE-429-16.xsd");
322 schema.push_back("Dolby-2012-AD.xsd");
323 schema.push_back("SMPTE-429-10-2008.xsd");
324 schema.push_back("xlink.xsd");
325 schema.push_back("SMPTE-335-2012.xsd");
326 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
327 schema.push_back("isdcf-mca.xsd");
328 schema.push_back("SMPTE-429-12-2008.xsd");
330 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
331 * Schemas that are not mentioned in this list are not read, and the things
332 * they describe are not checked.
335 for (auto i: schema) {
336 locations += String::compose("%1 %1 ", i, i);
339 parser.setExternalSchemaLocation(locations.c_str());
340 parser.setValidationSchemaFullChecking(true);
341 parser.setErrorHandler(&error_handler);
343 LocalFileResolver resolver (xsd_dtd_directory);
344 parser.setEntityResolver(&resolver);
347 parser.resetDocumentPool();
349 } catch (XMLException& e) {
350 throw MiscError(xml_ch_to_string(e.getMessage()));
351 } catch (DOMException& e) {
352 throw MiscError(xml_ch_to_string(e.getMessage()));
354 throw MiscError("Unknown exception from xerces");
358 XMLPlatformUtils::Terminate ();
360 for (auto i: error_handler.errors()) {
362 VerificationNote::Type::ERROR,
363 VerificationNote::Code::INVALID_XML,
365 boost::trim_copy(i.public_id() + " " + i.system_id()),
372 enum class VerifyAssetResult {
379 static VerifyAssetResult
380 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelEncryptableAsset> reel_file_asset, function<void (float)> progress)
382 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
384 auto pkls = dcp->pkls();
385 /* We've read this DCP in so it must have at least one PKL */
386 DCP_ASSERT (!pkls.empty());
388 auto asset = reel_file_asset->asset_ref().asset();
390 optional<string> pkl_hash;
392 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
398 DCP_ASSERT (pkl_hash);
400 auto cpl_hash = reel_file_asset->hash();
401 if (cpl_hash && *cpl_hash != *pkl_hash) {
402 return VerifyAssetResult::CPL_PKL_DIFFER;
405 if (actual_hash != *pkl_hash) {
406 return VerifyAssetResult::BAD;
409 return VerifyAssetResult::GOOD;
414 verify_language_tag (string tag, vector<VerificationNote>& notes)
417 LanguageTag test (tag);
418 } catch (LanguageTagError &) {
419 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
425 verify_picture_asset (shared_ptr<const ReelEncryptableAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
427 int biggest_frame = 0;
428 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
429 auto const duration = asset->intrinsic_duration ();
431 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
432 for (auto i: j2k_notes) {
433 if (find(notes.begin(), notes.end(), i) == notes.end()) {
439 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
440 auto reader = mono_asset->start_read ();
441 for (int64_t i = 0; i < duration; ++i) {
442 auto frame = reader->get_frame (i);
443 biggest_frame = max(biggest_frame, frame->size());
444 if (!mono_asset->encrypted() || mono_asset->key()) {
445 vector<VerificationNote> j2k_notes;
446 verify_j2k (frame, j2k_notes);
447 check_and_add (j2k_notes);
449 progress (float(i) / duration);
451 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
452 auto reader = stereo_asset->start_read ();
453 for (int64_t i = 0; i < duration; ++i) {
454 auto frame = reader->get_frame (i);
455 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
456 if (!stereo_asset->encrypted() || mono_asset->key()) {
457 vector<VerificationNote> j2k_notes;
458 verify_j2k (frame->left(), j2k_notes);
459 verify_j2k (frame->right(), j2k_notes);
460 check_and_add (j2k_notes);
462 progress (float(i) / duration);
467 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
468 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
469 if (biggest_frame > max_frame) {
471 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
473 } else if (biggest_frame > risky_frame) {
475 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
482 verify_main_picture_asset (
483 shared_ptr<const DCP> dcp,
484 shared_ptr<const ReelPictureAsset> reel_asset,
485 function<void (string, optional<boost::filesystem::path>)> stage,
486 function<void (float)> progress,
487 vector<VerificationNote>& notes
490 auto asset = reel_asset->asset();
491 auto const file = *asset->file();
492 stage ("Checking picture asset hash", file);
493 auto const r = verify_asset (dcp, reel_asset, progress);
495 case VerifyAssetResult::BAD:
497 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
500 case VerifyAssetResult::CPL_PKL_DIFFER:
502 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
508 stage ("Checking picture frame sizes", asset->file());
509 verify_picture_asset (reel_asset, file, notes, progress);
511 /* Only flat/scope allowed by Bv2.1 */
513 asset->size() != Size(2048, 858) &&
514 asset->size() != Size(1998, 1080) &&
515 asset->size() != Size(4096, 1716) &&
516 asset->size() != Size(3996, 2160)) {
518 VerificationNote::Type::BV21_ERROR,
519 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
520 String::compose("%1x%2", asset->size().width, asset->size().height),
525 /* Only 24, 25, 48fps allowed for 2K */
527 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
528 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
531 VerificationNote::Type::BV21_ERROR,
532 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
533 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
538 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
539 /* Only 24fps allowed for 4K */
540 if (asset->edit_rate() != Fraction(24, 1)) {
542 VerificationNote::Type::BV21_ERROR,
543 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
544 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
549 /* Only 2D allowed for 4K */
550 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
552 VerificationNote::Type::BV21_ERROR,
553 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
554 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
565 verify_main_sound_asset (
566 shared_ptr<const DCP> dcp,
567 shared_ptr<const ReelSoundAsset> reel_asset,
568 function<void (string, optional<boost::filesystem::path>)> stage,
569 function<void (float)> progress,
570 vector<VerificationNote>& notes
573 auto asset = reel_asset->asset();
574 stage ("Checking sound asset hash", asset->file());
575 auto const r = verify_asset (dcp, reel_asset, progress);
577 case VerifyAssetResult::BAD:
578 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
580 case VerifyAssetResult::CPL_PKL_DIFFER:
581 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
587 stage ("Checking sound asset metadata", asset->file());
589 if (auto lang = asset->language()) {
590 verify_language_tag (*lang, notes);
592 if (asset->sampling_rate() != 48000) {
593 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
599 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
601 /* XXX: is Language compulsory? */
602 if (reel_asset->language()) {
603 verify_language_tag (*reel_asset->language(), notes);
606 if (!reel_asset->entry_point()) {
607 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
608 } else if (reel_asset->entry_point().get()) {
609 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
615 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
617 /* XXX: is Language compulsory? */
618 if (reel_asset->language()) {
619 verify_language_tag (*reel_asset->language(), notes);
622 if (!reel_asset->entry_point()) {
623 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
624 } else if (reel_asset->entry_point().get()) {
625 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
632 boost::optional<string> subtitle_language;
636 /** Verify stuff that is common to both subtitles and closed captions */
638 verify_smpte_timed_text_asset (
639 shared_ptr<const SMPTESubtitleAsset> asset,
640 optional<int64_t> reel_asset_duration,
641 vector<VerificationNote>& notes
644 if (asset->language()) {
645 verify_language_tag (*asset->language(), notes);
647 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
650 auto const size = boost::filesystem::file_size(asset->file().get());
651 if (size > 115 * 1024 * 1024) {
653 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
657 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
658 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
660 auto fonts = asset->font_data ();
662 for (auto i: fonts) {
663 total_size += i.second.size();
665 if (total_size > 10 * 1024 * 1024) {
666 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
669 if (!asset->start_time()) {
670 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
671 } else if (asset->start_time() != Time()) {
672 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
675 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
678 VerificationNote::Type::BV21_ERROR,
679 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
680 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
687 /** Verify SMPTE subtitle-only stuff */
689 verify_smpte_subtitle_asset (
690 shared_ptr<const SMPTESubtitleAsset> asset,
691 vector<VerificationNote>& notes,
695 if (asset->language()) {
696 if (!state.subtitle_language) {
697 state.subtitle_language = *asset->language();
698 } else if (state.subtitle_language != *asset->language()) {
699 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
703 DCP_ASSERT (asset->resource_id());
704 if (asset->resource_id().get() != asset->xml_id()) {
705 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
708 if (asset->id() == asset->resource_id().get() || asset->id() == asset->xml_id()) {
709 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
714 /** Verify all subtitle stuff */
716 verify_subtitle_asset (
717 shared_ptr<const SubtitleAsset> asset,
718 optional<int64_t> reel_asset_duration,
719 function<void (string, optional<boost::filesystem::path>)> stage,
720 boost::filesystem::path xsd_dtd_directory,
721 vector<VerificationNote>& notes,
725 stage ("Checking subtitle XML", asset->file());
726 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
727 * gets passed through libdcp which may clean up and therefore hide errors.
729 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
731 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
733 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
734 verify_smpte_subtitle_asset (smpte, notes, state);
739 /** Verify all closed caption stuff */
741 verify_closed_caption_asset (
742 shared_ptr<const SubtitleAsset> asset,
743 optional<int64_t> reel_asset_duration,
744 function<void (string, optional<boost::filesystem::path>)> stage,
745 boost::filesystem::path xsd_dtd_directory,
746 vector<VerificationNote>& notes
749 stage ("Checking closed caption XML", asset->file());
750 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
751 * gets passed through libdcp which may clean up and therefore hide errors.
753 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
755 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
757 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
760 if (asset->raw_xml().size() > 256 * 1024) {
761 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()});
769 vector<shared_ptr<Reel>> reels,
771 vector<VerificationNote>& notes,
772 std::function<bool (shared_ptr<Reel>)> check,
773 std::function<string (shared_ptr<Reel>)> xml,
774 std::function<int64_t (shared_ptr<Reel>)> duration
777 /* end of last subtitle (in editable units) */
778 optional<int64_t> last_out;
779 auto too_short = false;
780 auto too_close = false;
781 auto too_early = false;
782 auto reel_overlap = false;
783 /* current reel start time (in editable units) */
784 int64_t reel_offset = 0;
786 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
787 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) {
788 if (node->name() == "Subtitle") {
789 Time in (node->string_attribute("TimeIn"), tcr);
793 Time out (node->string_attribute("TimeOut"), tcr);
797 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
800 auto length = out - in;
801 if (length.as_editable_units_ceil(er) < 15) {
805 /* XXX: this feels dubious - is it really what Bv2.1 means? */
806 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
807 if (distance >= 0 && distance < 2) {
811 last_out = reel_offset + out.as_editable_units_floor(er);
813 for (auto i: node->node_children()) {
814 parse(i, tcr, start_time, er, first_reel);
819 for (auto i = 0U; i < reels.size(); ++i) {
820 if (!check(reels[i])) {
824 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
825 * read in by libdcp's parser.
828 shared_ptr<cxml::Document> doc;
830 optional<Time> start_time;
832 doc = make_shared<cxml::Document>("SubtitleReel");
833 doc->read_string (xml(reels[i]));
834 tcr = doc->number_child<int>("TimeCodeRate");
835 auto start_time_string = doc->optional_string_child("StartTime");
836 if (start_time_string) {
837 start_time = Time(*start_time_string, tcr);
840 doc = make_shared<cxml::Document>("DCSubtitle");
841 doc->read_string (xml(reels[i]));
843 parse (doc, tcr, start_time, edit_rate, i == 0);
844 auto end = reel_offset + duration(reels[i]);
845 if (last_out && *last_out > end) {
851 if (last_out && *last_out > reel_offset) {
857 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
863 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
869 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
875 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
881 struct LinesCharactersResult
883 bool warning_length_exceeded = false;
884 bool error_length_exceeded = false;
885 bool line_count_exceeded = false;
891 verify_text_lines_and_characters (
892 shared_ptr<SubtitleAsset> asset,
895 LinesCharactersResult* result
901 Event (Time time_, float position_, int characters_)
903 , position (position_)
904 , characters (characters_)
907 Event (Time time_, shared_ptr<Event> start_)
913 int position; //< position from 0 at top of screen to 100 at bottom
915 shared_ptr<Event> start;
918 vector<shared_ptr<Event>> events;
920 auto position = [](shared_ptr<const SubtitleString> sub) {
921 switch (sub->v_align()) {
923 return lrintf(sub->v_position() * 100);
925 return lrintf((0.5f + sub->v_position()) * 100);
927 return lrintf((1.0f - sub->v_position()) * 100);
933 for (auto j: asset->subtitles()) {
934 auto text = dynamic_pointer_cast<const SubtitleString>(j);
936 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
937 events.push_back(in);
938 events.push_back(make_shared<Event>(text->out(), in));
942 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
943 return a->time < b->time;
946 map<int, int> current;
947 for (auto i: events) {
948 if (current.size() > 3) {
949 result->line_count_exceeded = true;
951 for (auto j: current) {
952 if (j.second >= warning_length) {
953 result->warning_length_exceeded = true;
955 if (j.second >= error_length) {
956 result->error_length_exceeded = true;
961 /* end of a subtitle */
962 DCP_ASSERT (current.find(i->start->position) != current.end());
963 if (current[i->start->position] == i->start->characters) {
964 current.erase(i->start->position);
966 current[i->start->position] -= i->start->characters;
969 /* start of a subtitle */
970 if (current.find(i->position) == current.end()) {
971 current[i->position] = i->characters;
973 current[i->position] += i->characters;
982 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
988 if (reels[0]->main_subtitle()) {
989 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
990 [](shared_ptr<Reel> reel) {
991 return static_cast<bool>(reel->main_subtitle());
993 [](shared_ptr<Reel> reel) {
994 return reel->main_subtitle()->asset()->raw_xml();
996 [](shared_ptr<Reel> reel) {
997 return reel->main_subtitle()->actual_duration();
1002 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1003 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1004 [i](shared_ptr<Reel> reel) {
1005 return i < reel->closed_captions().size();
1007 [i](shared_ptr<Reel> reel) {
1008 return reel->closed_captions()[i]->asset()->raw_xml();
1010 [i](shared_ptr<Reel> reel) {
1011 return reel->closed_captions()[i]->actual_duration();
1019 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1021 DCP_ASSERT (cpl->file());
1022 cxml::Document doc ("CompositionPlaylist");
1023 doc.read_file (cpl->file().get());
1025 auto missing = false;
1028 if (auto reel_list = doc.node_child("ReelList")) {
1029 auto reels = reel_list->node_children("Reel");
1030 if (!reels.empty()) {
1031 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1032 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1033 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1035 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1036 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1040 if (auto name = extension->optional_node_child("Name")) {
1041 if (name->content() != "Application") {
1042 malformed = "<Name> should be 'Application'";
1045 if (auto property_list = extension->optional_node_child("PropertyList")) {
1046 if (auto property = property_list->optional_node_child("Property")) {
1047 if (auto name = property->optional_node_child("Name")) {
1048 if (name->content() != "DCP Constraints Profile") {
1049 malformed = "<Name> property should be 'DCP Constraints Profile'";
1052 if (auto value = property->optional_node_child("Value")) {
1053 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1054 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1069 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1070 } else if (!malformed.empty()) {
1071 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1077 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1079 vector<string> encrypted;
1080 for (auto i: dcp->cpls()) {
1081 for (auto j: i->reel_file_assets()) {
1082 if (j->asset_ref().resolved()) {
1083 /* It's a bit surprising / broken but Interop subtitle assets are represented
1084 * in reels by ReelSubtitleAsset which inherits ReelEncryptableAsset, so it's possible for
1085 * ReelEncryptableAssets to have assets which are not MXFs.
1087 if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
1088 if (asset->encrypted()) {
1089 encrypted.push_back(j->asset_ref().id());
1096 for (auto i: pkl->asset_list()) {
1097 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1106 vector<VerificationNote>
1108 vector<boost::filesystem::path> directories,
1109 function<void (string, optional<boost::filesystem::path>)> stage,
1110 function<void (float)> progress,
1111 optional<boost::filesystem::path> xsd_dtd_directory
1114 if (!xsd_dtd_directory) {
1115 xsd_dtd_directory = resources_directory() / "xsd";
1117 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1119 vector<VerificationNote> notes;
1122 vector<shared_ptr<DCP>> dcps;
1123 for (auto i: directories) {
1124 dcps.push_back (make_shared<DCP>(i));
1127 for (auto dcp: dcps) {
1128 stage ("Checking DCP", dcp->directory());
1129 bool carry_on = true;
1132 } catch (MissingAssetmapError& e) {
1133 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1135 } catch (ReadError& e) {
1136 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1137 } catch (XMLError& e) {
1138 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1139 } catch (MXFFileError& e) {
1140 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1141 } catch (cxml::Error& e) {
1142 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1149 if (dcp->standard() != Standard::SMPTE) {
1150 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1153 for (auto cpl: dcp->cpls()) {
1154 stage ("Checking CPL", cpl->file());
1155 validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1157 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1158 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1161 for (auto const& i: cpl->additional_subtitle_languages()) {
1162 verify_language_tag (i, notes);
1165 if (cpl->release_territory()) {
1166 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") {
1167 auto terr = cpl->release_territory().get();
1168 /* Must be a valid region tag, or "001" */
1170 LanguageTag::RegionSubtag test (terr);
1172 if (terr != "001") {
1173 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1179 if (dcp->standard() == Standard::SMPTE) {
1180 if (!cpl->annotation_text()) {
1181 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1182 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1183 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1187 for (auto i: dcp->pkls()) {
1188 /* Check that the CPL's hash corresponds to the PKL */
1189 optional<string> h = i->hash(cpl->id());
1190 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1191 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1194 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1195 optional<string> required_annotation_text;
1196 for (auto j: i->asset_list()) {
1197 /* See if this is a CPL */
1198 for (auto k: dcp->cpls()) {
1199 if (j->id() == k->id()) {
1200 if (!required_annotation_text) {
1201 /* First CPL we have found; this is the required AnnotationText unless we find another */
1202 required_annotation_text = cpl->content_title_text();
1204 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1205 required_annotation_text = boost::none;
1211 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1212 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1216 /* set to true if any reel has a MainSubtitle */
1217 auto have_main_subtitle = false;
1218 /* set to true if any reel has no MainSubtitle */
1219 auto have_no_main_subtitle = false;
1220 /* fewest number of closed caption assets seen in a reel */
1221 size_t fewest_closed_captions = SIZE_MAX;
1222 /* most number of closed caption assets seen in a reel */
1223 size_t most_closed_captions = 0;
1224 map<Marker, Time> markers_seen;
1226 for (auto reel: cpl->reels()) {
1227 stage ("Checking reel", optional<boost::filesystem::path>());
1229 for (auto i: reel->assets()) {
1230 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1231 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1233 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1234 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1236 auto file_asset = dynamic_pointer_cast<ReelEncryptableAsset>(i);
1237 if (file_asset && !file_asset->hash()) {
1238 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1242 if (dcp->standard() == Standard::SMPTE) {
1243 boost::optional<int64_t> duration;
1244 for (auto i: reel->assets()) {
1246 duration = i->actual_duration();
1247 } else if (*duration != i->actual_duration()) {
1248 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1254 if (reel->main_picture()) {
1255 /* Check reel stuff */
1256 auto const frame_rate = reel->main_picture()->frame_rate();
1257 if (frame_rate.denominator != 1 ||
1258 (frame_rate.numerator != 24 &&
1259 frame_rate.numerator != 25 &&
1260 frame_rate.numerator != 30 &&
1261 frame_rate.numerator != 48 &&
1262 frame_rate.numerator != 50 &&
1263 frame_rate.numerator != 60 &&
1264 frame_rate.numerator != 96)) {
1266 VerificationNote::Type::ERROR,
1267 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1268 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1272 if (reel->main_picture()->asset_ref().resolved()) {
1273 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1277 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1278 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1281 if (reel->main_subtitle()) {
1282 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1283 if (reel->main_subtitle()->asset_ref().resolved()) {
1284 verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
1286 have_main_subtitle = true;
1288 have_no_main_subtitle = true;
1291 for (auto i: reel->closed_captions()) {
1292 verify_closed_caption_reel (i, notes);
1293 if (i->asset_ref().resolved()) {
1294 verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
1298 if (reel->main_markers()) {
1299 for (auto const& i: reel->main_markers()->get()) {
1300 markers_seen.insert (i);
1304 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1305 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1308 verify_text_timing (cpl->reels(), notes);
1310 if (dcp->standard() == Standard::SMPTE) {
1312 if (have_main_subtitle && have_no_main_subtitle) {
1313 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1316 if (fewest_closed_captions != most_closed_captions) {
1317 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1320 if (cpl->content_kind() == ContentKind::FEATURE) {
1321 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1322 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1324 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1325 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1329 auto ffoc = markers_seen.find(Marker::FFOC);
1330 if (ffoc == markers_seen.end()) {
1331 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1332 } else if (ffoc->second.e != 1) {
1333 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1336 auto lfoc = markers_seen.find(Marker::LFOC);
1337 if (lfoc == markers_seen.end()) {
1338 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1340 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1341 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1342 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1346 LinesCharactersResult result;
1347 for (auto reel: cpl->reels()) {
1348 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1349 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1353 if (result.line_count_exceeded) {
1354 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1356 if (result.error_length_exceeded) {
1357 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1358 } else if (result.warning_length_exceeded) {
1359 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1362 result = LinesCharactersResult();
1363 for (auto reel: cpl->reels()) {
1364 for (auto i: reel->closed_captions()) {
1366 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1371 if (result.line_count_exceeded) {
1372 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1374 if (result.error_length_exceeded) {
1375 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1378 if (!cpl->full_content_title_text()) {
1379 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1380 * can use it as a proxy for CompositionMetadataAsset's existence.
1382 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1383 } else if (!cpl->version_number()) {
1384 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1387 verify_extension_metadata (cpl, notes);
1389 if (cpl->any_encrypted()) {
1390 cxml::Document doc ("CompositionPlaylist");
1391 DCP_ASSERT (cpl->file());
1392 doc.read_file (cpl->file().get());
1393 if (!doc.optional_node_child("Signature")) {
1394 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1400 for (auto pkl: dcp->pkls()) {
1401 stage ("Checking PKL", pkl->file());
1402 validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1403 if (pkl_has_encrypted_assets(dcp, pkl)) {
1404 cxml::Document doc ("PackingList");
1405 doc.read_file (pkl->file().get());
1406 if (!doc.optional_node_child("Signature")) {
1407 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1412 if (dcp->asset_map_path()) {
1413 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1414 validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1416 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1425 dcp::note_to_string (VerificationNote note)
1427 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1429 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1430 * not "ClosedCaption assets must have an <EntryPoint> tag."
1432 * It's OK to use XML tag names where they are clear.
1433 * If both ID and filename are available, use only the ID.
1434 * End messages with a full stop.
1435 * Messages should not mention whether or not their errors are a part of Bv2.1.
1437 switch (note.code()) {
1438 case VerificationNote::Code::FAILED_READ:
1439 return *note.note();
1440 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1441 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1442 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1443 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1444 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1445 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1446 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1447 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1448 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1449 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1450 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1451 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1452 case VerificationNote::Code::EMPTY_ASSET_PATH:
1453 return "The asset map contains an empty asset path.";
1454 case VerificationNote::Code::MISSING_ASSET:
1455 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1456 case VerificationNote::Code::MISMATCHED_STANDARD:
1457 return "The DCP contains both SMPTE and Interop parts.";
1458 case VerificationNote::Code::INVALID_XML:
1459 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1460 case VerificationNote::Code::MISSING_ASSETMAP:
1461 return "No ASSETMAP or ASSETMAP.xml was found.";
1462 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1463 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1464 case VerificationNote::Code::INVALID_DURATION:
1465 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1466 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1467 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());
1468 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1469 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());
1470 case VerificationNote::Code::EXTERNAL_ASSET:
1471 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());
1472 case VerificationNote::Code::INVALID_STANDARD:
1473 return "This DCP does not use the SMPTE standard.";
1474 case VerificationNote::Code::INVALID_LANGUAGE:
1475 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1476 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1477 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1478 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1479 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1480 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1481 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1482 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1483 return "3D 4K DCPs are not allowed.";
1484 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1485 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1486 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1487 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1488 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1489 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());
1490 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1491 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1492 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1493 return "Some subtitle assets have different <Language> tags than others";
1494 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1495 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1496 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1497 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1498 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1499 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1500 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1501 return "At least one subtitle lasts less than 15 frames.";
1502 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1503 return "At least one pair of subtitles is separated by less than 2 frames.";
1504 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1505 return "At least one subtitle extends outside of its reel.";
1506 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1507 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1508 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1509 return "There are more than 52 characters in at least one subtitle line.";
1510 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1511 return "There are more than 79 characters in at least one subtitle line.";
1512 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1513 return "There are more than 3 closed caption lines in at least one place.";
1514 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1515 return "There are more than 32 characters in at least one closed caption line.";
1516 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1517 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1518 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1519 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1520 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1521 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1522 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1523 return "All assets in a reel do not have the same duration.";
1524 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1525 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1526 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1527 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1528 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1529 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1530 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1531 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1532 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1533 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1534 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1535 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1536 case VerificationNote::Code::MISSING_HASH:
1537 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1538 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1539 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1540 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1541 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1542 case VerificationNote::Code::MISSING_FFOC:
1543 return "There should be a FFOC (first frame of content) marker";
1544 case VerificationNote::Code::MISSING_LFOC:
1545 return "There should be a LFOC (last frame of content) marker";
1546 case VerificationNote::Code::INCORRECT_FFOC:
1547 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1548 case VerificationNote::Code::INCORRECT_LFOC:
1549 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1550 case VerificationNote::Code::MISSING_CPL_METADATA:
1551 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1552 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1553 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1554 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1555 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1556 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1557 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1558 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1559 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1560 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1561 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1562 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1563 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1564 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1565 return "Some assets are encrypted but some are not.";
1566 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1567 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1568 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1569 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1570 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1571 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1572 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1573 return "The JPEG2000 tile size is not the same as the image size.";
1574 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1575 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1576 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1577 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1578 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1579 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1580 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1581 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1582 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1583 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1584 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1585 return "POC marker found outside main header";
1586 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1587 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1588 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1589 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1590 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1591 return "No TLM marker was found in a JPEG2000 codestream.";
1592 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1593 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1594 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1595 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1596 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1598 vector<string> parts;
1599 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1600 DCP_ASSERT (parts.size() == 2);
1601 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]);
1610 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1612 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1617 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1619 s << note_to_string (note);
1621 s << " [" << note.note().get() << "]";
1624 s << " [" << note.file().get() << "]";
1627 s << " [" << note.line().get() << "]";