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 <libxml/parserInternals.h>
62 #include <xercesc/dom/DOMAttr.hpp>
63 #include <xercesc/dom/DOMDocument.hpp>
64 #include <xercesc/dom/DOMError.hpp>
65 #include <xercesc/dom/DOMErrorHandler.hpp>
66 #include <xercesc/dom/DOMException.hpp>
67 #include <xercesc/dom/DOMImplementation.hpp>
68 #include <xercesc/dom/DOMImplementationLS.hpp>
69 #include <xercesc/dom/DOMImplementationRegistry.hpp>
70 #include <xercesc/dom/DOMLSParser.hpp>
71 #include <xercesc/dom/DOMLocator.hpp>
72 #include <xercesc/dom/DOMNamedNodeMap.hpp>
73 #include <xercesc/dom/DOMNodeList.hpp>
74 #include <xercesc/framework/LocalFileInputSource.hpp>
75 #include <xercesc/framework/MemBufInputSource.hpp>
76 #include <xercesc/parsers/AbstractDOMParser.hpp>
77 #include <xercesc/parsers/XercesDOMParser.hpp>
78 #include <xercesc/sax/HandlerBase.hpp>
79 #include <xercesc/util/PlatformUtils.hpp>
80 #include <boost/algorithm/string.hpp>
89 using std::dynamic_pointer_cast;
91 using std::make_shared;
95 using std::shared_ptr;
98 using boost::optional;
99 using boost::function;
103 using namespace xercesc;
108 xml_ch_to_string (XMLCh const * a)
110 char* x = XMLString::transcode(a);
112 XMLString::release(&x);
117 class XMLValidationError
120 XMLValidationError (SAXParseException const & e)
121 : _message (xml_ch_to_string(e.getMessage()))
122 , _line (e.getLineNumber())
123 , _column (e.getColumnNumber())
124 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
125 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
130 string message () const {
134 uint64_t line () const {
138 uint64_t column () const {
142 string public_id () const {
146 string system_id () const {
159 class DCPErrorHandler : public ErrorHandler
162 void warning(const SAXParseException& e) override
164 maybe_add (XMLValidationError(e));
167 void error(const SAXParseException& e) override
169 maybe_add (XMLValidationError(e));
172 void fatalError(const SAXParseException& e) override
174 maybe_add (XMLValidationError(e));
177 void resetErrors() override {
181 list<XMLValidationError> errors () const {
186 void maybe_add (XMLValidationError e)
188 /* XXX: nasty hack */
190 e.message().find("schema document") != string::npos &&
191 e.message().find("has different target namespace from the one specified in instance document") != string::npos
196 _errors.push_back (e);
199 list<XMLValidationError> _errors;
206 StringToXMLCh (string a)
208 _buffer = XMLString::transcode(a.c_str());
211 StringToXMLCh (StringToXMLCh const&) = delete;
212 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
216 XMLString::release (&_buffer);
219 XMLCh const * get () const {
228 class LocalFileResolver : public EntityResolver
231 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
232 : _xsd_dtd_directory (xsd_dtd_directory)
234 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
235 * found without being here.
237 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
238 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
239 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
240 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
244 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
245 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
246 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
247 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
248 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
249 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
250 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
253 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
258 auto system_id_str = xml_ch_to_string (system_id);
259 auto p = _xsd_dtd_directory;
260 if (_files.find(system_id_str) == _files.end()) {
263 p /= _files[system_id_str];
265 StringToXMLCh ch (p.string());
266 return new LocalFileInputSource(ch.get());
270 void add (string uri, string file)
275 std::map<string, string> _files;
276 boost::filesystem::path _xsd_dtd_directory;
281 parse (XercesDOMParser& parser, boost::filesystem::path xml)
283 parser.parse(xml.c_str());
288 parse (XercesDOMParser& parser, string xml)
290 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
297 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
300 XMLPlatformUtils::Initialize ();
301 } catch (XMLException& e) {
302 throw MiscError ("Failed to initialise xerces library");
305 DCPErrorHandler error_handler;
307 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
309 XercesDOMParser parser;
310 parser.setValidationScheme(XercesDOMParser::Val_Always);
311 parser.setDoNamespaces(true);
312 parser.setDoSchema(true);
314 vector<string> schema;
315 schema.push_back("xml.xsd");
316 schema.push_back("xmldsig-core-schema.xsd");
317 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
318 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
319 schema.push_back("SMPTE-429-9-2007-AM.xsd");
320 schema.push_back("Main-Stereo-Picture-CPL.xsd");
321 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
322 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
323 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
324 schema.push_back("DCSubtitle.v1.mattsson.xsd");
325 schema.push_back("DCDMSubtitle-2010.xsd");
326 schema.push_back("DCDMSubtitle-2014.xsd");
327 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
328 schema.push_back("SMPTE-429-16.xsd");
329 schema.push_back("Dolby-2012-AD.xsd");
330 schema.push_back("SMPTE-429-10-2008.xsd");
331 schema.push_back("xlink.xsd");
332 schema.push_back("SMPTE-335-2012.xsd");
333 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
334 schema.push_back("isdcf-mca.xsd");
335 schema.push_back("SMPTE-429-12-2008.xsd");
337 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
338 * Schemas that are not mentioned in this list are not read, and the things
339 * they describe are not checked.
342 for (auto i: schema) {
343 locations += String::compose("%1 %1 ", i, i);
346 parser.setExternalSchemaLocation(locations.c_str());
347 parser.setValidationSchemaFullChecking(true);
348 parser.setErrorHandler(&error_handler);
350 LocalFileResolver resolver (xsd_dtd_directory);
351 parser.setEntityResolver(&resolver);
354 parser.resetDocumentPool();
356 } catch (XMLException& e) {
357 throw MiscError(xml_ch_to_string(e.getMessage()));
358 } catch (DOMException& e) {
359 throw MiscError(xml_ch_to_string(e.getMessage()));
361 throw MiscError("Unknown exception from xerces");
365 XMLPlatformUtils::Terminate ();
367 for (auto i: error_handler.errors()) {
369 VerificationNote::Type::ERROR,
370 VerificationNote::Code::INVALID_XML,
372 boost::trim_copy(i.public_id() + " " + i.system_id()),
379 enum class VerifyAssetResult {
386 static VerifyAssetResult
387 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
389 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
391 auto pkls = dcp->pkls();
392 /* We've read this DCP in so it must have at least one PKL */
393 DCP_ASSERT (!pkls.empty());
395 auto asset = reel_file_asset->asset_ref().asset();
397 optional<string> pkl_hash;
399 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
405 DCP_ASSERT (pkl_hash);
407 auto cpl_hash = reel_file_asset->hash();
408 if (cpl_hash && *cpl_hash != *pkl_hash) {
409 return VerifyAssetResult::CPL_PKL_DIFFER;
412 if (actual_hash != *pkl_hash) {
413 return VerifyAssetResult::BAD;
416 return VerifyAssetResult::GOOD;
421 verify_language_tag (string tag, vector<VerificationNote>& notes)
424 LanguageTag test (tag);
425 } catch (LanguageTagError &) {
426 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
432 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
434 int biggest_frame = 0;
435 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
436 auto const duration = asset->intrinsic_duration ();
438 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
439 for (auto i: j2k_notes) {
440 if (find(notes.begin(), notes.end(), i) == notes.end()) {
446 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
447 auto reader = mono_asset->start_read ();
448 for (int64_t i = 0; i < duration; ++i) {
449 auto frame = reader->get_frame (i);
450 biggest_frame = max(biggest_frame, frame->size());
451 if (!mono_asset->encrypted() || mono_asset->key()) {
452 vector<VerificationNote> j2k_notes;
453 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
454 check_and_add (j2k_notes);
456 progress (float(i) / duration);
458 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
459 auto reader = stereo_asset->start_read ();
460 for (int64_t i = 0; i < duration; ++i) {
461 auto frame = reader->get_frame (i);
462 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
463 if (!stereo_asset->encrypted() || stereo_asset->key()) {
464 vector<VerificationNote> j2k_notes;
465 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
466 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
467 check_and_add (j2k_notes);
469 progress (float(i) / duration);
474 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
475 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
476 if (biggest_frame > max_frame) {
478 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
480 } else if (biggest_frame > risky_frame) {
482 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
489 verify_main_picture_asset (
490 shared_ptr<const DCP> dcp,
491 shared_ptr<const ReelPictureAsset> reel_asset,
492 function<void (string, optional<boost::filesystem::path>)> stage,
493 function<void (float)> progress,
494 VerificationOptions options,
495 vector<VerificationNote>& notes
498 auto asset = reel_asset->asset();
499 auto const file = *asset->file();
501 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
502 stage ("Checking picture asset hash", file);
503 auto const r = verify_asset (dcp, reel_asset, progress);
505 case VerifyAssetResult::BAD:
507 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
510 case VerifyAssetResult::CPL_PKL_DIFFER:
512 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
520 stage ("Checking picture frame sizes", asset->file());
521 verify_picture_asset (reel_asset, file, notes, progress);
523 /* Only flat/scope allowed by Bv2.1 */
525 asset->size() != Size(2048, 858) &&
526 asset->size() != Size(1998, 1080) &&
527 asset->size() != Size(4096, 1716) &&
528 asset->size() != Size(3996, 2160)) {
530 VerificationNote::Type::BV21_ERROR,
531 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
532 String::compose("%1x%2", asset->size().width, asset->size().height),
537 /* Only 24, 25, 48fps allowed for 2K */
539 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
540 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
543 VerificationNote::Type::BV21_ERROR,
544 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
545 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
550 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
551 /* Only 24fps allowed for 4K */
552 if (asset->edit_rate() != Fraction(24, 1)) {
554 VerificationNote::Type::BV21_ERROR,
555 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
556 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
561 /* Only 2D allowed for 4K */
562 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
564 VerificationNote::Type::BV21_ERROR,
565 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
566 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
578 boost::optional<string> subtitle_language;
579 boost::optional<int> audio_channels;
584 verify_main_sound_asset (
585 shared_ptr<const DCP> dcp,
586 shared_ptr<const ReelSoundAsset> reel_asset,
587 function<void (string, optional<boost::filesystem::path>)> stage,
588 function<void (float)> progress,
589 VerificationOptions options,
590 vector<VerificationNote>& notes,
594 auto asset = reel_asset->asset();
595 auto const file = *asset->file();
597 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
598 stage("Checking sound asset hash", file);
599 auto const r = verify_asset (dcp, reel_asset, progress);
601 case VerifyAssetResult::BAD:
602 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
604 case VerifyAssetResult::CPL_PKL_DIFFER:
605 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
612 if (!state.audio_channels) {
613 state.audio_channels = asset->channels();
614 } else if (*state.audio_channels != asset->channels()) {
615 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
618 stage ("Checking sound asset metadata", file);
620 if (auto lang = asset->language()) {
621 verify_language_tag (*lang, notes);
623 if (asset->sampling_rate() != 48000) {
624 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
630 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
632 /* XXX: is Language compulsory? */
633 if (reel_asset->language()) {
634 verify_language_tag (*reel_asset->language(), notes);
637 if (!reel_asset->entry_point()) {
638 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
639 } else if (reel_asset->entry_point().get()) {
640 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
646 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
648 /* XXX: is Language compulsory? */
649 if (reel_asset->language()) {
650 verify_language_tag (*reel_asset->language(), notes);
653 if (!reel_asset->entry_point()) {
654 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
655 } else if (reel_asset->entry_point().get()) {
656 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
661 /** Verify stuff that is common to both subtitles and closed captions */
663 verify_smpte_timed_text_asset (
664 shared_ptr<const SMPTESubtitleAsset> asset,
665 optional<int64_t> reel_asset_duration,
666 vector<VerificationNote>& notes
669 if (asset->language()) {
670 verify_language_tag (*asset->language(), notes);
672 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
675 auto const size = boost::filesystem::file_size(asset->file().get());
676 if (size > 115 * 1024 * 1024) {
678 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
682 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
683 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
685 auto fonts = asset->font_data ();
687 for (auto i: fonts) {
688 total_size += i.second.size();
690 if (total_size > 10 * 1024 * 1024) {
691 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
694 if (!asset->start_time()) {
695 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
696 } else if (asset->start_time() != Time()) {
697 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
700 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
703 VerificationNote::Type::BV21_ERROR,
704 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
705 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
712 /** Verify Interop subtitle-only stuff */
714 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
716 if (asset->subtitles().empty()) {
717 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
719 auto const unresolved = asset->unresolved_fonts();
720 if (!unresolved.empty()) {
721 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
726 /** Verify SMPTE subtitle-only stuff */
728 verify_smpte_subtitle_asset (
729 shared_ptr<const SMPTESubtitleAsset> asset,
730 vector<VerificationNote>& notes,
734 if (asset->language()) {
735 if (!state.subtitle_language) {
736 state.subtitle_language = *asset->language();
737 } else if (state.subtitle_language != *asset->language()) {
738 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
742 DCP_ASSERT (asset->resource_id());
743 auto xml_id = asset->xml_id();
745 if (asset->resource_id().get() != xml_id) {
746 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
749 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
750 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
753 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
756 if (asset->raw_xml()) {
757 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
758 cxml::Document doc("SubtitleReel");
759 doc.read_string(*asset->raw_xml());
760 auto issue_date = doc.string_child("IssueDate");
761 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
762 if (!std::regex_match(issue_date, reg)) {
763 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
769 /** Verify all subtitle stuff */
771 verify_subtitle_asset (
772 shared_ptr<const SubtitleAsset> asset,
773 optional<int64_t> reel_asset_duration,
774 function<void (string, optional<boost::filesystem::path>)> stage,
775 boost::filesystem::path xsd_dtd_directory,
776 vector<VerificationNote>& notes,
780 stage ("Checking subtitle XML", asset->file());
781 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
782 * gets passed through libdcp which may clean up and therefore hide errors.
784 if (asset->raw_xml()) {
785 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
787 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
790 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
791 cxml::Document doc(root_node);
792 doc.read_string(asset->raw_xml().get());
793 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
795 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
801 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
803 verify_interop_subtitle_asset(interop, notes);
804 if (namespace_count(asset, "DCSubtitle") > 1) {
805 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
809 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
811 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
812 verify_smpte_subtitle_asset (smpte, notes, state);
813 /* This asset may be encrypted and in that case we'll have no raw_xml() */
814 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
815 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
821 /** Verify all closed caption stuff */
823 verify_closed_caption_asset (
824 shared_ptr<const SubtitleAsset> asset,
825 optional<int64_t> reel_asset_duration,
826 function<void (string, optional<boost::filesystem::path>)> stage,
827 boost::filesystem::path xsd_dtd_directory,
828 vector<VerificationNote>& notes
831 stage ("Checking closed caption XML", asset->file());
832 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
833 * gets passed through libdcp which may clean up and therefore hide errors.
835 auto raw_xml = asset->raw_xml();
837 validate_xml (*raw_xml, xsd_dtd_directory, notes);
838 if (raw_xml->size() > 256 * 1024) {
839 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
842 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
845 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
847 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
852 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
855 verify_text_details (
856 vector<shared_ptr<Reel>> reels,
858 vector<VerificationNote>& notes,
859 std::function<bool (shared_ptr<Reel>)> check,
860 std::function<optional<string> (shared_ptr<Reel>)> xml,
861 std::function<int64_t (shared_ptr<Reel>)> duration
864 /* end of last subtitle (in editable units) */
865 optional<int64_t> last_out;
866 auto too_short = false;
867 auto too_close = false;
868 auto too_early = false;
869 auto reel_overlap = false;
870 auto empty_text = false;
871 /* current reel start time (in editable units) */
872 int64_t reel_offset = 0;
873 vector<string> font_ids;
874 optional<string> missing_load_font_id;
876 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
877 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &font_ids, &missing_load_font_id](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
878 if (node->name() == "Subtitle") {
879 Time in (node->string_attribute("TimeIn"), tcr);
883 Time out (node->string_attribute("TimeOut"), tcr);
887 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
890 auto length = out - in;
891 if (length.as_editable_units_ceil(er) < 15) {
895 /* XXX: this feels dubious - is it really what Bv2.1 means? */
896 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
897 if (distance >= 0 && distance < 2) {
901 last_out = reel_offset + out.as_editable_units_floor(er);
902 } else if (node->name() == "Text") {
903 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
904 if (!node->content().empty()) {
907 for (auto i: node->node_children()) {
908 if (node_has_content(i)) {
914 if (!node_has_content(node)) {
917 } else if (node->name() == "LoadFont") {
918 if (auto const id = node->optional_string_attribute("Id")) {
919 font_ids.push_back(*id);
921 } else if (node->name() == "Font") {
922 if (auto const font_id = node->optional_string_attribute("Id")) {
923 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
924 missing_load_font_id = font_id;
928 for (auto i: node->node_children()) {
929 parse(i, tcr, start_time, er, first_reel);
933 for (auto i = 0U; i < reels.size(); ++i) {
934 if (!check(reels[i])) {
938 auto reel_xml = xml(reels[i]);
940 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
944 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
945 * read in by libdcp's parser.
948 shared_ptr<cxml::Document> doc;
950 optional<Time> start_time;
952 doc = make_shared<cxml::Document>("SubtitleReel");
953 doc->read_string (*reel_xml);
954 tcr = doc->number_child<int>("TimeCodeRate");
955 auto start_time_string = doc->optional_string_child("StartTime");
956 if (start_time_string) {
957 start_time = Time(*start_time_string, tcr);
960 doc = make_shared<cxml::Document>("DCSubtitle");
961 doc->read_string (*reel_xml);
963 parse (doc, tcr, start_time, edit_rate, i == 0);
964 auto end = reel_offset + duration(reels[i]);
965 if (last_out && *last_out > end) {
971 if (last_out && *last_out > reel_offset) {
977 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
983 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
989 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
995 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1001 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1005 if (missing_load_font_id) {
1006 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1013 verify_closed_caption_details (
1014 vector<shared_ptr<Reel>> reels,
1015 vector<VerificationNote>& notes
1018 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1019 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1020 for (auto i: node->node_children()) {
1021 if (i->name() == "Text") {
1022 text_or_image.push_back (i);
1024 find_text_or_image (i, text_or_image);
1029 auto mismatched_valign = false;
1030 auto incorrect_order = false;
1032 std::function<void (cxml::ConstNodePtr)> parse;
1033 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1034 if (node->name() == "Subtitle") {
1035 vector<cxml::ConstNodePtr> text_or_image;
1036 find_text_or_image (node, text_or_image);
1037 optional<string> last_valign;
1038 optional<float> last_vpos;
1039 for (auto i: text_or_image) {
1040 auto valign = i->optional_string_attribute("VAlign");
1042 valign = i->optional_string_attribute("Valign").get_value_or("center");
1044 auto vpos = i->optional_number_attribute<float>("VPosition");
1046 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1050 if (*last_valign != valign) {
1051 mismatched_valign = true;
1054 last_valign = valign;
1056 if (!mismatched_valign) {
1058 if (*last_valign == "top" || *last_valign == "center") {
1059 if (*vpos < *last_vpos) {
1060 incorrect_order = true;
1063 if (*vpos > *last_vpos) {
1064 incorrect_order = true;
1073 for (auto i: node->node_children()) {
1078 for (auto reel: reels) {
1079 for (auto ccap: reel->closed_captions()) {
1080 auto reel_xml = ccap->asset()->raw_xml();
1082 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1086 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1087 * read in by libdcp's parser.
1090 shared_ptr<cxml::Document> doc;
1092 optional<Time> start_time;
1094 doc = make_shared<cxml::Document>("SubtitleReel");
1095 doc->read_string (*reel_xml);
1097 doc = make_shared<cxml::Document>("DCSubtitle");
1098 doc->read_string (*reel_xml);
1104 if (mismatched_valign) {
1106 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1110 if (incorrect_order) {
1112 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1118 struct LinesCharactersResult
1120 bool warning_length_exceeded = false;
1121 bool error_length_exceeded = false;
1122 bool line_count_exceeded = false;
1128 verify_text_lines_and_characters (
1129 shared_ptr<SubtitleAsset> asset,
1132 LinesCharactersResult* result
1138 Event (Time time_, float position_, int characters_)
1140 , position (position_)
1141 , characters (characters_)
1144 Event (Time time_, shared_ptr<Event> start_)
1150 int position; //< position from 0 at top of screen to 100 at bottom
1152 shared_ptr<Event> start;
1155 vector<shared_ptr<Event>> events;
1157 auto position = [](shared_ptr<const SubtitleString> sub) {
1158 switch (sub->v_align()) {
1160 return lrintf(sub->v_position() * 100);
1161 case VAlign::CENTER:
1162 return lrintf((0.5f + sub->v_position()) * 100);
1163 case VAlign::BOTTOM:
1164 return lrintf((1.0f - sub->v_position()) * 100);
1170 for (auto j: asset->subtitles()) {
1171 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1173 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1174 events.push_back(in);
1175 events.push_back(make_shared<Event>(text->out(), in));
1179 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1180 return a->time < b->time;
1183 map<int, int> current;
1184 for (auto i: events) {
1185 if (current.size() > 3) {
1186 result->line_count_exceeded = true;
1188 for (auto j: current) {
1189 if (j.second > warning_length) {
1190 result->warning_length_exceeded = true;
1192 if (j.second > error_length) {
1193 result->error_length_exceeded = true;
1198 /* end of a subtitle */
1199 DCP_ASSERT (current.find(i->start->position) != current.end());
1200 if (current[i->start->position] == i->start->characters) {
1201 current.erase(i->start->position);
1203 current[i->start->position] -= i->start->characters;
1206 /* start of a subtitle */
1207 if (current.find(i->position) == current.end()) {
1208 current[i->position] = i->characters;
1210 current[i->position] += i->characters;
1219 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1221 if (reels.empty()) {
1225 if (reels[0]->main_subtitle()) {
1226 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1227 [](shared_ptr<Reel> reel) {
1228 return static_cast<bool>(reel->main_subtitle());
1230 [](shared_ptr<Reel> reel) {
1231 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1233 return interop->asset()->raw_xml();
1235 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1237 return smpte->asset()->raw_xml();
1239 [](shared_ptr<Reel> reel) {
1240 return reel->main_subtitle()->actual_duration();
1245 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1246 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1247 [i](shared_ptr<Reel> reel) {
1248 return i < reel->closed_captions().size();
1250 [i](shared_ptr<Reel> reel) {
1251 return reel->closed_captions()[i]->asset()->raw_xml();
1253 [i](shared_ptr<Reel> reel) {
1254 return reel->closed_captions()[i]->actual_duration();
1259 verify_closed_caption_details (reels, notes);
1264 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1266 DCP_ASSERT (cpl->file());
1267 cxml::Document doc ("CompositionPlaylist");
1268 doc.read_file (cpl->file().get());
1270 auto missing = false;
1273 if (auto reel_list = doc.node_child("ReelList")) {
1274 auto reels = reel_list->node_children("Reel");
1275 if (!reels.empty()) {
1276 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1277 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1278 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1280 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1281 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1285 if (auto name = extension->optional_node_child("Name")) {
1286 if (name->content() != "Application") {
1287 malformed = "<Name> should be 'Application'";
1290 if (auto property_list = extension->optional_node_child("PropertyList")) {
1291 if (auto property = property_list->optional_node_child("Property")) {
1292 if (auto name = property->optional_node_child("Name")) {
1293 if (name->content() != "DCP Constraints Profile") {
1294 malformed = "<Name> property should be 'DCP Constraints Profile'";
1297 if (auto value = property->optional_node_child("Value")) {
1298 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1299 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1314 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1315 } else if (!malformed.empty()) {
1316 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1322 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1324 vector<string> encrypted;
1325 for (auto i: dcp->cpls()) {
1326 for (auto j: i->reel_file_assets()) {
1327 if (j->asset_ref().resolved()) {
1328 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1329 if (mxf && mxf->encrypted()) {
1330 encrypted.push_back(j->asset_ref().id());
1336 for (auto i: pkl->assets()) {
1337 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1349 shared_ptr<const DCP> dcp,
1350 shared_ptr<const CPL> cpl,
1351 shared_ptr<const Reel> reel,
1352 optional<dcp::Size> main_picture_active_area,
1353 function<void (string, optional<boost::filesystem::path>)> stage,
1354 boost::filesystem::path xsd_dtd_directory,
1355 function<void (float)> progress,
1356 VerificationOptions options,
1357 vector<VerificationNote>& notes,
1359 bool* have_main_subtitle,
1360 bool* have_no_main_subtitle,
1361 size_t* most_closed_captions,
1362 size_t* fewest_closed_captions,
1363 map<Marker, Time>* markers_seen
1366 for (auto i: reel->assets()) {
1367 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1368 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1370 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1371 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1373 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1374 if (i->encryptable() && !file_asset->hash()) {
1375 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1379 if (dcp->standard() == Standard::SMPTE) {
1380 boost::optional<int64_t> duration;
1381 for (auto i: reel->assets()) {
1383 duration = i->actual_duration();
1384 } else if (*duration != i->actual_duration()) {
1385 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1391 if (reel->main_picture()) {
1392 /* Check reel stuff */
1393 auto const frame_rate = reel->main_picture()->frame_rate();
1394 if (frame_rate.denominator != 1 ||
1395 (frame_rate.numerator != 24 &&
1396 frame_rate.numerator != 25 &&
1397 frame_rate.numerator != 30 &&
1398 frame_rate.numerator != 48 &&
1399 frame_rate.numerator != 50 &&
1400 frame_rate.numerator != 60 &&
1401 frame_rate.numerator != 96)) {
1403 VerificationNote::Type::ERROR,
1404 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1405 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1409 if (reel->main_picture()->asset_ref().resolved()) {
1410 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1411 auto const asset_size = reel->main_picture()->asset()->size();
1412 if (main_picture_active_area) {
1413 if (main_picture_active_area->width > asset_size.width) {
1415 VerificationNote::Type::ERROR,
1416 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1417 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1421 if (main_picture_active_area->height > asset_size.height) {
1423 VerificationNote::Type::ERROR,
1424 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1425 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1433 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1434 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1437 if (reel->main_subtitle()) {
1438 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1439 if (reel->main_subtitle()->asset_ref().resolved()) {
1440 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1442 *have_main_subtitle = true;
1444 *have_no_main_subtitle = true;
1447 for (auto i: reel->closed_captions()) {
1448 verify_closed_caption_reel(i, notes);
1449 if (i->asset_ref().resolved()) {
1450 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1454 if (reel->main_markers()) {
1455 for (auto const& i: reel->main_markers()->get()) {
1456 markers_seen->insert(i);
1458 if (reel->main_markers()->entry_point()) {
1459 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1461 if (reel->main_markers()->duration()) {
1462 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1466 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1467 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1475 shared_ptr<const DCP> dcp,
1476 shared_ptr<const CPL> cpl,
1477 function<void (string, optional<boost::filesystem::path>)> stage,
1478 boost::filesystem::path xsd_dtd_directory,
1479 function<void (float)> progress,
1480 VerificationOptions options,
1481 vector<VerificationNote>& notes,
1485 stage("Checking CPL", cpl->file());
1486 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1488 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1489 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1492 for (auto const& i: cpl->additional_subtitle_languages()) {
1493 verify_language_tag(i, notes);
1496 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1497 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1498 * of the approved ones.
1500 auto all = ContentKind::all();
1501 auto name = cpl->content_kind().name();
1502 transform(name.begin(), name.end(), name.begin(), ::tolower);
1503 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1504 if (iter == all.end()) {
1505 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1509 if (cpl->release_territory()) {
1510 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") {
1511 auto terr = cpl->release_territory().get();
1512 /* Must be a valid region tag, or "001" */
1514 LanguageTag::RegionSubtag test(terr);
1516 if (terr != "001") {
1517 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1523 if (dcp->standard() == Standard::SMPTE) {
1524 if (!cpl->annotation_text()) {
1525 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1526 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1527 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1531 for (auto i: dcp->pkls()) {
1532 /* Check that the CPL's hash corresponds to the PKL */
1533 optional<string> h = i->hash(cpl->id());
1534 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1535 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1538 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1539 optional<string> required_annotation_text;
1540 for (auto j: i->assets()) {
1541 /* See if this is a CPL */
1542 for (auto k: dcp->cpls()) {
1543 if (j->id() == k->id()) {
1544 if (!required_annotation_text) {
1545 /* First CPL we have found; this is the required AnnotationText unless we find another */
1546 required_annotation_text = cpl->content_title_text();
1548 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1549 required_annotation_text = boost::none;
1555 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1556 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1560 /* set to true if any reel has a MainSubtitle */
1561 auto have_main_subtitle = false;
1562 /* set to true if any reel has no MainSubtitle */
1563 auto have_no_main_subtitle = false;
1564 /* fewest number of closed caption assets seen in a reel */
1565 size_t fewest_closed_captions = SIZE_MAX;
1566 /* most number of closed caption assets seen in a reel */
1567 size_t most_closed_captions = 0;
1568 map<Marker, Time> markers_seen;
1570 auto const main_picture_active_area = cpl->main_picture_active_area();
1571 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1573 VerificationNote::Type::ERROR,
1574 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1575 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1579 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1581 VerificationNote::Type::ERROR,
1582 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1583 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1588 for (auto reel: cpl->reels()) {
1589 stage("Checking reel", optional<boost::filesystem::path>());
1594 main_picture_active_area,
1601 &have_main_subtitle,
1602 &have_no_main_subtitle,
1603 &most_closed_captions,
1604 &fewest_closed_captions,
1609 verify_text_details(cpl->reels(), notes);
1611 if (dcp->standard() == Standard::SMPTE) {
1612 if (auto msc = cpl->main_sound_configuration()) {
1613 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1615 VerificationNote::Type::ERROR,
1616 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1617 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1623 if (have_main_subtitle && have_no_main_subtitle) {
1624 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1627 if (fewest_closed_captions != most_closed_captions) {
1628 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1631 if (cpl->content_kind() == ContentKind::FEATURE) {
1632 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1633 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1635 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1636 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1640 auto ffoc = markers_seen.find(Marker::FFOC);
1641 if (ffoc == markers_seen.end()) {
1642 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1643 } else if (ffoc->second.e != 1) {
1644 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1647 auto lfoc = markers_seen.find(Marker::LFOC);
1648 if (lfoc == markers_seen.end()) {
1649 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1651 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1652 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1653 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1657 LinesCharactersResult result;
1658 for (auto reel: cpl->reels()) {
1659 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1660 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1664 if (result.line_count_exceeded) {
1665 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1667 if (result.error_length_exceeded) {
1668 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1669 } else if (result.warning_length_exceeded) {
1670 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1673 result = LinesCharactersResult();
1674 for (auto reel: cpl->reels()) {
1675 for (auto i: reel->closed_captions()) {
1677 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1682 if (result.line_count_exceeded) {
1683 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1685 if (result.error_length_exceeded) {
1686 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1689 if (!cpl->read_composition_metadata()) {
1690 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1691 } else if (!cpl->version_number()) {
1692 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1695 verify_extension_metadata(cpl, notes);
1697 if (cpl->any_encrypted()) {
1698 cxml::Document doc("CompositionPlaylist");
1699 DCP_ASSERT(cpl->file());
1700 doc.read_file(cpl->file().get());
1701 if (!doc.optional_node_child("Signature")) {
1702 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1712 shared_ptr<const DCP> dcp,
1713 shared_ptr<const PKL> pkl,
1714 boost::filesystem::path xsd_dtd_directory,
1715 vector<VerificationNote>& notes
1718 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1720 if (pkl_has_encrypted_assets(dcp, pkl)) {
1721 cxml::Document doc("PackingList");
1722 doc.read_file(pkl->file().get());
1723 if (!doc.optional_node_child("Signature")) {
1724 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1728 set<string> uuid_set;
1729 for (auto asset: pkl->assets()) {
1730 if (!uuid_set.insert(asset->id()).second) {
1731 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1742 shared_ptr<const DCP> dcp,
1743 boost::filesystem::path xsd_dtd_directory,
1744 vector<VerificationNote>& notes
1747 auto asset_map = dcp->asset_map();
1748 DCP_ASSERT(asset_map);
1750 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1752 set<string> uuid_set;
1753 for (auto const& asset: asset_map->assets()) {
1754 if (!uuid_set.insert(asset.id()).second) {
1755 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1762 vector<VerificationNote>
1764 vector<boost::filesystem::path> directories,
1765 function<void (string, optional<boost::filesystem::path>)> stage,
1766 function<void (float)> progress,
1767 VerificationOptions options,
1768 optional<boost::filesystem::path> xsd_dtd_directory
1771 if (!xsd_dtd_directory) {
1772 xsd_dtd_directory = resources_directory() / "xsd";
1774 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1776 vector<VerificationNote> notes;
1779 vector<shared_ptr<DCP>> dcps;
1780 for (auto i: directories) {
1781 dcps.push_back (make_shared<DCP>(i));
1784 for (auto dcp: dcps) {
1785 stage ("Checking DCP", dcp->directory());
1786 bool carry_on = true;
1788 dcp->read (¬es, true);
1789 } catch (MissingAssetmapError& e) {
1790 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1792 } catch (ReadError& e) {
1793 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1794 } catch (XMLError& e) {
1795 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1796 } catch (MXFFileError& e) {
1797 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1798 } catch (BadURNUUIDError& e) {
1799 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1800 } catch (cxml::Error& e) {
1801 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1808 if (dcp->standard() != Standard::SMPTE) {
1809 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1812 for (auto cpl: dcp->cpls()) {
1825 for (auto pkl: dcp->pkls()) {
1826 stage("Checking PKL", pkl->file());
1827 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1830 if (dcp->asset_map_file()) {
1831 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1832 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1834 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1843 dcp::note_to_string (VerificationNote note)
1845 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1847 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1848 * not "ClosedCaption assets must have an <EntryPoint> tag."
1850 * It's OK to use XML tag names where they are clear.
1851 * If both ID and filename are available, use only the ID.
1852 * End messages with a full stop.
1853 * Messages should not mention whether or not their errors are a part of Bv2.1.
1855 switch (note.code()) {
1856 case VerificationNote::Code::FAILED_READ:
1857 return *note.note();
1858 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1859 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1860 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1861 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1862 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1863 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1864 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1865 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1866 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1867 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1868 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1869 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1870 case VerificationNote::Code::EMPTY_ASSET_PATH:
1871 return "The asset map contains an empty asset path.";
1872 case VerificationNote::Code::MISSING_ASSET:
1873 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1874 case VerificationNote::Code::MISMATCHED_STANDARD:
1875 return "The DCP contains both SMPTE and Interop parts.";
1876 case VerificationNote::Code::INVALID_XML:
1877 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1878 case VerificationNote::Code::MISSING_ASSETMAP:
1879 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1880 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1881 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1882 case VerificationNote::Code::INVALID_DURATION:
1883 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1884 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1885 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());
1886 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1887 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());
1888 case VerificationNote::Code::EXTERNAL_ASSET:
1889 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());
1890 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1891 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1892 case VerificationNote::Code::INVALID_STANDARD:
1893 return "This DCP does not use the SMPTE standard.";
1894 case VerificationNote::Code::INVALID_LANGUAGE:
1895 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1896 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1897 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1898 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1899 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1900 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1901 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1902 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1903 return "3D 4K DCPs are not allowed.";
1904 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1905 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1906 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1907 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1908 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1909 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());
1910 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1911 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1912 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1913 return "Some subtitle assets have different <Language> tags than others";
1914 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1915 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1916 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1917 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1918 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1919 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1920 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1921 return "At least one subtitle lasts less than 15 frames.";
1922 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1923 return "At least one pair of subtitles is separated by less than 2 frames.";
1924 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1925 return "At least one subtitle extends outside of its reel.";
1926 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1927 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1928 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1929 return "There are more than 52 characters in at least one subtitle line.";
1930 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1931 return "There are more than 79 characters in at least one subtitle line.";
1932 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1933 return "There are more than 3 closed caption lines in at least one place.";
1934 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1935 return "There are more than 32 characters in at least one closed caption line.";
1936 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1937 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1938 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1939 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1940 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1941 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1942 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1943 return "All assets in a reel do not have the same duration.";
1944 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1945 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1946 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1947 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1948 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1949 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1950 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1951 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1952 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1953 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1954 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1955 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1956 case VerificationNote::Code::MISSING_HASH:
1957 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1958 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1959 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1960 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1961 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1962 case VerificationNote::Code::MISSING_FFOC:
1963 return "There should be a FFOC (first frame of content) marker.";
1964 case VerificationNote::Code::MISSING_LFOC:
1965 return "There should be a LFOC (last frame of content) marker.";
1966 case VerificationNote::Code::INCORRECT_FFOC:
1967 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1968 case VerificationNote::Code::INCORRECT_LFOC:
1969 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1970 case VerificationNote::Code::MISSING_CPL_METADATA:
1971 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1972 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1973 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1974 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1975 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1976 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1977 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1978 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1979 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1980 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1981 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1982 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1983 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1984 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1985 return "Some assets are encrypted but some are not.";
1986 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1987 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1988 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1989 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1990 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1991 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1992 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1993 return "The JPEG2000 tile size is not the same as the image size.";
1994 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1995 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1996 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1997 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1998 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1999 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2000 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2001 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2002 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2003 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2004 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2005 return "POC marker found outside main header.";
2006 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2007 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2008 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2009 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2010 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2011 return "No TLM marker was found in a JPEG2000 codestream.";
2012 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2013 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2014 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2015 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2016 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2018 vector<string> parts;
2019 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2020 DCP_ASSERT (parts.size() == 2);
2021 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]);
2023 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2024 return "Some aspect of this DCP could not be checked because it is encrypted.";
2025 case VerificationNote::Code::EMPTY_TEXT:
2026 return "There is an empty <Text> node in a subtitle or closed caption.";
2027 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2028 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2029 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2030 return "Some closed captions are not listed in the order of their vertical position.";
2031 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2032 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2033 case VerificationNote::Code::UNEXPECTED_DURATION:
2034 return "There is an <Duration> node inside a <MainMarkers>.";
2035 case VerificationNote::Code::INVALID_CONTENT_KIND:
2036 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2037 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2038 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2039 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2040 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2041 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2042 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2043 case VerificationNote::Code::MISSING_SUBTITLE:
2044 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2045 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2046 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2047 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2048 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2049 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2050 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2051 case VerificationNote::Code::MISSING_FONT:
2052 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2053 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2054 return String::compose(
2055 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2056 note.frame().get(), note.component().get(), note.size().get()
2058 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2059 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2060 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2061 return String::compose("A subtitle or closed caption refers to a font with ID %1 that does not have a corresponding <LoadFont> node", note.id().get());
2069 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2071 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2076 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2078 if (a.type() != b.type()) {
2079 return a.type() < b.type();
2082 if (a.code() != b.code()) {
2083 return a.code() < b.code();
2086 if (a.note() != b.note()) {
2087 return a.note().get_value_or("") < b.note().get_value_or("");
2090 if (a.file() != b.file()) {
2091 return a.file().get_value_or("") < b.file().get_value_or("");
2094 return a.line().get_value_or(0) < b.line().get_value_or(0);
2099 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2101 s << note_to_string (note);
2103 s << " [" << note.note().get() << "]";
2106 s << " [" << note.file().get() << "]";
2109 s << " [" << note.line().get() << "]";