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", "DCDMSubtitle-2010.xsd");
246 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
247 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
248 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
249 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
252 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
257 auto system_id_str = xml_ch_to_string (system_id);
258 auto p = _xsd_dtd_directory;
259 if (_files.find(system_id_str) == _files.end()) {
262 p /= _files[system_id_str];
264 StringToXMLCh ch (p.string());
265 return new LocalFileInputSource(ch.get());
269 void add (string uri, string file)
274 std::map<string, string> _files;
275 boost::filesystem::path _xsd_dtd_directory;
280 parse (XercesDOMParser& parser, boost::filesystem::path xml)
282 parser.parse(xml.c_str());
287 parse (XercesDOMParser& parser, string xml)
289 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
296 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
299 XMLPlatformUtils::Initialize ();
300 } catch (XMLException& e) {
301 throw MiscError ("Failed to initialise xerces library");
304 DCPErrorHandler error_handler;
306 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
308 XercesDOMParser parser;
309 parser.setValidationScheme(XercesDOMParser::Val_Always);
310 parser.setDoNamespaces(true);
311 parser.setDoSchema(true);
313 vector<string> schema;
314 schema.push_back("xml.xsd");
315 schema.push_back("xmldsig-core-schema.xsd");
316 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
317 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
318 schema.push_back("SMPTE-429-9-2007-AM.xsd");
319 schema.push_back("Main-Stereo-Picture-CPL.xsd");
320 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
321 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
322 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
323 schema.push_back("DCSubtitle.v1.mattsson.xsd");
324 schema.push_back("DCDMSubtitle-2010.xsd");
325 schema.push_back("DCDMSubtitle-2014.xsd");
326 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
327 schema.push_back("SMPTE-429-16.xsd");
328 schema.push_back("Dolby-2012-AD.xsd");
329 schema.push_back("SMPTE-429-10-2008.xsd");
330 schema.push_back("xlink.xsd");
331 schema.push_back("SMPTE-335-2012.xsd");
332 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
333 schema.push_back("isdcf-mca.xsd");
334 schema.push_back("SMPTE-429-12-2008.xsd");
336 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
337 * Schemas that are not mentioned in this list are not read, and the things
338 * they describe are not checked.
341 for (auto i: schema) {
342 locations += String::compose("%1 %1 ", i, i);
345 parser.setExternalSchemaLocation(locations.c_str());
346 parser.setValidationSchemaFullChecking(true);
347 parser.setErrorHandler(&error_handler);
349 LocalFileResolver resolver (xsd_dtd_directory);
350 parser.setEntityResolver(&resolver);
353 parser.resetDocumentPool();
355 } catch (XMLException& e) {
356 throw MiscError(xml_ch_to_string(e.getMessage()));
357 } catch (DOMException& e) {
358 throw MiscError(xml_ch_to_string(e.getMessage()));
360 throw MiscError("Unknown exception from xerces");
364 XMLPlatformUtils::Terminate ();
366 for (auto i: error_handler.errors()) {
368 VerificationNote::Type::ERROR,
369 VerificationNote::Code::INVALID_XML,
371 boost::trim_copy(i.public_id() + " " + i.system_id()),
378 enum class VerifyAssetResult {
385 static VerifyAssetResult
386 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
388 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
390 auto pkls = dcp->pkls();
391 /* We've read this DCP in so it must have at least one PKL */
392 DCP_ASSERT (!pkls.empty());
394 auto asset = reel_file_asset->asset_ref().asset();
396 optional<string> pkl_hash;
398 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
404 DCP_ASSERT (pkl_hash);
406 auto cpl_hash = reel_file_asset->hash();
407 if (cpl_hash && *cpl_hash != *pkl_hash) {
408 return VerifyAssetResult::CPL_PKL_DIFFER;
411 if (actual_hash != *pkl_hash) {
412 return VerifyAssetResult::BAD;
415 return VerifyAssetResult::GOOD;
420 verify_language_tag (string tag, vector<VerificationNote>& notes)
423 LanguageTag test (tag);
424 } catch (LanguageTagError &) {
425 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
431 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
433 int biggest_frame = 0;
434 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
435 auto const duration = asset->intrinsic_duration ();
437 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
438 for (auto i: j2k_notes) {
439 if (find(notes.begin(), notes.end(), i) == notes.end()) {
445 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
446 auto reader = mono_asset->start_read ();
447 for (int64_t i = 0; i < duration; ++i) {
448 auto frame = reader->get_frame (i);
449 biggest_frame = max(biggest_frame, frame->size());
450 if (!mono_asset->encrypted() || mono_asset->key()) {
451 vector<VerificationNote> j2k_notes;
452 verify_j2k (frame, j2k_notes);
453 check_and_add (j2k_notes);
455 progress (float(i) / duration);
457 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
458 auto reader = stereo_asset->start_read ();
459 for (int64_t i = 0; i < duration; ++i) {
460 auto frame = reader->get_frame (i);
461 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
462 if (!stereo_asset->encrypted() || stereo_asset->key()) {
463 vector<VerificationNote> j2k_notes;
464 verify_j2k (frame->left(), j2k_notes);
465 verify_j2k (frame->right(), j2k_notes);
466 check_and_add (j2k_notes);
468 progress (float(i) / duration);
473 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
474 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
475 if (biggest_frame > max_frame) {
477 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
479 } else if (biggest_frame > risky_frame) {
481 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
488 verify_main_picture_asset (
489 shared_ptr<const DCP> dcp,
490 shared_ptr<const ReelPictureAsset> reel_asset,
491 function<void (string, optional<boost::filesystem::path>)> stage,
492 function<void (float)> progress,
493 VerificationOptions options,
494 vector<VerificationNote>& notes
497 auto asset = reel_asset->asset();
498 auto const file = *asset->file();
500 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
501 stage ("Checking picture asset hash", file);
502 auto const r = verify_asset (dcp, reel_asset, progress);
504 case VerifyAssetResult::BAD:
506 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
509 case VerifyAssetResult::CPL_PKL_DIFFER:
511 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
519 stage ("Checking picture frame sizes", asset->file());
520 verify_picture_asset (reel_asset, file, notes, progress);
522 /* Only flat/scope allowed by Bv2.1 */
524 asset->size() != Size(2048, 858) &&
525 asset->size() != Size(1998, 1080) &&
526 asset->size() != Size(4096, 1716) &&
527 asset->size() != Size(3996, 2160)) {
529 VerificationNote::Type::BV21_ERROR,
530 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
531 String::compose("%1x%2", asset->size().width, asset->size().height),
536 /* Only 24, 25, 48fps allowed for 2K */
538 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
539 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
542 VerificationNote::Type::BV21_ERROR,
543 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
544 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
549 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
550 /* Only 24fps allowed for 4K */
551 if (asset->edit_rate() != Fraction(24, 1)) {
553 VerificationNote::Type::BV21_ERROR,
554 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
555 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
560 /* Only 2D allowed for 4K */
561 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
563 VerificationNote::Type::BV21_ERROR,
564 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
565 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
576 verify_main_sound_asset (
577 shared_ptr<const DCP> dcp,
578 shared_ptr<const ReelSoundAsset> reel_asset,
579 function<void (string, optional<boost::filesystem::path>)> stage,
580 function<void (float)> progress,
581 VerificationOptions options,
582 vector<VerificationNote>& notes
585 auto asset = reel_asset->asset();
586 auto const file = *asset->file();
588 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
589 stage("Checking sound asset hash", file);
590 auto const r = verify_asset (dcp, reel_asset, progress);
592 case VerifyAssetResult::BAD:
593 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
595 case VerifyAssetResult::CPL_PKL_DIFFER:
596 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
603 stage ("Checking sound asset metadata", file);
605 if (auto lang = asset->language()) {
606 verify_language_tag (*lang, notes);
608 if (asset->sampling_rate() != 48000) {
609 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
615 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
617 /* XXX: is Language compulsory? */
618 if (reel_asset->language()) {
619 verify_language_tag (*reel_asset->language(), notes);
622 if (!reel_asset->entry_point()) {
623 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
624 } else if (reel_asset->entry_point().get()) {
625 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
631 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
633 /* XXX: is Language compulsory? */
634 if (reel_asset->language()) {
635 verify_language_tag (*reel_asset->language(), notes);
638 if (!reel_asset->entry_point()) {
639 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
640 } else if (reel_asset->entry_point().get()) {
641 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
648 boost::optional<string> subtitle_language;
652 /** Verify stuff that is common to both subtitles and closed captions */
654 verify_smpte_timed_text_asset (
655 shared_ptr<const SMPTESubtitleAsset> asset,
656 optional<int64_t> reel_asset_duration,
657 vector<VerificationNote>& notes
660 if (asset->language()) {
661 verify_language_tag (*asset->language(), notes);
663 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
666 auto const size = boost::filesystem::file_size(asset->file().get());
667 if (size > 115 * 1024 * 1024) {
669 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
673 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
674 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
676 auto fonts = asset->font_data ();
678 for (auto i: fonts) {
679 total_size += i.second.size();
681 if (total_size > 10 * 1024 * 1024) {
682 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
685 if (!asset->start_time()) {
686 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
687 } else if (asset->start_time() != Time()) {
688 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
691 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
694 VerificationNote::Type::BV21_ERROR,
695 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
696 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
703 /** Verify Interop subtitle-only stuff */
705 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
707 if (asset->subtitles().empty()) {
708 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
713 /** Verify SMPTE subtitle-only stuff */
715 verify_smpte_subtitle_asset (
716 shared_ptr<const SMPTESubtitleAsset> asset,
717 vector<VerificationNote>& notes,
721 if (asset->language()) {
722 if (!state.subtitle_language) {
723 state.subtitle_language = *asset->language();
724 } else if (state.subtitle_language != *asset->language()) {
725 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
729 DCP_ASSERT (asset->resource_id());
730 auto xml_id = asset->xml_id();
732 if (asset->resource_id().get() != xml_id) {
733 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
736 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
737 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
740 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
743 if (asset->raw_xml()) {
744 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
745 cxml::Document doc("SubtitleReel");
746 doc.read_string(*asset->raw_xml());
747 auto issue_date = doc.string_child("IssueDate");
748 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
749 if (!std::regex_match(issue_date, reg)) {
750 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
756 /** Verify all subtitle stuff */
758 verify_subtitle_asset (
759 shared_ptr<const SubtitleAsset> asset,
760 optional<int64_t> reel_asset_duration,
761 function<void (string, optional<boost::filesystem::path>)> stage,
762 boost::filesystem::path xsd_dtd_directory,
763 vector<VerificationNote>& notes,
767 stage ("Checking subtitle XML", asset->file());
768 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
769 * gets passed through libdcp which may clean up and therefore hide errors.
771 if (asset->raw_xml()) {
772 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
774 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
777 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
779 verify_interop_subtitle_asset(interop, notes);
782 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
784 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
785 verify_smpte_subtitle_asset (smpte, notes, state);
790 /** Verify all closed caption stuff */
792 verify_closed_caption_asset (
793 shared_ptr<const SubtitleAsset> asset,
794 optional<int64_t> reel_asset_duration,
795 function<void (string, optional<boost::filesystem::path>)> stage,
796 boost::filesystem::path xsd_dtd_directory,
797 vector<VerificationNote>& notes
800 stage ("Checking closed caption XML", asset->file());
801 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
802 * gets passed through libdcp which may clean up and therefore hide errors.
804 auto raw_xml = asset->raw_xml();
806 validate_xml (*raw_xml, xsd_dtd_directory, notes);
807 if (raw_xml->size() > 256 * 1024) {
808 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
811 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
814 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
816 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
821 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
824 verify_text_details (
825 vector<shared_ptr<Reel>> reels,
827 vector<VerificationNote>& notes,
828 std::function<bool (shared_ptr<Reel>)> check,
829 std::function<optional<string> (shared_ptr<Reel>)> xml,
830 std::function<int64_t (shared_ptr<Reel>)> duration
833 /* end of last subtitle (in editable units) */
834 optional<int64_t> last_out;
835 auto too_short = false;
836 auto too_close = false;
837 auto too_early = false;
838 auto reel_overlap = false;
839 auto empty_text = false;
840 /* current reel start time (in editable units) */
841 int64_t reel_offset = 0;
843 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
844 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) {
845 if (node->name() == "Subtitle") {
846 Time in (node->string_attribute("TimeIn"), tcr);
850 Time out (node->string_attribute("TimeOut"), tcr);
854 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
857 auto length = out - in;
858 if (length.as_editable_units_ceil(er) < 15) {
862 /* XXX: this feels dubious - is it really what Bv2.1 means? */
863 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
864 if (distance >= 0 && distance < 2) {
868 last_out = reel_offset + out.as_editable_units_floor(er);
869 } else if (node->name() == "Text") {
870 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
871 if (!node->content().empty()) {
874 for (auto i: node->node_children()) {
875 if (node_has_content(i)) {
881 if (!node_has_content(node)) {
886 for (auto i: node->node_children()) {
887 parse(i, tcr, start_time, er, first_reel);
891 for (auto i = 0U; i < reels.size(); ++i) {
892 if (!check(reels[i])) {
896 auto reel_xml = xml(reels[i]);
898 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
902 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
903 * read in by libdcp's parser.
906 shared_ptr<cxml::Document> doc;
908 optional<Time> start_time;
910 doc = make_shared<cxml::Document>("SubtitleReel");
911 doc->read_string (*reel_xml);
912 tcr = doc->number_child<int>("TimeCodeRate");
913 auto start_time_string = doc->optional_string_child("StartTime");
914 if (start_time_string) {
915 start_time = Time(*start_time_string, tcr);
918 doc = make_shared<cxml::Document>("DCSubtitle");
919 doc->read_string (*reel_xml);
921 parse (doc, tcr, start_time, edit_rate, i == 0);
922 auto end = reel_offset + duration(reels[i]);
923 if (last_out && *last_out > end) {
929 if (last_out && *last_out > reel_offset) {
935 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
941 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
947 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
953 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
959 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
967 verify_closed_caption_details (
968 vector<shared_ptr<Reel>> reels,
969 vector<VerificationNote>& notes
972 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
973 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
974 for (auto i: node->node_children()) {
975 if (i->name() == "Text") {
976 text_or_image.push_back (i);
978 find_text_or_image (i, text_or_image);
983 auto mismatched_valign = false;
984 auto incorrect_order = false;
986 std::function<void (cxml::ConstNodePtr)> parse;
987 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
988 if (node->name() == "Subtitle") {
989 vector<cxml::ConstNodePtr> text_or_image;
990 find_text_or_image (node, text_or_image);
991 optional<string> last_valign;
992 optional<float> last_vpos;
993 for (auto i: text_or_image) {
994 auto valign = i->optional_string_attribute("VAlign");
996 valign = i->optional_string_attribute("Valign").get_value_or("center");
998 auto vpos = i->optional_number_attribute<float>("VPosition");
1000 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1004 if (*last_valign != valign) {
1005 mismatched_valign = true;
1008 last_valign = valign;
1010 if (!mismatched_valign) {
1012 if (*last_valign == "top" || *last_valign == "center") {
1013 if (*vpos < *last_vpos) {
1014 incorrect_order = true;
1017 if (*vpos > *last_vpos) {
1018 incorrect_order = true;
1027 for (auto i: node->node_children()) {
1032 for (auto reel: reels) {
1033 for (auto ccap: reel->closed_captions()) {
1034 auto reel_xml = ccap->asset()->raw_xml();
1036 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1040 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1041 * read in by libdcp's parser.
1044 shared_ptr<cxml::Document> doc;
1046 optional<Time> start_time;
1048 doc = make_shared<cxml::Document>("SubtitleReel");
1049 doc->read_string (*reel_xml);
1051 doc = make_shared<cxml::Document>("DCSubtitle");
1052 doc->read_string (*reel_xml);
1058 if (mismatched_valign) {
1060 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1064 if (incorrect_order) {
1066 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1072 struct LinesCharactersResult
1074 bool warning_length_exceeded = false;
1075 bool error_length_exceeded = false;
1076 bool line_count_exceeded = false;
1082 verify_text_lines_and_characters (
1083 shared_ptr<SubtitleAsset> asset,
1086 LinesCharactersResult* result
1092 Event (Time time_, float position_, int characters_)
1094 , position (position_)
1095 , characters (characters_)
1098 Event (Time time_, shared_ptr<Event> start_)
1104 int position; //< position from 0 at top of screen to 100 at bottom
1106 shared_ptr<Event> start;
1109 vector<shared_ptr<Event>> events;
1111 auto position = [](shared_ptr<const SubtitleString> sub) {
1112 switch (sub->v_align()) {
1114 return lrintf(sub->v_position() * 100);
1115 case VAlign::CENTER:
1116 return lrintf((0.5f + sub->v_position()) * 100);
1117 case VAlign::BOTTOM:
1118 return lrintf((1.0f - sub->v_position()) * 100);
1124 for (auto j: asset->subtitles()) {
1125 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1127 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1128 events.push_back(in);
1129 events.push_back(make_shared<Event>(text->out(), in));
1133 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1134 return a->time < b->time;
1137 map<int, int> current;
1138 for (auto i: events) {
1139 if (current.size() > 3) {
1140 result->line_count_exceeded = true;
1142 for (auto j: current) {
1143 if (j.second > warning_length) {
1144 result->warning_length_exceeded = true;
1146 if (j.second > error_length) {
1147 result->error_length_exceeded = true;
1152 /* end of a subtitle */
1153 DCP_ASSERT (current.find(i->start->position) != current.end());
1154 if (current[i->start->position] == i->start->characters) {
1155 current.erase(i->start->position);
1157 current[i->start->position] -= i->start->characters;
1160 /* start of a subtitle */
1161 if (current.find(i->position) == current.end()) {
1162 current[i->position] = i->characters;
1164 current[i->position] += i->characters;
1173 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1175 if (reels.empty()) {
1179 if (reels[0]->main_subtitle()) {
1180 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1181 [](shared_ptr<Reel> reel) {
1182 return static_cast<bool>(reel->main_subtitle());
1184 [](shared_ptr<Reel> reel) {
1185 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1187 return interop->asset()->raw_xml();
1189 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1191 return smpte->asset()->raw_xml();
1193 [](shared_ptr<Reel> reel) {
1194 return reel->main_subtitle()->actual_duration();
1199 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1200 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1201 [i](shared_ptr<Reel> reel) {
1202 return i < reel->closed_captions().size();
1204 [i](shared_ptr<Reel> reel) {
1205 return reel->closed_captions()[i]->asset()->raw_xml();
1207 [i](shared_ptr<Reel> reel) {
1208 return reel->closed_captions()[i]->actual_duration();
1213 verify_closed_caption_details (reels, notes);
1218 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1220 DCP_ASSERT (cpl->file());
1221 cxml::Document doc ("CompositionPlaylist");
1222 doc.read_file (cpl->file().get());
1224 auto missing = false;
1227 if (auto reel_list = doc.node_child("ReelList")) {
1228 auto reels = reel_list->node_children("Reel");
1229 if (!reels.empty()) {
1230 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1231 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1232 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1234 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1235 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1239 if (auto name = extension->optional_node_child("Name")) {
1240 if (name->content() != "Application") {
1241 malformed = "<Name> should be 'Application'";
1244 if (auto property_list = extension->optional_node_child("PropertyList")) {
1245 if (auto property = property_list->optional_node_child("Property")) {
1246 if (auto name = property->optional_node_child("Name")) {
1247 if (name->content() != "DCP Constraints Profile") {
1248 malformed = "<Name> property should be 'DCP Constraints Profile'";
1251 if (auto value = property->optional_node_child("Value")) {
1252 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1253 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1268 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1269 } else if (!malformed.empty()) {
1270 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1276 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1278 vector<string> encrypted;
1279 for (auto i: dcp->cpls()) {
1280 for (auto j: i->reel_file_assets()) {
1281 if (j->asset_ref().resolved()) {
1282 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1283 if (mxf && mxf->encrypted()) {
1284 encrypted.push_back(j->asset_ref().id());
1290 for (auto i: pkl->assets()) {
1291 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1303 shared_ptr<const DCP> dcp,
1304 shared_ptr<const CPL> cpl,
1305 shared_ptr<const Reel> reel,
1306 optional<dcp::Size> main_picture_active_area,
1307 function<void (string, optional<boost::filesystem::path>)> stage,
1308 boost::filesystem::path xsd_dtd_directory,
1309 function<void (float)> progress,
1310 VerificationOptions options,
1311 vector<VerificationNote>& notes,
1313 bool* have_main_subtitle,
1314 bool* have_no_main_subtitle,
1315 size_t* most_closed_captions,
1316 size_t* fewest_closed_captions,
1317 map<Marker, Time>* markers_seen
1320 for (auto i: reel->assets()) {
1321 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1322 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1324 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1325 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1327 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1328 if (i->encryptable() && !file_asset->hash()) {
1329 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1333 if (dcp->standard() == Standard::SMPTE) {
1334 boost::optional<int64_t> duration;
1335 for (auto i: reel->assets()) {
1337 duration = i->actual_duration();
1338 } else if (*duration != i->actual_duration()) {
1339 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1345 if (reel->main_picture()) {
1346 /* Check reel stuff */
1347 auto const frame_rate = reel->main_picture()->frame_rate();
1348 if (frame_rate.denominator != 1 ||
1349 (frame_rate.numerator != 24 &&
1350 frame_rate.numerator != 25 &&
1351 frame_rate.numerator != 30 &&
1352 frame_rate.numerator != 48 &&
1353 frame_rate.numerator != 50 &&
1354 frame_rate.numerator != 60 &&
1355 frame_rate.numerator != 96)) {
1357 VerificationNote::Type::ERROR,
1358 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1359 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1363 if (reel->main_picture()->asset_ref().resolved()) {
1364 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1365 auto const asset_size = reel->main_picture()->asset()->size();
1366 if (main_picture_active_area) {
1367 if (main_picture_active_area->width > asset_size.width) {
1369 VerificationNote::Type::ERROR,
1370 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1371 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1375 if (main_picture_active_area->height > asset_size.height) {
1377 VerificationNote::Type::ERROR,
1378 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1379 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1387 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1388 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes);
1391 if (reel->main_subtitle()) {
1392 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1393 if (reel->main_subtitle()->asset_ref().resolved()) {
1394 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1396 *have_main_subtitle = true;
1398 *have_no_main_subtitle = true;
1401 for (auto i: reel->closed_captions()) {
1402 verify_closed_caption_reel(i, notes);
1403 if (i->asset_ref().resolved()) {
1404 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1408 if (reel->main_markers()) {
1409 for (auto const& i: reel->main_markers()->get()) {
1410 markers_seen->insert(i);
1412 if (reel->main_markers()->entry_point()) {
1413 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1415 if (reel->main_markers()->duration()) {
1416 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1420 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1421 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1429 shared_ptr<const DCP> dcp,
1430 shared_ptr<const CPL> cpl,
1431 function<void (string, optional<boost::filesystem::path>)> stage,
1432 boost::filesystem::path xsd_dtd_directory,
1433 function<void (float)> progress,
1434 VerificationOptions options,
1435 vector<VerificationNote>& notes,
1439 stage("Checking CPL", cpl->file());
1440 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1442 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1443 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1446 for (auto const& i: cpl->additional_subtitle_languages()) {
1447 verify_language_tag(i, notes);
1450 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1451 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1452 * of the approved ones.
1454 auto all = ContentKind::all();
1455 auto name = cpl->content_kind().name();
1456 transform(name.begin(), name.end(), name.begin(), ::tolower);
1457 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1458 if (iter == all.end()) {
1459 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1463 if (cpl->release_territory()) {
1464 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") {
1465 auto terr = cpl->release_territory().get();
1466 /* Must be a valid region tag, or "001" */
1468 LanguageTag::RegionSubtag test(terr);
1470 if (terr != "001") {
1471 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1477 if (dcp->standard() == Standard::SMPTE) {
1478 if (!cpl->annotation_text()) {
1479 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1480 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1481 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1485 for (auto i: dcp->pkls()) {
1486 /* Check that the CPL's hash corresponds to the PKL */
1487 optional<string> h = i->hash(cpl->id());
1488 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1489 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1492 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1493 optional<string> required_annotation_text;
1494 for (auto j: i->assets()) {
1495 /* See if this is a CPL */
1496 for (auto k: dcp->cpls()) {
1497 if (j->id() == k->id()) {
1498 if (!required_annotation_text) {
1499 /* First CPL we have found; this is the required AnnotationText unless we find another */
1500 required_annotation_text = cpl->content_title_text();
1502 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1503 required_annotation_text = boost::none;
1509 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1510 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1514 /* set to true if any reel has a MainSubtitle */
1515 auto have_main_subtitle = false;
1516 /* set to true if any reel has no MainSubtitle */
1517 auto have_no_main_subtitle = false;
1518 /* fewest number of closed caption assets seen in a reel */
1519 size_t fewest_closed_captions = SIZE_MAX;
1520 /* most number of closed caption assets seen in a reel */
1521 size_t most_closed_captions = 0;
1522 map<Marker, Time> markers_seen;
1524 auto const main_picture_active_area = cpl->main_picture_active_area();
1525 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1527 VerificationNote::Type::ERROR,
1528 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1529 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1533 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1535 VerificationNote::Type::ERROR,
1536 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1537 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1542 for (auto reel: cpl->reels()) {
1543 stage("Checking reel", optional<boost::filesystem::path>());
1548 main_picture_active_area,
1555 &have_main_subtitle,
1556 &have_no_main_subtitle,
1557 &most_closed_captions,
1558 &fewest_closed_captions,
1563 verify_text_details(cpl->reels(), notes);
1565 if (dcp->standard() == Standard::SMPTE) {
1567 if (have_main_subtitle && have_no_main_subtitle) {
1568 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1571 if (fewest_closed_captions != most_closed_captions) {
1572 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1575 if (cpl->content_kind() == ContentKind::FEATURE) {
1576 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1577 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1579 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1580 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1584 auto ffoc = markers_seen.find(Marker::FFOC);
1585 if (ffoc == markers_seen.end()) {
1586 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1587 } else if (ffoc->second.e != 1) {
1588 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1591 auto lfoc = markers_seen.find(Marker::LFOC);
1592 if (lfoc == markers_seen.end()) {
1593 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1595 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1596 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1597 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1601 LinesCharactersResult result;
1602 for (auto reel: cpl->reels()) {
1603 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1604 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1608 if (result.line_count_exceeded) {
1609 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1611 if (result.error_length_exceeded) {
1612 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1613 } else if (result.warning_length_exceeded) {
1614 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1617 result = LinesCharactersResult();
1618 for (auto reel: cpl->reels()) {
1619 for (auto i: reel->closed_captions()) {
1621 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1626 if (result.line_count_exceeded) {
1627 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1629 if (result.error_length_exceeded) {
1630 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1633 if (!cpl->read_composition_metadata()) {
1634 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1635 } else if (!cpl->version_number()) {
1636 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1639 verify_extension_metadata(cpl, notes);
1641 if (cpl->any_encrypted()) {
1642 cxml::Document doc("CompositionPlaylist");
1643 DCP_ASSERT(cpl->file());
1644 doc.read_file(cpl->file().get());
1645 if (!doc.optional_node_child("Signature")) {
1646 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1656 shared_ptr<const DCP> dcp,
1657 shared_ptr<const PKL> pkl,
1658 boost::filesystem::path xsd_dtd_directory,
1659 vector<VerificationNote>& notes
1662 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1664 if (pkl_has_encrypted_assets(dcp, pkl)) {
1665 cxml::Document doc("PackingList");
1666 doc.read_file(pkl->file().get());
1667 if (!doc.optional_node_child("Signature")) {
1668 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1672 set<string> uuid_set;
1673 for (auto asset: pkl->assets()) {
1674 if (!uuid_set.insert(asset->id()).second) {
1675 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1686 shared_ptr<const DCP> dcp,
1687 boost::filesystem::path xsd_dtd_directory,
1688 vector<VerificationNote>& notes
1691 auto asset_map = dcp->asset_map();
1692 DCP_ASSERT(asset_map);
1694 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1696 set<string> uuid_set;
1697 for (auto const& asset: asset_map->assets()) {
1698 if (!uuid_set.insert(asset.id()).second) {
1699 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1706 vector<VerificationNote>
1708 vector<boost::filesystem::path> directories,
1709 function<void (string, optional<boost::filesystem::path>)> stage,
1710 function<void (float)> progress,
1711 VerificationOptions options,
1712 optional<boost::filesystem::path> xsd_dtd_directory
1715 if (!xsd_dtd_directory) {
1716 xsd_dtd_directory = resources_directory() / "xsd";
1718 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1720 vector<VerificationNote> notes;
1723 vector<shared_ptr<DCP>> dcps;
1724 for (auto i: directories) {
1725 dcps.push_back (make_shared<DCP>(i));
1728 for (auto dcp: dcps) {
1729 stage ("Checking DCP", dcp->directory());
1730 bool carry_on = true;
1732 dcp->read (¬es, true);
1733 } catch (MissingAssetmapError& e) {
1734 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1736 } catch (ReadError& e) {
1737 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1738 } catch (XMLError& e) {
1739 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1740 } catch (MXFFileError& e) {
1741 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1742 } catch (cxml::Error& e) {
1743 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1750 if (dcp->standard() != Standard::SMPTE) {
1751 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1754 for (auto cpl: dcp->cpls()) {
1767 for (auto pkl: dcp->pkls()) {
1768 stage("Checking PKL", pkl->file());
1769 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1772 if (dcp->asset_map_file()) {
1773 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1774 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1776 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1785 dcp::note_to_string (VerificationNote note)
1787 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1789 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1790 * not "ClosedCaption assets must have an <EntryPoint> tag."
1792 * It's OK to use XML tag names where they are clear.
1793 * If both ID and filename are available, use only the ID.
1794 * End messages with a full stop.
1795 * Messages should not mention whether or not their errors are a part of Bv2.1.
1797 switch (note.code()) {
1798 case VerificationNote::Code::FAILED_READ:
1799 return *note.note();
1800 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1801 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1802 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1803 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1804 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1805 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1806 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1807 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1808 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1809 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1810 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1811 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1812 case VerificationNote::Code::EMPTY_ASSET_PATH:
1813 return "The asset map contains an empty asset path.";
1814 case VerificationNote::Code::MISSING_ASSET:
1815 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1816 case VerificationNote::Code::MISMATCHED_STANDARD:
1817 return "The DCP contains both SMPTE and Interop parts.";
1818 case VerificationNote::Code::INVALID_XML:
1819 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1820 case VerificationNote::Code::MISSING_ASSETMAP:
1821 return "No ASSETMAP or ASSETMAP.xml was found.";
1822 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1823 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1824 case VerificationNote::Code::INVALID_DURATION:
1825 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1826 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1827 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());
1828 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1829 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());
1830 case VerificationNote::Code::EXTERNAL_ASSET:
1831 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());
1832 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1833 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1834 case VerificationNote::Code::INVALID_STANDARD:
1835 return "This DCP does not use the SMPTE standard.";
1836 case VerificationNote::Code::INVALID_LANGUAGE:
1837 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1838 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1839 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1840 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1841 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1842 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1843 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1844 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1845 return "3D 4K DCPs are not allowed.";
1846 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1847 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1848 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1849 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1850 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1851 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());
1852 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1853 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1854 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1855 return "Some subtitle assets have different <Language> tags than others";
1856 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1857 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1858 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1859 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1860 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1861 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1862 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1863 return "At least one subtitle lasts less than 15 frames.";
1864 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1865 return "At least one pair of subtitles is separated by less than 2 frames.";
1866 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1867 return "At least one subtitle extends outside of its reel.";
1868 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1869 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1870 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1871 return "There are more than 52 characters in at least one subtitle line.";
1872 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1873 return "There are more than 79 characters in at least one subtitle line.";
1874 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1875 return "There are more than 3 closed caption lines in at least one place.";
1876 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1877 return "There are more than 32 characters in at least one closed caption line.";
1878 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1879 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1880 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1881 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1882 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1883 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1884 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1885 return "All assets in a reel do not have the same duration.";
1886 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1887 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1888 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1889 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1890 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1891 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1892 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1893 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1894 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1895 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1896 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1897 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1898 case VerificationNote::Code::MISSING_HASH:
1899 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1900 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1901 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1902 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1903 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1904 case VerificationNote::Code::MISSING_FFOC:
1905 return "There should be a FFOC (first frame of content) marker.";
1906 case VerificationNote::Code::MISSING_LFOC:
1907 return "There should be a LFOC (last frame of content) marker.";
1908 case VerificationNote::Code::INCORRECT_FFOC:
1909 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1910 case VerificationNote::Code::INCORRECT_LFOC:
1911 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1912 case VerificationNote::Code::MISSING_CPL_METADATA:
1913 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1914 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1915 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1916 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1917 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1918 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1919 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1920 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1921 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1922 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1923 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1924 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1925 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1926 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1927 return "Some assets are encrypted but some are not.";
1928 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1929 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1930 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1931 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1932 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1933 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1934 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1935 return "The JPEG2000 tile size is not the same as the image size.";
1936 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1937 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1938 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1939 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1940 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1941 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1942 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1943 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1944 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1945 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1946 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1947 return "POC marker found outside main header.";
1948 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1949 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1950 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1951 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1952 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1953 return "No TLM marker was found in a JPEG2000 codestream.";
1954 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1955 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1956 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1957 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1958 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1960 vector<string> parts;
1961 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1962 DCP_ASSERT (parts.size() == 2);
1963 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]);
1965 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1966 return "Some aspect of this DCP could not be checked because it is encrypted.";
1967 case VerificationNote::Code::EMPTY_TEXT:
1968 return "There is an empty <Text> node in a subtitle or closed caption.";
1969 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1970 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1971 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1972 return "Some closed captions are not listed in the order of their vertical position.";
1973 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1974 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1975 case VerificationNote::Code::UNEXPECTED_DURATION:
1976 return "There is an <Duration> node inside a <MainMarkers>.";
1977 case VerificationNote::Code::INVALID_CONTENT_KIND:
1978 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
1979 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
1980 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
1981 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
1982 return String::compose("The PKL %1 has more than one asset with the same ID", note.note().get());
1983 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
1984 return String::compose("The ASSETMAP %1 has more than one asset with the same ID", note.note().get());
1985 case VerificationNote::Code::MISSING_SUBTITLE:
1986 return String::compose("The subtitle asset %1 has no subtitles", note.note().get());
1987 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
1988 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
1996 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1998 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2003 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2005 if (a.type() != b.type()) {
2006 return a.type() < b.type();
2009 if (a.code() != b.code()) {
2010 return a.code() < b.code();
2013 if (a.note() != b.note()) {
2014 return a.note().get_value_or("") < b.note().get_value_or("");
2017 if (a.file() != b.file()) {
2018 return a.file().get_value_or("") < b.file().get_value_or("");
2021 return a.line().get_value_or(0) < b.line().get_value_or(0);
2026 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2028 s << note_to_string (note);
2030 s << " [" << note.note().get() << "]";
2033 s << " [" << note.file().get() << "]";
2036 s << " [" << note.line().get() << "]";