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 */
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;
874 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
875 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
876 if (node->name() == "Subtitle") {
877 Time in (node->string_attribute("TimeIn"), tcr);
881 Time out (node->string_attribute("TimeOut"), tcr);
885 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
888 auto length = out - in;
889 if (length.as_editable_units_ceil(er) < 15) {
893 /* XXX: this feels dubious - is it really what Bv2.1 means? */
894 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
895 if (distance >= 0 && distance < 2) {
899 last_out = reel_offset + out.as_editable_units_floor(er);
900 } else if (node->name() == "Text") {
901 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
902 if (!node->content().empty()) {
905 for (auto i: node->node_children()) {
906 if (node_has_content(i)) {
912 if (!node_has_content(node)) {
917 for (auto i: node->node_children()) {
918 parse(i, tcr, start_time, er, first_reel);
922 for (auto i = 0U; i < reels.size(); ++i) {
923 if (!check(reels[i])) {
927 auto reel_xml = xml(reels[i]);
929 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
933 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
934 * read in by libdcp's parser.
937 shared_ptr<cxml::Document> doc;
939 optional<Time> start_time;
941 doc = make_shared<cxml::Document>("SubtitleReel");
942 doc->read_string (*reel_xml);
943 tcr = doc->number_child<int>("TimeCodeRate");
944 auto start_time_string = doc->optional_string_child("StartTime");
945 if (start_time_string) {
946 start_time = Time(*start_time_string, tcr);
949 doc = make_shared<cxml::Document>("DCSubtitle");
950 doc->read_string (*reel_xml);
952 parse (doc, tcr, start_time, edit_rate, i == 0);
953 auto end = reel_offset + duration(reels[i]);
954 if (last_out && *last_out > end) {
960 if (last_out && *last_out > reel_offset) {
966 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
972 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
978 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
984 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
990 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
998 verify_closed_caption_details (
999 vector<shared_ptr<Reel>> reels,
1000 vector<VerificationNote>& notes
1003 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1004 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1005 for (auto i: node->node_children()) {
1006 if (i->name() == "Text") {
1007 text_or_image.push_back (i);
1009 find_text_or_image (i, text_or_image);
1014 auto mismatched_valign = false;
1015 auto incorrect_order = false;
1017 std::function<void (cxml::ConstNodePtr)> parse;
1018 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1019 if (node->name() == "Subtitle") {
1020 vector<cxml::ConstNodePtr> text_or_image;
1021 find_text_or_image (node, text_or_image);
1022 optional<string> last_valign;
1023 optional<float> last_vpos;
1024 for (auto i: text_or_image) {
1025 auto valign = i->optional_string_attribute("VAlign");
1027 valign = i->optional_string_attribute("Valign").get_value_or("center");
1029 auto vpos = i->optional_number_attribute<float>("VPosition");
1031 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1035 if (*last_valign != valign) {
1036 mismatched_valign = true;
1039 last_valign = valign;
1041 if (!mismatched_valign) {
1043 if (*last_valign == "top" || *last_valign == "center") {
1044 if (*vpos < *last_vpos) {
1045 incorrect_order = true;
1048 if (*vpos > *last_vpos) {
1049 incorrect_order = true;
1058 for (auto i: node->node_children()) {
1063 for (auto reel: reels) {
1064 for (auto ccap: reel->closed_captions()) {
1065 auto reel_xml = ccap->asset()->raw_xml();
1067 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1071 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1072 * read in by libdcp's parser.
1075 shared_ptr<cxml::Document> doc;
1077 optional<Time> start_time;
1079 doc = make_shared<cxml::Document>("SubtitleReel");
1080 doc->read_string (*reel_xml);
1082 doc = make_shared<cxml::Document>("DCSubtitle");
1083 doc->read_string (*reel_xml);
1089 if (mismatched_valign) {
1091 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1095 if (incorrect_order) {
1097 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1103 struct LinesCharactersResult
1105 bool warning_length_exceeded = false;
1106 bool error_length_exceeded = false;
1107 bool line_count_exceeded = false;
1113 verify_text_lines_and_characters (
1114 shared_ptr<SubtitleAsset> asset,
1117 LinesCharactersResult* result
1123 Event (Time time_, float position_, int characters_)
1125 , position (position_)
1126 , characters (characters_)
1129 Event (Time time_, shared_ptr<Event> start_)
1135 int position; //< position from 0 at top of screen to 100 at bottom
1137 shared_ptr<Event> start;
1140 vector<shared_ptr<Event>> events;
1142 auto position = [](shared_ptr<const SubtitleString> sub) {
1143 switch (sub->v_align()) {
1145 return lrintf(sub->v_position() * 100);
1146 case VAlign::CENTER:
1147 return lrintf((0.5f + sub->v_position()) * 100);
1148 case VAlign::BOTTOM:
1149 return lrintf((1.0f - sub->v_position()) * 100);
1155 for (auto j: asset->subtitles()) {
1156 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1158 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1159 events.push_back(in);
1160 events.push_back(make_shared<Event>(text->out(), in));
1164 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1165 return a->time < b->time;
1168 map<int, int> current;
1169 for (auto i: events) {
1170 if (current.size() > 3) {
1171 result->line_count_exceeded = true;
1173 for (auto j: current) {
1174 if (j.second > warning_length) {
1175 result->warning_length_exceeded = true;
1177 if (j.second > error_length) {
1178 result->error_length_exceeded = true;
1183 /* end of a subtitle */
1184 DCP_ASSERT (current.find(i->start->position) != current.end());
1185 if (current[i->start->position] == i->start->characters) {
1186 current.erase(i->start->position);
1188 current[i->start->position] -= i->start->characters;
1191 /* start of a subtitle */
1192 if (current.find(i->position) == current.end()) {
1193 current[i->position] = i->characters;
1195 current[i->position] += i->characters;
1204 verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1206 if (reels.empty()) {
1210 if (reels[0]->main_subtitle()) {
1211 verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1212 [](shared_ptr<Reel> reel) {
1213 return static_cast<bool>(reel->main_subtitle());
1215 [](shared_ptr<Reel> reel) {
1216 auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
1218 return interop->asset()->raw_xml();
1220 auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
1222 return smpte->asset()->raw_xml();
1224 [](shared_ptr<Reel> reel) {
1225 return reel->main_subtitle()->actual_duration();
1230 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1231 verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1232 [i](shared_ptr<Reel> reel) {
1233 return i < reel->closed_captions().size();
1235 [i](shared_ptr<Reel> reel) {
1236 return reel->closed_captions()[i]->asset()->raw_xml();
1238 [i](shared_ptr<Reel> reel) {
1239 return reel->closed_captions()[i]->actual_duration();
1244 verify_closed_caption_details (reels, notes);
1249 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1251 DCP_ASSERT (cpl->file());
1252 cxml::Document doc ("CompositionPlaylist");
1253 doc.read_file (cpl->file().get());
1255 auto missing = false;
1258 if (auto reel_list = doc.node_child("ReelList")) {
1259 auto reels = reel_list->node_children("Reel");
1260 if (!reels.empty()) {
1261 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1262 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1263 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1265 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1266 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1270 if (auto name = extension->optional_node_child("Name")) {
1271 if (name->content() != "Application") {
1272 malformed = "<Name> should be 'Application'";
1275 if (auto property_list = extension->optional_node_child("PropertyList")) {
1276 if (auto property = property_list->optional_node_child("Property")) {
1277 if (auto name = property->optional_node_child("Name")) {
1278 if (name->content() != "DCP Constraints Profile") {
1279 malformed = "<Name> property should be 'DCP Constraints Profile'";
1282 if (auto value = property->optional_node_child("Value")) {
1283 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1284 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1299 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1300 } else if (!malformed.empty()) {
1301 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1307 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1309 vector<string> encrypted;
1310 for (auto i: dcp->cpls()) {
1311 for (auto j: i->reel_file_assets()) {
1312 if (j->asset_ref().resolved()) {
1313 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1314 if (mxf && mxf->encrypted()) {
1315 encrypted.push_back(j->asset_ref().id());
1321 for (auto i: pkl->assets()) {
1322 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1334 shared_ptr<const DCP> dcp,
1335 shared_ptr<const CPL> cpl,
1336 shared_ptr<const Reel> reel,
1337 optional<dcp::Size> main_picture_active_area,
1338 function<void (string, optional<boost::filesystem::path>)> stage,
1339 boost::filesystem::path xsd_dtd_directory,
1340 function<void (float)> progress,
1341 VerificationOptions options,
1342 vector<VerificationNote>& notes,
1344 bool* have_main_subtitle,
1345 bool* have_no_main_subtitle,
1346 size_t* most_closed_captions,
1347 size_t* fewest_closed_captions,
1348 map<Marker, Time>* markers_seen
1351 for (auto i: reel->assets()) {
1352 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1353 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1355 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1356 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1358 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1359 if (i->encryptable() && !file_asset->hash()) {
1360 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1364 if (dcp->standard() == Standard::SMPTE) {
1365 boost::optional<int64_t> duration;
1366 for (auto i: reel->assets()) {
1368 duration = i->actual_duration();
1369 } else if (*duration != i->actual_duration()) {
1370 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1376 if (reel->main_picture()) {
1377 /* Check reel stuff */
1378 auto const frame_rate = reel->main_picture()->frame_rate();
1379 if (frame_rate.denominator != 1 ||
1380 (frame_rate.numerator != 24 &&
1381 frame_rate.numerator != 25 &&
1382 frame_rate.numerator != 30 &&
1383 frame_rate.numerator != 48 &&
1384 frame_rate.numerator != 50 &&
1385 frame_rate.numerator != 60 &&
1386 frame_rate.numerator != 96)) {
1388 VerificationNote::Type::ERROR,
1389 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1390 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1394 if (reel->main_picture()->asset_ref().resolved()) {
1395 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1396 auto const asset_size = reel->main_picture()->asset()->size();
1397 if (main_picture_active_area) {
1398 if (main_picture_active_area->width > asset_size.width) {
1400 VerificationNote::Type::ERROR,
1401 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1402 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1406 if (main_picture_active_area->height > asset_size.height) {
1408 VerificationNote::Type::ERROR,
1409 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1410 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1418 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1419 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1422 if (reel->main_subtitle()) {
1423 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1424 if (reel->main_subtitle()->asset_ref().resolved()) {
1425 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1427 *have_main_subtitle = true;
1429 *have_no_main_subtitle = true;
1432 for (auto i: reel->closed_captions()) {
1433 verify_closed_caption_reel(i, notes);
1434 if (i->asset_ref().resolved()) {
1435 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1439 if (reel->main_markers()) {
1440 for (auto const& i: reel->main_markers()->get()) {
1441 markers_seen->insert(i);
1443 if (reel->main_markers()->entry_point()) {
1444 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1446 if (reel->main_markers()->duration()) {
1447 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1451 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1452 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1460 shared_ptr<const DCP> dcp,
1461 shared_ptr<const CPL> cpl,
1462 function<void (string, optional<boost::filesystem::path>)> stage,
1463 boost::filesystem::path xsd_dtd_directory,
1464 function<void (float)> progress,
1465 VerificationOptions options,
1466 vector<VerificationNote>& notes,
1470 stage("Checking CPL", cpl->file());
1471 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1473 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1474 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1477 for (auto const& i: cpl->additional_subtitle_languages()) {
1478 verify_language_tag(i, notes);
1481 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1482 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1483 * of the approved ones.
1485 auto all = ContentKind::all();
1486 auto name = cpl->content_kind().name();
1487 transform(name.begin(), name.end(), name.begin(), ::tolower);
1488 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1489 if (iter == all.end()) {
1490 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1494 if (cpl->release_territory()) {
1495 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") {
1496 auto terr = cpl->release_territory().get();
1497 /* Must be a valid region tag, or "001" */
1499 LanguageTag::RegionSubtag test(terr);
1501 if (terr != "001") {
1502 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1508 if (dcp->standard() == Standard::SMPTE) {
1509 if (!cpl->annotation_text()) {
1510 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1511 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1512 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1516 for (auto i: dcp->pkls()) {
1517 /* Check that the CPL's hash corresponds to the PKL */
1518 optional<string> h = i->hash(cpl->id());
1519 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1520 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1523 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1524 optional<string> required_annotation_text;
1525 for (auto j: i->assets()) {
1526 /* See if this is a CPL */
1527 for (auto k: dcp->cpls()) {
1528 if (j->id() == k->id()) {
1529 if (!required_annotation_text) {
1530 /* First CPL we have found; this is the required AnnotationText unless we find another */
1531 required_annotation_text = cpl->content_title_text();
1533 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1534 required_annotation_text = boost::none;
1540 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1541 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1545 /* set to true if any reel has a MainSubtitle */
1546 auto have_main_subtitle = false;
1547 /* set to true if any reel has no MainSubtitle */
1548 auto have_no_main_subtitle = false;
1549 /* fewest number of closed caption assets seen in a reel */
1550 size_t fewest_closed_captions = SIZE_MAX;
1551 /* most number of closed caption assets seen in a reel */
1552 size_t most_closed_captions = 0;
1553 map<Marker, Time> markers_seen;
1555 auto const main_picture_active_area = cpl->main_picture_active_area();
1556 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1558 VerificationNote::Type::ERROR,
1559 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1560 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1564 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1566 VerificationNote::Type::ERROR,
1567 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1568 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1573 for (auto reel: cpl->reels()) {
1574 stage("Checking reel", optional<boost::filesystem::path>());
1579 main_picture_active_area,
1586 &have_main_subtitle,
1587 &have_no_main_subtitle,
1588 &most_closed_captions,
1589 &fewest_closed_captions,
1594 verify_text_details(cpl->reels(), notes);
1596 if (dcp->standard() == Standard::SMPTE) {
1597 if (auto msc = cpl->main_sound_configuration()) {
1598 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1600 VerificationNote::Type::ERROR,
1601 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1602 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1608 if (have_main_subtitle && have_no_main_subtitle) {
1609 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1612 if (fewest_closed_captions != most_closed_captions) {
1613 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1616 if (cpl->content_kind() == ContentKind::FEATURE) {
1617 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1618 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1620 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1621 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1625 auto ffoc = markers_seen.find(Marker::FFOC);
1626 if (ffoc == markers_seen.end()) {
1627 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1628 } else if (ffoc->second.e != 1) {
1629 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1632 auto lfoc = markers_seen.find(Marker::LFOC);
1633 if (lfoc == markers_seen.end()) {
1634 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1636 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1637 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1638 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1642 LinesCharactersResult result;
1643 for (auto reel: cpl->reels()) {
1644 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1645 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1649 if (result.line_count_exceeded) {
1650 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1652 if (result.error_length_exceeded) {
1653 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1654 } else if (result.warning_length_exceeded) {
1655 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1658 result = LinesCharactersResult();
1659 for (auto reel: cpl->reels()) {
1660 for (auto i: reel->closed_captions()) {
1662 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1667 if (result.line_count_exceeded) {
1668 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1670 if (result.error_length_exceeded) {
1671 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1674 if (!cpl->read_composition_metadata()) {
1675 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1676 } else if (!cpl->version_number()) {
1677 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1680 verify_extension_metadata(cpl, notes);
1682 if (cpl->any_encrypted()) {
1683 cxml::Document doc("CompositionPlaylist");
1684 DCP_ASSERT(cpl->file());
1685 doc.read_file(cpl->file().get());
1686 if (!doc.optional_node_child("Signature")) {
1687 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1697 shared_ptr<const DCP> dcp,
1698 shared_ptr<const PKL> pkl,
1699 boost::filesystem::path xsd_dtd_directory,
1700 vector<VerificationNote>& notes
1703 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1705 if (pkl_has_encrypted_assets(dcp, pkl)) {
1706 cxml::Document doc("PackingList");
1707 doc.read_file(pkl->file().get());
1708 if (!doc.optional_node_child("Signature")) {
1709 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1713 set<string> uuid_set;
1714 for (auto asset: pkl->assets()) {
1715 if (!uuid_set.insert(asset->id()).second) {
1716 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1727 shared_ptr<const DCP> dcp,
1728 boost::filesystem::path xsd_dtd_directory,
1729 vector<VerificationNote>& notes
1732 auto asset_map = dcp->asset_map();
1733 DCP_ASSERT(asset_map);
1735 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1737 set<string> uuid_set;
1738 for (auto const& asset: asset_map->assets()) {
1739 if (!uuid_set.insert(asset.id()).second) {
1740 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1747 vector<VerificationNote>
1749 vector<boost::filesystem::path> directories,
1750 function<void (string, optional<boost::filesystem::path>)> stage,
1751 function<void (float)> progress,
1752 VerificationOptions options,
1753 optional<boost::filesystem::path> xsd_dtd_directory
1756 if (!xsd_dtd_directory) {
1757 xsd_dtd_directory = resources_directory() / "xsd";
1759 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1761 vector<VerificationNote> notes;
1764 vector<shared_ptr<DCP>> dcps;
1765 for (auto i: directories) {
1766 dcps.push_back (make_shared<DCP>(i));
1769 for (auto dcp: dcps) {
1770 stage ("Checking DCP", dcp->directory());
1771 bool carry_on = true;
1773 dcp->read (¬es, true);
1774 } catch (MissingAssetmapError& e) {
1775 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1777 } catch (ReadError& e) {
1778 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1779 } catch (XMLError& e) {
1780 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1781 } catch (MXFFileError& e) {
1782 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1783 } catch (cxml::Error& e) {
1784 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1791 if (dcp->standard() != Standard::SMPTE) {
1792 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1795 for (auto cpl: dcp->cpls()) {
1808 for (auto pkl: dcp->pkls()) {
1809 stage("Checking PKL", pkl->file());
1810 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1813 if (dcp->asset_map_file()) {
1814 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1815 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1817 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1826 dcp::note_to_string (VerificationNote note)
1828 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1830 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1831 * not "ClosedCaption assets must have an <EntryPoint> tag."
1833 * It's OK to use XML tag names where they are clear.
1834 * If both ID and filename are available, use only the ID.
1835 * End messages with a full stop.
1836 * Messages should not mention whether or not their errors are a part of Bv2.1.
1838 switch (note.code()) {
1839 case VerificationNote::Code::FAILED_READ:
1840 return *note.note();
1841 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1842 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1843 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1844 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1845 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1846 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1847 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1848 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1849 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1850 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1851 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1852 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1853 case VerificationNote::Code::EMPTY_ASSET_PATH:
1854 return "The asset map contains an empty asset path.";
1855 case VerificationNote::Code::MISSING_ASSET:
1856 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1857 case VerificationNote::Code::MISMATCHED_STANDARD:
1858 return "The DCP contains both SMPTE and Interop parts.";
1859 case VerificationNote::Code::INVALID_XML:
1860 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1861 case VerificationNote::Code::MISSING_ASSETMAP:
1862 return "No ASSETMAP or ASSETMAP.xml was found.";
1863 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1864 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1865 case VerificationNote::Code::INVALID_DURATION:
1866 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1867 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1868 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());
1869 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1870 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());
1871 case VerificationNote::Code::EXTERNAL_ASSET:
1872 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());
1873 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1874 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1875 case VerificationNote::Code::INVALID_STANDARD:
1876 return "This DCP does not use the SMPTE standard.";
1877 case VerificationNote::Code::INVALID_LANGUAGE:
1878 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1879 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1880 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1881 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1882 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1883 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1884 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1885 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1886 return "3D 4K DCPs are not allowed.";
1887 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1888 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1889 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1890 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1891 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1892 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());
1893 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1894 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1895 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1896 return "Some subtitle assets have different <Language> tags than others";
1897 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1898 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1899 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1900 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1901 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1902 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1903 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1904 return "At least one subtitle lasts less than 15 frames.";
1905 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1906 return "At least one pair of subtitles is separated by less than 2 frames.";
1907 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1908 return "At least one subtitle extends outside of its reel.";
1909 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1910 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1911 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1912 return "There are more than 52 characters in at least one subtitle line.";
1913 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1914 return "There are more than 79 characters in at least one subtitle line.";
1915 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1916 return "There are more than 3 closed caption lines in at least one place.";
1917 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1918 return "There are more than 32 characters in at least one closed caption line.";
1919 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1920 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1921 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1922 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1923 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1924 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1925 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1926 return "All assets in a reel do not have the same duration.";
1927 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1928 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1929 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1930 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1931 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1932 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1933 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1934 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1935 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1936 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1937 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1938 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1939 case VerificationNote::Code::MISSING_HASH:
1940 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1941 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1942 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1943 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1944 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1945 case VerificationNote::Code::MISSING_FFOC:
1946 return "There should be a FFOC (first frame of content) marker.";
1947 case VerificationNote::Code::MISSING_LFOC:
1948 return "There should be a LFOC (last frame of content) marker.";
1949 case VerificationNote::Code::INCORRECT_FFOC:
1950 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1951 case VerificationNote::Code::INCORRECT_LFOC:
1952 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1953 case VerificationNote::Code::MISSING_CPL_METADATA:
1954 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1955 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1956 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1957 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1958 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1959 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1960 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1961 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1962 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1963 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1964 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1965 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1966 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
1967 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1968 return "Some assets are encrypted but some are not.";
1969 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
1970 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
1971 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
1972 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
1973 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
1974 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
1975 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
1976 return "The JPEG2000 tile size is not the same as the image size.";
1977 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
1978 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
1979 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
1980 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
1981 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
1982 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
1983 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
1984 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
1985 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
1986 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
1987 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
1988 return "POC marker found outside main header.";
1989 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
1990 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
1991 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
1992 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
1993 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
1994 return "No TLM marker was found in a JPEG2000 codestream.";
1995 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
1996 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
1997 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
1998 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
1999 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2001 vector<string> parts;
2002 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2003 DCP_ASSERT (parts.size() == 2);
2004 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]);
2006 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2007 return "Some aspect of this DCP could not be checked because it is encrypted.";
2008 case VerificationNote::Code::EMPTY_TEXT:
2009 return "There is an empty <Text> node in a subtitle or closed caption.";
2010 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2011 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2012 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2013 return "Some closed captions are not listed in the order of their vertical position.";
2014 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2015 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2016 case VerificationNote::Code::UNEXPECTED_DURATION:
2017 return "There is an <Duration> node inside a <MainMarkers>.";
2018 case VerificationNote::Code::INVALID_CONTENT_KIND:
2019 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2020 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2021 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2022 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2023 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2024 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2025 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2026 case VerificationNote::Code::MISSING_SUBTITLE:
2027 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2028 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2029 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2030 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2031 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2032 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2033 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2034 case VerificationNote::Code::MISSING_FONT:
2035 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2036 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2037 return String::compose(
2038 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2039 note.frame().get(), note.component().get(), note.size().get()
2041 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2042 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2050 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2052 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2057 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2059 if (a.type() != b.type()) {
2060 return a.type() < b.type();
2063 if (a.code() != b.code()) {
2064 return a.code() < b.code();
2067 if (a.note() != b.note()) {
2068 return a.note().get_value_or("") < b.note().get_value_or("");
2071 if (a.file() != b.file()) {
2072 return a.file().get_value_or("") < b.file().get_value_or("");
2075 return a.line().get_value_or(0) < b.line().get_value_or(0);
2080 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2082 s << note_to_string (note);
2084 s << " [" << note.note().get() << "]";
2087 s << " [" << note.file().get() << "]";
2090 s << " [" << note.line().get() << "]";