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) {
447 auto frame = reader->get_frame (i);
448 biggest_frame = max(biggest_frame, frame->size());
449 if (!mono_asset->encrypted() || mono_asset->key()) {
450 vector<VerificationNote> j2k_notes;
451 verify_j2k (frame, j2k_notes);
452 check_and_add (j2k_notes);
455 catch (ReadError const& e) {
456 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_MXF_CODESTREAM, string(e.what()) });
458 progress (float(i) / duration);
460 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
461 auto reader = stereo_asset->start_read ();
462 for (int64_t i = 0; i < duration; ++i) {
464 auto frame = reader->get_frame (i);
465 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
466 if (!stereo_asset->encrypted() || stereo_asset->key()) {
467 vector<VerificationNote> j2k_notes;
468 verify_j2k (frame->left(), j2k_notes);
469 verify_j2k (frame->right(), j2k_notes);
470 check_and_add (j2k_notes);
473 catch (ReadError const& e) {
474 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_MXF_CODESTREAM, string(e.what()) });
476 progress (float(i) / duration);
481 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
482 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
483 if (biggest_frame > max_frame) {
485 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
487 } else if (biggest_frame > risky_frame) {
489 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
496 verify_main_picture_asset (
497 shared_ptr<const DCP> dcp,
498 shared_ptr<const ReelPictureAsset> reel_asset,
499 function<void (string, optional<boost::filesystem::path>)> stage,
500 function<void (float)> progress,
501 VerificationOptions options,
502 vector<VerificationNote>& notes
505 auto asset = reel_asset->asset();
506 auto const file = *asset->file();
508 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
509 stage ("Checking picture asset hash", file);
510 auto const r = verify_asset (dcp, reel_asset, progress);
512 case VerifyAssetResult::BAD:
514 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
517 case VerifyAssetResult::CPL_PKL_DIFFER:
519 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
527 stage ("Checking picture frame sizes", asset->file());
528 verify_picture_asset (reel_asset, file, notes, progress);
530 /* Only flat/scope allowed by Bv2.1 */
532 asset->size() != Size(2048, 858) &&
533 asset->size() != Size(1998, 1080) &&
534 asset->size() != Size(4096, 1716) &&
535 asset->size() != Size(3996, 2160)) {
537 VerificationNote::Type::BV21_ERROR,
538 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
539 String::compose("%1x%2", asset->size().width, asset->size().height),
544 /* Only 24, 25, 48fps allowed for 2K */
546 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
547 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
550 VerificationNote::Type::BV21_ERROR,
551 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
552 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
557 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
558 /* Only 24fps allowed for 4K */
559 if (asset->edit_rate() != Fraction(24, 1)) {
561 VerificationNote::Type::BV21_ERROR,
562 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
563 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
568 /* Only 2D allowed for 4K */
569 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
571 VerificationNote::Type::BV21_ERROR,
572 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
573 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
584 verify_main_sound_asset (
585 shared_ptr<const DCP> dcp,
586 shared_ptr<const ReelSoundAsset> reel_asset,
587 function<void (string, optional<boost::filesystem::path>)> stage,
588 function<void (float)> progress,
589 VerificationOptions options,
590 vector<VerificationNote>& notes
593 auto asset = reel_asset->asset();
594 auto const file = *asset->file();
596 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
597 stage("Checking sound asset hash", file);
598 auto const r = verify_asset (dcp, reel_asset, progress);
600 case VerifyAssetResult::BAD:
601 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
603 case VerifyAssetResult::CPL_PKL_DIFFER:
604 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
611 stage ("Checking sound asset metadata", file);
613 if (auto lang = asset->language()) {
614 verify_language_tag (*lang, notes);
616 if (asset->sampling_rate() != 48000) {
617 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
623 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
625 /* XXX: is Language compulsory? */
626 if (reel_asset->language()) {
627 verify_language_tag (*reel_asset->language(), notes);
630 if (!reel_asset->entry_point()) {
631 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
632 } else if (reel_asset->entry_point().get()) {
633 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
639 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
641 /* XXX: is Language compulsory? */
642 if (reel_asset->language()) {
643 verify_language_tag (*reel_asset->language(), notes);
646 if (!reel_asset->entry_point()) {
647 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
648 } else if (reel_asset->entry_point().get()) {
649 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
656 boost::optional<string> subtitle_language;
660 /** Verify stuff that is common to both subtitles and closed captions */
662 verify_smpte_timed_text_asset (
663 shared_ptr<const SMPTESubtitleAsset> asset,
664 optional<int64_t> reel_asset_duration,
665 vector<VerificationNote>& notes
668 if (asset->language()) {
669 verify_language_tag (*asset->language(), notes);
671 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
674 auto const size = boost::filesystem::file_size(asset->file().get());
675 if (size > 115 * 1024 * 1024) {
677 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
681 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
682 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
684 auto fonts = asset->font_data ();
686 for (auto i: fonts) {
687 total_size += i.second.size();
689 if (total_size > 10 * 1024 * 1024) {
690 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
693 if (!asset->start_time()) {
694 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
695 } else if (asset->start_time() != Time()) {
696 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
699 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
702 VerificationNote::Type::BV21_ERROR,
703 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
704 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
711 /** Verify Interop subtitle-only stuff */
713 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
715 if (asset->subtitles().empty()) {
716 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
721 /** Verify SMPTE subtitle-only stuff */
723 verify_smpte_subtitle_asset (
724 shared_ptr<const SMPTESubtitleAsset> asset,
725 vector<VerificationNote>& notes,
729 if (asset->language()) {
730 if (!state.subtitle_language) {
731 state.subtitle_language = *asset->language();
732 } else if (state.subtitle_language != *asset->language()) {
733 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
737 DCP_ASSERT (asset->resource_id());
738 auto xml_id = asset->xml_id();
740 if (asset->resource_id().get() != xml_id) {
741 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
744 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
745 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
748 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
751 if (asset->raw_xml()) {
752 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
753 cxml::Document doc("SubtitleReel");
754 doc.read_string(*asset->raw_xml());
755 auto issue_date = doc.string_child("IssueDate");
756 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
757 if (!std::regex_match(issue_date, reg)) {
758 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
764 /** Verify all subtitle stuff */
766 verify_subtitle_asset (
767 shared_ptr<const SubtitleAsset> asset,
768 optional<int64_t> reel_asset_duration,
769 function<void (string, optional<boost::filesystem::path>)> stage,
770 boost::filesystem::path xsd_dtd_directory,
771 vector<VerificationNote>& notes,
775 stage ("Checking subtitle XML", asset->file());
776 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
777 * gets passed through libdcp which may clean up and therefore hide errors.
779 if (asset->raw_xml()) {
780 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
782 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
785 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
787 verify_interop_subtitle_asset(interop, notes);
790 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
792 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
793 verify_smpte_subtitle_asset (smpte, notes, state);
798 /** Verify all closed caption stuff */
800 verify_closed_caption_asset (
801 shared_ptr<const SubtitleAsset> asset,
802 optional<int64_t> reel_asset_duration,
803 function<void (string, optional<boost::filesystem::path>)> stage,
804 boost::filesystem::path xsd_dtd_directory,
805 vector<VerificationNote>& notes
808 stage ("Checking closed caption XML", asset->file());
809 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
810 * gets passed through libdcp which may clean up and therefore hide errors.
812 auto raw_xml = asset->raw_xml();
814 validate_xml (*raw_xml, xsd_dtd_directory, notes);
815 if (raw_xml->size() > 256 * 1024) {
816 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
819 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
822 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
824 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
829 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
832 verify_text_details (
833 vector<shared_ptr<Reel>> reels,
835 vector<VerificationNote>& notes,
836 std::function<bool (shared_ptr<Reel>)> check,
837 std::function<optional<string> (shared_ptr<Reel>)> xml,
838 std::function<int64_t (shared_ptr<Reel>)> duration
841 /* end of last subtitle (in editable units) */
842 optional<int64_t> last_out;
843 auto too_short = false;
844 auto too_close = false;
845 auto too_early = false;
846 auto reel_overlap = false;
847 auto empty_text = false;
848 /* current reel start time (in editable units) */
849 int64_t reel_offset = 0;
851 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
852 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) {
853 if (node->name() == "Subtitle") {
854 Time in (node->string_attribute("TimeIn"), tcr);
858 Time out (node->string_attribute("TimeOut"), tcr);
862 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
865 auto length = out - in;
866 if (length.as_editable_units_ceil(er) < 15) {
870 /* XXX: this feels dubious - is it really what Bv2.1 means? */
871 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
872 if (distance >= 0 && distance < 2) {
876 last_out = reel_offset + out.as_editable_units_floor(er);
877 } else if (node->name() == "Text") {
878 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
879 if (!node->content().empty()) {
882 for (auto i: node->node_children()) {
883 if (node_has_content(i)) {
889 if (!node_has_content(node)) {
894 for (auto i: node->node_children()) {
895 parse(i, tcr, start_time, er, first_reel);
899 for (auto i = 0U; i < reels.size(); ++i) {
900 if (!check(reels[i])) {
904 auto reel_xml = xml(reels[i]);
906 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
910 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
911 * read in by libdcp's parser.
914 shared_ptr<cxml::Document> doc;
916 optional<Time> start_time;
918 doc = make_shared<cxml::Document>("SubtitleReel");
919 doc->read_string (*reel_xml);
920 tcr = doc->number_child<int>("TimeCodeRate");
921 auto start_time_string = doc->optional_string_child("StartTime");
922 if (start_time_string) {
923 start_time = Time(*start_time_string, tcr);
926 doc = make_shared<cxml::Document>("DCSubtitle");
927 doc->read_string (*reel_xml);
929 parse (doc, tcr, start_time, edit_rate, i == 0);
930 auto end = reel_offset + duration(reels[i]);
931 if (last_out && *last_out > end) {
937 if (last_out && *last_out > reel_offset) {
943 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
949 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
955 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
961 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
967 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
975 verify_closed_caption_details (
976 vector<shared_ptr<Reel>> reels,
977 vector<VerificationNote>& notes
980 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
981 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
982 for (auto i: node->node_children()) {
983 if (i->name() == "Text") {
984 text_or_image.push_back (i);
986 find_text_or_image (i, text_or_image);
991 auto mismatched_valign = false;
992 auto incorrect_order = false;
994 std::function<void (cxml::ConstNodePtr)> parse;
995 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
996 if (node->name() == "Subtitle") {
997 vector<cxml::ConstNodePtr> text_or_image;
998 find_text_or_image (node, text_or_image);
999 optional<string> last_valign;
1000 optional<float> last_vpos;
1001 for (auto i: text_or_image) {
1002 auto valign = i->optional_string_attribute("VAlign");
1004 valign = i->optional_string_attribute("Valign").get_value_or("center");
1006 auto vpos = i->optional_number_attribute<float>("VPosition");
1008 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1012 if (*last_valign != valign) {
1013 mismatched_valign = true;
1016 last_valign = valign;
1018 if (!mismatched_valign) {
1020 if (*last_valign == "top" || *last_valign == "center") {
1021 if (*vpos < *last_vpos) {
1022 incorrect_order = true;
1025 if (*vpos > *last_vpos) {
1026 incorrect_order = true;
1035 for (auto i: node->node_children()) {
1040 for (auto reel: reels) {
1041 for (auto ccap: reel->closed_captions()) {
1042 auto reel_xml = ccap->asset()->raw_xml();
1044 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1048 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1049 * read in by libdcp's parser.
1052 shared_ptr<cxml::Document> doc;
1054 optional<Time> start_time;
1056 doc = make_shared<cxml::Document>("SubtitleReel");
1057 doc->read_string (*reel_xml);
1059 doc = make_shared<cxml::Document>("DCSubtitle");
1060 doc->read_string (*reel_xml);
1066 if (mismatched_valign) {
1068 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1072 if (incorrect_order) {
1074 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1080 struct LinesCharactersResult
1082 bool warning_length_exceeded = false;
1083 bool error_length_exceeded = false;
1084 bool line_count_exceeded = false;
1090 verify_text_lines_and_characters (
1091 shared_ptr<SubtitleAsset> asset,
1094 LinesCharactersResult* result
1100 Event (Time time_, float position_, int characters_)
1102 , position (position_)
1103 , characters (characters_)
1106 Event (Time time_, shared_ptr<Event> start_)
1112 int position; //< position from 0 at top of screen to 100 at bottom
1114 shared_ptr<Event> start;
1117 vector<shared_ptr<Event>> events;
1119 auto position = [](shared_ptr<const SubtitleString> sub) {
1120 switch (sub->v_align()) {
1122 return lrintf(sub->v_position() * 100);
1123 case VAlign::CENTER:
1124 return lrintf((0.5f + sub->v_position()) * 100);
1125 case VAlign::BOTTOM:
1126 return lrintf((1.0f - sub->v_position()) * 100);
1132 for (auto j: asset->subtitles()) {
1133 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1135 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1136 events.push_back(in);
1137 events.push_back(make_shared<Event>(text->out(), in));
1141 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1142 return a->time < b->time;
1145 map<int, int> current;
1146 for (auto i: events) {
1147 if (current.size() > 3) {
1148 result->line_count_exceeded = true;
1150 for (auto j: current) {
1151 if (j.second > warning_length) {
1152 result->warning_length_exceeded = true;
1154 if (j.second > error_length) {
1155 result->error_length_exceeded = true;
1160 /* end of a subtitle */
1161 DCP_ASSERT (current.find(i->start->position) != current.end());
1162 if (current[i->start->position] == i->start->characters) {
1163 current.erase(i->start->position);
1165 current[i->start->position] -= i->start->characters;
1168 /* start of a subtitle */
1169 if (current.find(i->position) == current.end()) {
1170 current[i->position] = i->characters;
1172 current[i->position] += i->characters;
1181 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1183 if (reels.empty()) {
1187 if (reels[0]->main_subtitle()) {
1188 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1189 [](shared_ptr<Reel> reel) {
1190 return static_cast<bool>(reel->main_subtitle());
1192 [](shared_ptr<Reel> reel) {
1193 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1195 return interop->asset()->raw_xml();
1197 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1199 return smpte->asset()->raw_xml();
1201 [](shared_ptr<Reel> reel) {
1202 return reel->main_subtitle()->actual_duration();
1207 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1208 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1209 [i](shared_ptr<Reel> reel) {
1210 return i < reel->closed_captions().size();
1212 [i](shared_ptr<Reel> reel) {
1213 return reel->closed_captions()[i]->asset()->raw_xml();
1215 [i](shared_ptr<Reel> reel) {
1216 return reel->closed_captions()[i]->actual_duration();
1221 verify_closed_caption_details (reels, notes);
1226 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1228 DCP_ASSERT (cpl->file());
1229 cxml::Document doc ("CompositionPlaylist");
1230 doc.read_file (cpl->file().get());
1232 auto missing = false;
1235 if (auto reel_list = doc.node_child("ReelList")) {
1236 auto reels = reel_list->node_children("Reel");
1237 if (!reels.empty()) {
1238 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1239 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1240 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1242 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1243 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1247 if (auto name = extension->optional_node_child("Name")) {
1248 if (name->content() != "Application") {
1249 malformed = "<Name> should be 'Application'";
1252 if (auto property_list = extension->optional_node_child("PropertyList")) {
1253 if (auto property = property_list->optional_node_child("Property")) {
1254 if (auto name = property->optional_node_child("Name")) {
1255 if (name->content() != "DCP Constraints Profile") {
1256 malformed = "<Name> property should be 'DCP Constraints Profile'";
1259 if (auto value = property->optional_node_child("Value")) {
1260 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1261 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1276 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1277 } else if (!malformed.empty()) {
1278 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1284 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1286 vector<string> encrypted;
1287 for (auto i: dcp->cpls()) {
1288 for (auto j: i->reel_file_assets()) {
1289 if (j->asset_ref().resolved()) {
1290 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1291 if (mxf && mxf->encrypted()) {
1292 encrypted.push_back(j->asset_ref().id());
1298 for (auto i: pkl->assets()) {
1299 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1311 shared_ptr<const DCP> dcp,
1312 shared_ptr<const CPL> cpl,
1313 shared_ptr<const Reel> reel,
1314 optional<dcp::Size> main_picture_active_area,
1315 function<void (string, optional<boost::filesystem::path>)> stage,
1316 boost::filesystem::path xsd_dtd_directory,
1317 function<void (float)> progress,
1318 VerificationOptions options,
1319 vector<VerificationNote>& notes,
1321 bool* have_main_subtitle,
1322 bool* have_no_main_subtitle,
1323 size_t* most_closed_captions,
1324 size_t* fewest_closed_captions,
1325 map<Marker, Time>* markers_seen
1328 for (auto i: reel->assets()) {
1329 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1330 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1332 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1333 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1335 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1336 if (i->encryptable() && !file_asset->hash()) {
1337 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1341 if (dcp->standard() == Standard::SMPTE) {
1342 boost::optional<int64_t> duration;
1343 for (auto i: reel->assets()) {
1345 duration = i->actual_duration();
1346 } else if (*duration != i->actual_duration()) {
1347 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1353 if (reel->main_picture()) {
1354 /* Check reel stuff */
1355 auto const frame_rate = reel->main_picture()->frame_rate();
1356 if (frame_rate.denominator != 1 ||
1357 (frame_rate.numerator != 24 &&
1358 frame_rate.numerator != 25 &&
1359 frame_rate.numerator != 30 &&
1360 frame_rate.numerator != 48 &&
1361 frame_rate.numerator != 50 &&
1362 frame_rate.numerator != 60 &&
1363 frame_rate.numerator != 96)) {
1365 VerificationNote::Type::ERROR,
1366 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1367 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1371 if (reel->main_picture()->asset_ref().resolved()) {
1372 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1373 auto const asset_size = reel->main_picture()->asset()->size();
1374 if (main_picture_active_area) {
1375 if (main_picture_active_area->width > asset_size.width) {
1377 VerificationNote::Type::ERROR,
1378 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1379 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1383 if (main_picture_active_area->height > asset_size.height) {
1385 VerificationNote::Type::ERROR,
1386 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1387 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1395 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1396 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes);
1399 if (reel->main_subtitle()) {
1400 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1401 if (reel->main_subtitle()->asset_ref().resolved()) {
1402 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1404 *have_main_subtitle = true;
1406 *have_no_main_subtitle = true;
1409 for (auto i: reel->closed_captions()) {
1410 verify_closed_caption_reel(i, notes);
1411 if (i->asset_ref().resolved()) {
1412 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1416 if (reel->main_markers()) {
1417 for (auto const& i: reel->main_markers()->get()) {
1418 markers_seen->insert(i);
1420 if (reel->main_markers()->entry_point()) {
1421 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1423 if (reel->main_markers()->duration()) {
1424 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1428 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1429 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1437 shared_ptr<const DCP> dcp,
1438 shared_ptr<const CPL> cpl,
1439 function<void (string, optional<boost::filesystem::path>)> stage,
1440 boost::filesystem::path xsd_dtd_directory,
1441 function<void (float)> progress,
1442 VerificationOptions options,
1443 vector<VerificationNote>& notes,
1447 stage("Checking CPL", cpl->file());
1448 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1450 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1451 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1454 for (auto const& i: cpl->additional_subtitle_languages()) {
1455 verify_language_tag(i, notes);
1458 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1459 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1460 * of the approved ones.
1462 auto all = ContentKind::all();
1463 auto name = cpl->content_kind().name();
1464 transform(name.begin(), name.end(), name.begin(), ::tolower);
1465 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1466 if (iter == all.end()) {
1467 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1471 if (cpl->release_territory()) {
1472 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") {
1473 auto terr = cpl->release_territory().get();
1474 /* Must be a valid region tag, or "001" */
1476 LanguageTag::RegionSubtag test(terr);
1478 if (terr != "001") {
1479 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1485 if (dcp->standard() == Standard::SMPTE) {
1486 if (!cpl->annotation_text()) {
1487 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1488 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1489 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1493 for (auto i: dcp->pkls()) {
1494 /* Check that the CPL's hash corresponds to the PKL */
1495 optional<string> h = i->hash(cpl->id());
1496 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1497 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1500 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1501 optional<string> required_annotation_text;
1502 for (auto j: i->assets()) {
1503 /* See if this is a CPL */
1504 for (auto k: dcp->cpls()) {
1505 if (j->id() == k->id()) {
1506 if (!required_annotation_text) {
1507 /* First CPL we have found; this is the required AnnotationText unless we find another */
1508 required_annotation_text = cpl->content_title_text();
1510 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1511 required_annotation_text = boost::none;
1517 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1518 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1522 /* set to true if any reel has a MainSubtitle */
1523 auto have_main_subtitle = false;
1524 /* set to true if any reel has no MainSubtitle */
1525 auto have_no_main_subtitle = false;
1526 /* fewest number of closed caption assets seen in a reel */
1527 size_t fewest_closed_captions = SIZE_MAX;
1528 /* most number of closed caption assets seen in a reel */
1529 size_t most_closed_captions = 0;
1530 map<Marker, Time> markers_seen;
1532 auto const main_picture_active_area = cpl->main_picture_active_area();
1533 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1535 VerificationNote::Type::ERROR,
1536 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1537 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1541 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1543 VerificationNote::Type::ERROR,
1544 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1545 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1550 for (auto reel: cpl->reels()) {
1551 stage("Checking reel", optional<boost::filesystem::path>());
1556 main_picture_active_area,
1563 &have_main_subtitle,
1564 &have_no_main_subtitle,
1565 &most_closed_captions,
1566 &fewest_closed_captions,
1571 verify_text_details(cpl->reels(), notes);
1573 if (dcp->standard() == Standard::SMPTE) {
1575 if (have_main_subtitle && have_no_main_subtitle) {
1576 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1579 if (fewest_closed_captions != most_closed_captions) {
1580 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1583 if (cpl->content_kind() == ContentKind::FEATURE) {
1584 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1585 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1587 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1588 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1592 auto ffoc = markers_seen.find(Marker::FFOC);
1593 if (ffoc == markers_seen.end()) {
1594 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1595 } else if (ffoc->second.e != 1) {
1596 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1599 auto lfoc = markers_seen.find(Marker::LFOC);
1600 if (lfoc == markers_seen.end()) {
1601 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1603 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1604 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1605 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1609 LinesCharactersResult result;
1610 for (auto reel: cpl->reels()) {
1611 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1612 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1616 if (result.line_count_exceeded) {
1617 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1619 if (result.error_length_exceeded) {
1620 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1621 } else if (result.warning_length_exceeded) {
1622 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1625 result = LinesCharactersResult();
1626 for (auto reel: cpl->reels()) {
1627 for (auto i: reel->closed_captions()) {
1629 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1634 if (result.line_count_exceeded) {
1635 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1637 if (result.error_length_exceeded) {
1638 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1641 if (!cpl->read_composition_metadata()) {
1642 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1643 } else if (!cpl->version_number()) {
1644 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1647 verify_extension_metadata(cpl, notes);
1649 if (cpl->any_encrypted()) {
1650 cxml::Document doc("CompositionPlaylist");
1651 DCP_ASSERT(cpl->file());
1652 doc.read_file(cpl->file().get());
1653 if (!doc.optional_node_child("Signature")) {
1654 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1664 shared_ptr<const DCP> dcp,
1665 shared_ptr<const PKL> pkl,
1666 boost::filesystem::path xsd_dtd_directory,
1667 vector<VerificationNote>& notes
1670 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1672 if (pkl_has_encrypted_assets(dcp, pkl)) {
1673 cxml::Document doc("PackingList");
1674 doc.read_file(pkl->file().get());
1675 if (!doc.optional_node_child("Signature")) {
1676 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1680 set<string> uuid_set;
1681 for (auto asset: pkl->assets()) {
1682 if (!uuid_set.insert(asset->id()).second) {
1683 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1694 shared_ptr<const DCP> dcp,
1695 boost::filesystem::path xsd_dtd_directory,
1696 vector<VerificationNote>& notes
1699 auto asset_map = dcp->asset_map();
1700 DCP_ASSERT(asset_map);
1702 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1704 set<string> uuid_set;
1705 for (auto const& asset: asset_map->assets()) {
1706 if (!uuid_set.insert(asset.id()).second) {
1707 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1714 vector<VerificationNote>
1716 vector<boost::filesystem::path> directories,
1717 function<void (string, optional<boost::filesystem::path>)> stage,
1718 function<void (float)> progress,
1719 VerificationOptions options,
1720 optional<boost::filesystem::path> xsd_dtd_directory
1723 if (!xsd_dtd_directory) {
1724 xsd_dtd_directory = resources_directory() / "xsd";
1726 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1728 vector<VerificationNote> notes;
1731 vector<shared_ptr<DCP>> dcps;
1732 for (auto i: directories) {
1733 dcps.push_back (make_shared<DCP>(i));
1736 for (auto dcp: dcps) {
1737 stage ("Checking DCP", dcp->directory());
1738 bool carry_on = true;
1740 dcp->read (¬es, true);
1741 } catch (MissingAssetmapError& e) {
1742 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1744 } catch (ReadError& e) {
1745 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1746 } catch (XMLError& e) {
1747 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1748 } catch (MXFFileError& e) {
1749 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1750 } catch (cxml::Error& e) {
1751 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1758 if (dcp->standard() != Standard::SMPTE) {
1759 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1762 for (auto cpl: dcp->cpls()) {
1775 for (auto pkl: dcp->pkls()) {
1776 stage("Checking PKL", pkl->file());
1777 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1780 if (dcp->asset_map_file()) {
1781 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1782 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1784 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1793 dcp::note_to_string (VerificationNote note)
1795 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1797 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1798 * not "ClosedCaption assets must have an <EntryPoint> tag."
1800 * It's OK to use XML tag names where they are clear.
1801 * If both ID and filename are available, use only the ID.
1802 * End messages with a full stop.
1803 * Messages should not mention whether or not their errors are a part of Bv2.1.
1805 switch (note.code()) {
1806 case VerificationNote::Code::FAILED_READ:
1807 return *note.note();
1808 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1809 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1810 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1811 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1812 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1813 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1814 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1815 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1816 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1817 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1818 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1819 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1820 case VerificationNote::Code::EMPTY_ASSET_PATH:
1821 return "The asset map contains an empty asset path.";
1822 case VerificationNote::Code::MISSING_ASSET:
1823 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1824 case VerificationNote::Code::MISMATCHED_STANDARD:
1825 return "The DCP contains both SMPTE and Interop parts.";
1826 case VerificationNote::Code::INVALID_XML:
1827 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1828 case VerificationNote::Code::MISSING_ASSETMAP:
1829 return "No ASSETMAP or ASSETMAP.xml was found.";
1830 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1831 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1832 case VerificationNote::Code::INVALID_DURATION:
1833 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1834 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1835 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());
1836 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1837 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());
1838 case VerificationNote::Code::EXTERNAL_ASSET:
1839 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());
1840 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1841 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1842 case VerificationNote::Code::INVALID_STANDARD:
1843 return "This DCP does not use the SMPTE standard.";
1844 case VerificationNote::Code::INVALID_LANGUAGE:
1845 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1846 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1847 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1848 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1849 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1850 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1851 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1852 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1853 return "3D 4K DCPs are not allowed.";
1854 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1855 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1856 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1857 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1858 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1859 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());
1860 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1861 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1862 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1863 return "Some subtitle assets have different <Language> tags than others";
1864 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1865 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1866 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1867 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1868 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1869 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1870 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1871 return "At least one subtitle lasts less than 15 frames.";
1872 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1873 return "At least one pair of subtitles is separated by less than 2 frames.";
1874 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1875 return "At least one subtitle extends outside of its reel.";
1876 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1877 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1878 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1879 return "There are more than 52 characters in at least one subtitle line.";
1880 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1881 return "There are more than 79 characters in at least one subtitle line.";
1882 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1883 return "There are more than 3 closed caption lines in at least one place.";
1884 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1885 return "There are more than 32 characters in at least one closed caption line.";
1886 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1887 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1888 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1889 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1890 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1891 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1892 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1893 return "All assets in a reel do not have the same duration.";
1894 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1895 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1896 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1897 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1898 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1899 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1900 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1901 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1902 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1903 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1904 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1905 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1906 case VerificationNote::Code::MISSING_HASH:
1907 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1908 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1909 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1910 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1911 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1912 case VerificationNote::Code::MISSING_FFOC:
1913 return "There should be a FFOC (first frame of content) marker.";
1914 case VerificationNote::Code::MISSING_LFOC:
1915 return "There should be a LFOC (last frame of content) marker.";
1916 case VerificationNote::Code::INCORRECT_FFOC:
1917 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1918 case VerificationNote::Code::INCORRECT_LFOC:
1919 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1920 case VerificationNote::Code::MISSING_CPL_METADATA:
1921 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1922 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1923 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1924 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1925 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1926 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1927 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1928 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1929 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1930 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1931 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1932 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1933 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1934 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1935 return "Some assets are encrypted but some are not.";
1936 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1937 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1938 case VerificationNote::Code::INVALID_MXF_CODESTREAM:
1939 return String::compose("The MXF codestream for at least one frame is invalid (%1).", note.note().get());
1940 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1941 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1942 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1943 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1944 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1945 return "The JPEG2000 tile size is not the same as the image size.";
1946 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1947 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1948 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1949 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1950 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1951 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1952 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1953 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1954 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1955 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1956 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1957 return "POC marker found outside main header.";
1958 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1959 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1960 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1961 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1962 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1963 return "No TLM marker was found in a JPEG2000 codestream.";
1964 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1965 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1966 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1967 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1968 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1970 vector<string> parts;
1971 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1972 DCP_ASSERT (parts.size() == 2);
1973 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]);
1975 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1976 return "Some aspect of this DCP could not be checked because it is encrypted.";
1977 case VerificationNote::Code::EMPTY_TEXT:
1978 return "There is an empty <Text> node in a subtitle or closed caption.";
1979 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1980 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1981 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1982 return "Some closed captions are not listed in the order of their vertical position.";
1983 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1984 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1985 case VerificationNote::Code::UNEXPECTED_DURATION:
1986 return "There is an <Duration> node inside a <MainMarkers>.";
1987 case VerificationNote::Code::INVALID_CONTENT_KIND:
1988 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
1989 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
1990 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
1991 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
1992 return String::compose("The PKL %1 has more than one asset with the same ID", note.note().get());
1993 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
1994 return String::compose("The ASSETMAP %1 has more than one asset with the same ID", note.note().get());
1995 case VerificationNote::Code::MISSING_SUBTITLE:
1996 return String::compose("The subtitle asset %1 has no subtitles", note.note().get());
1997 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
1998 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2006 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2008 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2013 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2015 if (a.type() != b.type()) {
2016 return a.type() < b.type();
2019 if (a.code() != b.code()) {
2020 return a.code() < b.code();
2023 if (a.note() != b.note()) {
2024 return a.note().get_value_or("") < b.note().get_value_or("");
2027 if (a.file() != b.file()) {
2028 return a.file().get_value_or("") < b.file().get_value_or("");
2031 return a.line().get_value_or(0) < b.line().get_value_or(0);
2036 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2038 s << note_to_string (note);
2040 s << " [" << note.note().get() << "]";
2043 s << " [" << note.file().get() << "]";
2046 s << " [" << note.line().get() << "]";