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),
577 boost::optional<string> subtitle_language;
578 boost::optional<int> audio_channels;
583 verify_main_sound_asset (
584 shared_ptr<const DCP> dcp,
585 shared_ptr<const ReelSoundAsset> reel_asset,
586 function<void (string, optional<boost::filesystem::path>)> stage,
587 function<void (float)> progress,
588 VerificationOptions options,
589 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 if (!state.audio_channels) {
612 state.audio_channels = asset->channels();
613 } else if (*state.audio_channels != asset->channels()) {
614 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
617 stage ("Checking sound asset metadata", file);
619 if (auto lang = asset->language()) {
620 verify_language_tag (*lang, notes);
622 if (asset->sampling_rate() != 48000) {
623 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
629 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
631 /* XXX: is Language compulsory? */
632 if (reel_asset->language()) {
633 verify_language_tag (*reel_asset->language(), notes);
636 if (!reel_asset->entry_point()) {
637 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
638 } else if (reel_asset->entry_point().get()) {
639 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
645 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
647 /* XXX: is Language compulsory? */
648 if (reel_asset->language()) {
649 verify_language_tag (*reel_asset->language(), notes);
652 if (!reel_asset->entry_point()) {
653 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
654 } else if (reel_asset->entry_point().get()) {
655 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
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() });
718 auto const unresolved = asset->unresolved_fonts();
719 if (!unresolved.empty()) {
720 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
725 /** Verify SMPTE subtitle-only stuff */
727 verify_smpte_subtitle_asset (
728 shared_ptr<const SMPTESubtitleAsset> asset,
729 vector<VerificationNote>& notes,
733 if (asset->language()) {
734 if (!state.subtitle_language) {
735 state.subtitle_language = *asset->language();
736 } else if (state.subtitle_language != *asset->language()) {
737 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
741 DCP_ASSERT (asset->resource_id());
742 auto xml_id = asset->xml_id();
744 if (asset->resource_id().get() != xml_id) {
745 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
748 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
749 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
752 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
755 if (asset->raw_xml()) {
756 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
757 cxml::Document doc("SubtitleReel");
758 doc.read_string(*asset->raw_xml());
759 auto issue_date = doc.string_child("IssueDate");
760 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
761 if (!std::regex_match(issue_date, reg)) {
762 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
768 /** Verify all subtitle stuff */
770 verify_subtitle_asset (
771 shared_ptr<const SubtitleAsset> asset,
772 optional<int64_t> reel_asset_duration,
773 function<void (string, optional<boost::filesystem::path>)> stage,
774 boost::filesystem::path xsd_dtd_directory,
775 vector<VerificationNote>& notes,
779 stage ("Checking subtitle XML", asset->file());
780 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
781 * gets passed through libdcp which may clean up and therefore hide errors.
783 if (asset->raw_xml()) {
784 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
786 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
789 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
791 verify_interop_subtitle_asset(interop, notes);
794 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
796 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
797 verify_smpte_subtitle_asset (smpte, notes, state);
802 /** Verify all closed caption stuff */
804 verify_closed_caption_asset (
805 shared_ptr<const SubtitleAsset> asset,
806 optional<int64_t> reel_asset_duration,
807 function<void (string, optional<boost::filesystem::path>)> stage,
808 boost::filesystem::path xsd_dtd_directory,
809 vector<VerificationNote>& notes
812 stage ("Checking closed caption XML", asset->file());
813 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
814 * gets passed through libdcp which may clean up and therefore hide errors.
816 auto raw_xml = asset->raw_xml();
818 validate_xml (*raw_xml, xsd_dtd_directory, notes);
819 if (raw_xml->size() > 256 * 1024) {
820 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
823 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
826 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
828 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
833 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
836 verify_text_details (
837 vector<shared_ptr<Reel>> reels,
839 vector<VerificationNote>& notes,
840 std::function<bool (shared_ptr<Reel>)> check,
841 std::function<optional<string> (shared_ptr<Reel>)> xml,
842 std::function<int64_t (shared_ptr<Reel>)> duration
845 /* end of last subtitle (in editable units) */
846 optional<int64_t> last_out;
847 auto too_short = false;
848 auto too_close = false;
849 auto too_early = false;
850 auto reel_overlap = false;
851 auto empty_text = false;
852 /* current reel start time (in editable units) */
853 int64_t reel_offset = 0;
855 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
856 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) {
857 if (node->name() == "Subtitle") {
858 Time in (node->string_attribute("TimeIn"), tcr);
862 Time out (node->string_attribute("TimeOut"), tcr);
866 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
869 auto length = out - in;
870 if (length.as_editable_units_ceil(er) < 15) {
874 /* XXX: this feels dubious - is it really what Bv2.1 means? */
875 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
876 if (distance >= 0 && distance < 2) {
880 last_out = reel_offset + out.as_editable_units_floor(er);
881 } else if (node->name() == "Text") {
882 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
883 if (!node->content().empty()) {
886 for (auto i: node->node_children()) {
887 if (node_has_content(i)) {
893 if (!node_has_content(node)) {
898 for (auto i: node->node_children()) {
899 parse(i, tcr, start_time, er, first_reel);
903 for (auto i = 0U; i < reels.size(); ++i) {
904 if (!check(reels[i])) {
908 auto reel_xml = xml(reels[i]);
910 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
914 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
915 * read in by libdcp's parser.
918 shared_ptr<cxml::Document> doc;
920 optional<Time> start_time;
922 doc = make_shared<cxml::Document>("SubtitleReel");
923 doc->read_string (*reel_xml);
924 tcr = doc->number_child<int>("TimeCodeRate");
925 auto start_time_string = doc->optional_string_child("StartTime");
926 if (start_time_string) {
927 start_time = Time(*start_time_string, tcr);
930 doc = make_shared<cxml::Document>("DCSubtitle");
931 doc->read_string (*reel_xml);
933 parse (doc, tcr, start_time, edit_rate, i == 0);
934 auto end = reel_offset + duration(reels[i]);
935 if (last_out && *last_out > end) {
941 if (last_out && *last_out > reel_offset) {
947 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
953 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
959 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
965 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
971 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
979 verify_closed_caption_details (
980 vector<shared_ptr<Reel>> reels,
981 vector<VerificationNote>& notes
984 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
985 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
986 for (auto i: node->node_children()) {
987 if (i->name() == "Text") {
988 text_or_image.push_back (i);
990 find_text_or_image (i, text_or_image);
995 auto mismatched_valign = false;
996 auto incorrect_order = false;
998 std::function<void (cxml::ConstNodePtr)> parse;
999 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1000 if (node->name() == "Subtitle") {
1001 vector<cxml::ConstNodePtr> text_or_image;
1002 find_text_or_image (node, text_or_image);
1003 optional<string> last_valign;
1004 optional<float> last_vpos;
1005 for (auto i: text_or_image) {
1006 auto valign = i->optional_string_attribute("VAlign");
1008 valign = i->optional_string_attribute("Valign").get_value_or("center");
1010 auto vpos = i->optional_number_attribute<float>("VPosition");
1012 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1016 if (*last_valign != valign) {
1017 mismatched_valign = true;
1020 last_valign = valign;
1022 if (!mismatched_valign) {
1024 if (*last_valign == "top" || *last_valign == "center") {
1025 if (*vpos < *last_vpos) {
1026 incorrect_order = true;
1029 if (*vpos > *last_vpos) {
1030 incorrect_order = true;
1039 for (auto i: node->node_children()) {
1044 for (auto reel: reels) {
1045 for (auto ccap: reel->closed_captions()) {
1046 auto reel_xml = ccap->asset()->raw_xml();
1048 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1052 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1053 * read in by libdcp's parser.
1056 shared_ptr<cxml::Document> doc;
1058 optional<Time> start_time;
1060 doc = make_shared<cxml::Document>("SubtitleReel");
1061 doc->read_string (*reel_xml);
1063 doc = make_shared<cxml::Document>("DCSubtitle");
1064 doc->read_string (*reel_xml);
1070 if (mismatched_valign) {
1072 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1076 if (incorrect_order) {
1078 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1084 struct LinesCharactersResult
1086 bool warning_length_exceeded = false;
1087 bool error_length_exceeded = false;
1088 bool line_count_exceeded = false;
1094 verify_text_lines_and_characters (
1095 shared_ptr<SubtitleAsset> asset,
1098 LinesCharactersResult* result
1104 Event (Time time_, float position_, int characters_)
1106 , position (position_)
1107 , characters (characters_)
1110 Event (Time time_, shared_ptr<Event> start_)
1116 int position; //< position from 0 at top of screen to 100 at bottom
1118 shared_ptr<Event> start;
1121 vector<shared_ptr<Event>> events;
1123 auto position = [](shared_ptr<const SubtitleString> sub) {
1124 switch (sub->v_align()) {
1126 return lrintf(sub->v_position() * 100);
1127 case VAlign::CENTER:
1128 return lrintf((0.5f + sub->v_position()) * 100);
1129 case VAlign::BOTTOM:
1130 return lrintf((1.0f - sub->v_position()) * 100);
1136 for (auto j: asset->subtitles()) {
1137 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1139 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1140 events.push_back(in);
1141 events.push_back(make_shared<Event>(text->out(), in));
1145 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1146 return a->time < b->time;
1149 map<int, int> current;
1150 for (auto i: events) {
1151 if (current.size() > 3) {
1152 result->line_count_exceeded = true;
1154 for (auto j: current) {
1155 if (j.second > warning_length) {
1156 result->warning_length_exceeded = true;
1158 if (j.second > error_length) {
1159 result->error_length_exceeded = true;
1164 /* end of a subtitle */
1165 DCP_ASSERT (current.find(i->start->position) != current.end());
1166 if (current[i->start->position] == i->start->characters) {
1167 current.erase(i->start->position);
1169 current[i->start->position] -= i->start->characters;
1172 /* start of a subtitle */
1173 if (current.find(i->position) == current.end()) {
1174 current[i->position] = i->characters;
1176 current[i->position] += i->characters;
1185 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1187 if (reels.empty()) {
1191 if (reels[0]->main_subtitle()) {
1192 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1193 [](shared_ptr<Reel> reel) {
1194 return static_cast<bool>(reel->main_subtitle());
1196 [](shared_ptr<Reel> reel) {
1197 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1199 return interop->asset()->raw_xml();
1201 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1203 return smpte->asset()->raw_xml();
1205 [](shared_ptr<Reel> reel) {
1206 return reel->main_subtitle()->actual_duration();
1211 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1212 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1213 [i](shared_ptr<Reel> reel) {
1214 return i < reel->closed_captions().size();
1216 [i](shared_ptr<Reel> reel) {
1217 return reel->closed_captions()[i]->asset()->raw_xml();
1219 [i](shared_ptr<Reel> reel) {
1220 return reel->closed_captions()[i]->actual_duration();
1225 verify_closed_caption_details (reels, notes);
1230 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1232 DCP_ASSERT (cpl->file());
1233 cxml::Document doc ("CompositionPlaylist");
1234 doc.read_file (cpl->file().get());
1236 auto missing = false;
1239 if (auto reel_list = doc.node_child("ReelList")) {
1240 auto reels = reel_list->node_children("Reel");
1241 if (!reels.empty()) {
1242 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1243 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1244 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1246 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1247 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1251 if (auto name = extension->optional_node_child("Name")) {
1252 if (name->content() != "Application") {
1253 malformed = "<Name> should be 'Application'";
1256 if (auto property_list = extension->optional_node_child("PropertyList")) {
1257 if (auto property = property_list->optional_node_child("Property")) {
1258 if (auto name = property->optional_node_child("Name")) {
1259 if (name->content() != "DCP Constraints Profile") {
1260 malformed = "<Name> property should be 'DCP Constraints Profile'";
1263 if (auto value = property->optional_node_child("Value")) {
1264 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1265 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1280 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1281 } else if (!malformed.empty()) {
1282 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1288 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1290 vector<string> encrypted;
1291 for (auto i: dcp->cpls()) {
1292 for (auto j: i->reel_file_assets()) {
1293 if (j->asset_ref().resolved()) {
1294 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1295 if (mxf && mxf->encrypted()) {
1296 encrypted.push_back(j->asset_ref().id());
1302 for (auto i: pkl->assets()) {
1303 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1315 shared_ptr<const DCP> dcp,
1316 shared_ptr<const CPL> cpl,
1317 shared_ptr<const Reel> reel,
1318 optional<dcp::Size> main_picture_active_area,
1319 function<void (string, optional<boost::filesystem::path>)> stage,
1320 boost::filesystem::path xsd_dtd_directory,
1321 function<void (float)> progress,
1322 VerificationOptions options,
1323 vector<VerificationNote>& notes,
1325 bool* have_main_subtitle,
1326 bool* have_no_main_subtitle,
1327 size_t* most_closed_captions,
1328 size_t* fewest_closed_captions,
1329 map<Marker, Time>* markers_seen
1332 for (auto i: reel->assets()) {
1333 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1334 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1336 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1337 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1339 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1340 if (i->encryptable() && !file_asset->hash()) {
1341 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1345 if (dcp->standard() == Standard::SMPTE) {
1346 boost::optional<int64_t> duration;
1347 for (auto i: reel->assets()) {
1349 duration = i->actual_duration();
1350 } else if (*duration != i->actual_duration()) {
1351 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1357 if (reel->main_picture()) {
1358 /* Check reel stuff */
1359 auto const frame_rate = reel->main_picture()->frame_rate();
1360 if (frame_rate.denominator != 1 ||
1361 (frame_rate.numerator != 24 &&
1362 frame_rate.numerator != 25 &&
1363 frame_rate.numerator != 30 &&
1364 frame_rate.numerator != 48 &&
1365 frame_rate.numerator != 50 &&
1366 frame_rate.numerator != 60 &&
1367 frame_rate.numerator != 96)) {
1369 VerificationNote::Type::ERROR,
1370 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1371 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1375 if (reel->main_picture()->asset_ref().resolved()) {
1376 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1377 auto const asset_size = reel->main_picture()->asset()->size();
1378 if (main_picture_active_area) {
1379 if (main_picture_active_area->width > asset_size.width) {
1381 VerificationNote::Type::ERROR,
1382 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1383 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1387 if (main_picture_active_area->height > asset_size.height) {
1389 VerificationNote::Type::ERROR,
1390 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1391 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1399 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1400 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1403 if (reel->main_subtitle()) {
1404 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1405 if (reel->main_subtitle()->asset_ref().resolved()) {
1406 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1408 *have_main_subtitle = true;
1410 *have_no_main_subtitle = true;
1413 for (auto i: reel->closed_captions()) {
1414 verify_closed_caption_reel(i, notes);
1415 if (i->asset_ref().resolved()) {
1416 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1420 if (reel->main_markers()) {
1421 for (auto const& i: reel->main_markers()->get()) {
1422 markers_seen->insert(i);
1424 if (reel->main_markers()->entry_point()) {
1425 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1427 if (reel->main_markers()->duration()) {
1428 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1432 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1433 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1441 shared_ptr<const DCP> dcp,
1442 shared_ptr<const CPL> cpl,
1443 function<void (string, optional<boost::filesystem::path>)> stage,
1444 boost::filesystem::path xsd_dtd_directory,
1445 function<void (float)> progress,
1446 VerificationOptions options,
1447 vector<VerificationNote>& notes,
1451 stage("Checking CPL", cpl->file());
1452 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1454 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1455 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1458 for (auto const& i: cpl->additional_subtitle_languages()) {
1459 verify_language_tag(i, notes);
1462 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1463 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1464 * of the approved ones.
1466 auto all = ContentKind::all();
1467 auto name = cpl->content_kind().name();
1468 transform(name.begin(), name.end(), name.begin(), ::tolower);
1469 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1470 if (iter == all.end()) {
1471 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1475 if (cpl->release_territory()) {
1476 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") {
1477 auto terr = cpl->release_territory().get();
1478 /* Must be a valid region tag, or "001" */
1480 LanguageTag::RegionSubtag test(terr);
1482 if (terr != "001") {
1483 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1489 if (dcp->standard() == Standard::SMPTE) {
1490 if (!cpl->annotation_text()) {
1491 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1492 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1493 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1497 for (auto i: dcp->pkls()) {
1498 /* Check that the CPL's hash corresponds to the PKL */
1499 optional<string> h = i->hash(cpl->id());
1500 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1501 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1504 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1505 optional<string> required_annotation_text;
1506 for (auto j: i->assets()) {
1507 /* See if this is a CPL */
1508 for (auto k: dcp->cpls()) {
1509 if (j->id() == k->id()) {
1510 if (!required_annotation_text) {
1511 /* First CPL we have found; this is the required AnnotationText unless we find another */
1512 required_annotation_text = cpl->content_title_text();
1514 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1515 required_annotation_text = boost::none;
1521 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1522 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1526 /* set to true if any reel has a MainSubtitle */
1527 auto have_main_subtitle = false;
1528 /* set to true if any reel has no MainSubtitle */
1529 auto have_no_main_subtitle = false;
1530 /* fewest number of closed caption assets seen in a reel */
1531 size_t fewest_closed_captions = SIZE_MAX;
1532 /* most number of closed caption assets seen in a reel */
1533 size_t most_closed_captions = 0;
1534 map<Marker, Time> markers_seen;
1536 auto const main_picture_active_area = cpl->main_picture_active_area();
1537 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1539 VerificationNote::Type::ERROR,
1540 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1541 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1545 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1547 VerificationNote::Type::ERROR,
1548 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1549 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1554 for (auto reel: cpl->reels()) {
1555 stage("Checking reel", optional<boost::filesystem::path>());
1560 main_picture_active_area,
1567 &have_main_subtitle,
1568 &have_no_main_subtitle,
1569 &most_closed_captions,
1570 &fewest_closed_captions,
1575 verify_text_details(cpl->reels(), notes);
1577 if (dcp->standard() == Standard::SMPTE) {
1578 if (auto msc = cpl->main_sound_configuration()) {
1579 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1581 VerificationNote::Type::ERROR,
1582 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1583 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1589 if (have_main_subtitle && have_no_main_subtitle) {
1590 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1593 if (fewest_closed_captions != most_closed_captions) {
1594 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1597 if (cpl->content_kind() == ContentKind::FEATURE) {
1598 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1599 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1601 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1602 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1606 auto ffoc = markers_seen.find(Marker::FFOC);
1607 if (ffoc == markers_seen.end()) {
1608 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1609 } else if (ffoc->second.e != 1) {
1610 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1613 auto lfoc = markers_seen.find(Marker::LFOC);
1614 if (lfoc == markers_seen.end()) {
1615 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1617 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1618 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1619 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1623 LinesCharactersResult result;
1624 for (auto reel: cpl->reels()) {
1625 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1626 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1630 if (result.line_count_exceeded) {
1631 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1633 if (result.error_length_exceeded) {
1634 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1635 } else if (result.warning_length_exceeded) {
1636 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1639 result = LinesCharactersResult();
1640 for (auto reel: cpl->reels()) {
1641 for (auto i: reel->closed_captions()) {
1643 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1648 if (result.line_count_exceeded) {
1649 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1651 if (result.error_length_exceeded) {
1652 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1655 if (!cpl->read_composition_metadata()) {
1656 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1657 } else if (!cpl->version_number()) {
1658 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1661 verify_extension_metadata(cpl, notes);
1663 if (cpl->any_encrypted()) {
1664 cxml::Document doc("CompositionPlaylist");
1665 DCP_ASSERT(cpl->file());
1666 doc.read_file(cpl->file().get());
1667 if (!doc.optional_node_child("Signature")) {
1668 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1678 shared_ptr<const DCP> dcp,
1679 shared_ptr<const PKL> pkl,
1680 boost::filesystem::path xsd_dtd_directory,
1681 vector<VerificationNote>& notes
1684 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1686 if (pkl_has_encrypted_assets(dcp, pkl)) {
1687 cxml::Document doc("PackingList");
1688 doc.read_file(pkl->file().get());
1689 if (!doc.optional_node_child("Signature")) {
1690 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1694 set<string> uuid_set;
1695 for (auto asset: pkl->assets()) {
1696 if (!uuid_set.insert(asset->id()).second) {
1697 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1708 shared_ptr<const DCP> dcp,
1709 boost::filesystem::path xsd_dtd_directory,
1710 vector<VerificationNote>& notes
1713 auto asset_map = dcp->asset_map();
1714 DCP_ASSERT(asset_map);
1716 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1718 set<string> uuid_set;
1719 for (auto const& asset: asset_map->assets()) {
1720 if (!uuid_set.insert(asset.id()).second) {
1721 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1728 vector<VerificationNote>
1730 vector<boost::filesystem::path> directories,
1731 function<void (string, optional<boost::filesystem::path>)> stage,
1732 function<void (float)> progress,
1733 VerificationOptions options,
1734 optional<boost::filesystem::path> xsd_dtd_directory
1737 if (!xsd_dtd_directory) {
1738 xsd_dtd_directory = resources_directory() / "xsd";
1740 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1742 vector<VerificationNote> notes;
1745 vector<shared_ptr<DCP>> dcps;
1746 for (auto i: directories) {
1747 dcps.push_back (make_shared<DCP>(i));
1750 for (auto dcp: dcps) {
1751 stage ("Checking DCP", dcp->directory());
1752 bool carry_on = true;
1754 dcp->read (¬es, true);
1755 } catch (MissingAssetmapError& e) {
1756 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1758 } catch (ReadError& e) {
1759 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1760 } catch (XMLError& e) {
1761 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1762 } catch (MXFFileError& e) {
1763 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1764 } catch (cxml::Error& e) {
1765 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1772 if (dcp->standard() != Standard::SMPTE) {
1773 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1776 for (auto cpl: dcp->cpls()) {
1789 for (auto pkl: dcp->pkls()) {
1790 stage("Checking PKL", pkl->file());
1791 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1794 if (dcp->asset_map_file()) {
1795 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1796 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1798 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1807 dcp::note_to_string (VerificationNote note)
1809 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1811 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1812 * not "ClosedCaption assets must have an <EntryPoint> tag."
1814 * It's OK to use XML tag names where they are clear.
1815 * If both ID and filename are available, use only the ID.
1816 * End messages with a full stop.
1817 * Messages should not mention whether or not their errors are a part of Bv2.1.
1819 switch (note.code()) {
1820 case VerificationNote::Code::FAILED_READ:
1821 return *note.note();
1822 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1823 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1824 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1825 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1826 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1827 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1828 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1829 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1830 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1831 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1832 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1833 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1834 case VerificationNote::Code::EMPTY_ASSET_PATH:
1835 return "The asset map contains an empty asset path.";
1836 case VerificationNote::Code::MISSING_ASSET:
1837 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1838 case VerificationNote::Code::MISMATCHED_STANDARD:
1839 return "The DCP contains both SMPTE and Interop parts.";
1840 case VerificationNote::Code::INVALID_XML:
1841 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1842 case VerificationNote::Code::MISSING_ASSETMAP:
1843 return "No ASSETMAP or ASSETMAP.xml was found.";
1844 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1845 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1846 case VerificationNote::Code::INVALID_DURATION:
1847 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1848 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1849 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());
1850 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1851 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());
1852 case VerificationNote::Code::EXTERNAL_ASSET:
1853 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());
1854 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1855 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1856 case VerificationNote::Code::INVALID_STANDARD:
1857 return "This DCP does not use the SMPTE standard.";
1858 case VerificationNote::Code::INVALID_LANGUAGE:
1859 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1860 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1861 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1862 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1863 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1864 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1865 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1866 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1867 return "3D 4K DCPs are not allowed.";
1868 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1869 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1870 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1871 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1872 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1873 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());
1874 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1875 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1876 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1877 return "Some subtitle assets have different <Language> tags than others";
1878 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1879 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1880 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1881 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1882 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1883 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1884 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1885 return "At least one subtitle lasts less than 15 frames.";
1886 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1887 return "At least one pair of subtitles is separated by less than 2 frames.";
1888 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1889 return "At least one subtitle extends outside of its reel.";
1890 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1891 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1892 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1893 return "There are more than 52 characters in at least one subtitle line.";
1894 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1895 return "There are more than 79 characters in at least one subtitle line.";
1896 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1897 return "There are more than 3 closed caption lines in at least one place.";
1898 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1899 return "There are more than 32 characters in at least one closed caption line.";
1900 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1901 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1902 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1903 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1904 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1905 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1906 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1907 return "All assets in a reel do not have the same duration.";
1908 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1909 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1910 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1911 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1912 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1913 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1914 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1915 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1916 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1917 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1918 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1919 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1920 case VerificationNote::Code::MISSING_HASH:
1921 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1922 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1923 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1924 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1925 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1926 case VerificationNote::Code::MISSING_FFOC:
1927 return "There should be a FFOC (first frame of content) marker.";
1928 case VerificationNote::Code::MISSING_LFOC:
1929 return "There should be a LFOC (last frame of content) marker.";
1930 case VerificationNote::Code::INCORRECT_FFOC:
1931 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1932 case VerificationNote::Code::INCORRECT_LFOC:
1933 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1934 case VerificationNote::Code::MISSING_CPL_METADATA:
1935 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1936 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1937 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1938 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1939 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1940 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1941 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1942 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1943 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1944 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1945 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1946 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1947 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1948 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1949 return "Some assets are encrypted but some are not.";
1950 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1951 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1952 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1953 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1954 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1955 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1956 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1957 return "The JPEG2000 tile size is not the same as the image size.";
1958 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1959 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1960 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1961 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1962 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1963 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1964 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1965 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1966 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1967 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1968 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1969 return "POC marker found outside main header.";
1970 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1971 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1972 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1973 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1974 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1975 return "No TLM marker was found in a JPEG2000 codestream.";
1976 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1977 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1978 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1979 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1980 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
1982 vector<string> parts;
1983 boost::split (parts, note.note().get(), boost::is_any_of(" "));
1984 DCP_ASSERT (parts.size() == 2);
1985 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]);
1987 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
1988 return "Some aspect of this DCP could not be checked because it is encrypted.";
1989 case VerificationNote::Code::EMPTY_TEXT:
1990 return "There is an empty <Text> node in a subtitle or closed caption.";
1991 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
1992 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
1993 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
1994 return "Some closed captions are not listed in the order of their vertical position.";
1995 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
1996 return "There is an <EntryPoint> node inside a <MainMarkers>.";
1997 case VerificationNote::Code::UNEXPECTED_DURATION:
1998 return "There is an <Duration> node inside a <MainMarkers>.";
1999 case VerificationNote::Code::INVALID_CONTENT_KIND:
2000 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2001 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2002 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2003 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2004 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2005 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2006 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2007 case VerificationNote::Code::MISSING_SUBTITLE:
2008 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2009 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2010 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2011 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2012 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2013 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2014 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2015 case VerificationNote::Code::MISSING_FONT:
2016 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2024 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2026 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2031 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2033 if (a.type() != b.type()) {
2034 return a.type() < b.type();
2037 if (a.code() != b.code()) {
2038 return a.code() < b.code();
2041 if (a.note() != b.note()) {
2042 return a.note().get_value_or("") < b.note().get_value_or("");
2045 if (a.file() != b.file()) {
2046 return a.file().get_value_or("") < b.file().get_value_or("");
2049 return a.line().get_value_or(0) < b.line().get_value_or(0);
2054 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2056 s << note_to_string (note);
2058 s << " [" << note.note().get() << "]";
2061 s << " [" << note.file().get() << "]";
2064 s << " [" << note.line().get() << "]";