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
44 #include "reel_closed_caption_asset.h"
45 #include "reel_picture_asset.h"
46 #include "reel_sound_asset.h"
47 #include "reel_subtitle_asset.h"
48 #include "interop_subtitle_asset.h"
49 #include "mono_picture_asset.h"
50 #include "mono_picture_frame.h"
51 #include "stereo_picture_asset.h"
52 #include "stereo_picture_frame.h"
53 #include "exceptions.h"
54 #include "compose.hpp"
55 #include "raw_convert.h"
56 #include "reel_markers_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include <xercesc/util/PlatformUtils.hpp>
59 #include <xercesc/parsers/XercesDOMParser.hpp>
60 #include <xercesc/parsers/AbstractDOMParser.hpp>
61 #include <xercesc/sax/HandlerBase.hpp>
62 #include <xercesc/dom/DOMImplementation.hpp>
63 #include <xercesc/dom/DOMImplementationLS.hpp>
64 #include <xercesc/dom/DOMImplementationRegistry.hpp>
65 #include <xercesc/dom/DOMLSParser.hpp>
66 #include <xercesc/dom/DOMException.hpp>
67 #include <xercesc/dom/DOMDocument.hpp>
68 #include <xercesc/dom/DOMNodeList.hpp>
69 #include <xercesc/dom/DOMError.hpp>
70 #include <xercesc/dom/DOMLocator.hpp>
71 #include <xercesc/dom/DOMNamedNodeMap.hpp>
72 #include <xercesc/dom/DOMAttr.hpp>
73 #include <xercesc/dom/DOMErrorHandler.hpp>
74 #include <xercesc/framework/LocalFileInputSource.hpp>
75 #include <xercesc/framework/MemBufInputSource.hpp>
76 #include <boost/algorithm/string.hpp>
89 using std::shared_ptr;
90 using std::make_shared;
91 using boost::optional;
92 using boost::function;
93 using std::dynamic_pointer_cast;
97 using namespace xercesc;
102 xml_ch_to_string (XMLCh const * a)
104 char* x = XMLString::transcode(a);
106 XMLString::release(&x);
111 class XMLValidationError
114 XMLValidationError (SAXParseException const & e)
115 : _message (xml_ch_to_string(e.getMessage()))
116 , _line (e.getLineNumber())
117 , _column (e.getColumnNumber())
118 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
119 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
124 string message () const {
128 uint64_t line () const {
132 uint64_t column () const {
136 string public_id () const {
140 string system_id () const {
153 class DCPErrorHandler : public ErrorHandler
156 void warning(const SAXParseException& e)
158 maybe_add (XMLValidationError(e));
161 void error(const SAXParseException& e)
163 maybe_add (XMLValidationError(e));
166 void fatalError(const SAXParseException& e)
168 maybe_add (XMLValidationError(e));
175 list<XMLValidationError> errors () const {
180 void maybe_add (XMLValidationError e)
182 /* XXX: nasty hack */
184 e.message().find("schema document") != string::npos &&
185 e.message().find("has different target namespace from the one specified in instance document") != string::npos
190 _errors.push_back (e);
193 list<XMLValidationError> _errors;
200 StringToXMLCh (string a)
202 _buffer = XMLString::transcode(a.c_str());
205 StringToXMLCh (StringToXMLCh const&) = delete;
206 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
210 XMLString::release (&_buffer);
213 XMLCh const * get () const {
222 class LocalFileResolver : public EntityResolver
225 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
226 : _xsd_dtd_directory (xsd_dtd_directory)
228 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
229 * found without being here.
231 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
232 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
233 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
234 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
235 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
236 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
237 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
238 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
239 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
240 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
241 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
242 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
243 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
246 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
251 auto system_id_str = xml_ch_to_string (system_id);
252 auto p = _xsd_dtd_directory;
253 if (_files.find(system_id_str) == _files.end()) {
256 p /= _files[system_id_str];
258 StringToXMLCh ch (p.string());
259 return new LocalFileInputSource(ch.get());
263 void add (string uri, string file)
268 std::map<string, string> _files;
269 boost::filesystem::path _xsd_dtd_directory;
274 parse (XercesDOMParser& parser, boost::filesystem::path xml)
276 parser.parse(xml.string().c_str());
281 parse (XercesDOMParser& parser, string xml)
283 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
290 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
293 XMLPlatformUtils::Initialize ();
294 } catch (XMLException& e) {
295 throw MiscError ("Failed to initialise xerces library");
298 DCPErrorHandler error_handler;
300 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
302 XercesDOMParser parser;
303 parser.setValidationScheme(XercesDOMParser::Val_Always);
304 parser.setDoNamespaces(true);
305 parser.setDoSchema(true);
307 vector<string> schema;
308 schema.push_back("xml.xsd");
309 schema.push_back("xmldsig-core-schema.xsd");
310 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
311 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
312 schema.push_back("SMPTE-429-9-2007-AM.xsd");
313 schema.push_back("Main-Stereo-Picture-CPL.xsd");
314 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
315 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
316 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
317 schema.push_back("DCSubtitle.v1.mattsson.xsd");
318 schema.push_back("DCDMSubtitle-2010.xsd");
319 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
320 schema.push_back("SMPTE-429-16.xsd");
321 schema.push_back("Dolby-2012-AD.xsd");
322 schema.push_back("SMPTE-429-10-2008.xsd");
323 schema.push_back("xlink.xsd");
324 schema.push_back("SMPTE-335-2012.xsd");
325 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
326 schema.push_back("isdcf-mca.xsd");
327 schema.push_back("SMPTE-429-12-2008.xsd");
329 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
330 * Schemas that are not mentioned in this list are not read, and the things
331 * they describe are not checked.
334 for (auto i: schema) {
335 locations += String::compose("%1 %1 ", i, i);
338 parser.setExternalSchemaLocation(locations.c_str());
339 parser.setValidationSchemaFullChecking(true);
340 parser.setErrorHandler(&error_handler);
342 LocalFileResolver resolver (xsd_dtd_directory);
343 parser.setEntityResolver(&resolver);
346 parser.resetDocumentPool();
348 } catch (XMLException& e) {
349 throw MiscError(xml_ch_to_string(e.getMessage()));
350 } catch (DOMException& e) {
351 throw MiscError(xml_ch_to_string(e.getMessage()));
353 throw MiscError("Unknown exception from xerces");
357 XMLPlatformUtils::Terminate ();
359 for (auto i: error_handler.errors()) {
361 VerificationNote::Type::ERROR,
362 VerificationNote::Code::INVALID_XML,
364 boost::trim_copy(i.public_id() + " " + i.system_id()),
371 enum class VerifyAssetResult {
378 static VerifyAssetResult
379 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
381 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
383 auto pkls = dcp->pkls();
384 /* We've read this DCP in so it must have at least one PKL */
385 DCP_ASSERT (!pkls.empty());
387 auto asset = reel_file_asset->asset_ref().asset();
389 optional<string> pkl_hash;
391 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
397 DCP_ASSERT (pkl_hash);
399 auto cpl_hash = reel_file_asset->hash();
400 if (cpl_hash && *cpl_hash != *pkl_hash) {
401 return VerifyAssetResult::CPL_PKL_DIFFER;
404 if (actual_hash != *pkl_hash) {
405 return VerifyAssetResult::BAD;
408 return VerifyAssetResult::GOOD;
413 verify_language_tag (string tag, vector<VerificationNote>& notes)
416 LanguageTag test (tag);
417 } catch (LanguageTagError &) {
418 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
423 enum class VerifyPictureAssetResult
426 FRAME_NEARLY_TOO_LARGE,
432 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
434 return frame->size ();
438 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
440 return max(frame->left()->size(), frame->right()->size());
444 template <class A, class R, class F>
445 optional<VerifyPictureAssetResult>
446 verify_picture_asset_type (shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
448 auto asset = dynamic_pointer_cast<A>(reel_file_asset->asset_ref().asset());
450 return optional<VerifyPictureAssetResult>();
453 int biggest_frame = 0;
454 auto reader = asset->start_read ();
455 auto const duration = asset->intrinsic_duration ();
456 for (int64_t i = 0; i < duration; ++i) {
457 shared_ptr<const F> frame = reader->get_frame (i);
458 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
459 progress (float(i) / duration);
462 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
463 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
464 if (biggest_frame > max_frame) {
465 return VerifyPictureAssetResult::BAD;
466 } else if (biggest_frame > risky_frame) {
467 return VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE;
470 return VerifyPictureAssetResult::GOOD;
474 static VerifyPictureAssetResult
475 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
477 auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_file_asset, progress);
479 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_file_asset, progress);
488 verify_main_picture_asset (
489 shared_ptr<const DCP> dcp,
490 shared_ptr<const ReelPictureAsset> reel_asset,
491 function<void (string, optional<boost::filesystem::path>)> stage,
492 function<void (float)> progress,
493 vector<VerificationNote>& notes
496 auto asset = reel_asset->asset();
497 auto const file = *asset->file();
498 stage ("Checking picture asset hash", file);
499 auto const r = verify_asset (dcp, reel_asset, progress);
501 case VerifyAssetResult::BAD:
503 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
506 case VerifyAssetResult::CPL_PKL_DIFFER:
508 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
514 stage ("Checking picture frame sizes", asset->file());
515 auto const pr = verify_picture_asset (reel_asset, progress);
517 case VerifyPictureAssetResult::BAD:
519 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
522 case VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE:
524 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
531 /* Only flat/scope allowed by Bv2.1 */
533 asset->size() != Size(2048, 858) &&
534 asset->size() != Size(1998, 1080) &&
535 asset->size() != Size(4096, 1716) &&
536 asset->size() != Size(3996, 2160)) {
538 VerificationNote::Type::BV21_ERROR,
539 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
540 String::compose("%1x%2", asset->size().width, asset->size().height),
545 /* Only 24, 25, 48fps allowed for 2K */
547 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
548 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
551 VerificationNote::Type::BV21_ERROR,
552 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
553 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
558 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
559 /* Only 24fps allowed for 4K */
560 if (asset->edit_rate() != Fraction(24, 1)) {
562 VerificationNote::Type::BV21_ERROR,
563 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
564 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
569 /* Only 2D allowed for 4K */
570 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
572 VerificationNote::Type::BV21_ERROR,
573 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
574 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
585 verify_main_sound_asset (
586 shared_ptr<const DCP> dcp,
587 shared_ptr<const ReelSoundAsset> reel_asset,
588 function<void (string, optional<boost::filesystem::path>)> stage,
589 function<void (float)> progress,
590 vector<VerificationNote>& notes
593 auto asset = reel_asset->asset();
594 stage ("Checking sound asset hash", asset->file());
595 auto const r = verify_asset (dcp, reel_asset, progress);
597 case VerifyAssetResult::BAD:
598 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
600 case VerifyAssetResult::CPL_PKL_DIFFER:
601 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
607 stage ("Checking sound asset metadata", asset->file());
609 verify_language_tag (asset->language(), notes);
610 if (asset->sampling_rate() != 48000) {
611 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
617 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> 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_SUBTITLE_ENTRY_POINT, reel_asset->id() });
626 } else if (reel_asset->entry_point().get()) {
627 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
633 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
635 /* XXX: is Language compulsory? */
636 if (reel_asset->language()) {
637 verify_language_tag (*reel_asset->language(), notes);
640 if (!reel_asset->entry_point()) {
641 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
642 } else if (reel_asset->entry_point().get()) {
643 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
650 boost::optional<string> subtitle_language;
654 /** Verify stuff that is common to both subtitles and closed captions */
656 verify_smpte_timed_text_asset (
657 shared_ptr<const SMPTESubtitleAsset> asset,
658 vector<VerificationNote>& notes
661 if (asset->language()) {
662 verify_language_tag (*asset->language(), notes);
664 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
667 auto const size = boost::filesystem::file_size(asset->file().get());
668 if (size > 115 * 1024 * 1024) {
670 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
674 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
675 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
677 auto fonts = asset->font_data ();
679 for (auto i: fonts) {
680 total_size += i.second.size();
682 if (total_size > 10 * 1024 * 1024) {
683 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
686 if (!asset->start_time()) {
687 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
688 } else if (asset->start_time() != Time()) {
689 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
694 /** Verify SMPTE subtitle-only stuff */
696 verify_smpte_subtitle_asset (
697 shared_ptr<const SMPTESubtitleAsset> asset,
698 vector<VerificationNote>& notes,
702 if (asset->language()) {
703 if (!state.subtitle_language) {
704 state.subtitle_language = *asset->language();
705 } else if (state.subtitle_language != *asset->language()) {
706 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
712 /** Verify all subtitle stuff */
714 verify_subtitle_asset (
715 shared_ptr<const SubtitleAsset> asset,
716 function<void (string, optional<boost::filesystem::path>)> stage,
717 boost::filesystem::path xsd_dtd_directory,
718 vector<VerificationNote>& notes,
722 stage ("Checking subtitle XML", asset->file());
723 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
724 * gets passed through libdcp which may clean up and therefore hide errors.
726 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
728 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
730 verify_smpte_timed_text_asset (smpte, notes);
731 verify_smpte_subtitle_asset (smpte, notes, state);
736 /** Verify all closed caption stuff */
738 verify_closed_caption_asset (
739 shared_ptr<const SubtitleAsset> asset,
740 function<void (string, optional<boost::filesystem::path>)> stage,
741 boost::filesystem::path xsd_dtd_directory,
742 vector<VerificationNote>& notes
745 stage ("Checking closed caption XML", asset->file());
746 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
747 * gets passed through libdcp which may clean up and therefore hide errors.
749 validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
751 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
753 verify_smpte_timed_text_asset (smpte, notes);
756 if (asset->raw_xml().size() > 256 * 1024) {
757 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()});
765 vector<shared_ptr<Reel>> reels,
767 vector<VerificationNote>& notes,
768 std::function<bool (shared_ptr<Reel>)> check,
769 std::function<string (shared_ptr<Reel>)> xml,
770 std::function<int64_t (shared_ptr<Reel>)> duration
773 /* end of last subtitle (in editable units) */
774 optional<int64_t> last_out;
775 auto too_short = false;
776 auto too_close = false;
777 auto too_early = false;
778 auto reel_overlap = false;
779 /* current reel start time (in editable units) */
780 int64_t reel_offset = 0;
782 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
783 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) {
784 if (node->name() == "Subtitle") {
785 Time in (node->string_attribute("TimeIn"), tcr);
789 Time out (node->string_attribute("TimeOut"), tcr);
793 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
796 auto length = out - in;
797 if (length.as_editable_units_ceil(er) < 15) {
801 /* XXX: this feels dubious - is it really what Bv2.1 means? */
802 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
803 if (distance >= 0 && distance < 2) {
807 last_out = reel_offset + out.as_editable_units_floor(er);
809 for (auto i: node->node_children()) {
810 parse(i, tcr, start_time, er, first_reel);
815 for (auto i = 0U; i < reels.size(); ++i) {
816 if (!check(reels[i])) {
820 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
821 * read in by libdcp's parser.
824 shared_ptr<cxml::Document> doc;
826 optional<Time> start_time;
828 doc = make_shared<cxml::Document>("SubtitleReel");
829 doc->read_string (xml(reels[i]));
830 tcr = doc->number_child<int>("TimeCodeRate");
831 auto start_time_string = doc->optional_string_child("StartTime");
832 if (start_time_string) {
833 start_time = Time(*start_time_string, tcr);
836 doc = make_shared<cxml::Document>("DCSubtitle");
837 doc->read_string (xml(reels[i]));
839 parse (doc, tcr, start_time, edit_rate, i == 0);
840 auto end = reel_offset + duration(reels[i]);
841 if (last_out && *last_out > end) {
847 if (last_out && *last_out > reel_offset) {
853 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
859 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
865 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
871 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
877 struct LinesCharactersResult
879 bool warning_length_exceeded = false;
880 bool error_length_exceeded = false;
881 bool line_count_exceeded = false;
887 verify_text_lines_and_characters (
888 shared_ptr<SubtitleAsset> asset,
891 LinesCharactersResult* result
897 Event (Time time_, float position_, int characters_)
899 , position (position_)
900 , characters (characters_)
903 Event (Time time_, shared_ptr<Event> start_)
909 int position; //< position from 0 at top of screen to 100 at bottom
911 shared_ptr<Event> start;
914 vector<shared_ptr<Event>> events;
916 auto position = [](shared_ptr<const SubtitleString> sub) {
917 switch (sub->v_align()) {
919 return lrintf(sub->v_position() * 100);
921 return lrintf((0.5f + sub->v_position()) * 100);
923 return lrintf((1.0f - sub->v_position()) * 100);
929 for (auto j: asset->subtitles()) {
930 auto text = dynamic_pointer_cast<const SubtitleString>(j);
932 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
933 events.push_back(in);
934 events.push_back(make_shared<Event>(text->out(), in));
938 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
939 return a->time < b->time;
942 map<int, int> current;
943 for (auto i: events) {
944 if (current.size() > 3) {
945 result->line_count_exceeded = true;
947 for (auto j: current) {
948 if (j.second >= warning_length) {
949 result->warning_length_exceeded = true;
951 if (j.second >= error_length) {
952 result->error_length_exceeded = true;
957 /* end of a subtitle */
958 DCP_ASSERT (current.find(i->start->position) != current.end());
959 if (current[i->start->position] == i->start->characters) {
960 current.erase(i->start->position);
962 current[i->start->position] -= i->start->characters;
965 /* start of a subtitle */
966 if (current.find(i->position) == current.end()) {
967 current[i->position] = i->characters;
969 current[i->position] += i->characters;
978 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
984 if (reels[0]->main_subtitle()) {
985 verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
986 [](shared_ptr<Reel> reel) {
987 return static_cast<bool>(reel->main_subtitle());
989 [](shared_ptr<Reel> reel) {
990 return reel->main_subtitle()->asset()->raw_xml();
992 [](shared_ptr<Reel> reel) {
993 return reel->main_subtitle()->actual_duration();
998 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
999 verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1000 [i](shared_ptr<Reel> reel) {
1001 return i < reel->closed_captions().size();
1003 [i](shared_ptr<Reel> reel) {
1004 return reel->closed_captions()[i]->asset()->raw_xml();
1006 [i](shared_ptr<Reel> reel) {
1007 return reel->closed_captions()[i]->actual_duration();
1015 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
1017 DCP_ASSERT (cpl->file());
1018 cxml::Document doc ("CompositionPlaylist");
1019 doc.read_file (cpl->file().get());
1021 auto missing = false;
1024 if (auto reel_list = doc.node_child("ReelList")) {
1025 auto reels = reel_list->node_children("Reel");
1026 if (!reels.empty()) {
1027 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1028 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1029 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1031 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1032 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1036 if (auto name = extension->optional_node_child("Name")) {
1037 if (name->content() != "Application") {
1038 malformed = "<Name> should be 'Application'";
1041 if (auto property_list = extension->optional_node_child("PropertyList")) {
1042 if (auto property = property_list->optional_node_child("Property")) {
1043 if (auto name = property->optional_node_child("Name")) {
1044 if (name->content() != "DCP Constraints Profile") {
1045 malformed = "<Name> property should be 'DCP Constraints Profile'";
1048 if (auto value = property->optional_node_child("Value")) {
1049 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1050 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1065 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1066 } else if (!malformed.empty()) {
1067 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1073 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1075 vector<string> encrypted;
1076 for (auto i: dcp->cpls()) {
1077 for (auto j: i->reel_file_assets()) {
1078 if (j->asset_ref().resolved()) {
1079 /* It's a bit surprising / broken but Interop subtitle assets are represented
1080 * in reels by ReelSubtitleAsset which inherits ReelFileAsset, so it's possible for
1081 * ReelFileAssets to have assets which are not MXFs.
1083 if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
1084 if (asset->encrypted()) {
1085 encrypted.push_back(j->asset_ref().id());
1092 for (auto i: pkl->asset_list()) {
1093 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1102 vector<VerificationNote>
1104 vector<boost::filesystem::path> directories,
1105 function<void (string, optional<boost::filesystem::path>)> stage,
1106 function<void (float)> progress,
1107 optional<boost::filesystem::path> xsd_dtd_directory
1110 if (!xsd_dtd_directory) {
1111 xsd_dtd_directory = resources_directory() / "xsd";
1113 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1115 vector<VerificationNote> notes;
1118 vector<shared_ptr<DCP>> dcps;
1119 for (auto i: directories) {
1120 dcps.push_back (make_shared<DCP>(i));
1123 for (auto dcp: dcps) {
1124 stage ("Checking DCP", dcp->directory());
1125 bool carry_on = true;
1128 } catch (MissingAssetmapError& e) {
1129 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1131 } catch (ReadError& e) {
1132 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1133 } catch (XMLError& e) {
1134 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1135 } catch (MXFFileError& e) {
1136 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1137 } catch (cxml::Error& e) {
1138 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1145 if (dcp->standard() != Standard::SMPTE) {
1146 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1149 for (auto cpl: dcp->cpls()) {
1150 stage ("Checking CPL", cpl->file());
1151 validate_xml (cpl->file().get(), *xsd_dtd_directory, notes);
1153 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1154 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1157 for (auto const& i: cpl->additional_subtitle_languages()) {
1158 verify_language_tag (i, notes);
1161 if (cpl->release_territory()) {
1162 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") {
1163 auto terr = cpl->release_territory().get();
1164 /* Must be a valid region tag, or "001" */
1166 LanguageTag::RegionSubtag test (terr);
1168 if (terr != "001") {
1169 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1175 if (dcp->standard() == Standard::SMPTE) {
1176 if (!cpl->annotation_text()) {
1177 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1178 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1179 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1183 for (auto i: dcp->pkls()) {
1184 /* Check that the CPL's hash corresponds to the PKL */
1185 optional<string> h = i->hash(cpl->id());
1186 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1187 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1190 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1191 optional<string> required_annotation_text;
1192 for (auto j: i->asset_list()) {
1193 /* See if this is a CPL */
1194 for (auto k: dcp->cpls()) {
1195 if (j->id() == k->id()) {
1196 if (!required_annotation_text) {
1197 /* First CPL we have found; this is the required AnnotationText unless we find another */
1198 required_annotation_text = cpl->content_title_text();
1200 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1201 required_annotation_text = boost::none;
1207 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1208 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1212 /* set to true if any reel has a MainSubtitle */
1213 auto have_main_subtitle = false;
1214 /* set to true if any reel has no MainSubtitle */
1215 auto have_no_main_subtitle = false;
1216 /* fewest number of closed caption assets seen in a reel */
1217 size_t fewest_closed_captions = SIZE_MAX;
1218 /* most number of closed caption assets seen in a reel */
1219 size_t most_closed_captions = 0;
1220 map<Marker, Time> markers_seen;
1222 for (auto reel: cpl->reels()) {
1223 stage ("Checking reel", optional<boost::filesystem::path>());
1225 for (auto i: reel->assets()) {
1226 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1227 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1229 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1230 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1232 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1233 if (file_asset && !file_asset->hash()) {
1234 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1238 if (dcp->standard() == Standard::SMPTE) {
1239 boost::optional<int64_t> duration;
1240 for (auto i: reel->assets()) {
1242 duration = i->actual_duration();
1243 } else if (*duration != i->actual_duration()) {
1244 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1250 if (reel->main_picture()) {
1251 /* Check reel stuff */
1252 auto const frame_rate = reel->main_picture()->frame_rate();
1253 if (frame_rate.denominator != 1 ||
1254 (frame_rate.numerator != 24 &&
1255 frame_rate.numerator != 25 &&
1256 frame_rate.numerator != 30 &&
1257 frame_rate.numerator != 48 &&
1258 frame_rate.numerator != 50 &&
1259 frame_rate.numerator != 60 &&
1260 frame_rate.numerator != 96)) {
1262 VerificationNote::Type::ERROR,
1263 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1264 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1268 if (reel->main_picture()->asset_ref().resolved()) {
1269 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1273 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1274 verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1277 if (reel->main_subtitle()) {
1278 verify_main_subtitle_reel (reel->main_subtitle(), notes);
1279 if (reel->main_subtitle()->asset_ref().resolved()) {
1280 verify_subtitle_asset (reel->main_subtitle()->asset(), stage, *xsd_dtd_directory, notes, state);
1282 have_main_subtitle = true;
1284 have_no_main_subtitle = true;
1287 for (auto i: reel->closed_captions()) {
1288 verify_closed_caption_reel (i, notes);
1289 if (i->asset_ref().resolved()) {
1290 verify_closed_caption_asset (i->asset(), stage, *xsd_dtd_directory, notes);
1294 if (reel->main_markers()) {
1295 for (auto const& i: reel->main_markers()->get()) {
1296 markers_seen.insert (i);
1300 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1301 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1304 verify_text_timing (cpl->reels(), notes);
1306 if (dcp->standard() == Standard::SMPTE) {
1308 if (have_main_subtitle && have_no_main_subtitle) {
1309 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1312 if (fewest_closed_captions != most_closed_captions) {
1313 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1316 if (cpl->content_kind() == ContentKind::FEATURE) {
1317 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1318 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1320 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1321 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1325 auto ffoc = markers_seen.find(Marker::FFOC);
1326 if (ffoc == markers_seen.end()) {
1327 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1328 } else if (ffoc->second.e != 1) {
1329 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1332 auto lfoc = markers_seen.find(Marker::LFOC);
1333 if (lfoc == markers_seen.end()) {
1334 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1336 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1337 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1338 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1342 LinesCharactersResult result;
1343 for (auto reel: cpl->reels()) {
1344 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1345 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1349 if (result.line_count_exceeded) {
1350 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1352 if (result.error_length_exceeded) {
1353 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1354 } else if (result.warning_length_exceeded) {
1355 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1358 result = LinesCharactersResult();
1359 for (auto reel: cpl->reels()) {
1360 for (auto i: reel->closed_captions()) {
1362 verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1367 if (result.line_count_exceeded) {
1368 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1370 if (result.error_length_exceeded) {
1371 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1374 if (!cpl->full_content_title_text()) {
1375 /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1376 * can use it as a proxy for CompositionMetadataAsset's existence.
1378 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1379 } else if (!cpl->version_number()) {
1380 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1383 verify_extension_metadata (cpl, notes);
1385 if (cpl->any_encrypted()) {
1386 cxml::Document doc ("CompositionPlaylist");
1387 DCP_ASSERT (cpl->file());
1388 doc.read_file (cpl->file().get());
1389 if (!doc.optional_node_child("Signature")) {
1390 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1396 for (auto pkl: dcp->pkls()) {
1397 stage ("Checking PKL", pkl->file());
1398 validate_xml (pkl->file().get(), *xsd_dtd_directory, notes);
1399 if (pkl_has_encrypted_assets(dcp, pkl)) {
1400 cxml::Document doc ("PackingList");
1401 doc.read_file (pkl->file().get());
1402 if (!doc.optional_node_child("Signature")) {
1403 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1408 if (dcp->asset_map_path()) {
1409 stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1410 validate_xml (dcp->asset_map_path().get(), *xsd_dtd_directory, notes);
1412 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1421 dcp::note_to_string (VerificationNote note)
1423 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1425 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1426 * not "ClosedCaption assets must have an <EntryPoint> tag."
1428 * It's OK to use XML tag names where they are clear.
1429 * If both ID and filename are available, use only the ID.
1430 * End messages with a full stop.
1431 * Messages should not mention whether or not their errors are a part of Bv2.1.
1433 switch (note.code()) {
1434 case VerificationNote::Code::FAILED_READ:
1435 return *note.note();
1436 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1437 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1438 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1439 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1440 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1441 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1442 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1443 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1444 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1445 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1446 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1447 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1448 case VerificationNote::Code::EMPTY_ASSET_PATH:
1449 return "The asset map contains an empty asset path.";
1450 case VerificationNote::Code::MISSING_ASSET:
1451 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1452 case VerificationNote::Code::MISMATCHED_STANDARD:
1453 return "The DCP contains both SMPTE and Interop parts.";
1454 case VerificationNote::Code::INVALID_XML:
1455 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1456 case VerificationNote::Code::MISSING_ASSETMAP:
1457 return "No ASSETMAP or ASSETMAP.xml was found.";
1458 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1459 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1460 case VerificationNote::Code::INVALID_DURATION:
1461 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1462 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1463 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());
1464 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1465 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());
1466 case VerificationNote::Code::EXTERNAL_ASSET:
1467 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());
1468 case VerificationNote::Code::INVALID_STANDARD:
1469 return "This DCP does not use the SMPTE standard.";
1470 case VerificationNote::Code::INVALID_LANGUAGE:
1471 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1472 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1473 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1474 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1475 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1476 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1477 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1478 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1479 return "3D 4K DCPs are not allowed.";
1480 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1481 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1482 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1483 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1484 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1485 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());
1486 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1487 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1488 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1489 return "Some subtitle assets have different <Language> tags than others";
1490 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1491 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1492 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1493 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1494 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1495 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1496 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1497 return "At least one subtitle lasts less than 15 frames.";
1498 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1499 return "At least one pair of subtitles is separated by less than 2 frames.";
1500 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1501 return "At least one subtitle extends outside of its reel.";
1502 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1503 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1504 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1505 return "There are more than 52 characters in at least one subtitle line.";
1506 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1507 return "There are more than 79 characters in at least one subtitle line.";
1508 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1509 return "There are more than 3 closed caption lines in at least one place.";
1510 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1511 return "There are more than 32 characters in at least one closed caption line.";
1512 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1513 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1514 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1515 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1516 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1517 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1518 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1519 return "All assets in a reel do not have the same duration.";
1520 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1521 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1522 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1523 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1524 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1525 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1526 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1527 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1528 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1529 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1530 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1531 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1532 case VerificationNote::Code::MISSING_HASH:
1533 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1534 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1535 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1536 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1537 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1538 case VerificationNote::Code::MISSING_FFOC:
1539 return "There should be a FFOC (first frame of content) marker";
1540 case VerificationNote::Code::MISSING_LFOC:
1541 return "There should be a LFOC (last frame of content) marker";
1542 case VerificationNote::Code::INCORRECT_FFOC:
1543 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1544 case VerificationNote::Code::INCORRECT_LFOC:
1545 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1546 case VerificationNote::Code::MISSING_CPL_METADATA:
1547 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1548 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1549 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1550 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1551 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1552 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1553 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1554 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1555 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1556 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1557 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1558 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1559 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1560 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1561 return "Some assets are encrypted but some are not.";
1562 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1563 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1)", note.note().get());
1564 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1565 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1566 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1567 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1568 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1569 return "The JPEG2000 tile size is not the same as the image size.";
1570 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1571 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1572 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1573 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1574 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1575 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1576 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1577 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1578 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1579 return String::compose("Incorrect POC marker content found (%1)", note.note().get());
1580 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1581 return "POC marker found outside main header";
1582 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1583 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1584 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1585 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1586 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1587 return "No TLM marker was found in a JPEG2000 codestream.";
1595 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1597 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1602 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1604 s << note_to_string (note);
1606 s << " [" << note.note().get() << "]";
1609 s << " [" << note.file().get() << "]";
1612 s << " [" << note.line().get() << "]";