2 Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 libdcp is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
35 /** @file src/verify.cc
36 * @brief dcp::verify() method and associated code
40 #include "compose.hpp"
43 #include "exceptions.h"
44 #include "interop_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "mono_picture_frame.h"
47 #include "raw_convert.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_smpte_subtitle_asset.h"
55 #include "reel_subtitle_asset.h"
56 #include "smpte_subtitle_asset.h"
57 #include "stereo_picture_asset.h"
58 #include "stereo_picture_frame.h"
60 #include "verify_j2k.h"
61 #include <libxml/parserInternals.h>
62 #include <xercesc/dom/DOMAttr.hpp>
63 #include <xercesc/dom/DOMDocument.hpp>
64 #include <xercesc/dom/DOMError.hpp>
65 #include <xercesc/dom/DOMErrorHandler.hpp>
66 #include <xercesc/dom/DOMException.hpp>
67 #include <xercesc/dom/DOMImplementation.hpp>
68 #include <xercesc/dom/DOMImplementationLS.hpp>
69 #include <xercesc/dom/DOMImplementationRegistry.hpp>
70 #include <xercesc/dom/DOMLSParser.hpp>
71 #include <xercesc/dom/DOMLocator.hpp>
72 #include <xercesc/dom/DOMNamedNodeMap.hpp>
73 #include <xercesc/dom/DOMNodeList.hpp>
74 #include <xercesc/framework/LocalFileInputSource.hpp>
75 #include <xercesc/framework/MemBufInputSource.hpp>
76 #include <xercesc/parsers/AbstractDOMParser.hpp>
77 #include <xercesc/parsers/XercesDOMParser.hpp>
78 #include <xercesc/sax/HandlerBase.hpp>
79 #include <xercesc/util/PlatformUtils.hpp>
80 #include <boost/algorithm/string.hpp>
89 using std::dynamic_pointer_cast;
91 using std::make_shared;
95 using std::shared_ptr;
98 using boost::optional;
99 using boost::function;
103 using namespace xercesc;
108 xml_ch_to_string (XMLCh const * a)
110 char* x = XMLString::transcode(a);
112 XMLString::release(&x);
117 class XMLValidationError
120 XMLValidationError (SAXParseException const & e)
121 : _message (xml_ch_to_string(e.getMessage()))
122 , _line (e.getLineNumber())
123 , _column (e.getColumnNumber())
124 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
125 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
130 string message () const {
134 uint64_t line () const {
138 uint64_t column () const {
142 string public_id () const {
146 string system_id () const {
159 class DCPErrorHandler : public ErrorHandler
162 void warning(const SAXParseException& e) override
164 maybe_add (XMLValidationError(e));
167 void error(const SAXParseException& e) override
169 maybe_add (XMLValidationError(e));
172 void fatalError(const SAXParseException& e) override
174 maybe_add (XMLValidationError(e));
177 void resetErrors() override {
181 list<XMLValidationError> errors () const {
186 void maybe_add (XMLValidationError e)
188 /* XXX: nasty hack */
190 e.message().find("schema document") != string::npos &&
191 e.message().find("has different target namespace from the one specified in instance document") != string::npos
196 _errors.push_back (e);
199 list<XMLValidationError> _errors;
206 StringToXMLCh (string a)
208 _buffer = XMLString::transcode(a.c_str());
211 StringToXMLCh (StringToXMLCh const&) = delete;
212 StringToXMLCh& operator= (StringToXMLCh const&) = delete;
216 XMLString::release (&_buffer);
219 XMLCh const * get () const {
228 class LocalFileResolver : public EntityResolver
231 LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
232 : _xsd_dtd_directory (xsd_dtd_directory)
234 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
235 * found without being here.
237 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
238 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
239 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
240 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
241 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
242 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
243 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
244 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
245 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
246 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
247 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
248 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
249 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
250 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
253 InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
258 auto system_id_str = xml_ch_to_string (system_id);
259 auto p = _xsd_dtd_directory;
260 if (_files.find(system_id_str) == _files.end()) {
263 p /= _files[system_id_str];
265 StringToXMLCh ch (p.string());
266 return new LocalFileInputSource(ch.get());
270 void add (string uri, string file)
275 std::map<string, string> _files;
276 boost::filesystem::path _xsd_dtd_directory;
281 parse (XercesDOMParser& parser, boost::filesystem::path xml)
283 parser.parse(xml.c_str());
288 parse (XercesDOMParser& parser, string xml)
290 xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
297 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
300 XMLPlatformUtils::Initialize ();
301 } catch (XMLException& e) {
302 throw MiscError ("Failed to initialise xerces library");
305 DCPErrorHandler error_handler;
307 /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
309 XercesDOMParser parser;
310 parser.setValidationScheme(XercesDOMParser::Val_Always);
311 parser.setDoNamespaces(true);
312 parser.setDoSchema(true);
314 vector<string> schema;
315 schema.push_back("xml.xsd");
316 schema.push_back("xmldsig-core-schema.xsd");
317 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
318 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
319 schema.push_back("SMPTE-429-9-2007-AM.xsd");
320 schema.push_back("Main-Stereo-Picture-CPL.xsd");
321 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
322 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
323 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
324 schema.push_back("DCSubtitle.v1.mattsson.xsd");
325 schema.push_back("DCDMSubtitle-2010.xsd");
326 schema.push_back("DCDMSubtitle-2014.xsd");
327 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
328 schema.push_back("SMPTE-429-16.xsd");
329 schema.push_back("Dolby-2012-AD.xsd");
330 schema.push_back("SMPTE-429-10-2008.xsd");
331 schema.push_back("xlink.xsd");
332 schema.push_back("SMPTE-335-2012.xsd");
333 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
334 schema.push_back("isdcf-mca.xsd");
335 schema.push_back("SMPTE-429-12-2008.xsd");
337 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
338 * Schemas that are not mentioned in this list are not read, and the things
339 * they describe are not checked.
342 for (auto i: schema) {
343 locations += String::compose("%1 %1 ", i, i);
346 parser.setExternalSchemaLocation(locations.c_str());
347 parser.setValidationSchemaFullChecking(true);
348 parser.setErrorHandler(&error_handler);
350 LocalFileResolver resolver (xsd_dtd_directory);
351 parser.setEntityResolver(&resolver);
354 parser.resetDocumentPool();
356 } catch (XMLException& e) {
357 throw MiscError(xml_ch_to_string(e.getMessage()));
358 } catch (DOMException& e) {
359 throw MiscError(xml_ch_to_string(e.getMessage()));
361 throw MiscError("Unknown exception from xerces");
365 XMLPlatformUtils::Terminate ();
367 for (auto i: error_handler.errors()) {
369 VerificationNote::Type::ERROR,
370 VerificationNote::Code::INVALID_XML,
372 boost::trim_copy(i.public_id() + " " + i.system_id()),
379 enum class VerifyAssetResult {
386 static VerifyAssetResult
387 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
389 auto const actual_hash = reel_file_asset->asset_ref()->hash(progress);
391 auto pkls = dcp->pkls();
392 /* We've read this DCP in so it must have at least one PKL */
393 DCP_ASSERT (!pkls.empty());
395 auto asset = reel_file_asset->asset_ref().asset();
397 optional<string> pkl_hash;
399 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
405 DCP_ASSERT (pkl_hash);
407 auto cpl_hash = reel_file_asset->hash();
408 if (cpl_hash && *cpl_hash != *pkl_hash) {
409 return VerifyAssetResult::CPL_PKL_DIFFER;
412 if (actual_hash != *pkl_hash) {
413 return VerifyAssetResult::BAD;
416 return VerifyAssetResult::GOOD;
421 verify_language_tag (string tag, vector<VerificationNote>& notes)
424 LanguageTag test (tag);
425 } catch (LanguageTagError &) {
426 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
432 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
434 int biggest_frame = 0;
435 auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
436 auto const duration = asset->intrinsic_duration ();
438 auto check_and_add = [¬es](vector<VerificationNote> const& j2k_notes) {
439 for (auto i: j2k_notes) {
440 if (find(notes.begin(), notes.end(), i) == notes.end()) {
446 if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
447 auto reader = mono_asset->start_read ();
448 for (int64_t i = 0; i < duration; ++i) {
449 auto frame = reader->get_frame (i);
450 biggest_frame = max(biggest_frame, frame->size());
451 if (!mono_asset->encrypted() || mono_asset->key()) {
452 vector<VerificationNote> j2k_notes;
453 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
454 check_and_add (j2k_notes);
456 progress (float(i) / duration);
458 } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
459 auto reader = stereo_asset->start_read ();
460 for (int64_t i = 0; i < duration; ++i) {
461 auto frame = reader->get_frame (i);
462 biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
463 if (!stereo_asset->encrypted() || stereo_asset->key()) {
464 vector<VerificationNote> j2k_notes;
465 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
466 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
467 check_and_add (j2k_notes);
469 progress (float(i) / duration);
474 static const int max_frame = rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
475 static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
476 if (biggest_frame > max_frame) {
478 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
480 } else if (biggest_frame > risky_frame) {
482 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
489 verify_main_picture_asset (
490 shared_ptr<const DCP> dcp,
491 shared_ptr<const ReelPictureAsset> reel_asset,
492 function<void (string, optional<boost::filesystem::path>)> stage,
493 function<void (float)> progress,
494 VerificationOptions options,
495 vector<VerificationNote>& notes
498 auto asset = reel_asset->asset();
499 auto const file = *asset->file();
501 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
502 stage ("Checking picture asset hash", file);
503 auto const r = verify_asset (dcp, reel_asset, progress);
505 case VerifyAssetResult::BAD:
507 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
510 case VerifyAssetResult::CPL_PKL_DIFFER:
512 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
520 stage ("Checking picture frame sizes", asset->file());
521 verify_picture_asset (reel_asset, file, notes, progress);
523 /* Only flat/scope allowed by Bv2.1 */
525 asset->size() != Size(2048, 858) &&
526 asset->size() != Size(1998, 1080) &&
527 asset->size() != Size(4096, 1716) &&
528 asset->size() != Size(3996, 2160)) {
530 VerificationNote::Type::BV21_ERROR,
531 VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
532 String::compose("%1x%2", asset->size().width, asset->size().height),
537 /* Only 24, 25, 48fps allowed for 2K */
539 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
540 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
543 VerificationNote::Type::BV21_ERROR,
544 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
545 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
550 if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
551 /* Only 24fps allowed for 4K */
552 if (asset->edit_rate() != Fraction(24, 1)) {
554 VerificationNote::Type::BV21_ERROR,
555 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
556 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
561 /* Only 2D allowed for 4K */
562 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
564 VerificationNote::Type::BV21_ERROR,
565 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
566 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
578 boost::optional<string> subtitle_language;
579 boost::optional<int> audio_channels;
584 verify_main_sound_asset (
585 shared_ptr<const DCP> dcp,
586 shared_ptr<const ReelSoundAsset> reel_asset,
587 function<void (string, optional<boost::filesystem::path>)> stage,
588 function<void (float)> progress,
589 VerificationOptions options,
590 vector<VerificationNote>& notes,
594 auto asset = reel_asset->asset();
595 auto const file = *asset->file();
597 if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || boost::filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
598 stage("Checking sound asset hash", file);
599 auto const r = verify_asset (dcp, reel_asset, progress);
601 case VerifyAssetResult::BAD:
602 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
604 case VerifyAssetResult::CPL_PKL_DIFFER:
605 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
612 if (!state.audio_channels) {
613 state.audio_channels = asset->channels();
614 } else if (*state.audio_channels != asset->channels()) {
615 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
618 stage ("Checking sound asset metadata", file);
620 if (auto lang = asset->language()) {
621 verify_language_tag (*lang, notes);
623 if (asset->sampling_rate() != 48000) {
624 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
630 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
632 /* XXX: is Language compulsory? */
633 if (reel_asset->language()) {
634 verify_language_tag (*reel_asset->language(), notes);
637 if (!reel_asset->entry_point()) {
638 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
639 } else if (reel_asset->entry_point().get()) {
640 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
646 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
648 /* XXX: is Language compulsory? */
649 if (reel_asset->language()) {
650 verify_language_tag (*reel_asset->language(), notes);
653 if (!reel_asset->entry_point()) {
654 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
655 } else if (reel_asset->entry_point().get()) {
656 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
661 /** Verify stuff that is common to both subtitles and closed captions */
663 verify_smpte_timed_text_asset (
664 shared_ptr<const SMPTESubtitleAsset> asset,
665 optional<int64_t> reel_asset_duration,
666 vector<VerificationNote>& notes
669 if (asset->language()) {
670 verify_language_tag (*asset->language(), notes);
672 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
675 auto const size = boost::filesystem::file_size(asset->file().get());
676 if (size > 115 * 1024 * 1024) {
678 { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
682 /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
683 * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
685 auto fonts = asset->font_data ();
687 for (auto i: fonts) {
688 total_size += i.second.size();
690 if (total_size > 10 * 1024 * 1024) {
691 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
694 if (!asset->start_time()) {
695 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
696 } else if (asset->start_time() != Time()) {
697 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
700 if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
703 VerificationNote::Type::BV21_ERROR,
704 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
705 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
712 /** Verify Interop subtitle-only stuff */
714 verify_interop_subtitle_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
716 if (asset->subtitles().empty()) {
717 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
719 auto const unresolved = asset->unresolved_fonts();
720 if (!unresolved.empty()) {
721 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
726 /** Verify SMPTE subtitle-only stuff */
728 verify_smpte_subtitle_asset (
729 shared_ptr<const SMPTESubtitleAsset> asset,
730 vector<VerificationNote>& notes,
734 if (asset->language()) {
735 if (!state.subtitle_language) {
736 state.subtitle_language = *asset->language();
737 } else if (state.subtitle_language != *asset->language()) {
738 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
742 DCP_ASSERT (asset->resource_id());
743 auto xml_id = asset->xml_id();
745 if (asset->resource_id().get() != xml_id) {
746 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
749 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
750 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
753 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
756 if (asset->raw_xml()) {
757 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
758 cxml::Document doc("SubtitleReel");
759 doc.read_string(*asset->raw_xml());
760 auto issue_date = doc.string_child("IssueDate");
761 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
762 if (!std::regex_match(issue_date, reg)) {
763 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
769 /** Verify all subtitle stuff */
771 verify_subtitle_asset (
772 shared_ptr<const SubtitleAsset> asset,
773 optional<int64_t> reel_asset_duration,
774 function<void (string, optional<boost::filesystem::path>)> stage,
775 boost::filesystem::path xsd_dtd_directory,
776 vector<VerificationNote>& notes,
780 stage ("Checking subtitle XML", asset->file());
781 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
782 * gets passed through libdcp which may clean up and therefore hide errors.
784 if (asset->raw_xml()) {
785 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
787 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
790 auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
791 cxml::Document doc(root_node);
792 doc.read_string(asset->raw_xml().get());
793 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
795 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
801 auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
803 verify_interop_subtitle_asset(interop, notes);
804 if (namespace_count(asset, "DCSubtitle") > 1) {
805 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
809 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
811 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
812 verify_smpte_subtitle_asset (smpte, notes, state);
813 /* This asset may be encrypted and in that case we'll have no raw_xml() */
814 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
815 notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
821 /** Verify all closed caption stuff */
823 verify_closed_caption_asset (
824 shared_ptr<const SubtitleAsset> asset,
825 optional<int64_t> reel_asset_duration,
826 function<void (string, optional<boost::filesystem::path>)> stage,
827 boost::filesystem::path xsd_dtd_directory,
828 vector<VerificationNote>& notes
831 stage ("Checking closed caption XML", asset->file());
832 /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
833 * gets passed through libdcp which may clean up and therefore hide errors.
835 auto raw_xml = asset->raw_xml();
837 validate_xml (*raw_xml, xsd_dtd_directory, notes);
838 if (raw_xml->size() > 256 * 1024) {
839 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
842 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
845 auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
847 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
852 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
855 verify_text_details (
856 dcp::Standard standard,
857 vector<shared_ptr<Reel>> reels,
859 vector<VerificationNote>& notes,
860 std::function<bool (shared_ptr<Reel>)> check,
861 std::function<optional<string> (shared_ptr<Reel>)> xml,
862 std::function<int64_t (shared_ptr<Reel>)> duration,
863 std::function<std::string (shared_ptr<Reel>)> id
866 /* end of last subtitle (in editable units) */
867 optional<int64_t> last_out;
868 auto too_short = false;
869 auto too_close = false;
870 auto too_early = false;
871 auto reel_overlap = false;
872 auto empty_text = false;
873 /* current reel start time (in editable units) */
874 int64_t reel_offset = 0;
875 optional<string> missing_load_font_id;
877 std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
879 parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
880 cxml::ConstNodePtr node,
882 optional<Time> start_time,
886 vector<string>& font_ids
888 if (node->name() == "Subtitle") {
889 Time in (node->string_attribute("TimeIn"), tcr);
893 Time out (node->string_attribute("TimeOut"), tcr);
897 if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
900 auto length = out - in;
901 if (length.as_editable_units_ceil(er) < 15) {
905 /* XXX: this feels dubious - is it really what Bv2.1 means? */
906 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
907 if (distance >= 0 && distance < 2) {
911 last_out = reel_offset + out.as_editable_units_floor(er);
912 } else if (node->name() == "Text") {
913 std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
914 if (!node->content().empty()) {
917 for (auto i: node->node_children()) {
918 if (node_has_content(i)) {
924 if (!node_has_content(node)) {
928 } else if (node->name() == "LoadFont") {
929 if (auto const id = node->optional_string_attribute("Id")) {
930 font_ids.push_back(*id);
931 } else if (auto const id = node->optional_string_attribute("ID")) {
932 font_ids.push_back(*id);
934 } else if (node->name() == "Font") {
935 if (auto const font_id = node->optional_string_attribute("Id")) {
936 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
937 missing_load_font_id = font_id;
941 for (auto i: node->node_children()) {
942 parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
946 for (auto i = 0U; i < reels.size(); ++i) {
947 if (!check(reels[i])) {
951 auto reel_xml = xml(reels[i]);
953 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
957 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
958 * read in by libdcp's parser.
961 shared_ptr<cxml::Document> doc;
963 optional<Time> start_time;
965 case dcp::Standard::INTEROP:
966 doc = make_shared<cxml::Document>("DCSubtitle");
967 doc->read_string (*reel_xml);
969 case dcp::Standard::SMPTE:
970 doc = make_shared<cxml::Document>("SubtitleReel");
971 doc->read_string (*reel_xml);
972 tcr = doc->number_child<int>("TimeCodeRate");
973 if (auto start_time_string = doc->optional_string_child("StartTime")) {
974 start_time = Time(*start_time_string, tcr);
978 bool has_text = false;
979 vector<string> font_ids;
980 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
981 auto end = reel_offset + duration(reels[i]);
982 if (last_out && *last_out > end) {
987 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
988 notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
992 if (last_out && *last_out > reel_offset) {
998 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1004 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1010 VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1016 VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1022 VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1026 if (missing_load_font_id) {
1027 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1034 verify_closed_caption_details (
1035 vector<shared_ptr<Reel>> reels,
1036 vector<VerificationNote>& notes
1039 std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1040 find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1041 for (auto i: node->node_children()) {
1042 if (i->name() == "Text") {
1043 text_or_image.push_back (i);
1045 find_text_or_image (i, text_or_image);
1050 auto mismatched_valign = false;
1051 auto incorrect_order = false;
1053 std::function<void (cxml::ConstNodePtr)> parse;
1054 parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1055 if (node->name() == "Subtitle") {
1056 vector<cxml::ConstNodePtr> text_or_image;
1057 find_text_or_image (node, text_or_image);
1058 optional<string> last_valign;
1059 optional<float> last_vpos;
1060 for (auto i: text_or_image) {
1061 auto valign = i->optional_string_attribute("VAlign");
1063 valign = i->optional_string_attribute("Valign").get_value_or("center");
1065 auto vpos = i->optional_number_attribute<float>("VPosition");
1067 vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1071 if (*last_valign != valign) {
1072 mismatched_valign = true;
1075 last_valign = valign;
1077 if (!mismatched_valign) {
1079 if (*last_valign == "top" || *last_valign == "center") {
1080 if (*vpos < *last_vpos) {
1081 incorrect_order = true;
1084 if (*vpos > *last_vpos) {
1085 incorrect_order = true;
1094 for (auto i: node->node_children()) {
1099 for (auto reel: reels) {
1100 for (auto ccap: reel->closed_captions()) {
1101 auto reel_xml = ccap->asset()->raw_xml();
1103 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1107 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1108 * read in by libdcp's parser.
1111 shared_ptr<cxml::Document> doc;
1113 optional<Time> start_time;
1115 doc = make_shared<cxml::Document>("SubtitleReel");
1116 doc->read_string (*reel_xml);
1118 doc = make_shared<cxml::Document>("DCSubtitle");
1119 doc->read_string (*reel_xml);
1125 if (mismatched_valign) {
1127 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1131 if (incorrect_order) {
1133 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1139 struct LinesCharactersResult
1141 bool warning_length_exceeded = false;
1142 bool error_length_exceeded = false;
1143 bool line_count_exceeded = false;
1149 verify_text_lines_and_characters (
1150 shared_ptr<SubtitleAsset> asset,
1153 LinesCharactersResult* result
1159 Event (Time time_, float position_, int characters_)
1161 , position (position_)
1162 , characters (characters_)
1165 Event (Time time_, shared_ptr<Event> start_)
1171 int position; //< position from 0 at top of screen to 100 at bottom
1173 shared_ptr<Event> start;
1176 vector<shared_ptr<Event>> events;
1178 auto position = [](shared_ptr<const SubtitleString> sub) {
1179 switch (sub->v_align()) {
1181 return lrintf(sub->v_position() * 100);
1182 case VAlign::CENTER:
1183 return lrintf((0.5f + sub->v_position()) * 100);
1184 case VAlign::BOTTOM:
1185 return lrintf((1.0f - sub->v_position()) * 100);
1191 for (auto j: asset->subtitles()) {
1192 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1194 auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1195 events.push_back(in);
1196 events.push_back(make_shared<Event>(text->out(), in));
1200 std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1201 return a->time < b->time;
1204 map<int, int> current;
1205 for (auto i: events) {
1206 if (current.size() > 3) {
1207 result->line_count_exceeded = true;
1209 for (auto j: current) {
1210 if (j.second > warning_length) {
1211 result->warning_length_exceeded = true;
1213 if (j.second > error_length) {
1214 result->error_length_exceeded = true;
1219 /* end of a subtitle */
1220 DCP_ASSERT (current.find(i->start->position) != current.end());
1221 if (current[i->start->position] == i->start->characters) {
1222 current.erase(i->start->position);
1224 current[i->start->position] -= i->start->characters;
1227 /* start of a subtitle */
1228 if (current.find(i->position) == current.end()) {
1229 current[i->position] = i->characters;
1231 current[i->position] += i->characters;
1240 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1242 if (reels.empty()) {
1246 if (reels[0]->main_subtitle()) {
1247 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1248 [](shared_ptr<Reel> reel) {
1249 return static_cast<bool>(reel->main_subtitle());
1251 [](shared_ptr<Reel> reel) {
1252 return reel->main_subtitle()->asset()->raw_xml();
1254 [](shared_ptr<Reel> reel) {
1255 return reel->main_subtitle()->actual_duration();
1257 [](shared_ptr<Reel> reel) {
1258 return reel->main_subtitle()->id();
1263 for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1264 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1265 [i](shared_ptr<Reel> reel) {
1266 return i < reel->closed_captions().size();
1268 [i](shared_ptr<Reel> reel) {
1269 return reel->closed_captions()[i]->asset()->raw_xml();
1271 [i](shared_ptr<Reel> reel) {
1272 return reel->closed_captions()[i]->actual_duration();
1274 [i](shared_ptr<Reel> reel) {
1275 return reel->closed_captions()[i]->id();
1280 verify_closed_caption_details (reels, notes);
1285 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1287 DCP_ASSERT (cpl->file());
1288 cxml::Document doc ("CompositionPlaylist");
1289 doc.read_file (cpl->file().get());
1291 auto missing = false;
1294 if (auto reel_list = doc.node_child("ReelList")) {
1295 auto reels = reel_list->node_children("Reel");
1296 if (!reels.empty()) {
1297 if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1298 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1299 if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1301 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1302 if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1306 if (auto name = extension->optional_node_child("Name")) {
1307 if (name->content() != "Application") {
1308 malformed = "<Name> should be 'Application'";
1311 if (auto property_list = extension->optional_node_child("PropertyList")) {
1312 if (auto property = property_list->optional_node_child("Property")) {
1313 if (auto name = property->optional_node_child("Name")) {
1314 if (name->content() != "DCP Constraints Profile") {
1315 malformed = "<Name> property should be 'DCP Constraints Profile'";
1318 if (auto value = property->optional_node_child("Value")) {
1319 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1320 malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1335 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1336 } else if (!malformed.empty()) {
1337 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1343 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1345 vector<string> encrypted;
1346 for (auto i: dcp->cpls()) {
1347 for (auto j: i->reel_file_assets()) {
1348 if (j->asset_ref().resolved()) {
1349 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1350 if (mxf && mxf->encrypted()) {
1351 encrypted.push_back(j->asset_ref().id());
1357 for (auto i: pkl->assets()) {
1358 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1370 shared_ptr<const DCP> dcp,
1371 shared_ptr<const CPL> cpl,
1372 shared_ptr<const Reel> reel,
1373 optional<dcp::Size> main_picture_active_area,
1374 function<void (string, optional<boost::filesystem::path>)> stage,
1375 boost::filesystem::path xsd_dtd_directory,
1376 function<void (float)> progress,
1377 VerificationOptions options,
1378 vector<VerificationNote>& notes,
1380 bool* have_main_subtitle,
1381 bool* have_no_main_subtitle,
1382 size_t* most_closed_captions,
1383 size_t* fewest_closed_captions,
1384 map<Marker, Time>* markers_seen
1387 for (auto i: reel->assets()) {
1388 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1389 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1391 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1392 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1394 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1395 if (i->encryptable() && !file_asset->hash()) {
1396 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1400 if (dcp->standard() == Standard::SMPTE) {
1401 boost::optional<int64_t> duration;
1402 for (auto i: reel->assets()) {
1404 duration = i->actual_duration();
1405 } else if (*duration != i->actual_duration()) {
1406 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1412 if (reel->main_picture()) {
1413 /* Check reel stuff */
1414 auto const frame_rate = reel->main_picture()->frame_rate();
1415 if (frame_rate.denominator != 1 ||
1416 (frame_rate.numerator != 24 &&
1417 frame_rate.numerator != 25 &&
1418 frame_rate.numerator != 30 &&
1419 frame_rate.numerator != 48 &&
1420 frame_rate.numerator != 50 &&
1421 frame_rate.numerator != 60 &&
1422 frame_rate.numerator != 96)) {
1424 VerificationNote::Type::ERROR,
1425 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1426 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1430 if (reel->main_picture()->asset_ref().resolved()) {
1431 verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1432 auto const asset_size = reel->main_picture()->asset()->size();
1433 if (main_picture_active_area) {
1434 if (main_picture_active_area->width > asset_size.width) {
1436 VerificationNote::Type::ERROR,
1437 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1438 String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1442 if (main_picture_active_area->height > asset_size.height) {
1444 VerificationNote::Type::ERROR,
1445 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1446 String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1454 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1455 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1458 if (reel->main_subtitle()) {
1459 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1460 if (reel->main_subtitle()->asset_ref().resolved()) {
1461 verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1463 *have_main_subtitle = true;
1465 *have_no_main_subtitle = true;
1468 for (auto i: reel->closed_captions()) {
1469 verify_closed_caption_reel(i, notes);
1470 if (i->asset_ref().resolved()) {
1471 verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1475 if (reel->main_markers()) {
1476 for (auto const& i: reel->main_markers()->get()) {
1477 markers_seen->insert(i);
1479 if (reel->main_markers()->entry_point()) {
1480 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1482 if (reel->main_markers()->duration()) {
1483 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1487 *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1488 *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1496 shared_ptr<const DCP> dcp,
1497 shared_ptr<const CPL> cpl,
1498 function<void (string, optional<boost::filesystem::path>)> stage,
1499 boost::filesystem::path xsd_dtd_directory,
1500 function<void (float)> progress,
1501 VerificationOptions options,
1502 vector<VerificationNote>& notes,
1506 stage("Checking CPL", cpl->file());
1507 validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1509 if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1510 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1513 for (auto const& i: cpl->additional_subtitle_languages()) {
1514 verify_language_tag(i, notes);
1517 if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1518 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1519 * of the approved ones.
1521 auto all = ContentKind::all();
1522 auto name = cpl->content_kind().name();
1523 transform(name.begin(), name.end(), name.begin(), ::tolower);
1524 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1525 if (iter == all.end()) {
1526 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1530 if (cpl->release_territory()) {
1531 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") {
1532 auto terr = cpl->release_territory().get();
1533 /* Must be a valid region tag, or "001" */
1535 LanguageTag::RegionSubtag test(terr);
1537 if (terr != "001") {
1538 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1544 if (dcp->standard() == Standard::SMPTE) {
1545 if (!cpl->annotation_text()) {
1546 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1547 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1548 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1552 for (auto i: dcp->pkls()) {
1553 /* Check that the CPL's hash corresponds to the PKL */
1554 optional<string> h = i->hash(cpl->id());
1555 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1556 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1559 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1560 optional<string> required_annotation_text;
1561 for (auto j: i->assets()) {
1562 /* See if this is a CPL */
1563 for (auto k: dcp->cpls()) {
1564 if (j->id() == k->id()) {
1565 if (!required_annotation_text) {
1566 /* First CPL we have found; this is the required AnnotationText unless we find another */
1567 required_annotation_text = cpl->content_title_text();
1569 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1570 required_annotation_text = boost::none;
1576 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1577 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1581 /* set to true if any reel has a MainSubtitle */
1582 auto have_main_subtitle = false;
1583 /* set to true if any reel has no MainSubtitle */
1584 auto have_no_main_subtitle = false;
1585 /* fewest number of closed caption assets seen in a reel */
1586 size_t fewest_closed_captions = SIZE_MAX;
1587 /* most number of closed caption assets seen in a reel */
1588 size_t most_closed_captions = 0;
1589 map<Marker, Time> markers_seen;
1591 auto const main_picture_active_area = cpl->main_picture_active_area();
1592 if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1594 VerificationNote::Type::ERROR,
1595 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1596 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1600 if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1602 VerificationNote::Type::ERROR,
1603 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1604 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1609 for (auto reel: cpl->reels()) {
1610 stage("Checking reel", optional<boost::filesystem::path>());
1615 main_picture_active_area,
1622 &have_main_subtitle,
1623 &have_no_main_subtitle,
1624 &most_closed_captions,
1625 &fewest_closed_captions,
1630 verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1632 if (dcp->standard() == Standard::SMPTE) {
1633 if (auto msc = cpl->main_sound_configuration()) {
1634 if (state.audio_channels && msc->channels() != *state.audio_channels) {
1636 VerificationNote::Type::ERROR,
1637 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1638 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1644 if (have_main_subtitle && have_no_main_subtitle) {
1645 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1648 if (fewest_closed_captions != most_closed_captions) {
1649 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1652 if (cpl->content_kind() == ContentKind::FEATURE) {
1653 if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1654 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1656 if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1657 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1661 auto ffoc = markers_seen.find(Marker::FFOC);
1662 if (ffoc == markers_seen.end()) {
1663 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1664 } else if (ffoc->second.e != 1) {
1665 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1668 auto lfoc = markers_seen.find(Marker::LFOC);
1669 if (lfoc == markers_seen.end()) {
1670 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1672 auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1673 if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1674 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1678 LinesCharactersResult result;
1679 for (auto reel: cpl->reels()) {
1680 if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1681 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1685 if (result.line_count_exceeded) {
1686 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1688 if (result.error_length_exceeded) {
1689 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1690 } else if (result.warning_length_exceeded) {
1691 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1694 result = LinesCharactersResult();
1695 for (auto reel: cpl->reels()) {
1696 for (auto i: reel->closed_captions()) {
1698 verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1703 if (result.line_count_exceeded) {
1704 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1706 if (result.error_length_exceeded) {
1707 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1710 if (!cpl->read_composition_metadata()) {
1711 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1712 } else if (!cpl->version_number()) {
1713 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1716 verify_extension_metadata(cpl, notes);
1718 if (cpl->any_encrypted()) {
1719 cxml::Document doc("CompositionPlaylist");
1720 DCP_ASSERT(cpl->file());
1721 doc.read_file(cpl->file().get());
1722 if (!doc.optional_node_child("Signature")) {
1723 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1733 shared_ptr<const DCP> dcp,
1734 shared_ptr<const PKL> pkl,
1735 boost::filesystem::path xsd_dtd_directory,
1736 vector<VerificationNote>& notes
1739 validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1741 if (pkl_has_encrypted_assets(dcp, pkl)) {
1742 cxml::Document doc("PackingList");
1743 doc.read_file(pkl->file().get());
1744 if (!doc.optional_node_child("Signature")) {
1745 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1749 set<string> uuid_set;
1750 for (auto asset: pkl->assets()) {
1751 if (!uuid_set.insert(asset->id()).second) {
1752 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1763 shared_ptr<const DCP> dcp,
1764 boost::filesystem::path xsd_dtd_directory,
1765 vector<VerificationNote>& notes
1768 auto asset_map = dcp->asset_map();
1769 DCP_ASSERT(asset_map);
1771 validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1773 set<string> uuid_set;
1774 for (auto const& asset: asset_map->assets()) {
1775 if (!uuid_set.insert(asset.id()).second) {
1776 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1783 vector<VerificationNote>
1785 vector<boost::filesystem::path> directories,
1786 function<void (string, optional<boost::filesystem::path>)> stage,
1787 function<void (float)> progress,
1788 VerificationOptions options,
1789 optional<boost::filesystem::path> xsd_dtd_directory
1792 if (!xsd_dtd_directory) {
1793 xsd_dtd_directory = resources_directory() / "xsd";
1795 *xsd_dtd_directory = boost::filesystem::canonical (*xsd_dtd_directory);
1797 vector<VerificationNote> notes;
1800 vector<shared_ptr<DCP>> dcps;
1801 for (auto i: directories) {
1802 dcps.push_back (make_shared<DCP>(i));
1805 for (auto dcp: dcps) {
1806 stage ("Checking DCP", dcp->directory());
1807 bool carry_on = true;
1809 dcp->read (¬es, true);
1810 } catch (MissingAssetmapError& e) {
1811 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1813 } catch (ReadError& e) {
1814 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1815 } catch (XMLError& e) {
1816 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1817 } catch (MXFFileError& e) {
1818 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1819 } catch (BadURNUUIDError& e) {
1820 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1821 } catch (cxml::Error& e) {
1822 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1829 if (dcp->standard() != Standard::SMPTE) {
1830 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1833 for (auto cpl: dcp->cpls()) {
1846 for (auto pkl: dcp->pkls()) {
1847 stage("Checking PKL", pkl->file());
1848 verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1851 if (dcp->asset_map_file()) {
1852 stage("Checking ASSETMAP", dcp->asset_map_file().get());
1853 verify_assetmap(dcp, *xsd_dtd_directory, notes);
1855 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1864 dcp::note_to_string (VerificationNote note)
1866 /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1868 * e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1869 * not "ClosedCaption assets must have an <EntryPoint> tag."
1871 * It's OK to use XML tag names where they are clear.
1872 * If both ID and filename are available, use only the ID.
1873 * End messages with a full stop.
1874 * Messages should not mention whether or not their errors are a part of Bv2.1.
1876 switch (note.code()) {
1877 case VerificationNote::Code::FAILED_READ:
1878 return *note.note();
1879 case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1880 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1881 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1882 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1883 case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1884 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1885 case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1886 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1887 case VerificationNote::Code::INCORRECT_SOUND_HASH:
1888 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1889 case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1890 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1891 case VerificationNote::Code::EMPTY_ASSET_PATH:
1892 return "The asset map contains an empty asset path.";
1893 case VerificationNote::Code::MISSING_ASSET:
1894 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1895 case VerificationNote::Code::MISMATCHED_STANDARD:
1896 return "The DCP contains both SMPTE and Interop parts.";
1897 case VerificationNote::Code::INVALID_XML:
1898 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1899 case VerificationNote::Code::MISSING_ASSETMAP:
1900 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1901 case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1902 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1903 case VerificationNote::Code::INVALID_DURATION:
1904 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1905 case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1906 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());
1907 case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1908 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());
1909 case VerificationNote::Code::EXTERNAL_ASSET:
1910 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());
1911 case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1912 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1913 case VerificationNote::Code::INVALID_STANDARD:
1914 return "This DCP does not use the SMPTE standard.";
1915 case VerificationNote::Code::INVALID_LANGUAGE:
1916 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1917 case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1918 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1919 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1920 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1921 case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1922 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1923 case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1924 return "3D 4K DCPs are not allowed.";
1925 case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1926 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1927 case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1928 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1929 case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1930 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());
1931 case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1932 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1933 case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1934 return "Some subtitle assets have different <Language> tags than others";
1935 case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1936 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1937 case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1938 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1939 case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1940 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1941 case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1942 return "At least one subtitle lasts less than 15 frames.";
1943 case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1944 return "At least one pair of subtitles is separated by less than 2 frames.";
1945 case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1946 return "At least one subtitle extends outside of its reel.";
1947 case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1948 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1949 case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1950 return "There are more than 52 characters in at least one subtitle line.";
1951 case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1952 return "There are more than 79 characters in at least one subtitle line.";
1953 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1954 return "There are more than 3 closed caption lines in at least one place.";
1955 case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1956 return "There are more than 32 characters in at least one closed caption line.";
1957 case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1958 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1959 case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1960 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1961 case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1962 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1963 case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1964 return "All assets in a reel do not have the same duration.";
1965 case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1966 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1967 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1968 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1969 case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1970 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1971 case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1972 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1973 case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1974 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1975 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1976 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1977 case VerificationNote::Code::MISSING_HASH:
1978 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1979 case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1980 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
1981 case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1982 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
1983 case VerificationNote::Code::MISSING_FFOC:
1984 return "There should be a FFOC (first frame of content) marker.";
1985 case VerificationNote::Code::MISSING_LFOC:
1986 return "There should be a LFOC (last frame of content) marker.";
1987 case VerificationNote::Code::INCORRECT_FFOC:
1988 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1989 case VerificationNote::Code::INCORRECT_LFOC:
1990 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1991 case VerificationNote::Code::MISSING_CPL_METADATA:
1992 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1993 case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1994 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1995 case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1996 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1997 case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1998 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1999 case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2000 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2001 case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2002 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2003 case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2004 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2005 case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2006 return "Some assets are encrypted but some are not.";
2007 case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2008 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2009 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2010 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2011 case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2012 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2013 case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2014 return "The JPEG2000 tile size is not the same as the image size.";
2015 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2016 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2017 case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2018 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2019 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2020 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2021 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2022 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2023 case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2024 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2025 case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2026 return "POC marker found outside main header.";
2027 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2028 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2029 case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2030 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2031 case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2032 return "No TLM marker was found in a JPEG2000 codestream.";
2033 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2034 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2035 case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2036 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2037 case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2039 vector<string> parts;
2040 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2041 DCP_ASSERT (parts.size() == 2);
2042 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]);
2044 case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2045 return "Some aspect of this DCP could not be checked because it is encrypted.";
2046 case VerificationNote::Code::EMPTY_TEXT:
2047 return "There is an empty <Text> node in a subtitle or closed caption.";
2048 case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2049 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2050 case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2051 return "Some closed captions are not listed in the order of their vertical position.";
2052 case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2053 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2054 case VerificationNote::Code::UNEXPECTED_DURATION:
2055 return "There is an <Duration> node inside a <MainMarkers>.";
2056 case VerificationNote::Code::INVALID_CONTENT_KIND:
2057 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2058 case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2059 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2060 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2061 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2062 case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2063 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2064 case VerificationNote::Code::MISSING_SUBTITLE:
2065 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2066 case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2067 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2068 case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2069 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2070 case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2071 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2072 case VerificationNote::Code::MISSING_FONT:
2073 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2074 case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2075 return String::compose(
2076 "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2077 note.frame().get(), note.component().get(), note.size().get()
2079 case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2080 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2081 case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2082 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());
2083 case VerificationNote::Code::MISSING_LOAD_FONT:
2084 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2092 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2094 return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2099 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2101 if (a.type() != b.type()) {
2102 return a.type() < b.type();
2105 if (a.code() != b.code()) {
2106 return a.code() < b.code();
2109 if (a.note() != b.note()) {
2110 return a.note().get_value_or("") < b.note().get_value_or("");
2113 if (a.file() != b.file()) {
2114 return a.file().get_value_or("") < b.file().get_value_or("");
2117 return a.line().get_value_or(0) < b.line().get_value_or(0);
2122 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2124 s << note_to_string (note);
2126 s << " [" << note.note().get() << "]";
2129 s << " [" << note.file().get() << "]";
2132 s << " [" << note.line().get() << "]";