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 /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
390 * We want to calculate the hash of the actual file contents here, so that we
391 * can check it. unset_hash() means that this calculation will happen on the
394 reel_file_asset->asset_ref()->unset_hash();
395 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
397 auto pkls = dcp->pkls();
398 /* We've read this DCP in so it must have at least one PKL */
399 DCP_ASSERT (!pkls.empty());
401 auto asset = reel_file_asset->asset_ref().asset();
403 optional<string> pkl_hash;
405 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
411 DCP_ASSERT (pkl_hash);
413 auto cpl_hash = reel_file_asset->hash();
414 if (cpl_hash && *cpl_hash != *pkl_hash) {
415 return VerifyAssetResult::CPL_PKL_DIFFER;
418 if (actual_hash != *pkl_hash) {
419 return VerifyAssetResult::BAD;
422 return VerifyAssetResult::GOOD;
427 verify_language_tag (string tag, vector<VerificationNote>& notes)
430 LanguageTag test (tag);
431 } catch (LanguageTagError &) {
432 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
438 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
440 int biggest_frame = 0;
441 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
442 auto const duration = asset->intrinsic_duration ();
444 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
445 for (auto i: j2k_notes) {
446 if (find(notes.begin(), notes.end(), i) == notes.end()) {
452 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
453 auto reader = mono_asset->start_read ();
454 for (int64_t i = 0; i < duration; ++i) {
455 auto frame = reader->get_frame (i);
456 biggest_frame = max(biggest_frame, frame->size());
457 if (!mono_asset->encrypted() || mono_asset->key()) {
458 vector<VerificationNote> j2k_notes;
459 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
460 check_and_add (j2k_notes);
462 progress (float(i) / duration);
464 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
465 auto reader = stereo_asset->start_read ();
466 for (int64_t i = 0; i < duration; ++i) {
467 auto frame = reader->get_frame (i);
468 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
469 if (!stereo_asset->encrypted() || stereo_asset->key()) {
470 vector<VerificationNote> j2k_notes;
471 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
472 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
473 check_and_add (j2k_notes);
475 progress (float(i) / duration);
480 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
481 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
482 if (biggest_frame > max_frame) {
484 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
486 } else if (biggest_frame > risky_frame) {
488 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
495 verify_main_picture_asset (
496 shared_ptr<const DCP> dcp,
497 shared_ptr<const ReelPictureAsset> reel_asset,
498 function<void (string, optional<boost::filesystem::path>)> stage,
499 function<void (float)> progress,
500 VerificationOptions options,
501 vector<VerificationNote>& notes
504 auto asset = reel_asset->asset();
505 auto const file = *asset->file();
507 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
508 stage ("Checking picture asset hash", file);
509 auto const r = verify_asset (dcp, reel_asset, progress);
511 case VerifyAssetResult::BAD:
513 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
516 case VerifyAssetResult::CPL_PKL_DIFFER:
518 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
526 stage ("Checking picture frame sizes", asset->file());
527 verify_picture_asset (reel_asset, file, notes, progress);
529 /* Only flat/scope allowed by Bv2.1 */
531 asset->size() != Size(2048, 858) &&
532 asset->size() != Size(1998, 1080) &&
533 asset->size() != Size(4096, 1716) &&
534 asset->size() != Size(3996, 2160)) {
536 VerificationNote::Type::BV21_ERROR,
537 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
538 String::compose("%1x%2", asset->size().width, asset->size().height),
543 /* Only 24, 25, 48fps allowed for 2K */
545 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
546 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
549 VerificationNote::Type::BV21_ERROR,
550 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
551 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
556 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
557 /* Only 24fps allowed for 4K */
558 if (asset->edit_rate() != Fraction(24, 1)) {
560 VerificationNote::Type::BV21_ERROR,
561 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
562 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
567 /* Only 2D allowed for 4K */
568 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
570 VerificationNote::Type::BV21_ERROR,
571 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
572 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
584 boost::optional<string> subtitle_language;
585 boost::optional<int> audio_channels;
590 verify_main_sound_asset (
591 shared_ptr<const DCP> dcp,
592 shared_ptr<const ReelSoundAsset> reel_asset,
593 function<void (string, optional<boost::filesystem::path>)> stage,
594 function<void (float)> progress,
595 VerificationOptions options,
596 vector<VerificationNote>& notes,
600 auto asset = reel_asset->asset();
601 auto const file = *asset->file();
603 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
604 stage("Checking sound asset hash", file);
605 auto const r = verify_asset (dcp, reel_asset, progress);
607 case VerifyAssetResult::BAD:
608 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
610 case VerifyAssetResult::CPL_PKL_DIFFER:
611 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
618 if (!state.audio_channels) {
619 state.audio_channels = asset->channels();
620 } else if (*state.audio_channels != asset->channels()) {
621 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
624 stage ("Checking sound asset metadata", file);
626 if (auto lang = asset->language()) {
627 verify_language_tag (*lang, notes);
629 if (asset->sampling_rate() != 48000) {
630 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
636 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
638 /* XXX: is Language compulsory? */
639 if (reel_asset->language()) {
640 verify_language_tag (*reel_asset->language(), notes);
643 if (!reel_asset->entry_point()) {
644 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
645 } else if (reel_asset->entry_point().get()) {
646 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
652 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
654 /* XXX: is Language compulsory? */
655 if (reel_asset->language()) {
656 verify_language_tag (*reel_asset->language(), notes);
659 if (!reel_asset->entry_point()) {
660 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
661 } else if (reel_asset->entry_point().get()) {
662 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
667 /** Verify stuff that is common to both subtitles and closed captions */
669 verify_smpte_timed_text_asset (
670 shared_ptr<const SMPTESubtitleAsset> asset,
671 optional<int64_t> reel_asset_duration,
672 vector<VerificationNote>& notes
675 if (asset->language()) {
676 verify_language_tag (*asset->language(), notes);
678 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
681 auto const size = boost::filesystem::file_size(asset->file().get());
682 if (size > 115 * 1024 * 1024) {
684 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
688 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
689 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
691 auto fonts = asset->font_data ();
693 for (auto i: fonts) {
694 total_size += i.second.size();
696 if (total_size > 10 * 1024 * 1024) {
697 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
700 if (!asset->start_time()) {
701 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
702 } else if (asset->start_time() != Time()) {
703 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
706 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
709 VerificationNote::Type::BV21_ERROR,
710 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
711 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
718 /** Verify Interop subtitle-only stuff */
720 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
722 if (asset->subtitles().empty()) {
723 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
725 auto const unresolved = asset->unresolved_fonts();
726 if (!unresolved.empty()) {
727 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
732 /** Verify SMPTE subtitle-only stuff */
734 verify_smpte_subtitle_asset (
735 shared_ptr<const SMPTESubtitleAsset> asset,
736 vector<VerificationNote>& notes,
740 if (asset->language()) {
741 if (!state.subtitle_language) {
742 state.subtitle_language = *asset->language();
743 } else if (state.subtitle_language != *asset->language()) {
744 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
748 DCP_ASSERT (asset->resource_id());
749 auto xml_id = asset->xml_id();
751 if (asset->resource_id().get() != xml_id) {
752 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
755 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
756 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
759 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
762 if (asset->raw_xml()) {
763 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
764 cxml::Document doc("SubtitleReel");
765 doc.read_string(*asset->raw_xml());
766 auto issue_date = doc.string_child("IssueDate");
767 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
768 if (!std::regex_match(issue_date, reg)) {
769 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
775 /** Verify all subtitle stuff */
777 verify_subtitle_asset (
778 shared_ptr<const SubtitleAsset> asset,
779 optional<int64_t> reel_asset_duration,
780 function<void (string, optional<boost::filesystem::path>)> stage,
781 boost::filesystem::path xsd_dtd_directory,
782 vector<VerificationNote>& notes,
786 stage ("Checking subtitle XML", asset->file());
787 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
788 * gets passed through libdcp which may clean up and therefore hide errors.
790 if (asset->raw_xml()) {
791 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
793 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
796 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
797 cxml::Document doc(root_node);
798 doc.read_string(asset->raw_xml().get());
799 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
801 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
807 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
809 verify_interop_subtitle_asset(interop, notes);
810 if (namespace_count(asset, "DCSubtitle") > 1) {
811 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
815 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
817 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
818 verify_smpte_subtitle_asset (smpte, notes, state);
819 /* This asset may be encrypted and in that case we'll have no raw_xml() */
820 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
821 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
827 /** Verify all closed caption stuff */
829 verify_closed_caption_asset (
830 shared_ptr<const SubtitleAsset> asset,
831 optional<int64_t> reel_asset_duration,
832 function<void (string, optional<boost::filesystem::path>)> stage,
833 boost::filesystem::path xsd_dtd_directory,
834 vector<VerificationNote>& notes
837 stage ("Checking closed caption XML", asset->file());
838 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
839 * gets passed through libdcp which may clean up and therefore hide errors.
841 auto raw_xml = asset->raw_xml();
843 validate_xml (*raw_xml, xsd_dtd_directory, notes);
844 if (raw_xml->size() > 256 * 1024) {
845 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
848 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
851 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
853 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
858 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
861 verify_text_details (
862 dcp::Standard standard,
863 vector<shared_ptr<Reel>> reels,
865 vector<VerificationNote>& notes,
866 std::function<bool (shared_ptr<Reel>)> check,
867 std::function<optional<string> (shared_ptr<Reel>)> xml,
868 std::function<int64_t (shared_ptr<Reel>)> duration,
869 std::function<std::string (shared_ptr<Reel>)> id
872 /* end of last subtitle (in editable units) */
873 optional<int64_t> last_out;
874 auto too_short = false;
875 auto too_close = false;
876 auto too_early = false;
877 auto reel_overlap = false;
878 auto empty_text = false;
879 /* current reel start time (in editable units) */
880 int64_t reel_offset = 0;
881 optional<string> missing_load_font_id;
883 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
885 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
886 cxml::ConstNodePtr node,
888 optional<Time> start_time,
892 vector<string>& font_ids
894 if (node->name() == "Subtitle") {
895 Time in (node->string_attribute("TimeIn"), tcr);
899 Time out (node->string_attribute("TimeOut"), tcr);
903 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
906 auto length = out - in;
907 if (length.as_editable_units_ceil(er) < 15) {
911 /* XXX: this feels dubious - is it really what Bv2.1 means? */
912 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
913 if (distance >= 0 && distance < 2) {
917 last_out = reel_offset + out.as_editable_units_floor(er);
918 } else if (node->name() == "Text") {
919 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
920 if (!node->content().empty()) {
923 for (auto i: node->node_children()) {
924 if (node_has_content(i)) {
930 if (!node_has_content(node)) {
934 } else if (node->name() == "LoadFont") {
935 if (auto const id = node->optional_string_attribute("Id")) {
936 font_ids.push_back(*id);
937 } else if (auto const id = node->optional_string_attribute("ID")) {
938 font_ids.push_back(*id);
940 } else if (node->name() == "Font") {
941 if (auto const font_id = node->optional_string_attribute("Id")) {
942 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
943 missing_load_font_id = font_id;
947 for (auto i: node->node_children()) {
948 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
952 for (auto i = 0U; i < reels.size(); ++i) {
953 if (!check(reels[i])) {
957 auto reel_xml = xml(reels[i]);
959 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
963 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
964 * read in by libdcp's parser.
967 shared_ptr<cxml::Document> doc;
969 optional<Time> start_time;
971 case dcp::Standard::INTEROP:
972 doc = make_shared<cxml::Document>("DCSubtitle");
973 doc->read_string (*reel_xml);
975 case dcp::Standard::SMPTE:
976 doc = make_shared<cxml::Document>("SubtitleReel");
977 doc->read_string (*reel_xml);
978 tcr = doc->number_child<int>("TimeCodeRate");
979 if (auto start_time_string = doc->optional_string_child("StartTime")) {
980 start_time = Time(*start_time_string, tcr);
984 bool has_text = false;
985 vector<string> font_ids;
986 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
987 auto end = reel_offset + duration(reels[i]);
988 if (last_out && *last_out > end) {
993 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
994 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
998 if (last_out && *last_out > reel_offset) {
1004 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1010 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1016 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1022 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1028 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1032 if (missing_load_font_id) {
1033 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1040 verify_closed_caption_details (
1041 vector<shared_ptr<Reel>> reels,
1042 vector<VerificationNote>& notes
1045 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1046 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1047 for (auto i: node->node_children()) {
1048 if (i->name() == "Text") {
1049 text_or_image.push_back (i);
1051 find_text_or_image (i, text_or_image);
1056 auto mismatched_valign = false;
1057 auto incorrect_order = false;
1059 std::function<void (cxml::ConstNodePtr)> parse;
1060 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1061 if (node->name() == "Subtitle") {
1062 vector<cxml::ConstNodePtr> text_or_image;
1063 find_text_or_image (node, text_or_image);
1064 optional<string> last_valign;
1065 optional<float> last_vpos;
1066 for (auto i: text_or_image) {
1067 auto valign = i->optional_string_attribute("VAlign");
1069 valign = i->optional_string_attribute("Valign").get_value_or("center");
1071 auto vpos = i->optional_number_attribute<float>("VPosition");
1073 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1077 if (*last_valign != valign) {
1078 mismatched_valign = true;
1081 last_valign = valign;
1083 if (!mismatched_valign) {
1085 if (*last_valign == "top" || *last_valign == "center") {
1086 if (*vpos < *last_vpos) {
1087 incorrect_order = true;
1090 if (*vpos > *last_vpos) {
1091 incorrect_order = true;
1100 for (auto i: node->node_children()) {
1105 for (auto reel: reels) {
1106 for (auto ccap: reel->closed_captions()) {
1107 auto reel_xml = ccap->asset()->raw_xml();
1109 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1113 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1114 * read in by libdcp's parser.
1117 shared_ptr<cxml::Document> doc;
1119 optional<Time> start_time;
1121 doc = make_shared<cxml::Document>("SubtitleReel");
1122 doc->read_string (*reel_xml);
1124 doc = make_shared<cxml::Document>("DCSubtitle");
1125 doc->read_string (*reel_xml);
1131 if (mismatched_valign) {
1133 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1137 if (incorrect_order) {
1139 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1145 struct LinesCharactersResult
1147 bool warning_length_exceeded = false;
1148 bool error_length_exceeded = false;
1149 bool line_count_exceeded = false;
1155 verify_text_lines_and_characters (
1156 shared_ptr<SubtitleAsset> asset,
1159 LinesCharactersResult* result
1165 Event (Time time_, float position_, int characters_)
1167 , position (position_)
1168 , characters (characters_)
1171 Event (Time time_, shared_ptr<Event> start_)
1177 int position; //< position from 0 at top of screen to 100 at bottom
1179 shared_ptr<Event> start;
1182 vector<shared_ptr<Event>> events;
1184 auto position = [](shared_ptr<const SubtitleString> sub) {
1185 switch (sub->v_align()) {
1187 return lrintf(sub->v_position() * 100);
1188 case VAlign::CENTER:
1189 return lrintf((0.5f + sub->v_position()) * 100);
1190 case VAlign::BOTTOM:
1191 return lrintf((1.0f - sub->v_position()) * 100);
1197 for (auto j: asset->subtitles()) {
1198 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1200 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1201 events.push_back(in);
1202 events.push_back(make_shared<Event>(text->out(), in));
1206 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1207 return a->time < b->time;
1210 map<int, int> current;
1211 for (auto i: events) {
1212 if (current.size() > 3) {
1213 result->line_count_exceeded = true;
1215 for (auto j: current) {
1216 if (j.second > warning_length) {
1217 result->warning_length_exceeded = true;
1219 if (j.second > error_length) {
1220 result->error_length_exceeded = true;
1225 /* end of a subtitle */
1226 DCP_ASSERT (current.find(i->start->position) != current.end());
1227 if (current[i->start->position] == i->start->characters) {
1228 current.erase(i->start->position);
1230 current[i->start->position] -= i->start->characters;
1233 /* start of a subtitle */
1234 if (current.find(i->position) == current.end()) {
1235 current[i->position] = i->characters;
1237 current[i->position] += i->characters;
1246 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1248 if (reels.empty()) {
1252 if (reels[0]->main_subtitle()) {
1253 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1254 [](shared_ptr<Reel> reel) {
1255 return static_cast<bool>(reel->main_subtitle());
1257 [](shared_ptr<Reel> reel) {
1258 return reel->main_subtitle()->asset()->raw_xml();
1260 [](shared_ptr<Reel> reel) {
1261 return reel->main_subtitle()->actual_duration();
1263 [](shared_ptr<Reel> reel) {
1264 return reel->main_subtitle()->id();
1269 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1270 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1271 [i](shared_ptr<Reel> reel) {
1272 return i < reel->closed_captions().size();
1274 [i](shared_ptr<Reel> reel) {
1275 return reel->closed_captions()[i]->asset()->raw_xml();
1277 [i](shared_ptr<Reel> reel) {
1278 return reel->closed_captions()[i]->actual_duration();
1280 [i](shared_ptr<Reel> reel) {
1281 return reel->closed_captions()[i]->id();
1286 verify_closed_caption_details (reels, notes);
1291 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1293 DCP_ASSERT (cpl->file());
1294 cxml::Document doc ("CompositionPlaylist");
1295 doc.read_file (cpl->file().get());
1297 auto missing = false;
1300 if (auto reel_list = doc.node_child("ReelList")) {
1301 auto reels = reel_list->node_children("Reel");
1302 if (!reels.empty()) {
1303 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1304 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1305 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1307 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1308 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1312 if (auto name = extension->optional_node_child("Name")) {
1313 if (name->content() != "Application") {
1314 malformed = "<Name> should be 'Application'";
1317 if (auto property_list = extension->optional_node_child("PropertyList")) {
1318 if (auto property = property_list->optional_node_child("Property")) {
1319 if (auto name = property->optional_node_child("Name")) {
1320 if (name->content() != "DCP Constraints Profile") {
1321 malformed = "<Name> property should be 'DCP Constraints Profile'";
1324 if (auto value = property->optional_node_child("Value")) {
1325 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1326 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1341 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1342 } else if (!malformed.empty()) {
1343 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1349 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1351 vector<string> encrypted;
1352 for (auto i: dcp->cpls()) {
1353 for (auto j: i->reel_file_assets()) {
1354 if (j->asset_ref().resolved()) {
1355 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1356 if (mxf && mxf->encrypted()) {
1357 encrypted.push_back(j->asset_ref().id());
1363 for (auto i: pkl->assets()) {
1364 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1376 shared_ptr<const DCP> dcp,
1377 shared_ptr<const CPL> cpl,
1378 shared_ptr<const Reel> reel,
1379 optional<dcp::Size> main_picture_active_area,
1380 function<void (string, optional<boost::filesystem::path>)> stage,
1381 boost::filesystem::path xsd_dtd_directory,
1382 function<void (float)> progress,
1383 VerificationOptions options,
1384 vector<VerificationNote>& notes,
1386 bool* have_main_subtitle,
1387 bool* have_no_main_subtitle,
1388 size_t* most_closed_captions,
1389 size_t* fewest_closed_captions,
1390 map<Marker, Time>* markers_seen
1393 for (auto i: reel->assets()) {
1394 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1395 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1397 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1398 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1400 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1401 if (i->encryptable() && !file_asset->hash()) {
1402 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1406 if (dcp->standard() == Standard::SMPTE) {
1407 boost::optional<int64_t> duration;
1408 for (auto i: reel->assets()) {
1410 duration = i->actual_duration();
1411 } else if (*duration != i->actual_duration()) {
1412 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1418 if (reel->main_picture()) {
1419 /* Check reel stuff */
1420 auto const frame_rate = reel->main_picture()->frame_rate();
1421 if (frame_rate.denominator != 1 ||
1422 (frame_rate.numerator != 24 &&
1423 frame_rate.numerator != 25 &&
1424 frame_rate.numerator != 30 &&
1425 frame_rate.numerator != 48 &&
1426 frame_rate.numerator != 50 &&
1427 frame_rate.numerator != 60 &&
1428 frame_rate.numerator != 96)) {
1430 VerificationNote::Type::ERROR,
1431 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1432 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1436 if (reel->main_picture()->asset_ref().resolved()) {
1437 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1438 auto const asset_size = reel->main_picture()->asset()->size();
1439 if (main_picture_active_area) {
1440 if (main_picture_active_area->width > asset_size.width) {
1442 VerificationNote::Type::ERROR,
1443 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1444 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1448 if (main_picture_active_area->height > asset_size.height) {
1450 VerificationNote::Type::ERROR,
1451 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1452 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1461 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1462 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1465 if (reel->main_subtitle()) {
1466 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1467 if (reel->main_subtitle()->asset_ref().resolved()) {
1468 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1470 *have_main_subtitle = true;
1472 *have_no_main_subtitle = true;
1475 for (auto i: reel->closed_captions()) {
1476 verify_closed_caption_reel(i, notes);
1477 if (i->asset_ref().resolved()) {
1478 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1482 if (reel->main_markers()) {
1483 for (auto const& i: reel->main_markers()->get()) {
1484 markers_seen->insert(i);
1486 if (reel->main_markers()->entry_point()) {
1487 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1489 if (reel->main_markers()->duration()) {
1490 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1494 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1495 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1503 shared_ptr<const DCP> dcp,
1504 shared_ptr<const CPL> cpl,
1505 function<void (string, optional<boost::filesystem::path>)> stage,
1506 boost::filesystem::path xsd_dtd_directory,
1507 function<void (float)> progress,
1508 VerificationOptions options,
1509 vector<VerificationNote>& notes,
1513 stage("Checking CPL", cpl->file());
1514 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1516 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1517 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1520 for (auto const& i: cpl->additional_subtitle_languages()) {
1521 verify_language_tag(i, notes);
1524 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1525 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1526 * of the approved ones.
1528 auto all = ContentKind::all();
1529 auto name = cpl->content_kind().name();
1530 transform(name.begin(), name.end(), name.begin(), ::tolower);
1531 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1532 if (iter == all.end()) {
1533 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1537 if (cpl->release_territory()) {
1538 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") {
1539 auto terr = cpl->release_territory().get();
1540 /* Must be a valid region tag, or "001" */
1542 LanguageTag::RegionSubtag test(terr);
1544 if (terr != "001") {
1545 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1551 if (dcp->standard() == Standard::SMPTE) {
1552 if (!cpl->annotation_text()) {
1553 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1554 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1555 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1559 for (auto i: dcp->pkls()) {
1560 /* Check that the CPL's hash corresponds to the PKL */
1561 optional<string> h = i->hash(cpl->id());
1562 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1563 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1566 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1567 optional<string> required_annotation_text;
1568 for (auto j: i->assets()) {
1569 /* See if this is a CPL */
1570 for (auto k: dcp->cpls()) {
1571 if (j->id() == k->id()) {
1572 if (!required_annotation_text) {
1573 /* First CPL we have found; this is the required AnnotationText unless we find another */
1574 required_annotation_text = cpl->content_title_text();
1576 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1577 required_annotation_text = boost::none;
1583 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1584 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1588 /* set to true if any reel has a MainSubtitle */
1589 auto have_main_subtitle = false;
1590 /* set to true if any reel has no MainSubtitle */
1591 auto have_no_main_subtitle = false;
1592 /* fewest number of closed caption assets seen in a reel */
1593 size_t fewest_closed_captions = SIZE_MAX;
1594 /* most number of closed caption assets seen in a reel */
1595 size_t most_closed_captions = 0;
1596 map<Marker, Time> markers_seen;
1598 auto const main_picture_active_area = cpl->main_picture_active_area();
1599 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1601 VerificationNote::Type::ERROR,
1602 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1603 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1607 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1609 VerificationNote::Type::ERROR,
1610 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1611 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1616 for (auto reel: cpl->reels()) {
1617 stage("Checking reel", optional<boost::filesystem::path>());
1622 main_picture_active_area,
1629 &have_main_subtitle,
1630 &have_no_main_subtitle,
1631 &most_closed_captions,
1632 &fewest_closed_captions,
1637 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1639 if (dcp->standard() == Standard::SMPTE) {
1640 if (auto msc = cpl->main_sound_configuration()) {
1641 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1643 VerificationNote::Type::ERROR,
1644 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1645 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1651 if (have_main_subtitle && have_no_main_subtitle) {
1652 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1655 if (fewest_closed_captions != most_closed_captions) {
1656 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1659 if (cpl->content_kind() == ContentKind::FEATURE) {
1660 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1661 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1663 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1664 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1668 auto ffoc = markers_seen.find(Marker::FFOC);
1669 if (ffoc == markers_seen.end()) {
1670 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1671 } else if (ffoc->second.e != 1) {
1672 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1675 auto lfoc = markers_seen.find(Marker::LFOC);
1676 if (lfoc == markers_seen.end()) {
1677 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1679 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1680 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1681 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1685 LinesCharactersResult result;
1686 for (auto reel: cpl->reels()) {
1687 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1688 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1692 if (result.line_count_exceeded) {
1693 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1695 if (result.error_length_exceeded) {
1696 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1697 } else if (result.warning_length_exceeded) {
1698 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1701 result = LinesCharactersResult();
1702 for (auto reel: cpl->reels()) {
1703 for (auto i: reel->closed_captions()) {
1705 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1710 if (result.line_count_exceeded) {
1711 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1713 if (result.error_length_exceeded) {
1714 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1717 if (!cpl->read_composition_metadata()) {
1718 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1719 } else if (!cpl->version_number()) {
1720 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1723 verify_extension_metadata(cpl, notes);
1725 if (cpl->any_encrypted()) {
1726 cxml::Document doc("CompositionPlaylist");
1727 DCP_ASSERT(cpl->file());
1728 doc.read_file(cpl->file().get());
1729 if (!doc.optional_node_child("Signature")) {
1730 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1740 shared_ptr<const DCP> dcp,
1741 shared_ptr<const PKL> pkl,
1742 boost::filesystem::path xsd_dtd_directory,
1743 vector<VerificationNote>& notes
1746 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1748 if (pkl_has_encrypted_assets(dcp, pkl)) {
1749 cxml::Document doc("PackingList");
1750 doc.read_file(pkl->file().get());
1751 if (!doc.optional_node_child("Signature")) {
1752 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1756 set<string> uuid_set;
1757 for (auto asset: pkl->assets()) {
1758 if (!uuid_set.insert(asset->id()).second) {
1759 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1770 shared_ptr<const DCP> dcp,
1771 boost::filesystem::path xsd_dtd_directory,
1772 vector<VerificationNote>& notes
1775 auto asset_map = dcp->asset_map();
1776 DCP_ASSERT(asset_map);
1778 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1780 set<string> uuid_set;
1781 for (auto const& asset: asset_map->assets()) {
1782 if (!uuid_set.insert(asset.id()).second) {
1783 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1790 vector<VerificationNote>
1792 vector<boost::filesystem::path> directories,
1793 function<void (string, optional<boost::filesystem::path>)> stage,
1794 function<void (float)> progress,
1795 VerificationOptions options,
1796 optional<boost::filesystem::path> xsd_dtd_directory
1799 if (!xsd_dtd_directory) {
1800 xsd_dtd_directory = resources_directory() / "xsd";
1802 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1804 vector<VerificationNote> notes;
1807 vector<shared_ptr<DCP>> dcps;
1808 for (auto i: directories) {
1809 dcps.push_back (make_shared<DCP>(i));
1812 for (auto dcp: dcps) {
1813 stage ("Checking DCP", dcp->directory());
1814 bool carry_on = true;
1816 dcp->read (¬es, true);
1817 } catch (MissingAssetmapError& e) {
1818 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1820 } catch (ReadError& e) {
1821 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1822 } catch (XMLError& e) {
1823 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1824 } catch (MXFFileError& e) {
1825 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1826 } catch (BadURNUUIDError& e) {
1827 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1828 } catch (cxml::Error& e) {
1829 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1836 if (dcp->standard() != Standard::SMPTE) {
1837 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1840 for (auto cpl: dcp->cpls()) {
1853 for (auto pkl: dcp->pkls()) {
1854 stage("Checking PKL", pkl->file());
1855 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1858 if (dcp->asset_map_file()) {
1859 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1860 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1862 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1871 dcp::note_to_string (VerificationNote note)
1873 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1875 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1876 * not "ClosedCaption assets must have an <EntryPoint> tag."
1878 * It's OK to use XML tag names where they are clear.
1879 * If both ID and filename are available, use only the ID.
1880 * End messages with a full stop.
1881 * Messages should not mention whether or not their errors are a part of Bv2.1.
1883 switch (note.code()) {
1884 case VerificationNote::Code::FAILED_READ:
1885 return *note.note();
1886 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1887 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1888 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1889 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1890 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1891 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1892 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1893 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1894 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1895 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1896 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1897 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1898 case VerificationNote::Code::EMPTY_ASSET_PATH:
1899 return "The asset map contains an empty asset path.";
1900 case VerificationNote::Code::MISSING_ASSET:
1901 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1902 case VerificationNote::Code::MISMATCHED_STANDARD:
1903 return "The DCP contains both SMPTE and Interop parts.";
1904 case VerificationNote::Code::INVALID_XML:
1905 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1906 case VerificationNote::Code::MISSING_ASSETMAP:
1907 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1908 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1909 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1910 case VerificationNote::Code::INVALID_DURATION:
1911 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1912 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1913 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());
1914 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1915 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());
1916 case VerificationNote::Code::EXTERNAL_ASSET:
1917 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());
1918 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1919 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1920 case VerificationNote::Code::INVALID_STANDARD:
1921 return "This DCP does not use the SMPTE standard.";
1922 case VerificationNote::Code::INVALID_LANGUAGE:
1923 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1924 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1925 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1926 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1927 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1928 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1929 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1930 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1931 return "3D 4K DCPs are not allowed.";
1932 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1933 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1934 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1935 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1936 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1937 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());
1938 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1939 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1940 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1941 return "Some subtitle assets have different <Language> tags than others";
1942 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1943 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1944 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1945 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1946 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1947 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1948 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1949 return "At least one subtitle lasts less than 15 frames.";
1950 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1951 return "At least one pair of subtitles is separated by less than 2 frames.";
1952 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1953 return "At least one subtitle extends outside of its reel.";
1954 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1955 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1956 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1957 return "There are more than 52 characters in at least one subtitle line.";
1958 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1959 return "There are more than 79 characters in at least one subtitle line.";
1960 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1961 return "There are more than 3 closed caption lines in at least one place.";
1962 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1963 return "There are more than 32 characters in at least one closed caption line.";
1964 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1965 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1966 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1967 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1968 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1969 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1970 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1971 return "All assets in a reel do not have the same duration.";
1972 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1973 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1974 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1975 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1976 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1977 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1978 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1979 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1980 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1981 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1982 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1983 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1984 case VerificationNote::Code::MISSING_HASH:
1985 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1986 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1987 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1988 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1989 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1990 case VerificationNote::Code::MISSING_FFOC:
1991 return "There should be a FFOC (first frame of content) marker.";
1992 case VerificationNote::Code::MISSING_LFOC:
1993 return "There should be a LFOC (last frame of content) marker.";
1994 case VerificationNote::Code::INCORRECT_FFOC:
1995 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1996 case VerificationNote::Code::INCORRECT_LFOC:
1997 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1998 case VerificationNote::Code::MISSING_CPL_METADATA:
1999 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2000 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2001 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2002 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2003 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2004 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2005 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2006 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2007 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2008 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2009 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2010 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2011 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2012 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2013 return "Some assets are encrypted but some are not.";
2014 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2015 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2016 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2017 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2018 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2019 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2020 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2021 return "The JPEG2000 tile size is not the same as the image size.";
2022 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2023 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2024 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2025 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2026 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2027 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2028 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2029 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2030 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2031 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2032 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2033 return "POC marker found outside main header.";
2034 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2035 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2036 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2037 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2038 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2039 return "No TLM marker was found in a JPEG2000 codestream.";
2040 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2041 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2042 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2043 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2044 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2046 vector<string> parts;
2047 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2048 DCP_ASSERT (parts.size() == 2);
2049 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]);
2051 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2052 return "Some aspect of this DCP could not be checked because it is encrypted.";
2053 case VerificationNote::Code::EMPTY_TEXT:
2054 return "There is an empty <Text> node in a subtitle or closed caption.";
2055 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2056 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2057 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2058 return "Some closed captions are not listed in the order of their vertical position.";
2059 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2060 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2061 case VerificationNote::Code::UNEXPECTED_DURATION:
2062 return "There is an <Duration> node inside a <MainMarkers>.";
2063 case VerificationNote::Code::INVALID_CONTENT_KIND:
2064 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2065 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2066 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2067 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2068 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2069 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2070 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2071 case VerificationNote::Code::MISSING_SUBTITLE:
2072 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2073 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2074 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2075 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2076 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2077 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2078 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2079 case VerificationNote::Code::MISSING_FONT:
2080 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2081 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2082 return String::compose(
2083 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2084 note.frame().get(), note.component().get(), note.size().get()
2086 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2087 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2088 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2089 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());
2090 case VerificationNote::Code::MISSING_LOAD_FONT:
2091 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2092 case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2093 return String::compose("The asset with ID %1 in the asset map actually has an id of %2", note.id().get(), note.other_id().get());
2101 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2103 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2108 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2110 if (a.type() != b.type()) {
2111 return a.type() < b.type();
2114 if (a.code() != b.code()) {
2115 return a.code() < b.code();
2118 if (a.note() != b.note()) {
2119 return a.note().get_value_or("") < b.note().get_value_or("");
2122 if (a.file() != b.file()) {
2123 return a.file().get_value_or("") < b.file().get_value_or("");
2126 return a.line().get_value_or(0) < b.line().get_value_or(0);
2131 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2133 s << note_to_string (note);
2135 s << " [" << note.note().get() << "]";
2138 s << " [" << note.file().get() << "]";
2141 s << " [" << note.line().get() << "]";