2 Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 libdcp is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
35 /** @file src/verify.cc
36 * @brief dcp::verify() method and associated code
40 #include "compose.hpp"
43 #include "exceptions.h"
44 #include "interop_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "mono_picture_frame.h"
47 #include "raw_convert.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_smpte_subtitle_asset.h"
55 #include "reel_subtitle_asset.h"
56 #include "smpte_subtitle_asset.h"
57 #include "stereo_picture_asset.h"
58 #include "stereo_picture_frame.h"
60 #include "verify_j2k.h"
61 #include <xercesc/dom/DOMAttr.hpp>
62 #include <xercesc/dom/DOMDocument.hpp>
63 #include <xercesc/dom/DOMError.hpp>
64 #include <xercesc/dom/DOMErrorHandler.hpp>
65 #include <xercesc/dom/DOMException.hpp>
66 #include <xercesc/dom/DOMImplementation.hpp>
67 #include <xercesc/dom/DOMImplementationLS.hpp>
68 #include <xercesc/dom/DOMImplementationRegistry.hpp>
69 #include <xercesc/dom/DOMLSParser.hpp>
70 #include <xercesc/dom/DOMLocator.hpp>
71 #include <xercesc/dom/DOMNamedNodeMap.hpp>
72 #include <xercesc/dom/DOMNodeList.hpp>
73 #include <xercesc/framework/LocalFileInputSource.hpp>
74 #include <xercesc/framework/MemBufInputSource.hpp>
75 #include <xercesc/parsers/AbstractDOMParser.hpp>
76 #include <xercesc/parsers/XercesDOMParser.hpp>
77 #include <xercesc/sax/HandlerBase.hpp>
78 #include <xercesc/util/PlatformUtils.hpp>
79 #include <boost/algorithm/string.hpp>
88 using std::dynamic_pointer_cast;
90 using std::make_shared;
94 using std::shared_ptr;
97 using boost::optional;
98 using boost::function;
102 using namespace xercesc;
107 xml_ch_to_string (XMLCh const * a)
109 char* x = XMLString::transcode(a);
111 XMLString::release(&x);
116 class XMLValidationError
119 XMLValidationError (SAXParseException const & e)
120 : _message (xml_ch_to_string(e.getMessage()))
121 , _line (e.getLineNumber())
122 , _column (e.getColumnNumber())
123 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
124 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
129 string message () const {
133 uint64_t line () const {
137 uint64_t column () const {
141 string public_id () const {
145 string system_id () const {
158 class DCPErrorHandler : public ErrorHandler
161 void warning(const SAXParseException& e) override
163 maybe_add (XMLValidationError(e));
166 void error(const SAXParseException& e) override
168 maybe_add (XMLValidationError(e));
171 void fatalError(const SAXParseException& e) override
173 maybe_add (XMLValidationError(e));
176 void resetErrors() override {
180 list<XMLValidationError> errors () const {
185 void maybe_add (XMLValidationError e)
187 /* XXX: nasty hack */
189 e.message().find("schema document") != string::npos &&
190 e.message().find("has different target namespace from the one specified in instance document") != string::npos
195 _errors.push_back (e);
198 list<XMLValidationError> _errors;
205 StringToXMLCh (string a)
207 _buffer = XMLString::transcode(a.c_str());
210 StringToXMLCh (StringToXMLCh const&) = delete;
211 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
215 XMLString::release (&_buffer);
218 XMLCh const * get () const {
227 class LocalFileResolver : public EntityResolver
230 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
231 : _xsd_dtd_directory (xsd_dtd_directory)
233 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
234 * found without being here.
236 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
237 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
238 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
239 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
240 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
244 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
245 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
246 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
247 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
248 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
251 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
256 auto system_id_str = xml_ch_to_string (system_id);
257 auto p = _xsd_dtd_directory;
258 if (_files.find(system_id_str) == _files.end()) {
261 p /= _files[system_id_str];
263 StringToXMLCh ch (p.string());
264 return new LocalFileInputSource(ch.get());
268 void add (string uri, string file)
273 std::map<string, string> _files;
274 boost::filesystem::path _xsd_dtd_directory;
279 parse (XercesDOMParser& parser, boost::filesystem::path xml)
281 parser.parse(xml.string().c_str());
286 parse (XercesDOMParser& parser, string xml)
288 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
295 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
298 XMLPlatformUtils::Initialize ();
299 } catch (XMLException& e) {
300 throw MiscError ("Failed to initialise xerces library");
303 DCPErrorHandler error_handler;
305 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
307 XercesDOMParser parser;
308 parser.setValidationScheme(XercesDOMParser::Val_Always);
309 parser.setDoNamespaces(true);
310 parser.setDoSchema(true);
312 vector<string> schema;
313 schema.push_back("xml.xsd");
314 schema.push_back("xmldsig-core-schema.xsd");
315 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
316 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
317 schema.push_back("SMPTE-429-9-2007-AM.xsd");
318 schema.push_back("Main-Stereo-Picture-CPL.xsd");
319 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
320 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
321 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
322 schema.push_back("DCSubtitle.v1.mattsson.xsd");
323 schema.push_back("DCDMSubtitle-2010.xsd");
324 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
325 schema.push_back("SMPTE-429-16.xsd");
326 schema.push_back("Dolby-2012-AD.xsd");
327 schema.push_back("SMPTE-429-10-2008.xsd");
328 schema.push_back("xlink.xsd");
329 schema.push_back("SMPTE-335-2012.xsd");
330 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
331 schema.push_back("isdcf-mca.xsd");
332 schema.push_back("SMPTE-429-12-2008.xsd");
334 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
335 * Schemas that are not mentioned in this list are not read, and the things
336 * they describe are not checked.
339 for (auto i: schema) {
340 locations += String::compose("%1 %1 ", i, i);
343 parser.setExternalSchemaLocation(locations.c_str());
344 parser.setValidationSchemaFullChecking(true);
345 parser.setErrorHandler(&error_handler);
347 LocalFileResolver resolver (xsd_dtd_directory);
348 parser.setEntityResolver(&resolver);
351 parser.resetDocumentPool();
353 } catch (XMLException& e) {
354 throw MiscError(xml_ch_to_string(e.getMessage()));
355 } catch (DOMException& e) {
356 throw MiscError(xml_ch_to_string(e.getMessage()));
358 throw MiscError("Unknown exception from xerces");
362 XMLPlatformUtils::Terminate ();
364 for (auto i: error_handler.errors()) {
366 VerificationNote::Type::ERROR,
367 VerificationNote::Code::INVALID_XML,
369 boost::trim_copy(i.public_id() + " " + i.system_id()),
376 enum class VerifyAssetResult {
383 static VerifyAssetResult
384 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
386 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
388 auto pkls = dcp->pkls();
389 /* We've read this DCP in so it must have at least one PKL */
390 DCP_ASSERT (!pkls.empty());
392 auto asset = reel_file_asset->asset_ref().asset();
394 optional<string> pkl_hash;
396 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
402 DCP_ASSERT (pkl_hash);
404 auto cpl_hash = reel_file_asset->hash();
405 if (cpl_hash && *cpl_hash != *pkl_hash) {
406 return VerifyAssetResult::CPL_PKL_DIFFER;
409 if (actual_hash != *pkl_hash) {
410 return VerifyAssetResult::BAD;
413 return VerifyAssetResult::GOOD;
418 verify_language_tag (string tag, vector<VerificationNote>& notes)
421 LanguageTag test (tag);
422 } catch (LanguageTagError &) {
423 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
429 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
431 int biggest_frame = 0;
432 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
433 auto const duration = asset->intrinsic_duration ();
435 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
436 for (auto i: j2k_notes) {
437 if (find(notes.begin(), notes.end(), i) == notes.end()) {
443 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
444 auto reader = mono_asset->start_read ();
445 for (int64_t i = 0; i < duration; ++i) {
446 auto frame = reader->get_frame (i);
447 biggest_frame = max(biggest_frame, frame->size());
448 if (!mono_asset->encrypted() || mono_asset->key()) {
449 vector<VerificationNote> j2k_notes;
450 verify_j2k (frame, j2k_notes);
451 check_and_add (j2k_notes);
453 progress (float(i) / duration);
455 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
456 auto reader = stereo_asset->start_read ();
457 for (int64_t i = 0; i < duration; ++i) {
458 auto frame = reader->get_frame (i);
459 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
460 if (!stereo_asset->encrypted() || stereo_asset->key()) {
461 vector<VerificationNote> j2k_notes;
462 verify_j2k (frame->left(), j2k_notes);
463 verify_j2k (frame->right(), j2k_notes);
464 check_and_add (j2k_notes);
466 progress (float(i) / duration);
471 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
472 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
473 if (biggest_frame > max_frame) {
475 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
477 } else if (biggest_frame > risky_frame) {
479 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
486 verify_main_picture_asset (
487 shared_ptr<const DCP> dcp,
488 shared_ptr<const ReelPictureAsset> reel_asset,
489 function<void (string, optional<boost::filesystem::path>)> stage,
490 function<void (float)> progress,
491 VerificationOptions options,
492 vector<VerificationNote>& notes
495 auto asset = reel_asset->asset();
496 auto const file = *asset->file();
498 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
499 stage ("Checking picture asset hash", file);
500 auto const r = verify_asset (dcp, reel_asset, progress);
502 case VerifyAssetResult::BAD:
504 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
507 case VerifyAssetResult::CPL_PKL_DIFFER:
509 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
517 stage ("Checking picture frame sizes", asset->file());
518 verify_picture_asset (reel_asset, file, notes, progress);
520 /* Only flat/scope allowed by Bv2.1 */
522 asset->size() != Size(2048, 858) &&
523 asset->size() != Size(1998, 1080) &&
524 asset->size() != Size(4096, 1716) &&
525 asset->size() != Size(3996, 2160)) {
527 VerificationNote::Type::BV21_ERROR,
528 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
529 String::compose("%1x%2", asset->size().width, asset->size().height),
534 /* Only 24, 25, 48fps allowed for 2K */
536 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
537 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
540 VerificationNote::Type::BV21_ERROR,
541 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
542 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
547 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
548 /* Only 24fps allowed for 4K */
549 if (asset->edit_rate() != Fraction(24, 1)) {
551 VerificationNote::Type::BV21_ERROR,
552 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
553 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
558 /* Only 2D allowed for 4K */
559 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
561 VerificationNote::Type::BV21_ERROR,
562 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
563 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
574 verify_main_sound_asset (
575 shared_ptr<const DCP> dcp,
576 shared_ptr<const ReelSoundAsset> reel_asset,
577 function<void (string, optional<boost::filesystem::path>)> stage,
578 function<void (float)> progress,
579 VerificationOptions options,
580 vector<VerificationNote>& notes
583 auto asset = reel_asset->asset();
584 auto const file = *asset->file();
586 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
587 stage("Checking sound asset hash", file);
588 auto const r = verify_asset (dcp, reel_asset, progress);
590 case VerifyAssetResult::BAD:
591 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
593 case VerifyAssetResult::CPL_PKL_DIFFER:
594 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
601 stage ("Checking sound asset metadata", file);
603 if (auto lang = asset->language()) {
604 verify_language_tag (*lang, notes);
606 if (asset->sampling_rate() != 48000) {
607 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
613 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
615 /* XXX: is Language compulsory? */
616 if (reel_asset->language()) {
617 verify_language_tag (*reel_asset->language(), notes);
620 if (!reel_asset->entry_point()) {
621 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
622 } else if (reel_asset->entry_point().get()) {
623 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
629 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
631 /* XXX: is Language compulsory? */
632 if (reel_asset->language()) {
633 verify_language_tag (*reel_asset->language(), notes);
636 if (!reel_asset->entry_point()) {
637 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
638 } else if (reel_asset->entry_point().get()) {
639 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
646 boost::optional<string> subtitle_language;
650 /** Verify stuff that is common to both subtitles and closed captions */
652 verify_smpte_timed_text_asset (
653 shared_ptr<const SMPTESubtitleAsset> asset,
654 optional<int64_t> reel_asset_duration,
655 vector<VerificationNote>& notes
658 if (asset->language()) {
659 verify_language_tag (*asset->language(), notes);
661 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
664 auto const size = boost::filesystem::file_size(asset->file().get());
665 if (size > 115 * 1024 * 1024) {
667 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
671 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
672 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
674 auto fonts = asset->font_data ();
676 for (auto i: fonts) {
677 total_size += i.second.size();
679 if (total_size > 10 * 1024 * 1024) {
680 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
683 if (!asset->start_time()) {
684 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
685 } else if (asset->start_time() != Time()) {
686 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
689 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
692 VerificationNote::Type::BV21_ERROR,
693 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
694 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
701 /** Verify Interop subtitle-only stuff */
703 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
705 if (asset->subtitles().empty()) {
706 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
711 /** Verify SMPTE subtitle-only stuff */
713 verify_smpte_subtitle_asset (
714 shared_ptr<const SMPTESubtitleAsset> asset,
715 vector<VerificationNote>& notes,
719 if (asset->language()) {
720 if (!state.subtitle_language) {
721 state.subtitle_language = *asset->language();
722 } else if (state.subtitle_language != *asset->language()) {
723 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
727 DCP_ASSERT (asset->resource_id());
728 auto xml_id = asset->xml_id();
730 if (asset->resource_id().get() != xml_id) {
731 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
734 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
735 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
738 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
741 if (asset->raw_xml()) {
742 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
743 cxml::Document doc("SubtitleReel");
744 doc.read_string(*asset->raw_xml());
745 auto issue_date = doc.string_child("IssueDate");
746 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
747 if (!std::regex_match(issue_date, reg)) {
748 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
754 /** Verify all subtitle stuff */
756 verify_subtitle_asset (
757 shared_ptr<const SubtitleAsset> asset,
758 optional<int64_t> reel_asset_duration,
759 function<void (string, optional<boost::filesystem::path>)> stage,
760 boost::filesystem::path xsd_dtd_directory,
761 vector<VerificationNote>& notes,
765 stage ("Checking subtitle XML", asset->file());
766 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
767 * gets passed through libdcp which may clean up and therefore hide errors.
769 if (asset->raw_xml()) {
770 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
772 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
775 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
777 verify_interop_subtitle_asset(interop, notes);
780 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
782 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
783 verify_smpte_subtitle_asset (smpte, notes, state);
788 /** Verify all closed caption stuff */
790 verify_closed_caption_asset (
791 shared_ptr<const SubtitleAsset> asset,
792 optional<int64_t> reel_asset_duration,
793 function<void (string, optional<boost::filesystem::path>)> stage,
794 boost::filesystem::path xsd_dtd_directory,
795 vector<VerificationNote>& notes
798 stage ("Checking closed caption XML", asset->file());
799 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
800 * gets passed through libdcp which may clean up and therefore hide errors.
802 auto raw_xml = asset->raw_xml();
804 validate_xml (*raw_xml, xsd_dtd_directory, notes);
805 if (raw_xml->size() > 256 * 1024) {
806 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
809 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
812 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
814 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
819 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
822 verify_text_details (
823 vector<shared_ptr<Reel>> reels,
825 vector<VerificationNote>& notes,
826 std::function<bool (shared_ptr<Reel>)> check,
827 std::function<optional<string> (shared_ptr<Reel>)> xml,
828 std::function<int64_t (shared_ptr<Reel>)> duration
831 /* end of last subtitle (in editable units) */
832 optional<int64_t> last_out;
833 auto too_short = false;
834 auto too_close = false;
835 auto too_early = false;
836 auto reel_overlap = false;
837 auto empty_text = false;
838 /* current reel start time (in editable units) */
839 int64_t reel_offset = 0;
841 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
842 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
843 if (node->name() == "Subtitle") {
844 Time in (node->string_attribute("TimeIn"), tcr);
848 Time out (node->string_attribute("TimeOut"), tcr);
852 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
855 auto length = out - in;
856 if (length.as_editable_units_ceil(er) < 15) {
860 /* XXX: this feels dubious - is it really what Bv2.1 means? */
861 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
862 if (distance >= 0 && distance < 2) {
866 last_out = reel_offset + out.as_editable_units_floor(er);
867 } else if (node->name() == "Text") {
868 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
869 if (!node->content().empty()) {
872 for (auto i: node->node_children()) {
873 if (node_has_content(i)) {
879 if (!node_has_content(node)) {
884 for (auto i: node->node_children()) {
885 parse(i, tcr, start_time, er, first_reel);
889 for (auto i = 0U; i < reels.size(); ++i) {
890 if (!check(reels[i])) {
894 auto reel_xml = xml(reels[i]);
896 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
900 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
901 * read in by libdcp's parser.
904 shared_ptr<cxml::Document> doc;
906 optional<Time> start_time;
908 doc = make_shared<cxml::Document>("SubtitleReel");
909 doc->read_string (*reel_xml);
910 tcr = doc->number_child<int>("TimeCodeRate");
911 auto start_time_string = doc->optional_string_child("StartTime");
912 if (start_time_string) {
913 start_time = Time(*start_time_string, tcr);
916 doc = make_shared<cxml::Document>("DCSubtitle");
917 doc->read_string (*reel_xml);
919 parse (doc, tcr, start_time, edit_rate, i == 0);
920 auto end = reel_offset + duration(reels[i]);
921 if (last_out && *last_out > end) {
927 if (last_out && *last_out > reel_offset) {
933 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
939 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
945 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
951 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
957 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
965 verify_closed_caption_details (
966 vector<shared_ptr<Reel>> reels,
967 vector<VerificationNote>& notes
970 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
971 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
972 for (auto i: node->node_children()) {
973 if (i->name() == "Text") {
974 text_or_image.push_back (i);
976 find_text_or_image (i, text_or_image);
981 auto mismatched_valign = false;
982 auto incorrect_order = false;
984 std::function<void (cxml::ConstNodePtr)> parse;
985 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
986 if (node->name() == "Subtitle") {
987 vector<cxml::ConstNodePtr> text_or_image;
988 find_text_or_image (node, text_or_image);
989 optional<string> last_valign;
990 optional<float> last_vpos;
991 for (auto i: text_or_image) {
992 auto valign = i->optional_string_attribute("VAlign");
994 valign = i->optional_string_attribute("Valign").get_value_or("center");
996 auto vpos = i->optional_number_attribute<float>("VPosition");
998 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1002 if (*last_valign != valign) {
1003 mismatched_valign = true;
1006 last_valign = valign;
1008 if (!mismatched_valign) {
1010 if (*last_valign == "top" || *last_valign == "center") {
1011 if (*vpos < *last_vpos) {
1012 incorrect_order = true;
1015 if (*vpos > *last_vpos) {
1016 incorrect_order = true;
1025 for (auto i: node->node_children()) {
1030 for (auto reel: reels) {
1031 for (auto ccap: reel->closed_captions()) {
1032 auto reel_xml = ccap->asset()->raw_xml();
1034 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1038 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1039 * read in by libdcp's parser.
1042 shared_ptr<cxml::Document> doc;
1044 optional<Time> start_time;
1046 doc = make_shared<cxml::Document>("SubtitleReel");
1047 doc->read_string (*reel_xml);
1049 doc = make_shared<cxml::Document>("DCSubtitle");
1050 doc->read_string (*reel_xml);
1056 if (mismatched_valign) {
1058 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1062 if (incorrect_order) {
1064 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1070 struct LinesCharactersResult
1072 bool warning_length_exceeded = false;
1073 bool error_length_exceeded = false;
1074 bool line_count_exceeded = false;
1080 verify_text_lines_and_characters (
1081 shared_ptr<SubtitleAsset> asset,
1084 LinesCharactersResult* result
1090 Event (Time time_, float position_, int characters_)
1092 , position (position_)
1093 , characters (characters_)
1096 Event (Time time_, shared_ptr<Event> start_)
1102 int position; //< position from 0 at top of screen to 100 at bottom
1104 shared_ptr<Event> start;
1107 vector<shared_ptr<Event>> events;
1109 auto position = [](shared_ptr<const SubtitleString> sub) {
1110 switch (sub->v_align()) {
1112 return lrintf(sub->v_position() * 100);
1113 case VAlign::CENTER:
1114 return lrintf((0.5f + sub->v_position()) * 100);
1115 case VAlign::BOTTOM:
1116 return lrintf((1.0f - sub->v_position()) * 100);
1122 for (auto j: asset->subtitles()) {
1123 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1125 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1126 events.push_back(in);
1127 events.push_back(make_shared<Event>(text->out(), in));
1131 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1132 return a->time < b->time;
1135 map<int, int> current;
1136 for (auto i: events) {
1137 if (current.size() > 3) {
1138 result->line_count_exceeded = true;
1140 for (auto j: current) {
1141 if (j.second > warning_length) {
1142 result->warning_length_exceeded = true;
1144 if (j.second > error_length) {
1145 result->error_length_exceeded = true;
1150 /* end of a subtitle */
1151 DCP_ASSERT (current.find(i->start->position) != current.end());
1152 if (current[i->start->position] == i->start->characters) {
1153 current.erase(i->start->position);
1155 current[i->start->position] -= i->start->characters;
1158 /* start of a subtitle */
1159 if (current.find(i->position) == current.end()) {
1160 current[i->position] = i->characters;
1162 current[i->position] += i->characters;
1171 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1173 if (reels.empty()) {
1177 if (reels[0]->main_subtitle()) {
1178 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1179 [](shared_ptr<Reel> reel) {
1180 return static_cast<bool>(reel->main_subtitle());
1182 [](shared_ptr<Reel> reel) {
1183 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1185 return interop->asset()->raw_xml();
1187 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1189 return smpte->asset()->raw_xml();
1191 [](shared_ptr<Reel> reel) {
1192 return reel->main_subtitle()->actual_duration();
1197 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1198 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1199 [i](shared_ptr<Reel> reel) {
1200 return i < reel->closed_captions().size();
1202 [i](shared_ptr<Reel> reel) {
1203 return reel->closed_captions()[i]->asset()->raw_xml();
1205 [i](shared_ptr<Reel> reel) {
1206 return reel->closed_captions()[i]->actual_duration();
1211 verify_closed_caption_details (reels, notes);
1216 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1218 DCP_ASSERT (cpl->file());
1219 cxml::Document doc ("CompositionPlaylist");
1220 doc.read_file (cpl->file().get());
1222 auto missing = false;
1225 if (auto reel_list = doc.node_child("ReelList")) {
1226 auto reels = reel_list->node_children("Reel");
1227 if (!reels.empty()) {
1228 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1229 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1230 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1232 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1233 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1237 if (auto name = extension->optional_node_child("Name")) {
1238 if (name->content() != "Application") {
1239 malformed = "<Name> should be 'Application'";
1242 if (auto property_list = extension->optional_node_child("PropertyList")) {
1243 if (auto property = property_list->optional_node_child("Property")) {
1244 if (auto name = property->optional_node_child("Name")) {
1245 if (name->content() != "DCP Constraints Profile") {
1246 malformed = "<Name> property should be 'DCP Constraints Profile'";
1249 if (auto value = property->optional_node_child("Value")) {
1250 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1251 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1266 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1267 } else if (!malformed.empty()) {
1268 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1274 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1276 vector<string> encrypted;
1277 for (auto i: dcp->cpls()) {
1278 for (auto j: i->reel_file_assets()) {
1279 if (j->asset_ref().resolved()) {
1280 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1281 if (mxf && mxf->encrypted()) {
1282 encrypted.push_back(j->asset_ref().id());
1288 for (auto i: pkl->assets()) {
1289 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1301 shared_ptr<const DCP> dcp,
1302 shared_ptr<const CPL> cpl,
1303 shared_ptr<const Reel> reel,
1304 optional<dcp::Size> main_picture_active_area,
1305 function<void (string, optional<boost::filesystem::path>)> stage,
1306 boost::filesystem::path xsd_dtd_directory,
1307 function<void (float)> progress,
1308 VerificationOptions options,
1309 vector<VerificationNote>& notes,
1311 bool* have_main_subtitle,
1312 bool* have_no_main_subtitle,
1313 size_t* most_closed_captions,
1314 size_t* fewest_closed_captions,
1315 map<Marker, Time>* markers_seen
1318 for (auto i: reel->assets()) {
1319 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1320 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1322 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1323 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1325 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1326 if (i->encryptable() && !file_asset->hash()) {
1327 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1331 if (dcp->standard() == Standard::SMPTE) {
1332 boost::optional<int64_t> duration;
1333 for (auto i: reel->assets()) {
1335 duration = i->actual_duration();
1336 } else if (*duration != i->actual_duration()) {
1337 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1343 if (reel->main_picture()) {
1344 /* Check reel stuff */
1345 auto const frame_rate = reel->main_picture()->frame_rate();
1346 if (frame_rate.denominator != 1 ||
1347 (frame_rate.numerator != 24 &&
1348 frame_rate.numerator != 25 &&
1349 frame_rate.numerator != 30 &&
1350 frame_rate.numerator != 48 &&
1351 frame_rate.numerator != 50 &&
1352 frame_rate.numerator != 60 &&
1353 frame_rate.numerator != 96)) {
1355 VerificationNote::Type::ERROR,
1356 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1357 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1361 if (reel->main_picture()->asset_ref().resolved()) {
1362 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1363 auto const asset_size = reel->main_picture()->asset()->size();
1364 if (main_picture_active_area) {
1365 if (main_picture_active_area->width > asset_size.width) {
1367 VerificationNote::Type::ERROR,
1368 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1369 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1373 if (main_picture_active_area->height > asset_size.height) {
1375 VerificationNote::Type::ERROR,
1376 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1377 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1385 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1386 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes);
1389 if (reel->main_subtitle()) {
1390 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1391 if (reel->main_subtitle()->asset_ref().resolved()) {
1392 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1394 *have_main_subtitle = true;
1396 *have_no_main_subtitle = true;
1399 for (auto i: reel->closed_captions()) {
1400 verify_closed_caption_reel(i, notes);
1401 if (i->asset_ref().resolved()) {
1402 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1406 if (reel->main_markers()) {
1407 for (auto const& i: reel->main_markers()->get()) {
1408 markers_seen->insert(i);
1410 if (reel->main_markers()->entry_point()) {
1411 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1413 if (reel->main_markers()->duration()) {
1414 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1418 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1419 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1427 shared_ptr<const DCP> dcp,
1428 shared_ptr<const CPL> cpl,
1429 function<void (string, optional<boost::filesystem::path>)> stage,
1430 boost::filesystem::path xsd_dtd_directory,
1431 function<void (float)> progress,
1432 VerificationOptions options,
1433 vector<VerificationNote>& notes,
1437 stage("Checking CPL", cpl->file());
1438 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1440 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1441 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1444 for (auto const& i: cpl->additional_subtitle_languages()) {
1445 verify_language_tag(i, notes);
1448 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1449 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1450 * of the approved ones.
1452 auto all = ContentKind::all();
1453 auto name = cpl->content_kind().name();
1454 transform(name.begin(), name.end(), name.begin(), ::tolower);
1455 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1456 if (iter == all.end()) {
1457 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1461 if (cpl->release_territory()) {
1462 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") {
1463 auto terr = cpl->release_territory().get();
1464 /* Must be a valid region tag, or "001" */
1466 LanguageTag::RegionSubtag test(terr);
1468 if (terr != "001") {
1469 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1475 if (dcp->standard() == Standard::SMPTE) {
1476 if (!cpl->annotation_text()) {
1477 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1478 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1479 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1483 for (auto i: dcp->pkls()) {
1484 /* Check that the CPL's hash corresponds to the PKL */
1485 optional<string> h = i->hash(cpl->id());
1486 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1487 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1490 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1491 optional<string> required_annotation_text;
1492 for (auto j: i->assets()) {
1493 /* See if this is a CPL */
1494 for (auto k: dcp->cpls()) {
1495 if (j->id() == k->id()) {
1496 if (!required_annotation_text) {
1497 /* First CPL we have found; this is the required AnnotationText unless we find another */
1498 required_annotation_text = cpl->content_title_text();
1500 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1501 required_annotation_text = boost::none;
1507 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1508 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1512 /* set to true if any reel has a MainSubtitle */
1513 auto have_main_subtitle = false;
1514 /* set to true if any reel has no MainSubtitle */
1515 auto have_no_main_subtitle = false;
1516 /* fewest number of closed caption assets seen in a reel */
1517 size_t fewest_closed_captions = SIZE_MAX;
1518 /* most number of closed caption assets seen in a reel */
1519 size_t most_closed_captions = 0;
1520 map<Marker, Time> markers_seen;
1522 auto const main_picture_active_area = cpl->main_picture_active_area();
1523 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1525 VerificationNote::Type::ERROR,
1526 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1527 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1531 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1533 VerificationNote::Type::ERROR,
1534 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1535 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1540 for (auto reel: cpl->reels()) {
1541 stage("Checking reel", optional<boost::filesystem::path>());
1546 main_picture_active_area,
1553 &have_main_subtitle,
1554 &have_no_main_subtitle,
1555 &most_closed_captions,
1556 &fewest_closed_captions,
1561 verify_text_details(cpl->reels(), notes);
1563 if (dcp->standard() == Standard::SMPTE) {
1565 if (have_main_subtitle && have_no_main_subtitle) {
1566 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1569 if (fewest_closed_captions != most_closed_captions) {
1570 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1573 if (cpl->content_kind() == ContentKind::FEATURE) {
1574 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1575 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1577 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1578 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1582 auto ffoc = markers_seen.find(Marker::FFOC);
1583 if (ffoc == markers_seen.end()) {
1584 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1585 } else if (ffoc->second.e != 1) {
1586 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1589 auto lfoc = markers_seen.find(Marker::LFOC);
1590 if (lfoc == markers_seen.end()) {
1591 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1593 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1594 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1595 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1599 LinesCharactersResult result;
1600 for (auto reel: cpl->reels()) {
1601 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1602 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1606 if (result.line_count_exceeded) {
1607 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1609 if (result.error_length_exceeded) {
1610 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1611 } else if (result.warning_length_exceeded) {
1612 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1615 result = LinesCharactersResult();
1616 for (auto reel: cpl->reels()) {
1617 for (auto i: reel->closed_captions()) {
1619 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1624 if (result.line_count_exceeded) {
1625 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1627 if (result.error_length_exceeded) {
1628 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1631 if (!cpl->read_composition_metadata()) {
1632 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1633 } else if (!cpl->version_number()) {
1634 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1637 verify_extension_metadata(cpl, notes);
1639 if (cpl->any_encrypted()) {
1640 cxml::Document doc("CompositionPlaylist");
1641 DCP_ASSERT(cpl->file());
1642 doc.read_file(cpl->file().get());
1643 if (!doc.optional_node_child("Signature")) {
1644 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1654 shared_ptr<const DCP> dcp,
1655 shared_ptr<const PKL> pkl,
1656 boost::filesystem::path xsd_dtd_directory,
1657 vector<VerificationNote>& notes
1660 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1662 if (pkl_has_encrypted_assets(dcp, pkl)) {
1663 cxml::Document doc("PackingList");
1664 doc.read_file(pkl->file().get());
1665 if (!doc.optional_node_child("Signature")) {
1666 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1670 set<string> uuid_set;
1671 for (auto asset: pkl->assets()) {
1672 if (!uuid_set.insert(asset->id()).second) {
1673 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1684 shared_ptr<const DCP> dcp,
1685 boost::filesystem::path xsd_dtd_directory,
1686 vector<VerificationNote>& notes
1689 auto asset_map = dcp->asset_map();
1690 DCP_ASSERT(asset_map);
1692 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1694 set<string> uuid_set;
1695 for (auto const& asset: asset_map->assets()) {
1696 if (!uuid_set.insert(asset.id()).second) {
1697 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1704 vector<VerificationNote>
1706 vector<boost::filesystem::path> directories,
1707 function<void (string, optional<boost::filesystem::path>)> stage,
1708 function<void (float)> progress,
1709 VerificationOptions options,
1710 optional<boost::filesystem::path> xsd_dtd_directory
1713 if (!xsd_dtd_directory) {
1714 xsd_dtd_directory = resources_directory() / "xsd";
1716 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1718 vector<VerificationNote> notes;
1721 vector<shared_ptr<DCP>> dcps;
1722 for (auto i: directories) {
1723 dcps.push_back (make_shared<DCP>(i));
1726 for (auto dcp: dcps) {
1727 stage ("Checking DCP", dcp->directory());
1728 bool carry_on = true;
1730 dcp->read (¬es, true);
1731 } catch (MissingAssetmapError& e) {
1732 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1734 } catch (ReadError& e) {
1735 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1736 } catch (XMLError& e) {
1737 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1738 } catch (MXFFileError& e) {
1739 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1740 } catch (cxml::Error& e) {
1741 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1748 if (dcp->standard() != Standard::SMPTE) {
1749 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1752 for (auto cpl: dcp->cpls()) {
1765 for (auto pkl: dcp->pkls()) {
1766 stage("Checking PKL", pkl->file());
1767 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1770 if (dcp->asset_map_file()) {
1771 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1772 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1774 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1783 dcp::note_to_string (VerificationNote note)
1785 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1787 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1788 * not "ClosedCaption assets must have an <EntryPoint> tag."
1790 * It's OK to use XML tag names where they are clear.
1791 * If both ID and filename are available, use only the ID.
1792 * End messages with a full stop.
1793 * Messages should not mention whether or not their errors are a part of Bv2.1.
1795 switch (note.code()) {
1796 case VerificationNote::Code::FAILED_READ:
1797 return *note.note();
1798 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1799 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1800 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1801 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1802 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1803 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1804 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1805 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1806 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1807 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1808 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1809 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1810 case VerificationNote::Code::EMPTY_ASSET_PATH:
1811 return "The asset map contains an empty asset path.";
1812 case VerificationNote::Code::MISSING_ASSET:
1813 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1814 case VerificationNote::Code::MISMATCHED_STANDARD:
1815 return "The DCP contains both SMPTE and Interop parts.";
1816 case VerificationNote::Code::INVALID_XML:
1817 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1818 case VerificationNote::Code::MISSING_ASSETMAP:
1819 return "No ASSETMAP or ASSETMAP.xml was found.";
1820 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1821 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1822 case VerificationNote::Code::INVALID_DURATION:
1823 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1824 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1825 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());
1826 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1827 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());
1828 case VerificationNote::Code::EXTERNAL_ASSET:
1829 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());
1830 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1831 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1832 case VerificationNote::Code::INVALID_STANDARD:
1833 return "This DCP does not use the SMPTE standard.";
1834 case VerificationNote::Code::INVALID_LANGUAGE:
1835 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1836 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1837 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1838 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1839 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1840 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1841 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1842 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1843 return "3D 4K DCPs are not allowed.";
1844 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1845 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1846 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1847 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1848 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1849 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());
1850 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1851 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1852 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1853 return "Some subtitle assets have different <Language> tags than others";
1854 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1855 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1856 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1857 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1858 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1859 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1860 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1861 return "At least one subtitle lasts less than 15 frames.";
1862 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1863 return "At least one pair of subtitles is separated by less than 2 frames.";
1864 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1865 return "At least one subtitle extends outside of its reel.";
1866 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1867 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1868 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1869 return "There are more than 52 characters in at least one subtitle line.";
1870 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1871 return "There are more than 79 characters in at least one subtitle line.";
1872 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1873 return "There are more than 3 closed caption lines in at least one place.";
1874 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1875 return "There are more than 32 characters in at least one closed caption line.";
1876 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1877 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1878 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1879 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1880 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1881 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1882 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1883 return "All assets in a reel do not have the same duration.";
1884 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1885 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1886 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1887 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1888 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1889 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1890 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1891 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1892 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1893 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1894 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1895 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1896 case VerificationNote::Code::MISSING_HASH:
1897 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1898 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1899 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1900 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1901 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1902 case VerificationNote::Code::MISSING_FFOC:
1903 return "There should be a FFOC (first frame of content) marker.";
1904 case VerificationNote::Code::MISSING_LFOC:
1905 return "There should be a LFOC (last frame of content) marker.";
1906 case VerificationNote::Code::INCORRECT_FFOC:
1907 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1908 case VerificationNote::Code::INCORRECT_LFOC:
1909 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1910 case VerificationNote::Code::MISSING_CPL_METADATA:
1911 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1912 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1913 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1914 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1915 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1916 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1917 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1918 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1919 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1920 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1921 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1922 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1923 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1924 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1925 return "Some assets are encrypted but some are not.";
1926 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1927 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1928 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1929 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1930 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1931 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1932 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1933 return "The JPEG2000 tile size is not the same as the image size.";
1934 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1935 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1936 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1937 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1938 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1939 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1940 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1941 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1942 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1943 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1944 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1945 return "POC marker found outside main header.";
1946 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1947 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1948 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1949 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1950 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1951 return "No TLM marker was found in a JPEG2000 codestream.";
1952 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1953 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1954 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1955 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1956 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1958 vector<string> parts;
1959 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1960 DCP_ASSERT (parts.size() == 2);
1961 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]);
1963 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1964 return "Some aspect of this DCP could not be checked because it is encrypted.";
1965 case VerificationNote::Code::EMPTY_TEXT:
1966 return "There is an empty <Text> node in a subtitle or closed caption.";
1967 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1968 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1969 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1970 return "Some closed captions are not listed in the order of their vertical position.";
1971 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1972 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1973 case VerificationNote::Code::UNEXPECTED_DURATION:
1974 return "There is an <Duration> node inside a <MainMarkers>.";
1975 case VerificationNote::Code::INVALID_CONTENT_KIND:
1976 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
1977 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
1978 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
1979 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
1980 return String::compose("The PKL %1 has more than one asset with the same ID", note.note().get());
1981 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
1982 return String::compose("The ASSETMAP %1 has more than one asset with the same ID", note.note().get());
1983 case VerificationNote::Code::MISSING_SUBTITLE:
1984 return String::compose("The subtitle asset %1 has no subtitles", note.note().get());
1985 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
1986 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
1994 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1996 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2001 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2003 if (a.type() != b.type()) {
2004 return a.type() < b.type();
2007 if (a.code() != b.code()) {
2008 return a.code() < b.code();
2011 if (a.note() != b.note()) {
2012 return a.note().get_value_or("") < b.note().get_value_or("");
2015 if (a.file() != b.file()) {
2016 return a.file().get_value_or("") < b.file().get_value_or("");
2019 return a.line().get_value_or(0) < b.line().get_value_or(0);
2024 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2026 s << note_to_string (note);
2028 s << " [" << note.note().get() << "]";
2031 s << " [" << note.file().get() << "]";
2034 s << " [" << note.line().get() << "]";